Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

XDRill ledger low level helpers example #5501

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions exp/xdrill/ledgerentries/ledger_entries.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package ledgerentries
191 changes: 191 additions & 0 deletions exp/xdrill/ledgers/ledgers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package ledgers

import (
"encoding/base64"
"fmt"
"time"

"github.com/stellar/go/exp/xdrill/utils"
"github.com/stellar/go/xdr"
)

type Ledgers struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be singular,Ledger, all the receiver methods imply such.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah that's a good catch. I'll make the packages, structs, and functions all singular instead of plural

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 3b90190

xdr.LedgerCloseMeta
}

func (l Ledgers) Sequence() uint32 {
return uint32(l.LedgerHeaderHistoryEntry().Header.LedgerSeq)

Check failure on line 17 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.LedgerHeaderHistoryEntry undefined (type Ledgers has no field or method LedgerHeaderHistoryEntry) (typecheck)
}

func (l Ledgers) ID() int64 {
return utils.NewID(int32(l.LedgerSequence()), 0, 0).ToInt64()

Check failure on line 21 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.LedgerSequence undefined (type Ledgers has no field or method LedgerSequence) (typecheck)
}

func (l Ledgers) Hash() string {
return utils.HashToHexString(l.LedgerHeaderHistoryEntry().Hash)

Check failure on line 25 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.LedgerHeaderHistoryEntry undefined (type Ledgers has no field or method LedgerHeaderHistoryEntry) (typecheck)
}

func (l Ledgers) PreviousHash() string {
return utils.HashToHexString(l.PreviousLedgerHash())

Check failure on line 29 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.PreviousLedgerHash undefined (type Ledgers has no field or method PreviousLedgerHash) (typecheck)
}

func (l Ledgers) CloseTime() int64 {
return l.LedgerCloseTime()

Check failure on line 33 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.LedgerCloseTime undefined (type Ledgers has no field or method LedgerCloseTime) (typecheck)
}

func (l Ledgers) ClosedAt() time.Time {
return time.Unix(l.CloseTime(), 0).UTC()
}

func (l Ledgers) TotalCoins() int64 {
return int64(l.LedgerHeaderHistoryEntry().Header.TotalCoins)

Check failure on line 41 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.LedgerHeaderHistoryEntry undefined (type Ledgers has no field or method LedgerHeaderHistoryEntry) (typecheck)
}

func (l Ledgers) FeePool() int64 {
return int64(l.LedgerHeaderHistoryEntry().Header.FeePool)
}

func (l Ledgers) BaseFee() uint32 {
return uint32(l.LedgerHeaderHistoryEntry().Header.BaseFee)
}

func (l Ledgers) BaseReserve() uint32 {
return uint32(l.LedgerHeaderHistoryEntry().Header.BaseReserve)
}

func (l Ledgers) MaxTxSetSize() uint32 {
return uint32(l.LedgerHeaderHistoryEntry().Header.MaxTxSetSize)
}

func (l Ledgers) LedgerVersion() uint32 {
return uint32(l.LedgerHeaderHistoryEntry().Header.LedgerVersion)
}

func (l Ledgers) GetSorobanFeeWrite1Kb() (int64, bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can probably drop the Get prefix in favor of convention, i.e. any fn here will return a value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 146dd52

lcmV1, ok := l.GetV1()

Check failure on line 65 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.GetV1 undefined (type Ledgers has no field or method GetV1) (typecheck)
if ok {
extV1, ok := lcmV1.Ext.GetV1()
if ok {
return int64(extV1.SorobanFeeWrite1Kb), true
}
}

return 0, false
}

func (l Ledgers) GetTotalByteSizeOfBucketList() (uint64, bool) {
lcmV1, ok := l.GetV1()

Check failure on line 77 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.GetV1 undefined (type Ledgers has no field or method GetV1) (typecheck)
if ok {
return uint64(lcmV1.TotalByteSizeOfBucketList), true
}

return 0, false
}

func (l Ledgers) GetNodeID() (string, bool) {
LedgerCloseValueSignature, ok := l.LedgerHeaderHistoryEntry().Header.ScpValue.Ext.GetLcValueSignature()
if ok {
nodeID, ok := utils.GetAddress(LedgerCloseValueSignature.NodeId)
if ok {
return nodeID, true
}
}

return "", false
}

func (l Ledgers) GetSignature() (string, bool) {
LedgerCloseValueSignature, ok := l.LedgerHeaderHistoryEntry().Header.ScpValue.Ext.GetLcValueSignature()
if ok {
return base64.StdEncoding.EncodeToString(LedgerCloseValueSignature.Signature), true
}

return "", false
}

func (l Ledgers) GetTransactionCounts() (successTxCount, failedTxCount int32, ok bool) {
transactions := getTransactionSet(l)
results := l.V0.TxProcessing

Check failure on line 108 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.V0 undefined (type Ledgers has no field or method V0) (typecheck)
txCount := len(transactions)
if txCount != len(results) {
return 0, 0, false
}

for i := 0; i < txCount; i++ {
if results[i].Result.Successful() {
successTxCount++
} else {
failedTxCount++
}
}

return successTxCount, failedTxCount, true
}

func (l Ledgers) GetOperationCounts() (operationCount, txSetOperationCount int32, ok bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs on each method should help promote these also, I'm not even sure what is the significance of txSetOperationCount ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah docs will help. I'll definitely add them in the real PR/implementation.

As for the difference between operationCount, txSetOperationCount is the total operation count (successful + failed) and only successful operation count.

The names are definitely confusing and unclear but I copied these from stellar-etl code that predates me lol
I will definitely rename/refactor these functions in the real PR/implementation

transactions := getTransactionSet(l)
results := l.V0.TxProcessing

Check failure on line 127 in exp/xdrill/ledgers/ledgers.go

View workflow job for this annotation

GitHub Actions / golangci

l.V0 undefined (type Ledgers has no field or method V0) (typecheck)
txCount := len(transactions)
if txCount != len(results) {
return 0, 0, false
}

for i := 0; i < txCount; i++ {
operations := transactions[i].Operations()
numberOfOps := int32(len(operations))
txSetOperationCount += numberOfOps

// for successful transactions, the operation count is based on the operations results slice
if results[i].Result.Successful() {
operationResults, ok := results[i].Result.OperationResults()
if !ok {
return 0, 0, false
}

operationCount += int32(len(operationResults))
}

}

return operationCount, txSetOperationCount, true
}

func getTransactionSet(l Ledgers) (transactionProcessing []xdr.TransactionEnvelope) {
Copy link
Contributor

@sreuland sreuland Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this be where new layer can reference the new non-xdr model instead, i.e. returning a []transactions.Transactions ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That wasn't the intent of this function. This was just carryover from stellar-etl to count the transactions/operations.

I think you're right though that it would be nice to include a function for users to call to get the non-xdr model instead of manually doing it

switch l.V {
case 0:
return l.V0.TxSet.Txs
case 1:
switch l.V1.TxSet.V {
case 0:
return getTransactionPhase(l.V1.TxSet.V1TxSet.Phases)
default:
panic(fmt.Sprintf("unsupported LedgerCloseMeta.V1.TxSet.V: %d", l.V1.TxSet.V))
}
default:
panic(fmt.Sprintf("unsupported LedgerCloseMeta.V: %d", l.V))
}
}

func getTransactionPhase(transactionPhase []xdr.TransactionPhase) (transactionEnvelope []xdr.TransactionEnvelope) {
transactionSlice := []xdr.TransactionEnvelope{}
for _, phase := range transactionPhase {
switch phase.V {
case 0:
components := phase.MustV0Components()
for _, component := range components {
switch component.Type {
case 0:
transactionSlice = append(transactionSlice, component.TxsMaybeDiscountedFee.Txs...)

default:
panic(fmt.Sprintf("Unsupported TxSetComponentType: %d", component.Type))
}

}
default:
panic(fmt.Sprintf("Unsupported TransactionPhase.V: %d", phase.V))
}
}

return transactionSlice
}
1 change: 1 addition & 0 deletions exp/xdrill/operations/operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package operations
9 changes: 9 additions & 0 deletions exp/xdrill/transactions/transactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package transactions

import (
"github.com/stellar/go/ingest"
)

type Transactions struct {
ingest.LedgerTransaction
}
104 changes: 104 additions & 0 deletions exp/xdrill/transform_ledger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package xdrill

import (
"fmt"
"time"

"github.com/stellar/go/exp/xdrill/ledgers"
"github.com/stellar/go/xdr"
)

type LedgerOutput struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this appears like state related to a closed ledger, could indicate a bit more like LedgerClosed, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah makes sense

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 146dd52

Sequence uint32 `json:"sequence"` // sequence number of the ledger
LedgerHash string `json:"ledger_hash"`
PreviousLedgerHash string `json:"previous_ledger_hash"`
LedgerHeader string `json:"ledger_header"` // base 64 encoding of the ledger header
TransactionCount int32 `json:"transaction_count"`
OperationCount int32 `json:"operation_count"` // counts only operations that were a part of successful transactions
SuccessfulTransactionCount int32 `json:"successful_transaction_count"`
FailedTransactionCount int32 `json:"failed_transaction_count"`
TxSetOperationCount string `json:"tx_set_operation_count"` // counts all operations, even those that are part of failed transactions
ClosedAt time.Time `json:"closed_at"` // UTC timestamp
TotalCoins int64 `json:"total_coins"`
FeePool int64 `json:"fee_pool"`
BaseFee uint32 `json:"base_fee"`
BaseReserve uint32 `json:"base_reserve"`
MaxTxSetSize uint32 `json:"max_tx_set_size"`
ProtocolVersion uint32 `json:"protocol_version"`
LedgerID int64 `json:"id"`
SorobanFeeWrite1Kb int64 `json:"soroban_fee_write_1kb"`
NodeID string `json:"node_id"`
Signature string `json:"signature"`
TotalByteSizeOfBucketList uint64 `json:"total_byte_size_of_bucket_list"`
}

func TransformLedger(lcm xdr.LedgerCloseMeta) (LedgerOutput, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this pattern scale well for the sdk, i.e. what will the next fn be named that transforms to a different model, the pattern implies something like TransformLedgerTo<DerivedModel>() <DerivedModel> which is probably fine, this fn is specifically TransformLedgerToOutput correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fn is specifically TransformLedgerToOutput correct?

Yes

I didn't think of the pattern for how to name custom processors. This is just how we define/call it in stellar-etl and was mostly used to serve as one possible way to call the low level helper functions.

It makes sense to standardize the name/pattern though. I think it would be hard though.
I think there's currently the idea from like

  • OffersProcessor() with HasOffer and DeriveOffer
  • stellar-etl has an input/*.go functions to read the ledgers --> transform/*.go like the above TransformLedger to process the data
  • TransformLedgerTo<DerivedModel>() <DerivedModel> TransformLedgerToOutput as noted above

And I think there'd then be another layer depending on what kind of output/data model the user wants for their application. Like a user could rightfully/wrongfully do:

func ProcessLedgerMetadata(lcm xdr.LedgerCloseMeta) {
  TransformLedgerToOutput()
  TransformOfferToOutput()
  
  if ingest.HasPayment(lcm) {
    ingest.DerivePayment(lcm)
  }
}

This might be worth bringing back to the design doc or in the team meeting to discuss

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, good suggestion on opening up to further design discussion, another option to consider may be to remove the transformer package to avoid grey area where app domain opinions inevitably start leaking in as the derived model, refactor this transform as a more factual attribute retrieval like ledgers.Ledgers.Closed() ClosedOutput, ideally this new sdk of low level helpers should give developers enough tooling that they can quickly write their own lcm-to-model transforms, rather than the sdk codifying transforms?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this new sdk of low level helpers should give developers enough tooling that they can quickly write their own lcm-to-model transforms, rather than the sdk codifying transforms?

Yes 100%

ledger := ledgers.Ledgers{
LedgerCloseMeta: lcm,
}

outputLedgerHeader, err := xdr.MarshalBase64(ledger.LedgerHeaderHistoryEntry().Header)
if err != nil {
return LedgerOutput{}, err
}

outputSuccessfulTransactionCount, outputFailedTransactionCount, ok := ledger.GetTransactionCounts()
if !ok {
return LedgerOutput{}, fmt.Errorf("could not get transaction counts")
}

outputOperationCount, outputTxSetOperationCount, ok := ledger.GetOperationCounts()
if !ok {
return LedgerOutput{}, fmt.Errorf("could not get operation counts")
}

var outputSorobanFeeWrite1Kb int64
sorobanFeeWrite1Kb, ok := ledger.GetSorobanFeeWrite1Kb()
if ok {
outputSorobanFeeWrite1Kb = sorobanFeeWrite1Kb
}

var outputTotalByteSizeOfBucketList uint64
totalByteSizeOfBucketList, ok := ledger.GetTotalByteSizeOfBucketList()
if ok {
outputTotalByteSizeOfBucketList = totalByteSizeOfBucketList
}

var outputNodeID string
nodeID, ok := ledger.GetNodeID()
if ok {
outputNodeID = nodeID
}

var outputSigature string
signature, ok := ledger.GetSignature()
if ok {
outputSigature = signature
}

ledgerOutput := LedgerOutput{
Sequence: ledger.LedgerSequence(),
LedgerHash: ledger.Hash(),
PreviousLedgerHash: ledger.Hash(),
LedgerHeader: outputLedgerHeader,
TransactionCount: outputSuccessfulTransactionCount,
OperationCount: outputOperationCount,
SuccessfulTransactionCount: outputSuccessfulTransactionCount,
FailedTransactionCount: outputFailedTransactionCount,
TxSetOperationCount: string(outputTxSetOperationCount),
ClosedAt: ledger.ClosedAt(),
TotalCoins: ledger.TotalCoins(),
FeePool: ledger.FeePool(),
BaseFee: ledger.BaseFee(),
BaseReserve: ledger.BaseReserve(),
MaxTxSetSize: ledger.MaxTxSetSize(),
ProtocolVersion: ledger.LedgerVersion(),
LedgerID: ledger.ID(),
SorobanFeeWrite1Kb: outputSorobanFeeWrite1Kb,
NodeID: outputNodeID,
Signature: outputSigature,
TotalByteSizeOfBucketList: outputTotalByteSizeOfBucketList,
}

return ledgerOutput, nil
}
Loading
Loading