From f53fb36b471f135ceef82fddfaf58a785b133173 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 15:34:39 -0400 Subject: [PATCH 01/35] update getTransactionByHash to use views --- .../internal/rpcv1/sqlitedb/transaction.go | 50 ++++----- cmd/stellar-rpc/internal/store/transaction.go | 104 ++++-------------- 2 files changed, 39 insertions(+), 115 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go index c0e949ed6..e0fd8cbe5 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go @@ -74,11 +74,7 @@ func (txn *transactionHandler) InsertTransactions(lcm xdr.LedgerCloseMeta) error reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(txn.passphrase, lcm) if err != nil { - return fmt.Errorf( - "failed to open transaction reader for ledger %d: %w", - lcm.LedgerSequence(), - err, - ) + return fmt.Errorf("failed to open transaction reader for ledger %d: %w", lcm.LedgerSequence(), err) } transactions := make(map[xdr.Hash]ingest.LedgerTransaction, txCount) @@ -159,17 +155,10 @@ func (txn *transactionHandler) trimTransactions(latestLedgerSeq uint32, retentio // // Errors occur if there are issues with the DB connection or the XDR is // corrupted somehow. If the transaction is not found, io.EOF is returned. -func (txn *transactionHandler) GetTransaction(ctx context.Context, hash xdr.Hash) ( - store.Transaction, error, -) { +func (txn *transactionHandler) GetTransaction(ctx context.Context, hash xdr.Hash) (store.Transaction, error) { start := time.Now() - tx := store.Transaction{} - lcm, ingestTx, err := txn.getTransactionByHash(ctx, hash) - if err != nil { - return tx, err - } - tx, err = store.ParseTransaction(lcm, ingestTx) + tx, err := txn.getTransactionByHash(ctx, hash) if err != nil { return tx, err } @@ -177,7 +166,7 @@ func (txn *transactionHandler) GetTransaction(ctx context.Context, hash xdr.Hash txn.log. WithField("txhash", hex.EncodeToString(hash[:])). WithField("duration", time.Since(start)). - Debugf("Fetched and encoded transaction from ledger %d", lcm.LedgerSequence()) + Debugf("Fetched and encoded transaction from ledger %d", tx.Ledger.Sequence) return tx, nil } @@ -189,11 +178,11 @@ func (txn *transactionHandler) GetTransaction(ctx context.Context, hash xdr.Hash // // Note: Caller must do input sanitization on the hash. func (txn *transactionHandler) getTransactionByHash(ctx context.Context, hash xdr.Hash) ( - xdr.LedgerCloseMeta, ingest.LedgerTransaction, error, + store.Transaction, error, ) { var rows []struct { - TxIndex int `db:"application_order"` - Lcm xdr.LedgerCloseMeta `db:"meta"` + TxIndex int `db:"application_order"` + Lcm xdr.LedgerCloseMetaView `db:"meta"` } rowQ := sq. Select("t.application_order", "lcm.meta"). @@ -203,27 +192,26 @@ func (txn *transactionHandler) getTransactionByHash(ctx context.Context, hash xd Limit(1) if err := txn.db.Select(ctx, &rows, rowQ); err != nil { - return xdr.LedgerCloseMeta{}, ingest.LedgerTransaction{}, - fmt.Errorf("db read failed for txhash %s: %w", hex.EncodeToString(hash[:]), err) + return store.Transaction{}, fmt.Errorf("db read failed for txhash %s: %w", hex.EncodeToString(hash[:]), err) } else if len(rows) < 1 { - return xdr.LedgerCloseMeta{}, ingest.LedgerTransaction{}, store.ErrNoTransaction + return store.Transaction{}, store.ErrNoTransaction } txIndex, lcm := rows[0].TxIndex, rows[0].Lcm - reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(txn.passphrase, lcm) + ledgerSeq, err := lcm.LedgerSequence() if err != nil { - return lcm, ingest.LedgerTransaction{}, - fmt.Errorf("failed to create ledger reader: %w", err) + return store.Transaction{}, fmt.Errorf("failed to get ledger sequence: %w", err) } - err = reader.Seek(txIndex - 1) + txnViewRange, err := ingest.LedgerTransactionViewRange(lcm, txIndex-1, 1, txn.passphrase) if err != nil { - return lcm, ingest.LedgerTransaction{}, - fmt.Errorf("failed to index to tx %d in ledger %d (txhash=%s): %w", - txIndex, lcm.LedgerSequence(), hash, err) + return store.Transaction{}, fmt.Errorf("failed to index to tx %d in ledger %d (txhash=%s): %w", + txIndex, ledgerSeq, hex.EncodeToString(hash[:]), err) } - - ledgerTx, err := reader.Read() - return lcm, ledgerTx, err + if len(txnViewRange) == 0 { + return store.Transaction{}, store.ErrNoTransaction + } + txView := txnViewRange[0] + return store.ParseTransaction(txView), nil } type transactionTableMigration struct { diff --git a/cmd/stellar-rpc/internal/store/transaction.go b/cmd/stellar-rpc/internal/store/transaction.go index 7fc05cd13..905361840 100644 --- a/cmd/stellar-rpc/internal/store/transaction.go +++ b/cmd/stellar-rpc/internal/store/transaction.go @@ -2,9 +2,8 @@ package store import ( "context" + "encoding/hex" "errors" - "fmt" - "strconv" "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/xdr" @@ -32,87 +31,24 @@ type TransactionReader interface { GetTransaction(ctx context.Context, hash xdr.Hash) (Transaction, error) } -func ParseTransaction(lcm xdr.LedgerCloseMeta, ingestTx ingest.LedgerTransaction) (Transaction, error) { - var tx Transaction - var err error - - // - // On-the-fly ingestion: extract all of the fields, return best effort. - // - tx.FeeBump = ingestTx.Envelope.IsFeeBump() - applicationOrder, convErr := strconv.ParseInt(strconv.FormatUint(uint64(ingestTx.Index), 10), 10, 32) - if convErr != nil { - return tx, fmt.Errorf("transaction index %d exceeds supported range", ingestTx.Index) - } - tx.ApplicationOrder = int32(applicationOrder) - tx.Successful = ingestTx.Result.Successful() - tx.Ledger = LedgerInfo{ - Sequence: lcm.LedgerSequence(), - CloseTime: lcm.LedgerCloseTime(), - } - tx.TransactionHash = ingestTx.Result.TransactionHash.HexString() - - if tx.Result, err = ingestTx.Result.Result.MarshalBinary(); err != nil { - return tx, fmt.Errorf("couldn't encode transaction Result: %w", err) - } - if tx.Meta, err = ingestTx.UnsafeMeta.MarshalBinary(); err != nil { - return tx, fmt.Errorf("couldn't encode transaction UnsafeMeta: %w", err) - } - if tx.Envelope, err = ingestTx.Envelope.MarshalBinary(); err != nil { - return tx, fmt.Errorf("couldn't encode transaction Envelope: %w", err) - } - - allEvents, err := ingestTx.GetTransactionEvents() - if err != nil { - return tx, fmt.Errorf("couldn't encode transaction Events: %w", err) - } - - diagEvents, err := ingestTx.GetDiagnosticEvents() - if err != nil { - return tx, errors.Join(errors.New("couldn't encode diagnostic events"), err) - } - - tx.Events = make([][]byte, 0, len(diagEvents)) - for i, event := range diagEvents { - bytes, ierr := event.MarshalBinary() - if ierr != nil { - return tx, fmt.Errorf("couldn't encode transaction DiagnosticEvent %d: %w", i, ierr) - } - tx.Events = append(tx.Events, bytes) - } - - if err = parseEvents(allEvents, &tx); err != nil { - return tx, err - } - - return tx, err -} - -// parseEvents parses diagnostic, transaction and contract events -func parseEvents(allEvents ingest.TransactionEvents, tx *Transaction) error { - // encode TransactionEvents - tx.TransactionEvents = make([][]byte, 0, len(allEvents.TransactionEvents)) - for i, event := range allEvents.TransactionEvents { - bytes, ierr := event.MarshalBinary() - if ierr != nil { - return fmt.Errorf("couldn't encode TransactionEvent %d: %w", i, ierr) - } - tx.TransactionEvents = append(tx.TransactionEvents, bytes) - } - - // encode ContractEvents (slice of slices) - tx.ContractEvents = make([][][]byte, 0, len(allEvents.OperationEvents)) - for opIndex, opEvents := range allEvents.OperationEvents { - events := make([][]byte, 0, len(opEvents)) - for i, event := range opEvents { - bytes, ierr := event.MarshalBinary() - if ierr != nil { - return fmt.Errorf("couldn't encode ContractEvent %d for operation %d: %w", i, opIndex, ierr) - } - events = append(events, bytes) - } - - tx.ContractEvents = append(tx.ContractEvents, events) +// ParseTransaction reshapes the SDK's raw-bytes transaction view into the +// serving contract's Transaction. The byte fields alias the view's ledger +// buffer rather than being copied. +func ParseTransaction(txView ingest.LedgerTransactionView) Transaction { + return Transaction{ + TransactionHash: hex.EncodeToString(txView.Hash[:]), + Result: txView.Result, + Meta: txView.Meta, + Envelope: txView.Envelope, + Events: txView.DiagnosticEvents, + FeeBump: txView.FeeBump, + ApplicationOrder: txView.ApplicationOrder, + Successful: txView.Successful, + Ledger: LedgerInfo{ + Sequence: txView.LedgerSequence, + CloseTime: txView.LedgerCloseTime, + }, + TransactionEvents: txView.TransactionEvents, + ContractEvents: txView.ContractEvents, } - return nil } From d1184703a5ea40d86ba50f553ab49b9ac3c121bd Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 15:35:29 -0400 Subject: [PATCH 02/35] add GetLedgerView to leaderReaderTx interface + implement --- .../internal/rpcv1/sqlitedb/ledger.go | 8 ++ .../internal/rpcv2/adapters/ledger_reader.go | 119 +++++++++++------- cmd/stellar-rpc/internal/store/ledger.go | 13 +- 3 files changed, 87 insertions(+), 53 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 8a235c89b..5d0c055e9 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -92,6 +92,14 @@ func (l ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.Led return getLedgerFromDB(ctx, l.tx, sequence) } +func (l ledgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { + meta, found, err := getLedgerRawFromDB(ctx, l.tx, sequence) + if err != nil || !found { + return nil, found, err + } + return xdr.LedgerCloseMetaView(meta), true, nil +} + func (l ledgerReaderTx) Done() error { return l.tx.Rollback() } diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index b3efa9023..6b09b8804 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -139,55 +139,10 @@ type ledgerReaderTx struct { } func (tx *ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { - // The request duration limiter answers the client at the deadline but only - // abandons the handler goroutine — it cannot stop it. Without this check - // the abandoned getTransactions walk would keep decoding the rest of its - // primed span (up to a whole chunk) while holding its read view, and the - // deletion grace margin is sized assuming walks stop within one iteration - // of their deadline. - if err := ctx.Err(); err != nil { - return xdr.LedgerCloseMeta{}, false, err - } - // ClampRange is the only place the servable window is enforced and no - // point-read path calls it, so gate here: without this a view acquired - // between ingestion's commit and its SetLatestLedger could return a ledger - // above the view's frozen latest. - if !inWindow(tx.view, sequence) { - return xdr.LedgerCloseMeta{}, false, nil - } - - if tx.next == nil { - // ScanLedgers' end is inclusive; the -1 keeps the span at exactly - // walkSpanCap ledgers, so a chunk-aligned start opens one chunk - // reader, not two. - scan, err := tx.view.ScanLedgers(sequence, min(tx.view.LatestLedger(), sequence+walkSpanCap-1)) - if err != nil { - return xdr.LedgerCloseMeta{}, false, err - } - tx.next, tx.stop = iter.Pull2(scan) - } - - entry, err, ok := tx.next() - if err != nil { + entry, found, err := tx.walk(ctx, sequence) + if err != nil || !found { return xdr.LedgerCloseMeta{}, false, err } - if !ok { - // The iterator ran dry: the caller walked sequentially but past the span - // primed above. getTransactions' span cap (methods.LedgerScanLimit) keeps - // its walks inside the span, so reaching this means a new caller without - // that cap. Fail loudly rather than serve a wrong-position read. - return xdr.LedgerCloseMeta{}, false, fmt.Errorf( - "adapters: ledger walk exhausted its primed %d-ledger span at ledger %d"+ - " — the calling handler must cap the request's ledger range", - walkSpanCap, sequence) - } - if entry.Seq != sequence { - // The walk contract (ascending, contiguous from the priming sequence) - // was broken. Fail loudly rather than serve the wrong ledger's data. - return xdr.LedgerCloseMeta{}, false, fmt.Errorf( - "adapters: non-sequential GetLedger: asked for ledger %d, the walk is at ledger %d", - sequence, entry.Seq) - } var lcm xdr.LedgerCloseMeta if err := lcm.UnmarshalBinary(entry.Bytes); err != nil { return xdr.LedgerCloseMeta{}, false, fmt.Errorf("adapters: unmarshal ledger %d: %w", sequence, err) @@ -195,6 +150,19 @@ func (tx *ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.L return lcm, true, nil } +// GetLedgerView is GetLedger without the decode; it advances the same walk. +// The walk's Entry.Bytes alias the scan's scratch buffer, overwritten on the +// next step, while the caller keeps slices of the view across steps +// (getTransactions accumulates a page of transactions whose bytes alias their +// ledgers) — so the bytes are copied out. +func (tx *ledgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { + entry, found, err := tx.walk(ctx, sequence) + if err != nil || !found { + return nil, false, err + } + return xdr.LedgerCloseMetaView(bytes.Clone(entry.Bytes)), true, nil +} + func (tx *ledgerReaderTx) GetLedgerRange(_ context.Context) (store.LedgerRange, error) { return getLedgerRange(tx.view) } @@ -246,6 +214,63 @@ func (tx *ledgerReaderTx) Done() error { return nil } +// walk serves the ascending, contiguous per-ledger walk described on +// store.LedgerReaderTx from a single ScanLedgers iterator primed on the first +// call. The returned Entry's Bytes alias the reader's scratch buffer and are +// only valid until the next call. +func (tx *ledgerReaderTx) walk(ctx context.Context, sequence uint32) (ledger.Entry, bool, error) { + // The request duration limiter answers the client at the deadline but only + // abandons the handler goroutine — it cannot stop it. Without this check + // the abandoned getTransactions walk would keep decoding the rest of its + // primed span (up to a whole chunk) while holding its read view, and the + // deletion grace margin is sized assuming walks stop within one iteration + // of their deadline. + if err := ctx.Err(); err != nil { + return ledger.Entry{}, false, err + } + // ClampRange is the only place the servable window is enforced and no + // point-read path calls it, so gate here: without this a view acquired + // between ingestion's commit and its SetLatestLedger could return a ledger + // above the view's frozen latest. + if !inWindow(tx.view, sequence) { + return ledger.Entry{}, false, nil + } + + if tx.next == nil { + // ScanLedgers' end is inclusive; the -1 keeps the span at exactly + // walkSpanCap ledgers, so a chunk-aligned start opens one chunk + // reader, not two. + scan, err := tx.view.ScanLedgers(sequence, min(tx.view.LatestLedger(), sequence+walkSpanCap-1)) + if err != nil { + return ledger.Entry{}, false, err + } + tx.next, tx.stop = iter.Pull2(scan) + } + + entry, err, ok := tx.next() + if err != nil { + return ledger.Entry{}, false, err + } + if !ok { + // The iterator ran dry: the caller walked sequentially but past the span + // primed above. getTransactions' span cap (methods.LedgerScanLimit) keeps + // its walks inside the span, so reaching this means a new caller without + // that cap. Fail loudly rather than serve a wrong-position read. + return ledger.Entry{}, false, fmt.Errorf( + "adapters: ledger walk exhausted its primed %d-ledger span at ledger %d"+ + " — the calling handler must cap the request's ledger range", + walkSpanCap, sequence) + } + if entry.Seq != sequence { + // The walk contract (ascending, contiguous from the priming sequence) + // was broken. Fail loudly rather than serve the wrong ledger's data. + return ledger.Entry{}, false, fmt.Errorf( + "adapters: non-sequential GetLedger: asked for ledger %d, the walk is at ledger %d", + sequence, entry.Seq) + } + return entry, true, nil +} + // inWindow reports whether seq falls inside the view's servable window // [OldestLedger, LatestLedger] — the one gate every point read must apply. // OldestLedger is always ≥ 2 (the floor sits on a chunk, and chunk 0 starts at diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index e589ae752..36107541e 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -50,14 +50,15 @@ type WithLedgerRawFn func(raw []byte) error // LedgerReaderTx is a read-only snapshot of the ledger store. Call Done to // release it. // -// GetLedger is a walk, not a free-form point read. Call it with ascending, -// contiguous sequences, starting from the first call's sequence. Read at most -// methods.LedgerScanLimit ledgers per Tx. This is getTransactions' access -// pattern. The v1 (SQL) backend accepts any pattern; the v2 backend serves -// the walk from a forward iterator primed on the first call and fails loudly -// on anything else. +// GetLedger and GetLedgerView are one walk, not free-form point reads. Call +// them with ascending, contiguous sequences, starting from the first call's +// sequence. Read at most methods.LedgerScanLimit ledgers per Tx. This is +// getTransactions' access pattern. The v1 (SQL) backend accepts any pattern; +// the v2 backend serves the walk from a forward iterator primed on the first +// call and fails loudly on anything else. type LedgerReaderTx interface { GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) + GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) GetLedgerRange(ctx context.Context) (LedgerRange, error) BatchGetLedgers(ctx context.Context, start uint32, end uint32) ([]LedgerMetadataChunk, error) Done() error From ead9b3077f78dd6c67f8de0baad5af0853d8ec2d Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 15:36:04 -0400 Subject: [PATCH 03/35] update mocks --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index 5930dd950..3f182e5c6 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -69,12 +69,22 @@ func (txn *MockTransactionHandler) InsertTransactions(lcm xdr.LedgerCloseMeta) e func (txn *MockTransactionHandler) GetTransaction(_ context.Context, hash xdr.Hash) ( store.Transaction, error, ) { - tx, ok := txn.txs[hash.HexString()] + lcm, ok := txn.txHashToMeta[hash.HexString()] if !ok { return store.Transaction{}, store.ErrNoTransaction } - itx, err := store.ParseTransaction(*txn.txHashToMeta[hash.HexString()], tx) - return itx, err + raw, err := lcm.MarshalBinary() + if err != nil { + return store.Transaction{}, err + } + txView, found, err := ingest.LedgerTransactionViewByHash(xdr.LedgerCloseMetaView(raw), hash, txn.passphrase) + if err != nil { + return store.Transaction{}, err + } + if !found { + return store.Transaction{}, store.ErrNoTransaction + } + return store.ParseTransaction(txView), nil } func (txn *MockTransactionHandler) RegisterMetrics(_, _ prometheus.Observer) {} From ddc1970ccf7cf27555b5d8fd6426e06b0d2b8c5c Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 16:12:58 -0400 Subject: [PATCH 04/35] update getTransactions handler --- .../internal/methods/get_transactions.go | 81 ++++++------------- 1 file changed, 24 insertions(+), 57 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_transactions.go b/cmd/stellar-rpc/internal/methods/get_transactions.go index 217ebb43c..7103e05bf 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions.go @@ -3,9 +3,7 @@ package methods import ( "context" "encoding/base64" - "errors" "fmt" - "io" "strconv" "github.com/creachadair/jrpc2" @@ -74,31 +72,29 @@ func (h transactionsRPCHandler) initializePagination( return *start, requestCursor, limit, nil } -// fetchLedgerData calls the meta table to fetch the corresponding ledger data. -func (h transactionsRPCHandler) fetchLedgerData(ctx context.Context, ledgerSeq uint32, +// fetchLedgerViewData calls the meta table to fetch the corresponding ledger data. +func (h transactionsRPCHandler) fetchLedgerViewData(ctx context.Context, ledgerSeq uint32, readTx store.LedgerReaderTx, -) (xdr.LedgerCloseMeta, error) { - ledger, found, err := readTx.GetLedger(ctx, ledgerSeq) +) (xdr.LedgerCloseMetaView, error) { + ledgerView, found, err := readTx.GetLedgerView(ctx, ledgerSeq) if err != nil { - return ledger, &jrpc2.Error{ + return ledgerView, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } } else if !found { - return ledger, &jrpc2.Error{ + return ledgerView, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: fmt.Sprintf("database does not contain metadata for ledger: %d", ledgerSeq), } } - return ledger, nil + return ledgerView, nil } -// processTransactionsInLedger cycles through all the transactions in a ledger, extracts the transaction info +// processTransactionsInLedgerView cycles through all the transactions in a ledger, extracts the transaction info // and builds the list of transactions. -// -//nolint:cyclop,funlen // one linear pagination pass; splitting would scatter the cursor math -func (h transactionsRPCHandler) processTransactionsInLedger( - ledger xdr.LedgerCloseMeta, start toid.ID, +func (h transactionsRPCHandler) processTransactionsInLedgerView( + ledger xdr.LedgerCloseMetaView, start toid.ID, txns *[]protocol.TransactionInfo, limit uint, format string, ) (*toid.ID, bool, error) { @@ -106,55 +102,26 @@ func (h transactionsRPCHandler) processTransactionsInLedger( if err != nil { return nil, false, &jrpc2.Error{Code: jrpc2.InvalidParams, Message: err.Error()} } - - reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(h.networkPassphrase, ledger) + ledgerSeq, err := ledger.LedgerSequence() if err != nil { - return nil, false, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: err.Error(), - } + return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} } + ledgerSeqInt32 := int32(ledgerSeq) //nolint:gosec // safe until ledger seq exceeds 2147483647 + // The cursor's tx-order offset only applies within the cursor's own ledger. startTxIdx := 1 - ledgerSeq := ledger.LedgerSequence() - ledgerSeqInt32, err := uint32ToInt32(ledgerSeq, "ledger sequence") - if err != nil { - return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} - } if ledgerSeqInt32 == start.LedgerSequence { startTxIdx = int(start.TransactionOrder) - if ierr := reader.Seek(startTxIdx - 1); ierr != nil && !errors.Is(ierr, io.EOF) { - return nil, false, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: ierr.Error(), - } - } } - - txCount := ledger.CountTransactions() + remaining := limitInt - len(*txns) + views, err := ingest.LedgerTransactionViewRange(ledger, startTxIdx-1, remaining, h.networkPassphrase) + if err != nil { + return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} + } cursor := toid.New(ledgerSeqInt32, 0, 1) - for i := startTxIdx; i <= txCount; i++ { - cursor.TransactionOrder = int32(i) - - ingestTx, err := reader.Read() - if err != nil { - if errors.Is(err, io.EOF) { - break - } - return nil, false, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: err.Error(), - } - } - - tx, err := store.ParseTransaction(ledger, ingestTx) - if err != nil { - return nil, false, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: err.Error(), - } - } - + for _, v := range views { + tx := store.ParseTransaction(v) + cursor.TransactionOrder = tx.ApplicationOrder txInfo := protocol.TransactionInfo{ TransactionDetails: protocol.TransactionDetails{ TransactionHash: tx.TransactionHash, @@ -272,12 +239,12 @@ func (h transactionsRPCHandler) getTransactionsByLedgerSequence(ctx context.Cont Message: "cursor ledger sequence cannot be negative", } } - ledger, err := h.fetchLedgerData(ctx, uint32(ledgerSeq), readTx) + ledgerView, err := h.fetchLedgerViewData(ctx, uint32(ledgerSeq), readTx) if err != nil { return protocol.GetTransactionsResponse{}, err } - cursor, done, err = h.processTransactionsInLedger(ledger, start, &txns, limit, request.Format) + cursor, done, err = h.processTransactionsInLedgerView(ledgerView, start, &txns, limit, request.Format) if err != nil { return protocol.GetTransactionsResponse{}, err } From 7c0d6b875d9c5d88d7be58c678cf5137751648fa Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 16:13:18 -0400 Subject: [PATCH 05/35] update mocks --- cmd/stellar-rpc/internal/methods/mocks.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index 7f9094108..a6ffaddad 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -76,6 +76,13 @@ func (m *MockLedgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xd return args.Get(0).(xdr.LedgerCloseMeta), args.Bool(1), args.Error(2) //nolint:forcetypeassert } +func (m *MockLedgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) ( + xdr.LedgerCloseMetaView, bool, error, +) { + args := m.Called(ctx, sequence) + return args.Get(0).(xdr.LedgerCloseMetaView), args.Bool(1), args.Error(2) //nolint:forcetypeassert +} + func (m *MockLedgerReaderTx) Done() error { args := m.Called() return args.Error(0) From eb2380d779176a80fac9aa524086eaf723a0446a Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 16:13:51 -0400 Subject: [PATCH 06/35] update tests w/ new golden strings --- cmd/stellar-rpc/internal/methods/get_ledgers_test.go | 4 ++-- cmd/stellar-rpc/internal/methods/get_transaction_test.go | 6 +++++- cmd/stellar-rpc/internal/methods/get_transactions_test.go | 8 ++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go index f13a7fb81..59fde9c0b 100644 --- a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go +++ b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go @@ -22,8 +22,8 @@ var expectedLedgerInfo = protocol.LedgerInfo{ Hash: "0000000000000000000000000000000000000000000000000000000000000000", Sequence: 1, LedgerCloseTime: 125, - LedgerHeader: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", //nolint:lll - LedgerMetadata: "AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAACAAABAIAAAAAAAAAAPww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAABAAAAAP///50AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAEzmSAb0wlZuZ7vERyxkacbwbERSS/IM82EYhemLKdUAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", //nolint:lll + LedgerHeader: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", //nolint:lll + LedgerMetadata: "AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAACAAABAIAAAAAAAAAAPww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAABAAAAAP///50AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAEzmSAb0wlZuZ7vERyxkacbwbERSS/IM82EYhemLKdUAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", //nolint:lll } func setupTestDB(t *testing.T, numLedgers int) *sqlitedb.DB { diff --git a/cmd/stellar-rpc/internal/methods/get_transaction_test.go b/cmd/stellar-rpc/internal/methods/get_transaction_test.go index c365e2001..6b2c2aad2 100644 --- a/cmd/stellar-rpc/internal/methods/get_transaction_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transaction_test.go @@ -274,7 +274,11 @@ func txMeta(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { TxApplyProcessing: xdr.TransactionMeta{ V: 3, Operations: &[]xdr.OperationMeta{}, - V3: &xdr.TransactionMetaV3{}, + // The envelope is soroban (Ext V1), so its V3 meta must carry + // SorobanMeta, as on the real network. + V3: &xdr.TransactionMetaV3{SorobanMeta: &xdr.SorobanTransactionMeta{ + ReturnValue: xdr.ScVal{Type: xdr.ScValTypeScvVoid}, + }}, }, Result: xdr.TransactionResultPair{ TransactionHash: txHash(acctSeq), diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index 40a56fed6..44d3f0da6 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -32,7 +32,7 @@ var expectedTransactionInfo = protocol.TransactionInfo{ FeeBump: false, Ledger: 1, EnvelopeXDR: "AAAAAgAAAQCAAAAAAAAAAD8MNL+TrQ2ZcdBMzJD3BVEcg4qtlzSkovsNegP8f+iaAAAAAQAAAAD///+dAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", //nolint:lll - ResultMetaXDR: "AAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAA", + ResultMetaXDR: "AAAAAwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAA==", ResultXDR: "AAAAAAAAAGQAAAAAAAAAAAAAAAA=", DiagnosticEventsXDR: []string{}, Events: protocol.Events{ @@ -323,7 +323,11 @@ func createTestLedger(sequence uint32) xdr.LedgerCloseMeta { TxApplyProcessing: xdr.TransactionMeta{ V: 3, Operations: &[]xdr.OperationMeta{}, - V3: &xdr.TransactionMetaV3{}, + // The envelope is soroban (Ext V1), so its V3 meta must carry + // SorobanMeta, as on the real network. + V3: &xdr.TransactionMetaV3{SorobanMeta: &xdr.SorobanTransactionMeta{ + ReturnValue: xdr.ScVal{Type: xdr.ScValTypeScvVoid}, + }}, }, Result: xdr.TransactionResultPair{ TransactionHash: txHash(sequence), From e69a1e7017de2c4c460b47b7c0a0c53b20a2e79e Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 19:47:28 -0400 Subject: [PATCH 07/35] port finalizations, reduce comment verbosity --- .../internal/rpcv2/adapters/ledger_reader.go | 14 +++++--------- cmd/stellar-rpc/internal/store/ledger.go | 7 +++---- cmd/stellar-rpc/internal/store/transaction.go | 5 ++--- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index 6b09b8804..f88a7bde9 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -150,11 +150,9 @@ func (tx *ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.L return lcm, true, nil } -// GetLedgerView is GetLedger without the decode; it advances the same walk. -// The walk's Entry.Bytes alias the scan's scratch buffer, overwritten on the -// next step, while the caller keeps slices of the view across steps -// (getTransactions accumulates a page of transactions whose bytes alias their -// ledgers) — so the bytes are copied out. +// GetLedgerView is GetLedger without the decode and advances the same walk. +// Entry.Bytes is overwritten on the next step and callers keep slices of the +// view across steps, so clone. func (tx *ledgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { entry, found, err := tx.walk(ctx, sequence) if err != nil || !found { @@ -214,10 +212,8 @@ func (tx *ledgerReaderTx) Done() error { return nil } -// walk serves the ascending, contiguous per-ledger walk described on -// store.LedgerReaderTx from a single ScanLedgers iterator primed on the first -// call. The returned Entry's Bytes alias the reader's scratch buffer and are -// only valid until the next call. +// walk is the store.LedgerReaderTx walk: one ScanLedgers iterator primed on +// the first call. The returned Entry.Bytes are valid only until the next call. func (tx *ledgerReaderTx) walk(ctx context.Context, sequence uint32) (ledger.Entry, bool, error) { // The request duration limiter answers the client at the deadline but only // abandons the handler goroutine — it cannot stop it. Without this check diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index 36107541e..08fba014b 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -52,10 +52,9 @@ type WithLedgerRawFn func(raw []byte) error // // GetLedger and GetLedgerView are one walk, not free-form point reads. Call // them with ascending, contiguous sequences, starting from the first call's -// sequence. Read at most methods.LedgerScanLimit ledgers per Tx. This is -// getTransactions' access pattern. The v1 (SQL) backend accepts any pattern; -// the v2 backend serves the walk from a forward iterator primed on the first -// call and fails loudly on anything else. +// sequence. Read at most methods.LedgerScanLimit ledgers per Tx. The v1 (SQL) +// backend accepts any pattern, while the v2 backend only walks from a forward +// iterator primed on the first call. type LedgerReaderTx interface { GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) diff --git a/cmd/stellar-rpc/internal/store/transaction.go b/cmd/stellar-rpc/internal/store/transaction.go index 905361840..e7ae62d0f 100644 --- a/cmd/stellar-rpc/internal/store/transaction.go +++ b/cmd/stellar-rpc/internal/store/transaction.go @@ -31,9 +31,8 @@ type TransactionReader interface { GetTransaction(ctx context.Context, hash xdr.Hash) (Transaction, error) } -// ParseTransaction reshapes the SDK's raw-bytes transaction view into the -// serving contract's Transaction. The byte fields alias the view's ledger -// buffer rather than being copied. +// ParseTransaction reshapes an SDK transaction view into a Transaction; the +// byte fields alias the view's buffer. func ParseTransaction(txView ingest.LedgerTransactionView) Transaction { return Transaction{ TransactionHash: hex.EncodeToString(txView.Hash[:]), From 9bbacce953cce43a596225681d27745c3afc9a56 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 31 Aug 2026 14:46:34 -0400 Subject: [PATCH 08/35] restore sparseLedgerReader Tx-level GetLedgerView --- .../internal/methods/get_transactions_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index 44d3f0da6..933aec4af 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -385,6 +385,15 @@ func (r *sparseLedgerReader) GetLedger(_ context.Context, seq uint32) (xdr.Ledge return createEmptyTestLedger(seq), true, nil } +func (r *sparseLedgerReader) GetLedgerView(_ context.Context, seq uint32) (xdr.LedgerCloseMetaView, bool, error) { + r.gets++ + raw, err := createEmptyTestLedger(seq).MarshalBinary() + if err != nil { + return nil, false, err + } + return xdr.LedgerCloseMetaView(raw), true, nil +} + func (r *sparseLedgerReader) WithLedgerRaw( _ context.Context, seq uint32, fn store.WithLedgerRawFn, ) (bool, error) { From 0e5bce89c18ba7743e946cac8fb1d2d319010d66 Mon Sep 17 00:00:00 2001 From: Tamir Sen Date: Mon, 31 Aug 2026 00:41:27 +0000 Subject: [PATCH 09/35] store: a raw, loan-shaped ledger accessor on the serving read Tx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getTransactions costs ~100ms per limit=50 page, and a CPU profile over 40 such requests attributes 1.83s cum to xdr.LedgerEntryChanges.DecodeFrom plus ~2.9s of GC drain/scan on top: the shared handler fully XDR-unmarshals the entire multi-MB LedgerCloseMeta once per ledger it walks, to read a handful of per-transaction fields off it. It has no choice — the serving interface only offers GetLedger, which returns a decoded xdr.LedgerCloseMeta — yet BOTH backends hold the ledger as raw XDR bytes and decode on the way out. Add the seam that lets a handler skip that decode: WithLedgerRaw(ctx, sequence, fn) (found bool, err error) on store.LedgerReaderTx — the Tx/walk variant, because the paging path is the only caller and the one-shot store.LedgerReader has none. Loan-shaped rather than returning bytes: the v2 backend can then hand over its chunk reader's scratch buffer directly, with no copy and no lifetime question, since the bytes' validity ends with fn. The contract is documented on the interface — valid only inside fn, read-only, must not be retained — alongside the walk-cursor rule the two accessors now share. The backends implement it as pass-throughs of what they already hold: - rpcv2 adapter: GetLedger's walk (deadline check, window gate, priming, the two loud contract failures) moves into a shared walkTo, so the decoding and raw accessors advance one cursor and cannot drift on where the walk is. WithLedgerRaw is walkTo plus fn — no decode at all. - rpcv1 sqlitedb: selects the same `meta` column of ledger_close_meta into a []byte instead of an xdr.LedgerCloseMeta, so the row scan never runs UnmarshalBinary. The lent bytes are ours to lend: scanning a BLOB into a *[]byte goes through database/sql's convertAssign, which clones the driver's row buffer (only sql.RawBytes opts out), and BatchGetLedgers already depends on exactly that by retaining the slices it selects. GetLedger stays: every other handler still uses it. No caller uses the new seam yet. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Y7opQUW9UZPKzE6B3tz9b5 (cherry picked from commit d8e0797e37ab5a2176d8a943dfdb094017066778) --- cmd/stellar-rpc/internal/methods/mocks.go | 7 ++ .../internal/rpcv1/sqlitedb/ledger.go | 12 ++ .../internal/rpcv1/sqlitedb/ledger_test.go | 54 +++++++++ .../internal/rpcv2/adapters/ledger_reader.go | 26 ++++- .../rpcv2/adapters/ledger_reader_test.go | 103 ++++++++++++++++++ cmd/stellar-rpc/internal/store/ledger.go | 15 ++- 6 files changed, 207 insertions(+), 10 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index a6ffaddad..dd3e79889 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -83,6 +83,13 @@ func (m *MockLedgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) return args.Get(0).(xdr.LedgerCloseMetaView), args.Bool(1), args.Error(2) //nolint:forcetypeassert } +func (m *MockLedgerReaderTx) WithLedgerRaw( + ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, +) (bool, error) { + args := m.Called(ctx, sequence, fn) + return args.Bool(0), args.Error(1) +} + func (m *MockLedgerReaderTx) Done() error { args := m.Called() return args.Error(0) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 5d0c055e9..7df776c8a 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -100,6 +100,18 @@ func (l ledgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) (xdr return xdr.LedgerCloseMetaView(meta), true, nil } +// WithLedgerRaw lends the ledger's stored meta blob without decoding it. The +// blob is ours to lend: database/sql clones each BLOB scanned into a *[]byte. +func (l ledgerReaderTx) WithLedgerRaw( + ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, +) (bool, error) { + meta, found, err := getLedgerRawFromDB(ctx, l.tx, sequence) + if err != nil || !found { + return found, err + } + return true, fn(meta) +} + func (l ledgerReaderTx) Done() error { return l.tx.Rollback() } diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index eb81dd7dc..f3cd8d44d 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -3,6 +3,7 @@ package sqlitedb import ( "bytes" "context" + "errors" "io" "path" "testing" @@ -409,3 +410,56 @@ func setupBenchmarkingDB(b *testing.B) (*DB, []xdr.LedgerCloseMeta) { require.NoError(b, write.Commit(lcms[len(lcms)-1], nil)) return testDB, lcms } + +// TestWithLedgerRaw pins the raw accessor against the decoding one: the blob it +// lends must re-marshal to exactly what GetLedger decodes, an absent ledger +// must report found=false without running fn, and a callback error must come +// back verbatim with found=true. +func TestWithLedgerRaw(t *testing.T) { + db := NewTestDB(t) + daemon := host.MakeNoOpDaemon() + write := NewReadWriter(logger, db, daemon, 10, passphrase) + for seq := uint32(1); seq <= 3; seq++ { + tx, err := write.NewTx(t.Context()) + require.NoError(t, err) + lcm := createLedger(seq) + require.NoError(t, tx.LedgerWriter().InsertLedger(lcm)) + require.NoError(t, tx.Commit(lcm, nil)) + } + + reader := NewLedgerReader(db) + readTx, err := reader.NewTx(t.Context()) + require.NoError(t, err) + defer func() { _ = readTx.Done() }() + + for seq := uint32(1); seq <= 3; seq++ { + lcm, ok, err := readTx.GetLedger(t.Context(), seq) + require.NoError(t, err) + require.True(t, ok) + want, err := lcm.MarshalBinary() + require.NoError(t, err) + + var got []byte + found, err := readTx.WithLedgerRaw(t.Context(), seq, func(raw []byte) error { + got = append([]byte(nil), raw...) // the loan forbids retaining raw + return nil + }) + require.NoError(t, err) + require.True(t, found) + assert.Equal(t, want, got, "ledger %d", seq) + } + + ran := false + found, err := readTx.WithLedgerRaw(t.Context(), 99, func([]byte) error { + ran = true + return nil + }) + require.NoError(t, err) + assert.False(t, found) + assert.False(t, ran, "fn must not run for an absent ledger") + + boom := errors.New("boom") + found, err = readTx.WithLedgerRaw(t.Context(), 1, func([]byte) error { return boom }) + assert.ErrorIs(t, err, boom) + assert.True(t, found, "the ledger was there; only the callback failed") +} diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index f88a7bde9..826b8d27b 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -125,15 +125,16 @@ func (r *LedgerReader) NewTx(ctx context.Context) (store.LedgerReaderTx, error) } // ledgerReaderTx satisfies store.LedgerReaderTx over the request's read view -// (the serving wrapper owns and releases it — Done does not). GetLedger serves -// getTransactions' ascending, contiguous per-ledger walk by pulling from a -// single ScanLedgers iterator primed on the first call; GetLedgerRange and -// BatchGetLedgers read through the same view but never touch that iterator. +// (the serving wrapper owns and releases it — Done does not). GetLedger and +// WithLedgerRaw serve getTransactions' ascending, contiguous per-ledger walk +// by pulling from a single ScanLedgers iterator primed on the first call — +// one iterator between them, see walk; GetLedgerRange and BatchGetLedgers +// read through the same view but never touch that iterator. type ledgerReaderTx struct { view *query.ReadView // next/stop are the pull ends of the walk iterator; nil until the first - // GetLedger primes them. + // walk step primes them. next func() (ledger.Entry, error, bool) stop func() } @@ -161,6 +162,19 @@ func (tx *ledgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) (x return xdr.LedgerCloseMetaView(bytes.Clone(entry.Bytes)), true, nil } +// WithLedgerRaw is GetLedger without the decode or the clone: it lends the +// step's bytes straight from the chunk reader's scratch buffer, which the +// next step overwrites — fn must not retain them. +func (tx *ledgerReaderTx) WithLedgerRaw( + ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, +) (bool, error) { + entry, found, err := tx.walk(ctx, sequence) + if err != nil || !found { + return found, err + } + return true, fn(entry.Bytes) +} + func (tx *ledgerReaderTx) GetLedgerRange(_ context.Context) (store.LedgerRange, error) { return getLedgerRange(tx.view) } @@ -261,7 +275,7 @@ func (tx *ledgerReaderTx) walk(ctx context.Context, sequence uint32) (ledger.Ent // The walk contract (ascending, contiguous from the priming sequence) // was broken. Fail loudly rather than serve the wrong ledger's data. return ledger.Entry{}, false, fmt.Errorf( - "adapters: non-sequential GetLedger: asked for ledger %d, the walk is at ledger %d", + "adapters: non-sequential ledger walk: asked for ledger %d, the walk is at ledger %d", sequence, entry.Seq) } return entry, true, nil diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go index 70f9ef6d2..81cd519c5 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go @@ -519,6 +519,53 @@ func TestWithLedgerRaw_MissDoesNotRunFn(t *testing.T) { } } +func TestTxWithLedgerRaw_LendsTheSameBytesGetLedgerDecodes(t *testing.T) { + ctx, reader, c0, _ := sparseFixture(t) + + decodeTx, err := reader.NewTx(ctx) + require.NoError(t, err) + defer func() { _ = decodeTx.Done() }() + rawTx, err := reader.NewTx(ctx) + require.NoError(t, err) + defer func() { _ = rawTx.Done() }() + + for seq := c0.FirstLedger(); seq <= c0.FirstLedger()+3; seq++ { + lcm, ok, err := decodeTx.GetLedger(context.Background(), seq) + require.NoError(t, err) + require.True(t, ok, "ledger %d", seq) + want, err := lcm.MarshalBinary() + require.NoError(t, err) + + var got []byte + found, err := rawTx.WithLedgerRaw(context.Background(), seq, func(raw []byte) error { + // The loan forbids retaining raw, so clone inside fn. + got = bytes.Clone(raw) + return nil + }) + require.NoError(t, err) + require.True(t, found, "ledger %d", seq) + assert.Equal(t, want, got, "ledger %d", seq) + } +} + +func TestTxWithLedgerRaw_MissDoesNotRunFn(t *testing.T) { + ctx, reader, c0, c1 := sparseFixture(t) + tx, err := reader.NewTx(ctx) + require.NoError(t, err) + defer func() { _ = tx.Done() }() + + for _, seq := range []uint32{0, 1, c0.FirstLedger() - 1, c1.FirstLedger() + 1} { + ran := false + found, err := tx.WithLedgerRaw(context.Background(), seq, func([]byte) error { + ran = true + return nil + }) + assert.NoError(t, err) + assert.False(t, found, "ledger %d", seq) + assert.False(t, ran, "fn must not run for an absent ledger %d", seq) + } +} + func TestWithLedgerRaw_CallbackErrorSurfacesAsFound(t *testing.T) { ctx, reader, c0, _ := sparseFixture(t) boom := errors.New("boom") @@ -527,3 +574,59 @@ func TestWithLedgerRaw_CallbackErrorSurfacesAsFound(t *testing.T) { assert.ErrorIs(t, err, boom) assert.True(t, found) } + +func TestTxWithLedgerRaw_CallbackErrorSurfacesAsFound(t *testing.T) { + ctx, reader, c0, _ := sparseFixture(t) + tx, err := reader.NewTx(ctx) + require.NoError(t, err) + defer func() { _ = tx.Done() }() + + boom := errors.New("boom") + // found stays true: the ledger WAS there, the caller's own callback failed. + found, err := tx.WithLedgerRaw(context.Background(), c0.FirstLedger(), + func([]byte) error { return boom }) + assert.ErrorIs(t, err, boom) + assert.True(t, found) +} + +func TestTxWithLedgerRaw_SharesTheWalkWithGetLedger(t *testing.T) { + ctx, reader, c0, _ := sparseFixture(t) + tx, err := reader.NewTx(ctx) + require.NoError(t, err) + defer func() { _ = tx.Done() }() + + _, ok, err := tx.GetLedger(context.Background(), c0.FirstLedger()) + require.NoError(t, err) + require.True(t, ok) + + // One cursor: the raw accessor picks up where GetLedger left off, and + // skipping ahead breaks the same contract just as loudly. + found, err := tx.WithLedgerRaw(context.Background(), c0.FirstLedger()+1, + func([]byte) error { return nil }) + require.NoError(t, err) + require.True(t, found) + + _, err = tx.WithLedgerRaw(context.Background(), c0.FirstLedger()+3, + func([]byte) error { return nil }) + assert.ErrorContains(t, err, "non-sequential") +} + +func TestTxWithLedgerRaw_StopsOnCanceledContext(t *testing.T) { + baseCtx, reader, c0, _ := sparseFixture(t) + tx, err := reader.NewTx(baseCtx) + require.NoError(t, err) + defer func() { _ = tx.Done() }() + + ctx, cancel := context.WithCancel(baseCtx) + _, err = tx.WithLedgerRaw(ctx, c0.FirstLedger(), func([]byte) error { return nil }) + require.NoError(t, err) + + cancel() + ran := false + _, err = tx.WithLedgerRaw(ctx, c0.FirstLedger()+1, func([]byte) error { + ran = true + return nil + }) + require.ErrorIs(t, err, context.Canceled) + assert.False(t, ran) +} diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index 08fba014b..390019231 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -50,14 +50,21 @@ type WithLedgerRawFn func(raw []byte) error // LedgerReaderTx is a read-only snapshot of the ledger store. Call Done to // release it. // -// GetLedger and GetLedgerView are one walk, not free-form point reads. Call -// them with ascending, contiguous sequences, starting from the first call's -// sequence. Read at most methods.LedgerScanLimit ledgers per Tx. The v1 (SQL) -// backend accepts any pattern, while the v2 backend only walks from a forward +// GetLedger, GetLedgerView, and WithLedgerRaw are one walk, not free-form +// point reads. Call them with ascending, contiguous sequences, starting from +// the first call's sequence, and use only one of them per Tx: they share a +// single cursor, so interleaving them consumes positions from each other. +// Read at most methods.LedgerScanLimit ledgers per Tx. The v1 (SQL) backend +// accepts any pattern, while the v2 backend only walks from a forward // iterator primed on the first call. +// +// WithLedgerRaw is GetLedger without the decode: fn borrows the marshaled +// LCM under WithLedgerRawFn's loan terms. found=false means fn never ran; +// fn's own error comes back verbatim with found=true. type LedgerReaderTx interface { GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) + WithLedgerRaw(ctx context.Context, sequence uint32, fn WithLedgerRawFn) (bool, error) GetLedgerRange(ctx context.Context) (LedgerRange, error) BatchGetLedgers(ctx context.Context, start uint32, end uint32) ([]LedgerMetadataChunk, error) Done() error From cdbf2e73bb044a8cb4358b920bc28f4a0cb1503d Mon Sep 17 00:00:00 2001 From: Tamir Sen Date: Mon, 31 Aug 2026 00:43:23 +0000 Subject: [PATCH 10/35] store: promote the view-to-Transaction reshape out of the v2 adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adapters.transactionFromView turns an ingest.LedgerTransactionView into the store.Transaction the serving handlers format. The shared getTransactions handler is about to need exactly that reshape, and a second copy of it in internal/methods would be a copy that can silently disagree with this one. Move it to internal/store as TransactionFromView, next to ParseTransaction — its parsed-path parallel — and have the adapter call it. Behavior is unchanged: same fields, same aliasing, same caller-side rule about only handing it a view whose bytes were already copied out (that note moves to the adapter's call site, which is where the guarantee actually holds). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Y7opQUW9UZPKzE6B3tz9b5 (cherry picked from commit ff21f43094c2fecca15cd6e0de92bf0611eb6821) --- .../rpcv2/adapters/transaction_reader.go | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go index 1929384d5..03b591e66 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/rpcv2/observability" @@ -51,26 +50,8 @@ func (r *TransactionReader) GetTransaction(ctx context.Context, hash xdr.Hash) ( if !found { return store.Transaction{}, store.ErrNoTransaction } - return transactionFromView(txv), nil -} - -// transactionFromView reshapes the SDK's view into the serving contract, -// aliasing its byte fields. -// -// Only a compactView-produced view may be passed: the type does not carry that, -// and a view straight off the SDK still aliases the ledger it was read from. -func transactionFromView(txv ingest.LedgerTransactionView) store.Transaction { - return store.Transaction{ - TransactionHash: xdr.Hash(txv.Hash).HexString(), - Result: txv.Result, - Meta: txv.Meta, - Envelope: txv.Envelope, - Events: txv.DiagnosticEvents, - FeeBump: txv.FeeBump, - ApplicationOrder: txv.ApplicationOrder, - Successful: txv.Successful, - Ledger: store.LedgerInfo{Sequence: txv.LedgerSequence, CloseTime: txv.LedgerCloseTime}, - TransactionEvents: txv.TransactionEvents, - ContractEvents: txv.ContractEvents, - } + // Only a compactView-produced view may be reshaped here: the type does not + // carry that, and a view straight off the SDK still aliases the ledger it + // was read from — probe.GetTransaction guarantees it (see compactView). + return store.ParseTransaction(txv), nil } From 84566e7f766223a2cb5db1c4e43966d326e41f2e Mon Sep 17 00:00:00 2001 From: Tamir Sen Date: Mon, 31 Aug 2026 01:02:39 +0000 Subject: [PATCH 11/35] methods: walk getTransactions' page over raw ledger bytes, not a decoded LCM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A limit=50 getTransactions page costs ~100ms. Profiling 40 of them attributes 1.83s cum to xdr.LedgerEntryChanges.DecodeFrom and ~2.9s to GC drain/scan on top of it, and neither is work the response needs: the page loop fully XDR-unmarshaled every ledger it touched — LedgerEntryChanges, bucket entries, the lot — to read a handful of per-transaction fields back out and base64 them straight to the client. Read the ledger through the SDK's zero-copy views instead. Each ledger is now borrowed as raw bytes via the new store.LedgerReaderTx.WithLedgerRaw, and ingest.LedgerTransactionViewRange walks its TxProcessing to materialize only the page's worth of transactions — nothing is unmarshaled, and the walk stops at the page limit rather than at the ledger's end. store.TransactionFromView reshapes each one into the store.Transaction the response renderer already takes, so the whole formatting half of the handler is untouched (it moves into transactionInfo, shared verbatim with the differential's reference below). The same precedent already serves getTransaction by hash in the v2 tree (rpcv2/stores/txhash). One field does not survive the move unaided. For a TransactionMeta V3 whose envelope declares Soroban data, the parsed reader always reports exactly one operation slice, even when the meta carries no SorobanMeta at all (GetTransactionEvents: OperationEvents = make([][]xdr.ContractEvent, 1)), while the view extractor leaves it empty — so contractEventsXdr would come back as [] where it used to be [[]]. stellar-core emits exactly that shape for a Soroban transaction charged but never executed, so it is reachable on real protocol 20-22 history. repairV3OperationArity restores it with one union-discriminant read of the meta plus, only for a V3 meta that came back with no operations, one view walk of the envelope for the Soroban flag the SDK computes internally and does not expose. Still no decode. Everything else — envelope, result, meta, diagnostic/transaction/contract events, hash, apply order, fee-bump flag, ledger sequence and close time — comes off the views byte for byte. The correctness argument is the differential: the pre-change extraction is reconstructed in get_transactions_differential_test.go and both paths are run over one sqlite corpus that sweeps LCM V1/V2, TransactionMeta V1/V3/V4, classic and Soroban and fee-bump envelopes, present/absent/empty SorobanMeta, per-operation and top-level and diagnostic events, empty ledgers, five-tx ledgers, both response formats, page limits that land mid-ledger and on boundaries, explicit cursors, and a full cursor round-trip where each path drives its own cursor chain. Every comparison is json.Marshal byte equality of the whole GetTransactionsResponse (not JSONEq — a reordered or vanished field must fail). ~410 subtests; a corpus-vacuity guard and a renderer field-mapping pin keep them from passing for the wrong reason. Every pre-existing getTransactions test passes unmodified; the only edit to one was giving the sparseLedgerReader stub the new interface method. Behavior note: an unreadable ledger's extraction error is now InternalError throughout. Before, a failure inside the transaction reader (only reachable on a corrupt stored ledger) surfaced as InvalidParams while the same corruption caught by the reader's constructor surfaced as InternalError; the view path cannot tell those apart, and a corrupt stored ledger is not the client's fault. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Y7opQUW9UZPKzE6B3tz9b5 (cherry picked from commit 2042a38959657ff7974a6084b74bdea27d5b88a4) [cherry-pick resolution: repairV3OperationArity moved from methods into store.ParseTransaction so the v1 and v2 by-hash view reads are repaired too, not only the pager; TransactionFromView folded into the existing view-shaped store.ParseTransaction; the differential's legacy reference extraction is frozen test-locally as legacyParseTransaction since the decode-based store.ParseTransaction no longer exists on this branch.] --- .../internal/methods/get_transactions.go | 188 ++-- .../get_transactions_differential_test.go | 953 ++++++++++++++++++ .../internal/rpcv1/sqlitedb/mocks.go | 2 +- .../internal/rpcv1/sqlitedb/transaction.go | 2 +- .../rpcv2/adapters/transaction_reader.go | 2 +- cmd/stellar-rpc/internal/store/transaction.go | 56 +- 6 files changed, 1123 insertions(+), 80 deletions(-) create mode 100644 cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go diff --git a/cmd/stellar-rpc/internal/methods/get_transactions.go b/cmd/stellar-rpc/internal/methods/get_transactions.go index 7103e05bf..7a2d93949 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions.go @@ -72,29 +72,43 @@ func (h transactionsRPCHandler) initializePagination( return *start, requestCursor, limit, nil } -// fetchLedgerViewData calls the meta table to fetch the corresponding ledger data. -func (h transactionsRPCHandler) fetchLedgerViewData(ctx context.Context, ledgerSeq uint32, - readTx store.LedgerReaderTx, -) (xdr.LedgerCloseMetaView, error) { - ledgerView, found, err := readTx.GetLedgerView(ctx, ledgerSeq) - if err != nil { - return ledgerView, &jrpc2.Error{ +// readLedgerPage borrows ledgerSeq's raw LedgerCloseMeta and runs the page +// extraction inside the loan. Extraction errors are already *jrpc2.Error and +// pass through unchanged; only the read's own outcomes are classified here. +func (h transactionsRPCHandler) readLedgerPage( + ctx context.Context, ledgerSeq uint32, readTx store.LedgerReaderTx, + start toid.ID, txns *[]protocol.TransactionInfo, limit uint, format string, +) (*toid.ID, bool, error) { + var cursor *toid.ID + var done bool + var procErr error + found, err := readTx.WithLedgerRaw(ctx, ledgerSeq, func(raw []byte) error { + cursor, done, procErr = h.processTransactionsInLedger(raw, start, txns, limit, format) + return procErr + }) + switch { + case procErr != nil: + return nil, false, procErr + case err != nil: + return nil, false, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } - } else if !found { - return ledgerView, &jrpc2.Error{ + case !found: + return nil, false, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: fmt.Sprintf("database does not contain metadata for ledger: %d", ledgerSeq), } } - return ledgerView, nil + return cursor, done, nil } -// processTransactionsInLedgerView cycles through all the transactions in a ledger, extracts the transaction info -// and builds the list of transactions. -func (h transactionsRPCHandler) processTransactionsInLedgerView( - ledger xdr.LedgerCloseMetaView, start toid.ID, +// processTransactionsInLedger extracts the page's worth of transactions from +// raw — a marshaled LedgerCloseMeta on loan from WithLedgerRaw — through the +// SDK's zero-copy views. Every byte field below aliases raw until it is +// base64- or JSON-encoded into txns; nothing that aliases raw outlives this call. +func (h transactionsRPCHandler) processTransactionsInLedger( + raw []byte, start toid.ID, txns *[]protocol.TransactionInfo, limit uint, format string, ) (*toid.ID, bool, error) { @@ -102,88 +116,116 @@ func (h transactionsRPCHandler) processTransactionsInLedgerView( if err != nil { return nil, false, &jrpc2.Error{Code: jrpc2.InvalidParams, Message: err.Error()} } - ledgerSeq, err := ledger.LedgerSequence() + lcmView := xdr.LedgerCloseMetaView(raw) + ledgerSeq, err := lcmView.LedgerSequence() + if err != nil { + return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} + } + ledgerSeqInt32, err := uint32ToInt32(ledgerSeq, "ledger sequence") if err != nil { return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} } - ledgerSeqInt32 := int32(ledgerSeq) //nolint:gosec // safe until ledger seq exceeds 2147483647 // The cursor's tx-order offset only applies within the cursor's own ledger. startTxIdx := 1 if ledgerSeqInt32 == start.LedgerSequence { startTxIdx = int(start.TransactionOrder) } + cursor := toid.New(ledgerSeqInt32, 0, 1) remaining := limitInt - len(*txns) - views, err := ingest.LedgerTransactionViewRange(ledger, startTxIdx-1, remaining, h.networkPassphrase) - if err != nil { - return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} + if remaining <= 0 { + return cursor, true, nil } - cursor := toid.New(ledgerSeqInt32, 0, 1) - for _, v := range views { - tx := store.ParseTransaction(v) - cursor.TransactionOrder = tx.ApplicationOrder - txInfo := protocol.TransactionInfo{ - TransactionDetails: protocol.TransactionDetails{ - TransactionHash: tx.TransactionHash, - ApplicationOrder: tx.ApplicationOrder, - FeeBump: tx.FeeBump, - Ledger: tx.Ledger.Sequence, - }, - LedgerCloseTime: tx.Ledger.CloseTime, + + // One walk of this ledger's TxProcessing, materializing only the page's + // worth of transactions. Apply indices are 0-based here; application order + // (and the cursor) is 1-based. A startTxIdx past the last transaction + // yields nothing, which is how a spent cursor lands on an empty page. + txViews, err := ingest.LedgerTransactionViewRange(lcmView, startTxIdx-1, remaining, h.networkPassphrase) + if err != nil { + return nil, false, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: err.Error(), } + } - switch format { - case protocol.FormatJSON: - result, envelope, meta, convErr := transactionToJSON(tx) - if convErr != nil { - return nil, false, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: convErr.Error(), - } + for i, txView := range txViews { + cursor.TransactionOrder = int32(startTxIdx + i) + + tx, perr := store.ParseTransaction(txView) + if perr != nil { + return nil, false, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: perr.Error(), } + } - diagEvents, convErr := jsonifySlice(xdr.DiagnosticEvent{}, tx.Events) - if convErr != nil { - return nil, false, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: convErr.Error(), - } + txInfo, ferr := transactionInfo(tx, format) + if ferr != nil { + return nil, false, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: ferr.Error(), } + } - txInfo.ResultJSON = result - txInfo.ResultMetaJSON = meta - txInfo.EnvelopeJSON = envelope - txInfo.DiagnosticEventsJSON = diagEvents + *txns = append(*txns, txInfo) + if len(*txns) >= limitInt { + return cursor, true, nil + } + } - txInfo.Events, convErr = BuildEventsJSONFromTransaction(tx) - if convErr != nil { - return nil, false, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: convErr.Error(), - } - } + return cursor, false, nil +} - default: - txInfo.ResultXDR = base64.StdEncoding.EncodeToString(tx.Result) - txInfo.ResultMetaXDR = base64.StdEncoding.EncodeToString(tx.Meta) - txInfo.EnvelopeXDR = base64.StdEncoding.EncodeToString(tx.Envelope) - txInfo.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) +// transactionInfo renders one extracted transaction as a page entry. Pure +// formatting — the differential test drives both extractions through it. +func transactionInfo(tx store.Transaction, format string) (protocol.TransactionInfo, error) { + txInfo := protocol.TransactionInfo{ + TransactionDetails: protocol.TransactionDetails{ + TransactionHash: tx.TransactionHash, + ApplicationOrder: tx.ApplicationOrder, + FeeBump: tx.FeeBump, + Ledger: tx.Ledger.Sequence, + }, + LedgerCloseTime: tx.Ledger.CloseTime, + } - txInfo.Events = BuildEventsXDRFromTransaction(tx) + switch format { + case protocol.FormatJSON: + result, envelope, meta, err := transactionToJSON(tx) + if err != nil { + return txInfo, err } - txInfo.Status = protocol.TransactionStatusFailed - if tx.Successful { - txInfo.Status = protocol.TransactionStatusSuccess + diagEvents, err := jsonifySlice(xdr.DiagnosticEvent{}, tx.Events) + if err != nil { + return txInfo, err } - *txns = append(*txns, txInfo) - if len(*txns) >= limitInt { - return cursor, true, nil + txInfo.ResultJSON = result + txInfo.ResultMetaJSON = meta + txInfo.EnvelopeJSON = envelope + txInfo.DiagnosticEventsJSON = diagEvents + + txInfo.Events, err = BuildEventsJSONFromTransaction(tx) + if err != nil { + return txInfo, err } + + default: + txInfo.ResultXDR = base64.StdEncoding.EncodeToString(tx.Result) + txInfo.ResultMetaXDR = base64.StdEncoding.EncodeToString(tx.Meta) + txInfo.EnvelopeXDR = base64.StdEncoding.EncodeToString(tx.Envelope) + txInfo.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) + + txInfo.Events = BuildEventsXDRFromTransaction(tx) } - return cursor, false, nil + txInfo.Status = protocol.TransactionStatusFailed + if tx.Successful { + txInfo.Status = protocol.TransactionStatusSuccess + } + return txInfo, nil } // getTransactionsByLedgerSequence fetches transactions between the start and end ledgers, inclusive of both. @@ -239,12 +281,8 @@ func (h transactionsRPCHandler) getTransactionsByLedgerSequence(ctx context.Cont Message: "cursor ledger sequence cannot be negative", } } - ledgerView, err := h.fetchLedgerViewData(ctx, uint32(ledgerSeq), readTx) - if err != nil { - return protocol.GetTransactionsResponse{}, err - } - - cursor, done, err = h.processTransactionsInLedgerView(ledgerView, start, &txns, limit, request.Format) + cursor, done, err = h.readLedgerPage( + ctx, uint32(ledgerSeq), readTx, start, &txns, limit, request.Format) if err != nil { return protocol.GetTransactionsResponse{}, err } diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go new file mode 100644 index 000000000..65e1f6fa5 --- /dev/null +++ b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go @@ -0,0 +1,953 @@ +package methods + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "strconv" + "testing" + + "github.com/creachadair/jrpc2" + "github.com/stretchr/testify/require" + + "github.com/stellar/go-stellar-sdk/ingest" + "github.com/stellar/go-stellar-sdk/network" + protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" + "github.com/stellar/go-stellar-sdk/support/log" + "github.com/stellar/go-stellar-sdk/toid" + "github.com/stellar/go-stellar-sdk/xdr" + + "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/host" + "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/rpcv1/sqlitedb" + "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/store" + "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" +) + +// This file is the correctness harness for the getTransactions page loop's +// move from "unmarshal the whole LedgerCloseMeta, then range over it" to "walk +// the raw bytes through the SDK's zero-copy views". +// +// legacyGetTransactionsByLedgerSequence and legacyProcessTransactionsInLedger +// below are the decode-based extraction as it stood before that change, +// reconstructed here rather than left behind in the production file: they +// exist only as the differential's reference, and the tests assert that the +// two paths' protocol.GetTransactionsResponse values serialize to byte- +// identical JSON over a corpus that sweeps meta versions, envelope shapes, +// event shapes, page boundaries, empty ledgers and cursor round-trips. + +// legacyGetTransactionsByLedgerSequence is the pre-view-walk pagination loop: +// same handler, same cursor math, but reading each ledger through +// LedgerReaderTx.GetLedger's decoded xdr.LedgerCloseMeta. +func legacyGetTransactionsByLedgerSequence( + ctx context.Context, h transactionsRPCHandler, request protocol.GetTransactionsRequest, +) (protocol.GetTransactionsResponse, error) { + readTx, err := h.ledgerReader.NewTx(ctx) + if err != nil { + return protocol.GetTransactionsResponse{}, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} + } + defer func() { _ = readTx.Done() }() + + ledgerRange, err := readTx.GetLedgerRange(ctx) + if err != nil { + return protocol.GetTransactionsResponse{}, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} + } + + if err := request.IsValid(h.maxLimit, ledgerRange.ToLedgerSeqRange()); err != nil { + return protocol.GetTransactionsResponse{}, &jrpc2.Error{Code: jrpc2.InvalidRequest, Message: err.Error()} + } + + start, requestCursor, limit, err := h.initializePagination(request) + if err != nil { + return protocol.GetTransactionsResponse{}, err + } + + txns := make([]protocol.TransactionInfo, 0, limit) + var done bool + cursor := toid.New(0, 0, 0) + endLedger := min(int64(ledgerRange.LastLedger.Sequence), int64(start.LedgerSequence)+LedgerScanLimit-1) + for ledgerSeq := start.LedgerSequence; int64(ledgerSeq) <= endLedger; ledgerSeq++ { + if ledgerSeq < 0 { + return protocol.GetTransactionsResponse{}, &jrpc2.Error{ + Code: jrpc2.InvalidParams, + Message: "cursor ledger sequence cannot be negative", + } + } + ledger, found, gerr := readTx.GetLedger(ctx, uint32(ledgerSeq)) + if gerr != nil { + return protocol.GetTransactionsResponse{}, &jrpc2.Error{Code: jrpc2.InternalError, Message: gerr.Error()} + } else if !found { + return protocol.GetTransactionsResponse{}, &jrpc2.Error{ + Code: jrpc2.InvalidParams, + Message: fmt.Sprintf("database does not contain metadata for ledger: %d", ledgerSeq), + } + } + + cursor, done, err = legacyProcessTransactionsInLedger(h, ledger, start, &txns, limit, request.Format) + if err != nil { + return protocol.GetTransactionsResponse{}, err + } + if done { + break + } + } + + if requestCursor != nil && cursor.ToInt64() < requestCursor.ToInt64() { + cursor = requestCursor + } + + return protocol.GetTransactionsResponse{ + Transactions: txns, + LatestLedger: ledgerRange.LastLedger.Sequence, + LatestLedgerCloseTime: ledgerRange.LastLedger.CloseTime, + OldestLedger: ledgerRange.FirstLedger.Sequence, + OldestLedgerCloseTime: ledgerRange.FirstLedger.CloseTime, + Cursor: cursor.String(), + }, nil +} + +// legacyProcessTransactionsInLedger is the pre-view-walk per-ledger extraction: +// ingest.NewLedgerTransactionReaderFromLedgerCloseMeta over a decoded ledger, +// then store.ParseTransaction per transaction. Everything downstream of that — +// rendering a store.Transaction into a page entry — is the production +// renderer, unchanged by this migration and shared here deliberately. +func legacyProcessTransactionsInLedger( + h transactionsRPCHandler, ledger xdr.LedgerCloseMeta, start toid.ID, + txns *[]protocol.TransactionInfo, limit uint, format string, +) (*toid.ID, bool, error) { + limitInt, err := strconv.Atoi(strconv.FormatUint(uint64(limit), 10)) + if err != nil { + return nil, false, &jrpc2.Error{Code: jrpc2.InvalidParams, Message: err.Error()} + } + + reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(h.networkPassphrase, ledger) + if err != nil { + return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} + } + + startTxIdx := 1 + ledgerSeq := ledger.LedgerSequence() + ledgerSeqInt32, err := uint32ToInt32(ledgerSeq, "ledger sequence") + if err != nil { + return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} + } + if ledgerSeqInt32 == start.LedgerSequence { + startTxIdx = int(start.TransactionOrder) + if ierr := reader.Seek(startTxIdx - 1); ierr != nil && !errors.Is(ierr, io.EOF) { + return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: ierr.Error()} + } + } + + txCount := ledger.CountTransactions() + cursor := toid.New(ledgerSeqInt32, 0, 1) + for i := startTxIdx; i <= txCount; i++ { + cursor.TransactionOrder = int32(i) + + ingestTx, rerr := reader.Read() + if rerr != nil { + if errors.Is(rerr, io.EOF) { + break + } + return nil, false, &jrpc2.Error{Code: jrpc2.InvalidParams, Message: rerr.Error()} + } + + tx, perr := legacyParseTransaction(ledger, ingestTx) + if perr != nil { + return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: perr.Error()} + } + + // The renderer is shared with the production path on purpose: it is + // pure formatting of a store.Transaction and this change does not + // touch it, so what the differential compares is the EXTRACTION that + // produced tx, not the rendering of it. + txInfo, ferr := transactionInfo(tx, format) + if ferr != nil { + return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: ferr.Error()} + } + + *txns = append(*txns, txInfo) + if len(*txns) >= limitInt { + return cursor, true, nil + } + } + + return cursor, false, nil +} + +// legacyParseTransaction is the decode-based store.ParseTransaction as it +// stood before the view migration, frozen here as the reference extraction. +func legacyParseTransaction(lcm xdr.LedgerCloseMeta, ingestTx ingest.LedgerTransaction) (store.Transaction, error) { + var tx store.Transaction + var err error + + tx.FeeBump = ingestTx.Envelope.IsFeeBump() + tx.ApplicationOrder = int32(ingestTx.Index) //nolint:gosec // corpus indices are tiny + tx.Successful = ingestTx.Result.Successful() + tx.Ledger = store.LedgerInfo{Sequence: lcm.LedgerSequence(), CloseTime: lcm.LedgerCloseTime()} + tx.TransactionHash = ingestTx.Result.TransactionHash.HexString() + + if tx.Result, err = ingestTx.Result.Result.MarshalBinary(); err != nil { + return tx, err + } + if tx.Meta, err = ingestTx.UnsafeMeta.MarshalBinary(); err != nil { + return tx, err + } + if tx.Envelope, err = ingestTx.Envelope.MarshalBinary(); err != nil { + return tx, err + } + + allEvents, err := ingestTx.GetTransactionEvents() + if err != nil { + return tx, err + } + diagEvents, err := ingestTx.GetDiagnosticEvents() + if err != nil { + return tx, err + } + tx.Events = make([][]byte, 0, len(diagEvents)) + for _, event := range diagEvents { + raw, ierr := event.MarshalBinary() + if ierr != nil { + return tx, ierr + } + tx.Events = append(tx.Events, raw) + } + tx.TransactionEvents = make([][]byte, 0, len(allEvents.TransactionEvents)) + for _, event := range allEvents.TransactionEvents { + raw, ierr := event.MarshalBinary() + if ierr != nil { + return tx, ierr + } + tx.TransactionEvents = append(tx.TransactionEvents, raw) + } + tx.ContractEvents = make([][][]byte, 0, len(allEvents.OperationEvents)) + for _, opEvents := range allEvents.OperationEvents { + events := make([][]byte, 0, len(opEvents)) + for _, event := range opEvents { + raw, ierr := event.MarshalBinary() + if ierr != nil { + return tx, ierr + } + events = append(events, raw) + } + tx.ContractEvents = append(tx.ContractEvents, events) + } + return tx, nil +} + +// +// ---- the corpus ---- +// + +// diffTxSpec is one transaction in a corpus ledger: an envelope shape paired +// with an apply-processing meta. +type diffTxSpec struct { + envelope xdr.TransactionEnvelope + meta xdr.TransactionMeta + succeeds bool +} + +func diffSymbolVal() xdr.ScVal { + sym := xdr.ScSymbol("COUNTER") + return xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &sym} +} + +func diffContractEvent() xdr.ContractEvent { + val := diffSymbolVal() + id := xdr.ContractId{7} + return xdr.ContractEvent{ + ContractId: &id, + Type: xdr.ContractEventTypeContract, + Body: xdr.ContractEventBody{ + V: 0, + V0: &xdr.ContractEventV0{Topics: []xdr.ScVal{val}, Data: val}, + }, + } +} + +// diffClassicEnvelope is a plain (non-Soroban) v1 envelope: Tx.Ext stays at +// discriminant 0, which is what makes it classic. +func diffClassicEnvelope(acctSeq uint32) xdr.TransactionEnvelope { + env, err := xdr.NewTransactionEnvelope(xdr.EnvelopeTypeEnvelopeTypeTx, xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + Fee: 1, + SeqNum: xdr.SequenceNumber(acctSeq), + SourceAccount: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), + }, + }) + if err != nil { + panic(err) + } + return env +} + +// diffFeeBumpEnvelope wraps inner in a fee bump. +func diffFeeBumpEnvelope(inner xdr.TransactionEnvelope) xdr.TransactionEnvelope { + return xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTxFeeBump, + FeeBump: &xdr.FeeBumpTransactionEnvelope{ + Tx: xdr.FeeBumpTransaction{ + FeeSource: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), + Fee: 200, + InnerTx: xdr.FeeBumpTransactionInnerTx{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: inner.V1, + }, + }, + }, + } +} + +func diffMetaV1() xdr.TransactionMeta { + return xdr.TransactionMeta{V: 1, V1: &xdr.TransactionMetaV1{Operations: []xdr.OperationMeta{}}} +} + +// diffMetaV3NoSoroban is the straggler corner: a V3 meta with no SorobanMeta +// at all. Paired with a Soroban envelope it is the one shape where the SDK's +// view extractor and the parsed reader disagree on operation-slice arity, so +// the corpus pins it deliberately (see repairV3OperationArity). +func diffMetaV3NoSoroban() xdr.TransactionMeta { + return xdr.TransactionMeta{V: 3, Operations: &[]xdr.OperationMeta{}, V3: &xdr.TransactionMetaV3{}} +} + +func diffMetaV3WithEvents(events []xdr.ContractEvent, diags []xdr.DiagnosticEvent) xdr.TransactionMeta { + return xdr.TransactionMeta{ + V: 3, + Operations: &[]xdr.OperationMeta{}, + V3: &xdr.TransactionMetaV3{SorobanMeta: &xdr.SorobanTransactionMeta{ + Events: events, + DiagnosticEvents: diags, + ReturnValue: diffSymbolVal(), + }}, + } +} + +func diffMetaV4(ops []xdr.OperationMetaV2, txEvents []xdr.TransactionEvent, + diags []xdr.DiagnosticEvent, +) xdr.TransactionMeta { + return xdr.TransactionMeta{V: 4, V4: &xdr.TransactionMetaV4{ + Operations: ops, + Events: txEvents, + DiagnosticEvents: diags, + }} +} + +// diffResultFor builds the TransactionResultPair for spec: a fee bump carries +// an inner result pair, a plain transaction does not. +func diffResultFor(t *testing.T, spec diffTxSpec) xdr.TransactionResultPair { + t.Helper() + hash, err := network.HashTransactionInEnvelope(spec.envelope, NetworkPassphrase) + require.NoError(t, err) + + code := xdr.TransactionResultCodeTxSuccess + if !spec.succeeds { + code = xdr.TransactionResultCodeTxBadSeq + } + opResults := []xdr.OperationResult{} + res := xdr.TransactionResultResult{Code: code, Results: &opResults} + + if spec.envelope.Type == xdr.EnvelopeTypeEnvelopeTypeTxFeeBump { + innerHash, ierr := network.HashTransactionInEnvelope(xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, V1: spec.envelope.FeeBump.Tx.InnerTx.V1, + }, NetworkPassphrase) + require.NoError(t, ierr) + outer := xdr.TransactionResultCodeTxFeeBumpInnerSuccess + if !spec.succeeds { + outer = xdr.TransactionResultCodeTxFeeBumpInnerFailed + } + res = xdr.TransactionResultResult{ + Code: outer, + InnerResultPair: &xdr.InnerTransactionResultPair{ + TransactionHash: innerHash, + Result: xdr.InnerTransactionResult{ + FeeCharged: 100, + Result: xdr.InnerTransactionResultResult{Code: code, Results: &opResults}, + }, + }, + } + } + + return xdr.TransactionResultPair{ + TransactionHash: hash, + Result: xdr.TransactionResult{FeeCharged: 100, Result: res}, + } +} + +// diffLCM assembles a LedgerCloseMeta of the given wire version (1 or 2) at +// sequence seq holding specs. Both versions matter: their TxProcessing arrays +// are different element types, which the view dispatcher walks with different +// code. +func diffLCM(t *testing.T, version int32, seq uint32, specs ...diffTxSpec) xdr.LedgerCloseMeta { + t.Helper() + envs := make([]xdr.TransactionEnvelope, 0, len(specs)) + for _, spec := range specs { + envs = append(envs, spec.envelope) + } + components := []xdr.TxSetComponent{{ + Type: xdr.TxSetComponentTypeTxsetCompTxsMaybeDiscountedFee, + TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{Txs: envs}, + }} + header := xdr.LedgerHeaderHistoryEntry{Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{CloseTime: xdr.TimePoint(ledgerCloseTime(seq))}, + LedgerSeq: xdr.Uint32(seq), + }} + txSet := xdr.GeneralizedTransactionSet{V: 1, V1TxSet: &xdr.TransactionSetV1{ + PreviousLedgerHash: xdr.Hash{1}, + Phases: []xdr.TransactionPhase{{V: 0, V0Components: &components}}, + }} + + if version == 1 { + proc := make([]xdr.TransactionResultMeta, 0, len(specs)) + for _, spec := range specs { + proc = append(proc, xdr.TransactionResultMeta{ + Result: diffResultFor(t, spec), + TxApplyProcessing: spec.meta, + }) + } + return xdr.LedgerCloseMeta{V: 1, V1: &xdr.LedgerCloseMetaV1{ + LedgerHeader: header, TxSet: txSet, TxProcessing: proc, + }} + } + + proc := make([]xdr.TransactionResultMetaV1, 0, len(specs)) + for _, spec := range specs { + proc = append(proc, xdr.TransactionResultMetaV1{ + Result: diffResultFor(t, spec), + TxApplyProcessing: spec.meta, + }) + } + return xdr.LedgerCloseMeta{V: 2, V2: &xdr.LedgerCloseMetaV2{ + LedgerHeader: header, TxSet: txSet, TxProcessing: proc, + }} +} + +// differentialCorpus is the contiguous ledger run [corpusFirstLedger, +// corpusLastLedger] the differential sweeps over. +// Every ledger is a deliberate shape; the comment on each says which axis it +// covers. Account sequence numbers are unique across the corpus so no two +// transactions share an envelope hash (the TxSet is paired to TxProcessing by +// hash, so a collision would hide a mispairing). +func differentialCorpus(t *testing.T) []xdr.LedgerCloseMeta { + t.Helper() + ev := diffContractEvent() + diag := xdr.DiagnosticEvent{InSuccessfulContractCall: true, Event: ev} + failedDiag := xdr.DiagnosticEvent{InSuccessfulContractCall: false, Event: ev} + txEvent := xdr.TransactionEvent{ + Stage: xdr.TransactionEventStageTransactionEventStageAfterAllTxs, + Event: ev, + } + + return []xdr.LedgerCloseMeta{ + // 1: the shape the pre-existing tests use — one TxSet envelope, two + // TxProcessing entries sharing its hash, V3 meta with no SorobanMeta + // on a Soroban envelope (the straggler corner). + createTestLedger(101), + // 2: LCM V1, classic envelopes, V1 meta — no events anywhere. + diffLCM(t, 1, 102, + diffTxSpec{diffClassicEnvelope(200), diffMetaV1(), true}, + diffTxSpec{diffClassicEnvelope(201), diffMetaV1(), false}, + ), + // 3: an empty ledger mid-corpus — the page must walk straight past it. + createEmptyTestLedger(103), + // 4: LCM V2, Soroban envelope, V3 meta WITH SorobanMeta: contract + // events and diagnostic events both present. + diffLCM(t, 2, 104, + diffTxSpec{txEnvelope(202), diffMetaV3WithEvents( + []xdr.ContractEvent{ev, ev}, []xdr.DiagnosticEvent{diag, failedDiag}), true}, + ), + // 5: V3 SorobanMeta present but with no events at all — the "with + // SorobanMeta, empty lists" case, distinct from the absent one. + diffLCM(t, 2, 105, + diffTxSpec{txEnvelope(203), diffMetaV3WithEvents(nil, nil), true}, + ), + // 6: V4 meta — per-operation events across two operations, top-level + // transaction events, and diagnostics. + diffLCM(t, 2, 106, + diffTxSpec{diffClassicEnvelope(204), diffMetaV4( + []xdr.OperationMetaV2{{Events: []xdr.ContractEvent{ev}}, {}}, + []xdr.TransactionEvent{txEvent}, + []xdr.DiagnosticEvent{diag}, + ), true}, + diffTxSpec{diffClassicEnvelope(205), diffMetaV4(nil, nil, nil), false}, + ), + // 7: fee bumps — over a classic inner and over a Soroban inner, one + // succeeding and one failing, on V4 and V3 metas. + diffLCM(t, 2, 107, + diffTxSpec{ + diffFeeBumpEnvelope(diffClassicEnvelope(206)), + diffMetaV4([]xdr.OperationMetaV2{{Events: []xdr.ContractEvent{ev}}}, nil, nil), true, + }, + diffTxSpec{ + diffFeeBumpEnvelope(txEnvelope(207)), + diffMetaV3WithEvents([]xdr.ContractEvent{ev}, []xdr.DiagnosticEvent{diag}), false, + }, + diffTxSpec{diffFeeBumpEnvelope(txEnvelope(208)), diffMetaV3NoSoroban(), true}, + ), + // 8: five transactions in one ledger, so a limit can land anywhere + // inside it and the next page has to resume mid-ledger. + diffLCM(t, 2, 108, + diffTxSpec{diffClassicEnvelope(209), diffMetaV1(), true}, + diffTxSpec{txEnvelope(210), diffMetaV3NoSoroban(), false}, + diffTxSpec{txEnvelope(211), diffMetaV3WithEvents([]xdr.ContractEvent{ev}, nil), true}, + diffTxSpec{diffClassicEnvelope(212), diffMetaV4( + []xdr.OperationMetaV2{{Events: []xdr.ContractEvent{ev, ev}}}, nil, + []xdr.DiagnosticEvent{diag}), true}, + diffTxSpec{diffFeeBumpEnvelope(diffClassicEnvelope(213)), diffMetaV1(), false}, + ), + // 9: LCM V1 carrying a V3 meta — the older ledger envelope with a + // newer transaction meta, which real history contains. + diffLCM(t, 1, 109, + diffTxSpec{txEnvelope(214), diffMetaV3WithEvents( + []xdr.ContractEvent{ev}, []xdr.DiagnosticEvent{diag}), true}, + diffTxSpec{diffClassicEnvelope(215), diffMetaV3NoSoroban(), true}, + ), + // 10: another empty ledger, this time at the tip, so a walk that runs + // off the end of the corpus ends on one. + createEmptyTestLedger(110), + } +} + +// corpusFirstLedger / corpusLastLedger bracket differentialCorpus. The run +// starts at 101 because the shared fixture helpers (createTestLedger, +// createEmptyTestLedger) offset their sequences by 100. +const ( + corpusFirstLedger = 101 + corpusLastLedger = 110 +) + +// setupDifferentialDB writes differentialCorpus into a fresh sqlite store. +func setupDifferentialDB(t *testing.T) *sqlitedb.DB { + t.Helper() + corpus := differentialCorpus(t) + require.Len(t, corpus, corpusLastLedger-corpusFirstLedger+1) + testDB := NewTestDB(t) + daemon := host.MakeNoOpDaemon() + for i, lcm := range corpus { + require.Equal(t, uint32(corpusFirstLedger+i), lcm.LedgerSequence(), + "the corpus must be one contiguous run") + tx, err := sqlitedb.NewReadWriter(log.DefaultLogger, testDB, daemon, 100, passphrase).NewTx(t.Context()) + require.NoError(t, err) + require.NoError(t, tx.LedgerWriter().InsertLedger(lcm)) + require.NoError(t, tx.Commit(lcm, nil)) + } + return testDB +} + +func differentialHandler(testDB *sqlitedb.DB) transactionsRPCHandler { + return transactionsRPCHandler{ + ledgerReader: sqlitedb.NewLedgerReader(testDB), + maxLimit: 100, + defaultLimit: 10, + networkPassphrase: NetworkPassphrase, + } +} + +// assertSameResponse runs both extractions over the same request and asserts +// their responses serialize identically, byte for byte. +func assertSameResponse( + t *testing.T, h transactionsRPCHandler, request protocol.GetTransactionsRequest, +) protocol.GetTransactionsResponse { + t.Helper() + wantResp, wantErr := legacyGetTransactionsByLedgerSequence(context.TODO(), h, request) + gotResp, gotErr := h.getTransactionsByLedgerSequence(context.TODO(), request) + + if wantErr != nil { + require.Error(t, gotErr) + require.Equal(t, wantErr.Error(), gotErr.Error()) + return gotResp + } + require.NoError(t, gotErr) + + wantJSON, err := json.Marshal(wantResp) + require.NoError(t, err) + gotJSON, err := json.Marshal(gotResp) + require.NoError(t, err) + // Byte equality, deliberately, not require.JSONEq's semantic equality: the + // point of the differential is that the wire bytes are unchanged, and + // JSONEq would forgive a field that appeared, vanished, or reordered. + require.Equal(t, string(wantJSON), string(gotJSON)) //nolint:testifylint // see above + return gotResp +} + +// TestGetTransactions_ViewWalkMatchesParsedPath sweeps start ledgers, page +// limits and both response formats over the corpus, asserting byte-identical +// responses. The limits are chosen so pages end inside a ledger as well as on +// its boundary. +func TestGetTransactions_ViewWalkMatchesParsedPath(t *testing.T) { + testDB := setupDifferentialDB(t) + h := differentialHandler(testDB) + + formats := []string{"", protocol.FormatJSON} + limits := []uint{1, 2, 3, 4, 5, 7, 11, 100} + + for _, format := range formats { + for startLedger := corpusFirstLedger; startLedger <= corpusLastLedger; startLedger++ { + for _, limit := range limits { + name := fmt.Sprintf("format=%q/start=%d/limit=%d", format, startLedger, limit) + t.Run(name, func(t *testing.T) { + assertSameResponse(t, h, protocol.GetTransactionsRequest{ + Format: format, + StartLedger: uint32(startLedger), + Pagination: &protocol.LedgerPaginationOptions{Limit: limit}, + }) + }) + } + } + } +} + +// TestGetTransactions_ViewWalkMatchesParsedPath_DefaultPagination covers the +// requests that carry no pagination block at all, so the handler's default +// limit applies. +func TestGetTransactions_ViewWalkMatchesParsedPath_DefaultPagination(t *testing.T) { + testDB := setupDifferentialDB(t) + h := differentialHandler(testDB) + + for _, format := range []string{"", protocol.FormatJSON} { + for startLedger := corpusFirstLedger; startLedger <= corpusLastLedger; startLedger++ { + t.Run(fmt.Sprintf("format=%q/start=%d", format, startLedger), func(t *testing.T) { + assertSameResponse(t, h, protocol.GetTransactionsRequest{ + Format: format, + StartLedger: uint32(startLedger), + }) + }) + } + } +} + +// TestGetTransactions_ViewWalkCursorRoundTrip pages through the whole corpus +// with each extraction driving its OWN cursor chain, so a cursor that differed +// by one would send the two paths down diverging pages. Every page must match +// byte for byte, and both must terminate on the same page count. +func TestGetTransactions_ViewWalkCursorRoundTrip(t *testing.T) { + testDB := setupDifferentialDB(t) + h := differentialHandler(testDB) + + for _, limit := range []uint{1, 2, 3, 5} { + t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) { + request := protocol.GetTransactionsRequest{ + StartLedger: corpusFirstLedger, + Pagination: &protocol.LedgerPaginationOptions{Limit: limit}, + } + legacyReq, viewReq := request, request + + var legacyPages, viewPages int + for { + want, err := legacyGetTransactionsByLedgerSequence(context.TODO(), h, legacyReq) + require.NoError(t, err) + got, err := h.getTransactionsByLedgerSequence(context.TODO(), viewReq) + require.NoError(t, err) + + wantJSON, err := json.Marshal(want) + require.NoError(t, err) + gotJSON, err := json.Marshal(got) + require.NoError(t, err) + //nolint:testifylint // byte equality, not JSONEq's semantic equality + require.Equal(t, string(wantJSON), string(gotJSON), "page %d", legacyPages) + + legacyPages++ + viewPages++ + if len(want.Transactions) == 0 { + break + } + require.Less(t, legacyPages, 200, "paging did not terminate") + + legacyReq = protocol.GetTransactionsRequest{ + Pagination: &protocol.LedgerPaginationOptions{Cursor: want.Cursor, Limit: limit}, + } + viewReq = protocol.GetTransactionsRequest{ + Pagination: &protocol.LedgerPaginationOptions{Cursor: got.Cursor, Limit: limit}, + } + } + require.Equal(t, legacyPages, viewPages) + require.Greater(t, legacyPages, 1, "the corpus must take more than one page") + }) + } +} + +// TestGetTransactions_ViewWalkMatchesParsedPath_Cursors starts each request +// from an explicit cursor, including cursors that land mid-ledger, on a +// ledger's last transaction, past a ledger's last transaction, and past the +// tip (where the request's own cursor has to be echoed back). +func TestGetTransactions_ViewWalkMatchesParsedPath_Cursors(t *testing.T) { + testDB := setupDifferentialDB(t) + h := differentialHandler(testDB) + + for ledger := corpusFirstLedger; ledger <= corpusLastLedger; ledger++ { + for txOrder := range 7 { + for _, limit := range []uint{1, 3, 10} { + cursor := toid.New(int32(ledger), int32(txOrder), 1).String() + name := fmt.Sprintf("cursor=%d.%d/limit=%d", ledger, txOrder, limit) + t.Run(name, func(t *testing.T) { + assertSameResponse(t, h, protocol.GetTransactionsRequest{ + Pagination: &protocol.LedgerPaginationOptions{Cursor: cursor, Limit: limit}, + }) + }) + } + } + } +} + +// TestGetTransactions_ViewWalkMatchesParsedPath_EmptyLedgersOnly pins the +// all-empty corpus separately: with no transaction anywhere, the response is +// driven entirely by the cursor math, which the view walk must not have +// shifted. +func TestGetTransactions_ViewWalkMatchesParsedPath_EmptyLedgersOnly(t *testing.T) { + testDB := setupDBNoTxs(t, 5) + h := differentialHandler(testDB) + + for start := 1; start <= 5; start++ { + for _, limit := range []uint{1, 10} { + t.Run(fmt.Sprintf("start=%d/limit=%d", start, limit), func(t *testing.T) { + assertSameResponse(t, h, protocol.GetTransactionsRequest{ + StartLedger: uint32(start), + Pagination: &protocol.LedgerPaginationOptions{Limit: limit}, + }) + }) + } + } +} + +// TestGetTransactions_ViewWalkCorpusIsNotVacuous guards the differential +// itself: a corpus that silently produced no transactions, or only one shape +// of them, would make every comparison above pass for the wrong reason. +func TestGetTransactions_ViewWalkCorpusIsNotVacuous(t *testing.T) { + testDB := setupDifferentialDB(t) + h := differentialHandler(testDB) + + resp, err := h.getTransactionsByLedgerSequence(context.TODO(), protocol.GetTransactionsRequest{ + StartLedger: corpusFirstLedger, + Pagination: &protocol.LedgerPaginationOptions{Limit: 100}, + }) + require.NoError(t, err) + require.GreaterOrEqual(t, len(resp.Transactions), 15, "the corpus must carry real transactions") + + var feeBumps, failed, withContractEvents, withTxEvents, withDiagnostics int + for _, tx := range resp.Transactions { + if tx.FeeBump { + feeBumps++ + } + if tx.Status == protocol.TransactionStatusFailed { + failed++ + } + for _, op := range tx.Events.ContractEventsXDR { + if len(op) > 0 { + withContractEvents++ + break + } + } + if len(tx.Events.TransactionEventsXDR) > 0 { + withTxEvents++ + } + if len(tx.DiagnosticEventsXDR) > 0 { + withDiagnostics++ + } + } + require.Positive(t, feeBumps, "fee-bump transactions") + require.Positive(t, failed, "failed transactions") + require.Positive(t, withContractEvents, "transactions with contract events") + require.Positive(t, withTxEvents, "transactions with transaction events") + require.Positive(t, withDiagnostics, "transactions with diagnostic events") +} + +// TestRepairV3OperationArity pins the straggler repair (inside +// store.ParseTransaction) directly: it fires only for a V3 meta on a Soroban +// envelope that came back with no operations, and leaves every other shape +// exactly as the view extractor produced it. +func TestRepairV3OperationArity(t *testing.T) { + marshal := func(v interface{ MarshalBinary() ([]byte, error) }) []byte { + raw, err := v.MarshalBinary() + require.NoError(t, err) + return raw + } + + sorobanEnv := marshal(txEnvelope(300)) + classicEnv := marshal(diffClassicEnvelope(301)) + feeBumpSorobanEnv := marshal(diffFeeBumpEnvelope(txEnvelope(302))) + metaV3 := diffMetaV3NoSoroban() + metaV4 := diffMetaV4(nil, nil, nil) + metaV1 := diffMetaV1() + + tests := []struct { + name string + txView ingest.LedgerTransactionView + expected [][][]byte + }{ + { + "v3 soroban envelope, no operations: one empty operation slice", + ingest.LedgerTransactionView{Envelope: sorobanEnv, Meta: marshal(&metaV3), ContractEvents: [][][]byte{}}, + [][][]byte{{}}, + }, + { + "v3 fee bump over a soroban inner: also repaired", + ingest.LedgerTransactionView{Envelope: feeBumpSorobanEnv, Meta: marshal(&metaV3), ContractEvents: [][][]byte{}}, + [][][]byte{{}}, + }, + { + "v3 classic envelope: left empty", + ingest.LedgerTransactionView{Envelope: classicEnv, Meta: marshal(&metaV3), ContractEvents: [][][]byte{}}, + [][][]byte{}, + }, + { + "v4 meta: left empty whatever the envelope", + ingest.LedgerTransactionView{Envelope: sorobanEnv, Meta: marshal(&metaV4), ContractEvents: [][][]byte{}}, + [][][]byte{}, + }, + { + "v1 meta: left empty", + ingest.LedgerTransactionView{Envelope: sorobanEnv, Meta: marshal(&metaV1), ContractEvents: [][][]byte{}}, + [][][]byte{}, + }, + { + "already has operations: untouched", + ingest.LedgerTransactionView{ + Envelope: sorobanEnv, + Meta: marshal(&metaV3), + ContractEvents: [][][]byte{{[]byte("x")}}, + }, + [][][]byte{{[]byte("x")}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tx, err := store.ParseTransaction(tt.txView) + require.NoError(t, err) + require.Equal(t, tt.expected, tx.ContractEvents) + }) + } +} + +// TestTransactionInfo_FieldMapping pins the renderer both extractions share. +// The differential above cannot see a bug in shared code — it would show up +// identically on both sides — so the renderer's field-by-field wiring is +// pinned here instead, with every byte source distinct so that crossing any +// two of them fails. +func TestTransactionInfo_FieldMapping(t *testing.T) { + marshal := func(v interface{ MarshalBinary() ([]byte, error) }) []byte { + raw, err := v.MarshalBinary() + require.NoError(t, err) + return raw + } + symEvent := func(name string) xdr.ContractEvent { + sym := xdr.ScSymbol(name) + val := xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &sym} + id := xdr.ContractId{9} + return xdr.ContractEvent{ + ContractId: &id, + Type: xdr.ContractEventTypeContract, + Body: xdr.ContractEventBody{V: 0, V0: &xdr.ContractEventV0{Topics: []xdr.ScVal{val}, Data: val}}, + } + } + + result := xdr.TransactionResult{ + FeeCharged: 4242, + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxSuccess, + Results: &[]xdr.OperationResult{}, + }, + } + meta := diffMetaV4(nil, nil, nil) + envelope := diffClassicEnvelope(999) + diagnostic := xdr.DiagnosticEvent{InSuccessfulContractCall: true, Event: symEvent("DIAGNOSTIC")} + txEvent := xdr.TransactionEvent{ + Stage: xdr.TransactionEventStageTransactionEventStageAfterAllTxs, + Event: symEvent("TRANSACTION"), + } + opEventA, opEventB := symEvent("OPERATION_A"), symEvent("OPERATION_B") + + tx := store.Transaction{ + TransactionHash: "deadbeef", + Result: marshal(&result), + Meta: marshal(&meta), + Envelope: marshal(&envelope), + Events: [][]byte{marshal(&diagnostic)}, + TransactionEvents: [][]byte{marshal(&txEvent)}, + ContractEvents: [][][]byte{{marshal(&opEventA)}, {marshal(&opEventB)}}, + FeeBump: true, + ApplicationOrder: 7, + Successful: true, + Ledger: store.LedgerInfo{Sequence: 4242, CloseTime: 987}, + } + + // Every byte source distinct, so any crossed assignment below changes an + // encoding rather than reproducing it. + seen := map[string]string{} + for name, raw := range map[string][]byte{ + "result": tx.Result, "meta": tx.Meta, "envelope": tx.Envelope, + "diagnostic": tx.Events[0], "txEvent": tx.TransactionEvents[0], + "opA": tx.ContractEvents[0][0], "opB": tx.ContractEvents[1][0], + } { + encoded := base64.StdEncoding.EncodeToString(raw) + require.NotContains(t, seen, encoded, "%s collides with %s", name, seen[encoded]) + seen[encoded] = name + } + + t.Run("xdr", func(t *testing.T) { + got, err := transactionInfo(tx, "") + require.NoError(t, err) + + require.Equal(t, "deadbeef", got.TransactionHash) + require.Equal(t, int32(7), got.ApplicationOrder) + require.True(t, got.FeeBump) + require.Equal(t, uint32(4242), got.Ledger) + require.Equal(t, int64(987), got.LedgerCloseTime) + require.Equal(t, protocol.TransactionStatusSuccess, got.Status) + + b64 := base64.StdEncoding.EncodeToString + require.Equal(t, b64(tx.Result), got.ResultXDR) + require.Equal(t, b64(tx.Meta), got.ResultMetaXDR) + require.Equal(t, b64(tx.Envelope), got.EnvelopeXDR) + require.Equal(t, []string{b64(tx.Events[0])}, got.DiagnosticEventsXDR) + require.Equal(t, []string{b64(tx.TransactionEvents[0])}, got.Events.TransactionEventsXDR) + require.Equal(t, [][]string{ + {b64(tx.ContractEvents[0][0])}, + {b64(tx.ContractEvents[1][0])}, + }, got.Events.ContractEventsXDR) + + require.Empty(t, got.ResultJSON) + require.Empty(t, got.EnvelopeJSON) + require.Empty(t, got.ResultMetaJSON) + require.Empty(t, got.DiagnosticEventsJSON) + require.Empty(t, got.Events.ContractEventsJSON) + require.Empty(t, got.Events.TransactionEventsJSON) + }) + + t.Run("json", func(t *testing.T) { + failed := tx + failed.Successful = false + got, err := transactionInfo(failed, protocol.FormatJSON) + require.NoError(t, err) + require.Equal(t, protocol.TransactionStatusFailed, got.Status) + + // Each JSON field must be the conversion of ITS OWN source, so compare + // against the same converter run on that source directly. + wantJSON := func(typ any, raw []byte) string { + converted, cerr := xdr2json.ConvertBytes(typ, raw) + require.NoError(t, cerr) + return string(converted) + } + require.JSONEq(t, wantJSON(xdr.TransactionResult{}, tx.Result), string(got.ResultJSON)) + require.JSONEq(t, wantJSON(xdr.TransactionMeta{}, tx.Meta), string(got.ResultMetaJSON)) + require.JSONEq(t, wantJSON(xdr.TransactionEnvelope{}, tx.Envelope), string(got.EnvelopeJSON)) + require.Len(t, got.DiagnosticEventsJSON, 1) + require.JSONEq(t, wantJSON(xdr.DiagnosticEvent{}, tx.Events[0]), string(got.DiagnosticEventsJSON[0])) + require.Len(t, got.Events.TransactionEventsJSON, 1) + require.JSONEq(t, wantJSON(xdr.TransactionEvent{}, tx.TransactionEvents[0]), + string(got.Events.TransactionEventsJSON[0])) + require.Len(t, got.Events.ContractEventsJSON, 2) + require.JSONEq(t, wantJSON(xdr.ContractEvent{}, tx.ContractEvents[0][0]), + string(got.Events.ContractEventsJSON[0][0])) + require.JSONEq(t, wantJSON(xdr.ContractEvent{}, tx.ContractEvents[1][0]), + string(got.Events.ContractEventsJSON[1][0])) + + require.Empty(t, got.ResultXDR) + require.Empty(t, got.EnvelopeXDR) + require.Empty(t, got.ResultMetaXDR) + require.Empty(t, got.DiagnosticEventsXDR) + require.Empty(t, got.Events.ContractEventsXDR) + require.Empty(t, got.Events.TransactionEventsXDR) + }) +} diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index 3f182e5c6..e9a85916f 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -84,7 +84,7 @@ func (txn *MockTransactionHandler) GetTransaction(_ context.Context, hash xdr.Ha if !found { return store.Transaction{}, store.ErrNoTransaction } - return store.ParseTransaction(txView), nil + return store.ParseTransaction(txView) } func (txn *MockTransactionHandler) RegisterMetrics(_, _ prometheus.Observer) {} diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go index e0fd8cbe5..9b0df3da0 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go @@ -211,7 +211,7 @@ func (txn *transactionHandler) getTransactionByHash(ctx context.Context, hash xd return store.Transaction{}, store.ErrNoTransaction } txView := txnViewRange[0] - return store.ParseTransaction(txView), nil + return store.ParseTransaction(txView) } type transactionTableMigration struct { diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go index 03b591e66..f2e1cdc38 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go @@ -53,5 +53,5 @@ func (r *TransactionReader) GetTransaction(ctx context.Context, hash xdr.Hash) ( // Only a compactView-produced view may be reshaped here: the type does not // carry that, and a view straight off the SDK still aliases the ledger it // was read from — probe.GetTransaction guarantees it (see compactView). - return store.ParseTransaction(txv), nil + return store.ParseTransaction(txv) } diff --git a/cmd/stellar-rpc/internal/store/transaction.go b/cmd/stellar-rpc/internal/store/transaction.go index e7ae62d0f..f3c41209a 100644 --- a/cmd/stellar-rpc/internal/store/transaction.go +++ b/cmd/stellar-rpc/internal/store/transaction.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "errors" + "fmt" "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/xdr" @@ -33,8 +34,8 @@ type TransactionReader interface { // ParseTransaction reshapes an SDK transaction view into a Transaction; the // byte fields alias the view's buffer. -func ParseTransaction(txView ingest.LedgerTransactionView) Transaction { - return Transaction{ +func ParseTransaction(txView ingest.LedgerTransactionView) (Transaction, error) { + tx := Transaction{ TransactionHash: hex.EncodeToString(txView.Hash[:]), Result: txView.Result, Meta: txView.Meta, @@ -50,4 +51,55 @@ func ParseTransaction(txView ingest.LedgerTransactionView) Transaction { TransactionEvents: txView.TransactionEvents, ContractEvents: txView.ContractEvents, } + err := repairV3OperationArity(&tx) + return tx, err +} + +// repairV3OperationArity aligns ContractEvents with the decode path's arity in +// the one shape where the SDK's view extractor disagrees: TransactionMeta V3 + +// Soroban envelope + absent SorobanMeta (a Soroban tx charged but never +// executed — real on protocol 20-22 history) must serve [[]], not []. Pure +// view reads, no decode. DELETE at the go-stellar-sdk pin bump that includes +// stellar/go-stellar-sdk#5997, which repairs the arity inside the SDK. +func repairV3OperationArity(tx *Transaction) error { + if len(tx.ContractEvents) > 0 { + return nil + } + metaVersion, err := xdr.TransactionMetaView(tx.Meta).V() + if err != nil { + return fmt.Errorf("couldn't read transaction meta version: %w", err) + } + if metaVersion != 3 { + return nil + } + soroban, err := envelopeIsSoroban(tx.Envelope) + if err != nil { + return err + } + if soroban { + tx.ContractEvents = [][][]byte{{}} + } + return nil +} + +// envelopeIsSoroban reports whether a marshaled envelope carries +// SorobanTransactionData (Tx.Ext discriminant 1; the inner tx's for a fee +// bump). The SDK computes this flag internally but does not expose it. +func envelopeIsSoroban(raw []byte) (bool, error) { + env := xdr.TransactionEnvelopeView(raw) + var soroban bool + err := xdr.TryVoid(func() { + switch env.MustType() { + case xdr.EnvelopeTypeEnvelopeTypeTx: + soroban = env.MustV1().MustTx().MustExt().MustV() == 1 + case xdr.EnvelopeTypeEnvelopeTypeTxFeeBump: + soroban = env.MustFeeBump().MustTx().MustInnerTx().MustV1().MustTx().MustExt().MustV() == 1 + default: + // TX_V0 predates Soroban; no other discriminant is a tx envelope. + } + }) + if err != nil { + return false, fmt.Errorf("couldn't read the envelope's Soroban flag: %w", err) + } + return soroban, nil } From de730105c9d8371b34bd5a421d39c4c79d0a7cd2 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 31 Aug 2026 18:50:43 -0400 Subject: [PATCH 12/35] drop Tx-level GetLedgerView, restore the divergence-pinning fixtures The pager now borrows raw ledgers through WithLedgerRaw, so LedgerReaderTx's owned-copy GetLedgerView has no callers left: remove it from the interface, both backends, and the mocks/stubs. Revert the V3 fixture edits (and the getLedgers goldens derived from them): a Soroban envelope with absent SorobanMeta is real protocol 20-22 history, and the old fixtures pin the [[]] contractEventsXdr arity that repairV3OperationArity (in store.ParseTransaction) now preserves on the view path. Both fixtures die with that repair at the SDK pin bump for stellar/go-stellar-sdk#5997. Co-Authored-By: Claude Fable 5 --- .../internal/methods/get_ledgers_test.go | 4 ++-- .../internal/methods/get_transaction_test.go | 9 ++++----- .../get_transactions_differential_test.go | 4 +++- .../internal/methods/get_transactions_test.go | 20 +++++-------------- cmd/stellar-rpc/internal/methods/mocks.go | 7 ------- .../internal/rpcv1/sqlitedb/ledger.go | 8 -------- .../internal/rpcv2/adapters/ledger_reader.go | 11 ---------- cmd/stellar-rpc/internal/store/ledger.go | 15 +++++++------- 8 files changed, 21 insertions(+), 57 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go index 59fde9c0b..f13a7fb81 100644 --- a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go +++ b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go @@ -22,8 +22,8 @@ var expectedLedgerInfo = protocol.LedgerInfo{ Hash: "0000000000000000000000000000000000000000000000000000000000000000", Sequence: 1, LedgerCloseTime: 125, - LedgerHeader: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", //nolint:lll - LedgerMetadata: "AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAACAAABAIAAAAAAAAAAPww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAABAAAAAP///50AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAEzmSAb0wlZuZ7vERyxkacbwbERSS/IM82EYhemLKdUAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", //nolint:lll + LedgerHeader: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", //nolint:lll + LedgerMetadata: "AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAACAAABAIAAAAAAAAAAPww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAABAAAAAP///50AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAEzmSAb0wlZuZ7vERyxkacbwbERSS/IM82EYhemLKdUAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", //nolint:lll } func setupTestDB(t *testing.T, numLedgers int) *sqlitedb.DB { diff --git a/cmd/stellar-rpc/internal/methods/get_transaction_test.go b/cmd/stellar-rpc/internal/methods/get_transaction_test.go index 6b2c2aad2..4ac4e631c 100644 --- a/cmd/stellar-rpc/internal/methods/get_transaction_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transaction_test.go @@ -274,11 +274,10 @@ func txMeta(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { TxApplyProcessing: xdr.TransactionMeta{ V: 3, Operations: &[]xdr.OperationMeta{}, - // The envelope is soroban (Ext V1), so its V3 meta must carry - // SorobanMeta, as on the real network. - V3: &xdr.TransactionMetaV3{SorobanMeta: &xdr.SorobanTransactionMeta{ - ReturnValue: xdr.ScVal{Type: xdr.ScValTypeScvVoid}, - }}, + // Soroban envelope with NO SorobanMeta: a Soroban tx charged + // but never executed (real on protocol 20-22 history). Pins + // the [[]] contractEventsXdr arity the view path must serve. + V3: &xdr.TransactionMetaV3{}, }, Result: xdr.TransactionResultPair{ TransactionHash: txHash(acctSeq), diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go index 65e1f6fa5..3974a3bde 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go @@ -183,7 +183,7 @@ func legacyParseTransaction(lcm xdr.LedgerCloseMeta, ingestTx ingest.LedgerTrans var err error tx.FeeBump = ingestTx.Envelope.IsFeeBump() - tx.ApplicationOrder = int32(ingestTx.Index) //nolint:gosec // corpus indices are tiny + tx.ApplicationOrder = int32(ingestTx.Index) tx.Successful = ingestTx.Result.Successful() tx.Ledger = store.LedgerInfo{Sequence: lcm.LedgerSequence(), CloseTime: lcm.LedgerCloseTime()} tx.TransactionHash = ingestTx.Result.TransactionHash.HexString() @@ -825,6 +825,8 @@ func TestRepairV3OperationArity(t *testing.T) { // identically on both sides — so the renderer's field-by-field wiring is // pinned here instead, with every byte source distinct so that crossing any // two of them fails. +// +//nolint:funlen // one field-by-field pin; splitting it would scatter the wiring func TestTransactionInfo_FieldMapping(t *testing.T) { marshal := func(v interface{ MarshalBinary() ([]byte, error) }) []byte { raw, err := v.MarshalBinary() diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index 933aec4af..33de52266 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -32,7 +32,7 @@ var expectedTransactionInfo = protocol.TransactionInfo{ FeeBump: false, Ledger: 1, EnvelopeXDR: "AAAAAgAAAQCAAAAAAAAAAD8MNL+TrQ2ZcdBMzJD3BVEcg4qtlzSkovsNegP8f+iaAAAAAQAAAAD///+dAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", //nolint:lll - ResultMetaXDR: "AAAAAwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAA==", + ResultMetaXDR: "AAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAA", ResultXDR: "AAAAAAAAAGQAAAAAAAAAAAAAAAA=", DiagnosticEventsXDR: []string{}, Events: protocol.Events{ @@ -323,11 +323,10 @@ func createTestLedger(sequence uint32) xdr.LedgerCloseMeta { TxApplyProcessing: xdr.TransactionMeta{ V: 3, Operations: &[]xdr.OperationMeta{}, - // The envelope is soroban (Ext V1), so its V3 meta must carry - // SorobanMeta, as on the real network. - V3: &xdr.TransactionMetaV3{SorobanMeta: &xdr.SorobanTransactionMeta{ - ReturnValue: xdr.ScVal{Type: xdr.ScValTypeScvVoid}, - }}, + // Soroban envelope with NO SorobanMeta: a Soroban tx charged but + // never executed (real on protocol 20-22 history). This pins the + // [[]] contractEventsXdr arity the view path must preserve. + V3: &xdr.TransactionMetaV3{}, }, Result: xdr.TransactionResultPair{ TransactionHash: txHash(sequence), @@ -385,15 +384,6 @@ func (r *sparseLedgerReader) GetLedger(_ context.Context, seq uint32) (xdr.Ledge return createEmptyTestLedger(seq), true, nil } -func (r *sparseLedgerReader) GetLedgerView(_ context.Context, seq uint32) (xdr.LedgerCloseMetaView, bool, error) { - r.gets++ - raw, err := createEmptyTestLedger(seq).MarshalBinary() - if err != nil { - return nil, false, err - } - return xdr.LedgerCloseMetaView(raw), true, nil -} - func (r *sparseLedgerReader) WithLedgerRaw( _ context.Context, seq uint32, fn store.WithLedgerRawFn, ) (bool, error) { diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index dd3e79889..de5377481 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -76,13 +76,6 @@ func (m *MockLedgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xd return args.Get(0).(xdr.LedgerCloseMeta), args.Bool(1), args.Error(2) //nolint:forcetypeassert } -func (m *MockLedgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) ( - xdr.LedgerCloseMetaView, bool, error, -) { - args := m.Called(ctx, sequence) - return args.Get(0).(xdr.LedgerCloseMetaView), args.Bool(1), args.Error(2) //nolint:forcetypeassert -} - func (m *MockLedgerReaderTx) WithLedgerRaw( ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, ) (bool, error) { diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 7df776c8a..4f8772343 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -92,14 +92,6 @@ func (l ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.Led return getLedgerFromDB(ctx, l.tx, sequence) } -func (l ledgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { - meta, found, err := getLedgerRawFromDB(ctx, l.tx, sequence) - if err != nil || !found { - return nil, found, err - } - return xdr.LedgerCloseMetaView(meta), true, nil -} - // WithLedgerRaw lends the ledger's stored meta blob without decoding it. The // blob is ours to lend: database/sql clones each BLOB scanned into a *[]byte. func (l ledgerReaderTx) WithLedgerRaw( diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index 826b8d27b..4575c93a1 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -151,17 +151,6 @@ func (tx *ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.L return lcm, true, nil } -// GetLedgerView is GetLedger without the decode and advances the same walk. -// Entry.Bytes is overwritten on the next step and callers keep slices of the -// view across steps, so clone. -func (tx *ledgerReaderTx) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { - entry, found, err := tx.walk(ctx, sequence) - if err != nil || !found { - return nil, false, err - } - return xdr.LedgerCloseMetaView(bytes.Clone(entry.Bytes)), true, nil -} - // WithLedgerRaw is GetLedger without the decode or the clone: it lends the // step's bytes straight from the chunk reader's scratch buffer, which the // next step overwrites — fn must not retain them. diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index 390019231..aff11a16b 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -50,20 +50,19 @@ type WithLedgerRawFn func(raw []byte) error // LedgerReaderTx is a read-only snapshot of the ledger store. Call Done to // release it. // -// GetLedger, GetLedgerView, and WithLedgerRaw are one walk, not free-form -// point reads. Call them with ascending, contiguous sequences, starting from -// the first call's sequence, and use only one of them per Tx: they share a -// single cursor, so interleaving them consumes positions from each other. -// Read at most methods.LedgerScanLimit ledgers per Tx. The v1 (SQL) backend -// accepts any pattern, while the v2 backend only walks from a forward -// iterator primed on the first call. +// GetLedger and WithLedgerRaw are one walk, not free-form point reads. Call +// them with ascending, contiguous sequences, starting from the first call's +// sequence, and use only one of the two per Tx: they share a single cursor, +// so interleaving them consumes positions from each other. Read at most +// methods.LedgerScanLimit ledgers per Tx. The v1 (SQL) backend accepts any +// pattern, while the v2 backend only walks from a forward iterator primed on +// the first call. // // WithLedgerRaw is GetLedger without the decode: fn borrows the marshaled // LCM under WithLedgerRawFn's loan terms. found=false means fn never ran; // fn's own error comes back verbatim with found=true. type LedgerReaderTx interface { GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) - GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) WithLedgerRaw(ctx context.Context, sequence uint32, fn WithLedgerRawFn) (bool, error) GetLedgerRange(ctx context.Context) (LedgerRange, error) BatchGetLedgers(ctx context.Context, start uint32, end uint32) ([]LedgerMetadataChunk, error) From e8a358161a8e3b6ca5c8d92e5e5711c0ae6f16f9 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 1 Sep 2026 13:17:59 -0400 Subject: [PATCH 13/35] rename to withLedgerRawFromDB, specify loan in comment --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 4f8772343..3c7d61a24 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -97,7 +97,7 @@ func (l ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.Led func (l ledgerReaderTx) WithLedgerRaw( ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, ) (bool, error) { - meta, found, err := getLedgerRawFromDB(ctx, l.tx, sequence) + meta, found, err := withLedgerRawFromDB(ctx, l.tx, sequence) if err != nil || !found { return found, err } @@ -163,7 +163,7 @@ func (r ledgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr.Ledge // WithLedgerRaw lends the ledger's stored meta blob without decoding it. func (r ledgerReader) WithLedgerRaw(ctx context.Context, sequence uint32, fn store.WithLedgerRawFn) (bool, error) { - meta, found, err := getLedgerRawFromDB(ctx, r.db, sequence) + meta, found, err := withLedgerRawFromDB(ctx, r.db, sequence) if err != nil || !found { return found, err } @@ -245,7 +245,7 @@ type ledgerRangeRow struct { func ledgerInfoFromRow(ctx context.Context, db readDB, row ledgerRangeRow) (store.LedgerInfo, error) { closeTime, err := xdr.LedgerCloseMetaView(row.MetaPrefix).LedgerCloseTime() if err != nil { - meta, found, dbErr := getLedgerRawFromDB(ctx, db, row.Sequence) + meta, found, dbErr := withLedgerRawFromDB(ctx, db, row.Sequence) if dbErr != nil { return store.LedgerInfo{}, dbErr } @@ -368,7 +368,7 @@ func (l ledgerWriter) trimLedgers(latestLedgerSeq uint32, retentionWindow uint32 // getLedgerFromDB fetches a single ledger from the database. func getLedgerFromDB(ctx context.Context, db readDB, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { - meta, found, err := getLedgerRawFromDB(ctx, db, sequence) + meta, found, err := withLedgerRawFromDB(ctx, db, sequence) if err != nil || !found { return xdr.LedgerCloseMeta{}, false, err } @@ -379,9 +379,10 @@ func getLedgerFromDB(ctx context.Context, db readDB, sequence uint32) (xdr.Ledge return lcm, true, nil } -// getLedgerRawFromDB is a helper function that encapsulates the common logic -// for fetching a single ledger's bytes from the database. -func getLedgerRawFromDB(ctx context.Context, db readDB, sequence uint32) ([]byte, bool, error) { +// withLedgerRawFromDB is a helper function that encapsulates the common logic +// for fetching a single ledger's bytes from the database. The bytes returned +// are lent and shouldn't be reused by a later statement. +func withLedgerRawFromDB(ctx context.Context, db readDB, sequence uint32) ([]byte, bool, error) { sql := sq.Select("meta").From(ledgerCloseMetaTableName).Where(sq.Eq{"sequence": sequence}) var results [][]byte if err := db.Select(ctx, &results, sql); err != nil { From c4f606b64d45d3798dffb4ab748f65820668d43c Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 1 Sep 2026 13:54:14 -0400 Subject: [PATCH 14/35] bump SDK past the view-arity fix, delete the interim repair getevents-v2 took main (go-stellar-sdk#5997 included), so the pin moves to 2cc23142 and the SDK now aligns the V3-no-SorobanMeta operation arity itself: delete repairV3OperationArity, envelopeIsSoroban, and their unit test, and return store.ParseTransaction to its plain single-value reshape. The fixture and differential pins for the [[]] shape stay and now pass on the SDK alone. The bump also picks up the protocols/rpc EventInfo.InSuccessfulContractCall deletion (go-stellar-sdk#5995): drop the field from getEvents' response assembly and test expectations. xdr.DiagnosticEvent's field is unaffected. Co-Authored-By: Claude Fable 5 --- .../internal/methods/get_events_test.go | 140 +++++++++--------- .../internal/methods/get_transactions.go | 9 +- .../get_transactions_differential_test.go | 73 +-------- .../internal/rpcv1/sqlitedb/mocks.go | 2 +- .../internal/rpcv1/sqlitedb/transaction.go | 2 +- .../rpcv2/adapters/transaction_reader.go | 2 +- cmd/stellar-rpc/internal/store/transaction.go | 56 +------ 7 files changed, 79 insertions(+), 205 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_events_test.go b/cmd/stellar-rpc/internal/methods/get_events_test.go index 2dbd60e36..2286905fe 100644 --- a/cmd/stellar-rpc/internal/methods/get_events_test.go +++ b/cmd/stellar-rpc/internal/methods/get_events_test.go @@ -146,16 +146,16 @@ func TestGetEvents(t *testing.T) { }) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - OpIndex: 0, - TxIndex: uint32(i + 1), + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + OpIndex: 0, + TxIndex: uint32(i + 1), }) } cursor := protocol.MaxCursor @@ -305,16 +305,16 @@ func TestGetEvents(t *testing.T) { require.NoError(t, err) expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{counterXdr, value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), - TxIndex: 5, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{counterXdr, value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), + TxIndex: 5, + OpIndex: 0, }, } @@ -496,16 +496,16 @@ func TestGetEvents(t *testing.T) { // flexible topic length matching expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{counterXdr, value, value, counterXdr}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), - TxIndex: 5, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{counterXdr, value, value, counterXdr}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), + TxIndex: 5, + OpIndex: 0, }, } require.NoError(t, err) @@ -670,16 +670,16 @@ func TestGetEvents(t *testing.T) { require.NoError(t, err) expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr, value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), - TxIndex: 4, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr, value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), + TxIndex: 4, + OpIndex: 0, }, } cursor := protocol.MaxCursor @@ -758,16 +758,16 @@ func TestGetEvents(t *testing.T) { id := protocol.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 1}.String() expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeSystem, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr}, - ValueXDR: counterXdr, - TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), - TxIndex: 1, - OpIndex: 0, + EventType: protocol.EventTypeSystem, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr}, + ValueXDR: counterXdr, + TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), + TxIndex: 1, + OpIndex: 0, }, } cursor := protocol.MaxCursor @@ -842,16 +842,16 @@ func TestGetEvents(t *testing.T) { value, err := xdr.MarshalBase64(txMeta[i].MustV3().SorobanMeta.Events[0].Body.MustV0().Data) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - TxIndex: uint32(i + 1), - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + TxIndex: uint32(i + 1), + OpIndex: 0, }) } cursor := expected[len(expected)-1].ID @@ -954,16 +954,16 @@ func TestGetEvents(t *testing.T) { expectedXdr, err := xdr.MarshalBase64(xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &symbols[i]}) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 5, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr}, - ValueXDR: expectedXdr, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - TxIndex: uint32(i + 1), - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 5, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr}, + ValueXDR: expectedXdr, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + TxIndex: uint32(i + 1), + OpIndex: 0, }) } cursor := expected[len(expected)-1].ID diff --git a/cmd/stellar-rpc/internal/methods/get_transactions.go b/cmd/stellar-rpc/internal/methods/get_transactions.go index 7a2d93949..7cce04175 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions.go @@ -152,14 +152,7 @@ func (h transactionsRPCHandler) processTransactionsInLedger( for i, txView := range txViews { cursor.TransactionOrder = int32(startTxIdx + i) - tx, perr := store.ParseTransaction(txView) - if perr != nil { - return nil, false, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: perr.Error(), - } - } - + tx := store.ParseTransaction(txView) txInfo, ferr := transactionInfo(tx, format) if ferr != nil { return nil, false, &jrpc2.Error{ diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go index 3974a3bde..1aa349ba0 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go @@ -306,8 +306,9 @@ func diffMetaV1() xdr.TransactionMeta { // diffMetaV3NoSoroban is the straggler corner: a V3 meta with no SorobanMeta // at all. Paired with a Soroban envelope it is the one shape where the SDK's -// view extractor and the parsed reader disagree on operation-slice arity, so -// the corpus pins it deliberately (see repairV3OperationArity). +// view extractor historically disagreed with the parsed reader on +// operation-slice arity (fixed upstream in go-stellar-sdk#5997), so the +// corpus pins it deliberately. func diffMetaV3NoSoroban() xdr.TransactionMeta { return xdr.TransactionMeta{V: 3, Operations: &[]xdr.OperationMeta{}, V3: &xdr.TransactionMetaV3{}} } @@ -752,74 +753,6 @@ func TestGetTransactions_ViewWalkCorpusIsNotVacuous(t *testing.T) { require.Positive(t, withDiagnostics, "transactions with diagnostic events") } -// TestRepairV3OperationArity pins the straggler repair (inside -// store.ParseTransaction) directly: it fires only for a V3 meta on a Soroban -// envelope that came back with no operations, and leaves every other shape -// exactly as the view extractor produced it. -func TestRepairV3OperationArity(t *testing.T) { - marshal := func(v interface{ MarshalBinary() ([]byte, error) }) []byte { - raw, err := v.MarshalBinary() - require.NoError(t, err) - return raw - } - - sorobanEnv := marshal(txEnvelope(300)) - classicEnv := marshal(diffClassicEnvelope(301)) - feeBumpSorobanEnv := marshal(diffFeeBumpEnvelope(txEnvelope(302))) - metaV3 := diffMetaV3NoSoroban() - metaV4 := diffMetaV4(nil, nil, nil) - metaV1 := diffMetaV1() - - tests := []struct { - name string - txView ingest.LedgerTransactionView - expected [][][]byte - }{ - { - "v3 soroban envelope, no operations: one empty operation slice", - ingest.LedgerTransactionView{Envelope: sorobanEnv, Meta: marshal(&metaV3), ContractEvents: [][][]byte{}}, - [][][]byte{{}}, - }, - { - "v3 fee bump over a soroban inner: also repaired", - ingest.LedgerTransactionView{Envelope: feeBumpSorobanEnv, Meta: marshal(&metaV3), ContractEvents: [][][]byte{}}, - [][][]byte{{}}, - }, - { - "v3 classic envelope: left empty", - ingest.LedgerTransactionView{Envelope: classicEnv, Meta: marshal(&metaV3), ContractEvents: [][][]byte{}}, - [][][]byte{}, - }, - { - "v4 meta: left empty whatever the envelope", - ingest.LedgerTransactionView{Envelope: sorobanEnv, Meta: marshal(&metaV4), ContractEvents: [][][]byte{}}, - [][][]byte{}, - }, - { - "v1 meta: left empty", - ingest.LedgerTransactionView{Envelope: sorobanEnv, Meta: marshal(&metaV1), ContractEvents: [][][]byte{}}, - [][][]byte{}, - }, - { - "already has operations: untouched", - ingest.LedgerTransactionView{ - Envelope: sorobanEnv, - Meta: marshal(&metaV3), - ContractEvents: [][][]byte{{[]byte("x")}}, - }, - [][][]byte{{[]byte("x")}}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tx, err := store.ParseTransaction(tt.txView) - require.NoError(t, err) - require.Equal(t, tt.expected, tx.ContractEvents) - }) - } -} - // TestTransactionInfo_FieldMapping pins the renderer both extractions share. // The differential above cannot see a bug in shared code — it would show up // identically on both sides — so the renderer's field-by-field wiring is diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index e9a85916f..3f182e5c6 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -84,7 +84,7 @@ func (txn *MockTransactionHandler) GetTransaction(_ context.Context, hash xdr.Ha if !found { return store.Transaction{}, store.ErrNoTransaction } - return store.ParseTransaction(txView) + return store.ParseTransaction(txView), nil } func (txn *MockTransactionHandler) RegisterMetrics(_, _ prometheus.Observer) {} diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go index 9b0df3da0..e0fd8cbe5 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go @@ -211,7 +211,7 @@ func (txn *transactionHandler) getTransactionByHash(ctx context.Context, hash xd return store.Transaction{}, store.ErrNoTransaction } txView := txnViewRange[0] - return store.ParseTransaction(txView) + return store.ParseTransaction(txView), nil } type transactionTableMigration struct { diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go index f2e1cdc38..03b591e66 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go @@ -53,5 +53,5 @@ func (r *TransactionReader) GetTransaction(ctx context.Context, hash xdr.Hash) ( // Only a compactView-produced view may be reshaped here: the type does not // carry that, and a view straight off the SDK still aliases the ledger it // was read from — probe.GetTransaction guarantees it (see compactView). - return store.ParseTransaction(txv) + return store.ParseTransaction(txv), nil } diff --git a/cmd/stellar-rpc/internal/store/transaction.go b/cmd/stellar-rpc/internal/store/transaction.go index f3c41209a..e7ae62d0f 100644 --- a/cmd/stellar-rpc/internal/store/transaction.go +++ b/cmd/stellar-rpc/internal/store/transaction.go @@ -4,7 +4,6 @@ import ( "context" "encoding/hex" "errors" - "fmt" "github.com/stellar/go-stellar-sdk/ingest" "github.com/stellar/go-stellar-sdk/xdr" @@ -34,8 +33,8 @@ type TransactionReader interface { // ParseTransaction reshapes an SDK transaction view into a Transaction; the // byte fields alias the view's buffer. -func ParseTransaction(txView ingest.LedgerTransactionView) (Transaction, error) { - tx := Transaction{ +func ParseTransaction(txView ingest.LedgerTransactionView) Transaction { + return Transaction{ TransactionHash: hex.EncodeToString(txView.Hash[:]), Result: txView.Result, Meta: txView.Meta, @@ -51,55 +50,4 @@ func ParseTransaction(txView ingest.LedgerTransactionView) (Transaction, error) TransactionEvents: txView.TransactionEvents, ContractEvents: txView.ContractEvents, } - err := repairV3OperationArity(&tx) - return tx, err -} - -// repairV3OperationArity aligns ContractEvents with the decode path's arity in -// the one shape where the SDK's view extractor disagrees: TransactionMeta V3 + -// Soroban envelope + absent SorobanMeta (a Soroban tx charged but never -// executed — real on protocol 20-22 history) must serve [[]], not []. Pure -// view reads, no decode. DELETE at the go-stellar-sdk pin bump that includes -// stellar/go-stellar-sdk#5997, which repairs the arity inside the SDK. -func repairV3OperationArity(tx *Transaction) error { - if len(tx.ContractEvents) > 0 { - return nil - } - metaVersion, err := xdr.TransactionMetaView(tx.Meta).V() - if err != nil { - return fmt.Errorf("couldn't read transaction meta version: %w", err) - } - if metaVersion != 3 { - return nil - } - soroban, err := envelopeIsSoroban(tx.Envelope) - if err != nil { - return err - } - if soroban { - tx.ContractEvents = [][][]byte{{}} - } - return nil -} - -// envelopeIsSoroban reports whether a marshaled envelope carries -// SorobanTransactionData (Tx.Ext discriminant 1; the inner tx's for a fee -// bump). The SDK computes this flag internally but does not expose it. -func envelopeIsSoroban(raw []byte) (bool, error) { - env := xdr.TransactionEnvelopeView(raw) - var soroban bool - err := xdr.TryVoid(func() { - switch env.MustType() { - case xdr.EnvelopeTypeEnvelopeTypeTx: - soroban = env.MustV1().MustTx().MustExt().MustV() == 1 - case xdr.EnvelopeTypeEnvelopeTypeTxFeeBump: - soroban = env.MustFeeBump().MustTx().MustInnerTx().MustV1().MustTx().MustExt().MustV() == 1 - default: - // TX_V0 predates Soroban; no other discriminant is a tx envelope. - } - }) - if err != nil { - return false, fmt.Errorf("couldn't read the envelope's Soroban flag: %w", err) - } - return soroban, nil } From 7490db078ff595e3f90018e8e546d6ea766434da Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 1 Sep 2026 13:57:20 -0400 Subject: [PATCH 15/35] bump SDK + remove InSuccessfulContractCall from getEventsV1 protocol struct --- cmd/stellar-rpc/internal/methods/get_transactions.go | 2 +- .../internal/methods/get_transactions_differential_test.go | 3 +-- cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go | 2 +- cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go | 2 +- cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go | 2 +- cmd/stellar-rpc/internal/store/transaction.go | 4 ++-- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_transactions.go b/cmd/stellar-rpc/internal/methods/get_transactions.go index 7cce04175..d325c845d 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions.go @@ -152,7 +152,7 @@ func (h transactionsRPCHandler) processTransactionsInLedger( for i, txView := range txViews { cursor.TransactionOrder = int32(startTxIdx + i) - tx := store.ParseTransaction(txView) + tx := store.ParseTransactionView(txView) txInfo, ferr := transactionInfo(tx, format) if ferr != nil { return nil, false, &jrpc2.Error{ diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go index 1aa349ba0..e0b5e2793 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go @@ -307,8 +307,7 @@ func diffMetaV1() xdr.TransactionMeta { // diffMetaV3NoSoroban is the straggler corner: a V3 meta with no SorobanMeta // at all. Paired with a Soroban envelope it is the one shape where the SDK's // view extractor historically disagreed with the parsed reader on -// operation-slice arity (fixed upstream in go-stellar-sdk#5997), so the -// corpus pins it deliberately. +// operation-slice arity, so the corpus pins it deliberately. func diffMetaV3NoSoroban() xdr.TransactionMeta { return xdr.TransactionMeta{V: 3, Operations: &[]xdr.OperationMeta{}, V3: &xdr.TransactionMetaV3{}} } diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index 3f182e5c6..df6d9a9cf 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -84,7 +84,7 @@ func (txn *MockTransactionHandler) GetTransaction(_ context.Context, hash xdr.Ha if !found { return store.Transaction{}, store.ErrNoTransaction } - return store.ParseTransaction(txView), nil + return store.ParseTransactionView(txView), nil } func (txn *MockTransactionHandler) RegisterMetrics(_, _ prometheus.Observer) {} diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go index e0fd8cbe5..5193f8c73 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go @@ -211,7 +211,7 @@ func (txn *transactionHandler) getTransactionByHash(ctx context.Context, hash xd return store.Transaction{}, store.ErrNoTransaction } txView := txnViewRange[0] - return store.ParseTransaction(txView), nil + return store.ParseTransactionView(txView), nil } type transactionTableMigration struct { diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go index 03b591e66..ef34399b4 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/transaction_reader.go @@ -53,5 +53,5 @@ func (r *TransactionReader) GetTransaction(ctx context.Context, hash xdr.Hash) ( // Only a compactView-produced view may be reshaped here: the type does not // carry that, and a view straight off the SDK still aliases the ledger it // was read from — probe.GetTransaction guarantees it (see compactView). - return store.ParseTransaction(txv), nil + return store.ParseTransactionView(txv), nil } diff --git a/cmd/stellar-rpc/internal/store/transaction.go b/cmd/stellar-rpc/internal/store/transaction.go index e7ae62d0f..af81ed4f1 100644 --- a/cmd/stellar-rpc/internal/store/transaction.go +++ b/cmd/stellar-rpc/internal/store/transaction.go @@ -31,9 +31,9 @@ type TransactionReader interface { GetTransaction(ctx context.Context, hash xdr.Hash) (Transaction, error) } -// ParseTransaction reshapes an SDK transaction view into a Transaction; the +// ParseTransactionView reshapes an SDK transaction view into a Transaction; the // byte fields alias the view's buffer. -func ParseTransaction(txView ingest.LedgerTransactionView) Transaction { +func ParseTransactionView(txView ingest.LedgerTransactionView) Transaction { return Transaction{ TransactionHash: hex.EncodeToString(txView.Hash[:]), Result: txView.Result, From 12a916c1cfe6fe5c4a5f66a0ae0dcc406f160b52 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 16:13:51 -0400 Subject: [PATCH 16/35] update tests w/ new golden strings --- cmd/stellar-rpc/internal/methods/get_ledgers_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go index f13a7fb81..59fde9c0b 100644 --- a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go +++ b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go @@ -22,8 +22,8 @@ var expectedLedgerInfo = protocol.LedgerInfo{ Hash: "0000000000000000000000000000000000000000000000000000000000000000", Sequence: 1, LedgerCloseTime: 125, - LedgerHeader: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", //nolint:lll - LedgerMetadata: "AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAACAAABAIAAAAAAAAAAPww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAABAAAAAP///50AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAEzmSAb0wlZuZ7vERyxkacbwbERSS/IM82EYhemLKdUAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", //nolint:lll + LedgerHeader: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", //nolint:lll + LedgerMetadata: "AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAACAAABAIAAAAAAAAAAPww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAABAAAAAP///50AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAEzmSAb0wlZuZ7vERyxkacbwbERSS/IM82EYhemLKdUAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", //nolint:lll } func setupTestDB(t *testing.T, numLedgers int) *sqlitedb.DB { From 7eae4d9ccaaaca4a037a731fb29e23cf5286cf1b Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 31 Aug 2026 14:46:34 -0400 Subject: [PATCH 17/35] restore sparseLedgerReader Tx-level GetLedgerView --- .../internal/methods/get_transactions_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index 33de52266..dcfc4f11c 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -384,6 +384,15 @@ func (r *sparseLedgerReader) GetLedger(_ context.Context, seq uint32) (xdr.Ledge return createEmptyTestLedger(seq), true, nil } +func (r *sparseLedgerReader) GetLedgerView(_ context.Context, seq uint32) (xdr.LedgerCloseMetaView, bool, error) { + r.gets++ + raw, err := createEmptyTestLedger(seq).MarshalBinary() + if err != nil { + return nil, false, err + } + return xdr.LedgerCloseMetaView(raw), true, nil +} + func (r *sparseLedgerReader) WithLedgerRaw( _ context.Context, seq uint32, fn store.WithLedgerRawFn, ) (bool, error) { From 20b8882deaf3bd3fba3cca243a001dcc5bea18d0 Mon Sep 17 00:00:00 2001 From: Tamir Sen Date: Mon, 31 Aug 2026 00:41:27 +0000 Subject: [PATCH 18/35] store: a raw, loan-shaped ledger accessor on the serving read Tx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getTransactions costs ~100ms per limit=50 page, and a CPU profile over 40 such requests attributes 1.83s cum to xdr.LedgerEntryChanges.DecodeFrom plus ~2.9s of GC drain/scan on top: the shared handler fully XDR-unmarshals the entire multi-MB LedgerCloseMeta once per ledger it walks, to read a handful of per-transaction fields off it. It has no choice — the serving interface only offers GetLedger, which returns a decoded xdr.LedgerCloseMeta — yet BOTH backends hold the ledger as raw XDR bytes and decode on the way out. Add the seam that lets a handler skip that decode: WithLedgerRaw(ctx, sequence, fn) (found bool, err error) on store.LedgerReaderTx — the Tx/walk variant, because the paging path is the only caller and the one-shot store.LedgerReader has none. Loan-shaped rather than returning bytes: the v2 backend can then hand over its chunk reader's scratch buffer directly, with no copy and no lifetime question, since the bytes' validity ends with fn. The contract is documented on the interface — valid only inside fn, read-only, must not be retained — alongside the walk-cursor rule the two accessors now share. The backends implement it as pass-throughs of what they already hold: - rpcv2 adapter: GetLedger's walk (deadline check, window gate, priming, the two loud contract failures) moves into a shared walkTo, so the decoding and raw accessors advance one cursor and cannot drift on where the walk is. WithLedgerRaw is walkTo plus fn — no decode at all. - rpcv1 sqlitedb: selects the same `meta` column of ledger_close_meta into a []byte instead of an xdr.LedgerCloseMeta, so the row scan never runs UnmarshalBinary. The lent bytes are ours to lend: scanning a BLOB into a *[]byte goes through database/sql's convertAssign, which clones the driver's row buffer (only sql.RawBytes opts out), and BatchGetLedgers already depends on exactly that by retaining the slices it selects. GetLedger stays: every other handler still uses it. No caller uses the new seam yet. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Y7opQUW9UZPKzE6B3tz9b5 (cherry picked from commit d8e0797e37ab5a2176d8a943dfdb094017066778) --- cmd/stellar-rpc/internal/methods/mocks.go | 7 +++++++ cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go | 12 ++++++++++++ .../internal/rpcv2/adapters/ledger_reader.go | 13 +++++++++++++ 3 files changed, 32 insertions(+) diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index de5377481..8402406d7 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -83,6 +83,13 @@ func (m *MockLedgerReaderTx) WithLedgerRaw( return args.Bool(0), args.Error(1) } +func (m *MockLedgerReaderTx) WithLedgerRaw( + ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, +) (bool, error) { + args := m.Called(ctx, sequence, fn) + return args.Bool(0), args.Error(1) +} + func (m *MockLedgerReaderTx) Done() error { args := m.Called() return args.Error(0) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 3c7d61a24..63072979a 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -104,6 +104,18 @@ func (l ledgerReaderTx) WithLedgerRaw( return true, fn(meta) } +// WithLedgerRaw lends the ledger's stored meta blob without decoding it. The +// blob is ours to lend: database/sql clones each BLOB scanned into a *[]byte. +func (l ledgerReaderTx) WithLedgerRaw( + ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, +) (bool, error) { + meta, found, err := getLedgerRawFromDB(ctx, l.tx, sequence) + if err != nil || !found { + return found, err + } + return true, fn(meta) +} + func (l ledgerReaderTx) Done() error { return l.tx.Rollback() } diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index 4575c93a1..0162c10eb 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -164,6 +164,19 @@ func (tx *ledgerReaderTx) WithLedgerRaw( return true, fn(entry.Bytes) } +// WithLedgerRaw is GetLedger without the decode or the clone: it lends the +// step's bytes straight from the chunk reader's scratch buffer, which the +// next step overwrites — fn must not retain them. +func (tx *ledgerReaderTx) WithLedgerRaw( + ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, +) (bool, error) { + entry, found, err := tx.walk(ctx, sequence) + if err != nil || !found { + return found, err + } + return true, fn(entry.Bytes) +} + func (tx *ledgerReaderTx) GetLedgerRange(_ context.Context) (store.LedgerRange, error) { return getLedgerRange(tx.view) } From c7b0991ae7336493729685f47e5acc54b6b4068c Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 31 Aug 2026 18:50:43 -0400 Subject: [PATCH 19/35] drop Tx-level GetLedgerView, restore the divergence-pinning fixtures The pager now borrows raw ledgers through WithLedgerRaw, so LedgerReaderTx's owned-copy GetLedgerView has no callers left: remove it from the interface, both backends, and the mocks/stubs. Revert the V3 fixture edits (and the getLedgers goldens derived from them): a Soroban envelope with absent SorobanMeta is real protocol 20-22 history, and the old fixtures pin the [[]] contractEventsXdr arity that repairV3OperationArity (in store.ParseTransaction) now preserves on the view path. Both fixtures die with that repair at the SDK pin bump for stellar/go-stellar-sdk#5997. Co-Authored-By: Claude Fable 5 --- cmd/stellar-rpc/internal/methods/get_ledgers_test.go | 4 ++-- .../internal/methods/get_transactions_test.go | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go index 59fde9c0b..f13a7fb81 100644 --- a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go +++ b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go @@ -22,8 +22,8 @@ var expectedLedgerInfo = protocol.LedgerInfo{ Hash: "0000000000000000000000000000000000000000000000000000000000000000", Sequence: 1, LedgerCloseTime: 125, - LedgerHeader: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", //nolint:lll - LedgerMetadata: "AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAACAAABAIAAAAAAAAAAPww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAABAAAAAP///50AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAEzmSAb0wlZuZ7vERyxkacbwbERSS/IM82EYhemLKdUAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", //nolint:lll + LedgerHeader: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", //nolint:lll + LedgerMetadata: "AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAACAAABAIAAAAAAAAAAPww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAABAAAAAP///50AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAEzmSAb0wlZuZ7vERyxkacbwbERSS/IM82EYhemLKdUAAAAAAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", //nolint:lll } func setupTestDB(t *testing.T, numLedgers int) *sqlitedb.DB { diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index dcfc4f11c..33de52266 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -384,15 +384,6 @@ func (r *sparseLedgerReader) GetLedger(_ context.Context, seq uint32) (xdr.Ledge return createEmptyTestLedger(seq), true, nil } -func (r *sparseLedgerReader) GetLedgerView(_ context.Context, seq uint32) (xdr.LedgerCloseMetaView, bool, error) { - r.gets++ - raw, err := createEmptyTestLedger(seq).MarshalBinary() - if err != nil { - return nil, false, err - } - return xdr.LedgerCloseMetaView(raw), true, nil -} - func (r *sparseLedgerReader) WithLedgerRaw( _ context.Context, seq uint32, fn store.WithLedgerRawFn, ) (bool, error) { From 7c8493a38061f2738c25466f9fed3a0b3d75525c Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 4 Sep 2026 13:09:20 -0400 Subject: [PATCH 20/35] undo needless whitespace changes --- .../internal/methods/get_events_test.go | 140 +++++++++--------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_events_test.go b/cmd/stellar-rpc/internal/methods/get_events_test.go index 2286905fe..2dbd60e36 100644 --- a/cmd/stellar-rpc/internal/methods/get_events_test.go +++ b/cmd/stellar-rpc/internal/methods/get_events_test.go @@ -146,16 +146,16 @@ func TestGetEvents(t *testing.T) { }) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - OpIndex: 0, - TxIndex: uint32(i + 1), + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + OpIndex: 0, + TxIndex: uint32(i + 1), }) } cursor := protocol.MaxCursor @@ -305,16 +305,16 @@ func TestGetEvents(t *testing.T) { require.NoError(t, err) expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{counterXdr, value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), - TxIndex: 5, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{counterXdr, value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), + TxIndex: 5, + OpIndex: 0, }, } @@ -496,16 +496,16 @@ func TestGetEvents(t *testing.T) { // flexible topic length matching expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{counterXdr, value, value, counterXdr}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), - TxIndex: 5, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{counterXdr, value, value, counterXdr}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), + TxIndex: 5, + OpIndex: 0, }, } require.NoError(t, err) @@ -670,16 +670,16 @@ func TestGetEvents(t *testing.T) { require.NoError(t, err) expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr, value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), - TxIndex: 4, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr, value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), + TxIndex: 4, + OpIndex: 0, }, } cursor := protocol.MaxCursor @@ -758,16 +758,16 @@ func TestGetEvents(t *testing.T) { id := protocol.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 1}.String() expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeSystem, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr}, - ValueXDR: counterXdr, - TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), - TxIndex: 1, - OpIndex: 0, + EventType: protocol.EventTypeSystem, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr}, + ValueXDR: counterXdr, + TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), + TxIndex: 1, + OpIndex: 0, }, } cursor := protocol.MaxCursor @@ -842,16 +842,16 @@ func TestGetEvents(t *testing.T) { value, err := xdr.MarshalBase64(txMeta[i].MustV3().SorobanMeta.Events[0].Body.MustV0().Data) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - TxIndex: uint32(i + 1), - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + TxIndex: uint32(i + 1), + OpIndex: 0, }) } cursor := expected[len(expected)-1].ID @@ -954,16 +954,16 @@ func TestGetEvents(t *testing.T) { expectedXdr, err := xdr.MarshalBase64(xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &symbols[i]}) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 5, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr}, - ValueXDR: expectedXdr, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - TxIndex: uint32(i + 1), - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 5, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr}, + ValueXDR: expectedXdr, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + TxIndex: uint32(i + 1), + OpIndex: 0, }) } cursor := expected[len(expected)-1].ID From c81f9d786026f40f001136125015c9fa2c19e88b Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 4 Sep 2026 13:58:32 -0400 Subject: [PATCH 21/35] bump sdk to version with SHA d8c8acf --- go.mod | 23 +++++++++++++++++++++++ go.sum | 58 ++++++++++++++++++++++++++++------------------------------ 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index b1cd757dc..4edc89931 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,10 @@ require ( github.com/edsrzf/mmap-go v1.2.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect +<<<<<<< HEAD github.com/go-gorp/gorp/v3 v3.1.0 // indirect +======= +>>>>>>> af569551 (bump sdk to version with SHA d8c8acf) github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect @@ -81,8 +84,13 @@ require ( github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect +<<<<<<< HEAD go.opentelemetry.io/otel/sdk v1.46.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect +======= + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect +>>>>>>> af569551 (bump sdk to version with SHA d8c8acf) go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect ) @@ -141,14 +149,25 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect +<<<<<<< HEAD go.opentelemetry.io/otel v1.46.0 // indirect go.opentelemetry.io/otel/metric v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect +======= + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect +>>>>>>> af569551 (bump sdk to version with SHA d8c8acf) go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.55.0 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect +<<<<<<< HEAD golang.org/x/mod v0.38.0 golang.org/x/net v0.58.0 // indirect +======= + golang.org/x/mod v0.35.0 + golang.org/x/net v0.55.0 // indirect +>>>>>>> af569551 (bump sdk to version with SHA d8c8acf) golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 @@ -158,7 +177,11 @@ require ( google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect +<<<<<<< HEAD google.golang.org/grpc v1.83.2 // indirect +======= + google.golang.org/grpc v1.83.1 // indirect +>>>>>>> af569551 (bump sdk to version with SHA d8c8acf) google.golang.org/protobuf v1.36.11 // indirect gopkg.in/djherbis/atime.v1 v1.0.0 // indirect gopkg.in/djherbis/stream.v1 v1.3.1 // indirect diff --git a/go.sum b/go.sum index 2828a53be..254f714bc 100644 --- a/go.sum +++ b/go.sum @@ -159,8 +159,8 @@ github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpj github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= -github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -364,8 +364,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= -github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -402,48 +402,46 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= -go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= -go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= -go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU= -go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4= -go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= -go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= -go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= -go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= -go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= -go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= -go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= @@ -461,12 +459,12 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -495,8 +493,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= +google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 0736b1a2e47bef7811526c3e388a4195fb9118be Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 4 Sep 2026 14:10:12 -0400 Subject: [PATCH 22/35] move getTransactions differential suite to its own PR Co-Authored-By: Claude Fable 5.1 --- .../get_transactions_differential_test.go | 887 ------------------ 1 file changed, 887 deletions(-) delete mode 100644 cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go deleted file mode 100644 index e0b5e2793..000000000 --- a/cmd/stellar-rpc/internal/methods/get_transactions_differential_test.go +++ /dev/null @@ -1,887 +0,0 @@ -package methods - -import ( - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "strconv" - "testing" - - "github.com/creachadair/jrpc2" - "github.com/stretchr/testify/require" - - "github.com/stellar/go-stellar-sdk/ingest" - "github.com/stellar/go-stellar-sdk/network" - protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" - "github.com/stellar/go-stellar-sdk/support/log" - "github.com/stellar/go-stellar-sdk/toid" - "github.com/stellar/go-stellar-sdk/xdr" - - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/host" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/rpcv1/sqlitedb" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/store" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" -) - -// This file is the correctness harness for the getTransactions page loop's -// move from "unmarshal the whole LedgerCloseMeta, then range over it" to "walk -// the raw bytes through the SDK's zero-copy views". -// -// legacyGetTransactionsByLedgerSequence and legacyProcessTransactionsInLedger -// below are the decode-based extraction as it stood before that change, -// reconstructed here rather than left behind in the production file: they -// exist only as the differential's reference, and the tests assert that the -// two paths' protocol.GetTransactionsResponse values serialize to byte- -// identical JSON over a corpus that sweeps meta versions, envelope shapes, -// event shapes, page boundaries, empty ledgers and cursor round-trips. - -// legacyGetTransactionsByLedgerSequence is the pre-view-walk pagination loop: -// same handler, same cursor math, but reading each ledger through -// LedgerReaderTx.GetLedger's decoded xdr.LedgerCloseMeta. -func legacyGetTransactionsByLedgerSequence( - ctx context.Context, h transactionsRPCHandler, request protocol.GetTransactionsRequest, -) (protocol.GetTransactionsResponse, error) { - readTx, err := h.ledgerReader.NewTx(ctx) - if err != nil { - return protocol.GetTransactionsResponse{}, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} - } - defer func() { _ = readTx.Done() }() - - ledgerRange, err := readTx.GetLedgerRange(ctx) - if err != nil { - return protocol.GetTransactionsResponse{}, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} - } - - if err := request.IsValid(h.maxLimit, ledgerRange.ToLedgerSeqRange()); err != nil { - return protocol.GetTransactionsResponse{}, &jrpc2.Error{Code: jrpc2.InvalidRequest, Message: err.Error()} - } - - start, requestCursor, limit, err := h.initializePagination(request) - if err != nil { - return protocol.GetTransactionsResponse{}, err - } - - txns := make([]protocol.TransactionInfo, 0, limit) - var done bool - cursor := toid.New(0, 0, 0) - endLedger := min(int64(ledgerRange.LastLedger.Sequence), int64(start.LedgerSequence)+LedgerScanLimit-1) - for ledgerSeq := start.LedgerSequence; int64(ledgerSeq) <= endLedger; ledgerSeq++ { - if ledgerSeq < 0 { - return protocol.GetTransactionsResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: "cursor ledger sequence cannot be negative", - } - } - ledger, found, gerr := readTx.GetLedger(ctx, uint32(ledgerSeq)) - if gerr != nil { - return protocol.GetTransactionsResponse{}, &jrpc2.Error{Code: jrpc2.InternalError, Message: gerr.Error()} - } else if !found { - return protocol.GetTransactionsResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: fmt.Sprintf("database does not contain metadata for ledger: %d", ledgerSeq), - } - } - - cursor, done, err = legacyProcessTransactionsInLedger(h, ledger, start, &txns, limit, request.Format) - if err != nil { - return protocol.GetTransactionsResponse{}, err - } - if done { - break - } - } - - if requestCursor != nil && cursor.ToInt64() < requestCursor.ToInt64() { - cursor = requestCursor - } - - return protocol.GetTransactionsResponse{ - Transactions: txns, - LatestLedger: ledgerRange.LastLedger.Sequence, - LatestLedgerCloseTime: ledgerRange.LastLedger.CloseTime, - OldestLedger: ledgerRange.FirstLedger.Sequence, - OldestLedgerCloseTime: ledgerRange.FirstLedger.CloseTime, - Cursor: cursor.String(), - }, nil -} - -// legacyProcessTransactionsInLedger is the pre-view-walk per-ledger extraction: -// ingest.NewLedgerTransactionReaderFromLedgerCloseMeta over a decoded ledger, -// then store.ParseTransaction per transaction. Everything downstream of that — -// rendering a store.Transaction into a page entry — is the production -// renderer, unchanged by this migration and shared here deliberately. -func legacyProcessTransactionsInLedger( - h transactionsRPCHandler, ledger xdr.LedgerCloseMeta, start toid.ID, - txns *[]protocol.TransactionInfo, limit uint, format string, -) (*toid.ID, bool, error) { - limitInt, err := strconv.Atoi(strconv.FormatUint(uint64(limit), 10)) - if err != nil { - return nil, false, &jrpc2.Error{Code: jrpc2.InvalidParams, Message: err.Error()} - } - - reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(h.networkPassphrase, ledger) - if err != nil { - return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} - } - - startTxIdx := 1 - ledgerSeq := ledger.LedgerSequence() - ledgerSeqInt32, err := uint32ToInt32(ledgerSeq, "ledger sequence") - if err != nil { - return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: err.Error()} - } - if ledgerSeqInt32 == start.LedgerSequence { - startTxIdx = int(start.TransactionOrder) - if ierr := reader.Seek(startTxIdx - 1); ierr != nil && !errors.Is(ierr, io.EOF) { - return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: ierr.Error()} - } - } - - txCount := ledger.CountTransactions() - cursor := toid.New(ledgerSeqInt32, 0, 1) - for i := startTxIdx; i <= txCount; i++ { - cursor.TransactionOrder = int32(i) - - ingestTx, rerr := reader.Read() - if rerr != nil { - if errors.Is(rerr, io.EOF) { - break - } - return nil, false, &jrpc2.Error{Code: jrpc2.InvalidParams, Message: rerr.Error()} - } - - tx, perr := legacyParseTransaction(ledger, ingestTx) - if perr != nil { - return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: perr.Error()} - } - - // The renderer is shared with the production path on purpose: it is - // pure formatting of a store.Transaction and this change does not - // touch it, so what the differential compares is the EXTRACTION that - // produced tx, not the rendering of it. - txInfo, ferr := transactionInfo(tx, format) - if ferr != nil { - return nil, false, &jrpc2.Error{Code: jrpc2.InternalError, Message: ferr.Error()} - } - - *txns = append(*txns, txInfo) - if len(*txns) >= limitInt { - return cursor, true, nil - } - } - - return cursor, false, nil -} - -// legacyParseTransaction is the decode-based store.ParseTransaction as it -// stood before the view migration, frozen here as the reference extraction. -func legacyParseTransaction(lcm xdr.LedgerCloseMeta, ingestTx ingest.LedgerTransaction) (store.Transaction, error) { - var tx store.Transaction - var err error - - tx.FeeBump = ingestTx.Envelope.IsFeeBump() - tx.ApplicationOrder = int32(ingestTx.Index) - tx.Successful = ingestTx.Result.Successful() - tx.Ledger = store.LedgerInfo{Sequence: lcm.LedgerSequence(), CloseTime: lcm.LedgerCloseTime()} - tx.TransactionHash = ingestTx.Result.TransactionHash.HexString() - - if tx.Result, err = ingestTx.Result.Result.MarshalBinary(); err != nil { - return tx, err - } - if tx.Meta, err = ingestTx.UnsafeMeta.MarshalBinary(); err != nil { - return tx, err - } - if tx.Envelope, err = ingestTx.Envelope.MarshalBinary(); err != nil { - return tx, err - } - - allEvents, err := ingestTx.GetTransactionEvents() - if err != nil { - return tx, err - } - diagEvents, err := ingestTx.GetDiagnosticEvents() - if err != nil { - return tx, err - } - tx.Events = make([][]byte, 0, len(diagEvents)) - for _, event := range diagEvents { - raw, ierr := event.MarshalBinary() - if ierr != nil { - return tx, ierr - } - tx.Events = append(tx.Events, raw) - } - tx.TransactionEvents = make([][]byte, 0, len(allEvents.TransactionEvents)) - for _, event := range allEvents.TransactionEvents { - raw, ierr := event.MarshalBinary() - if ierr != nil { - return tx, ierr - } - tx.TransactionEvents = append(tx.TransactionEvents, raw) - } - tx.ContractEvents = make([][][]byte, 0, len(allEvents.OperationEvents)) - for _, opEvents := range allEvents.OperationEvents { - events := make([][]byte, 0, len(opEvents)) - for _, event := range opEvents { - raw, ierr := event.MarshalBinary() - if ierr != nil { - return tx, ierr - } - events = append(events, raw) - } - tx.ContractEvents = append(tx.ContractEvents, events) - } - return tx, nil -} - -// -// ---- the corpus ---- -// - -// diffTxSpec is one transaction in a corpus ledger: an envelope shape paired -// with an apply-processing meta. -type diffTxSpec struct { - envelope xdr.TransactionEnvelope - meta xdr.TransactionMeta - succeeds bool -} - -func diffSymbolVal() xdr.ScVal { - sym := xdr.ScSymbol("COUNTER") - return xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &sym} -} - -func diffContractEvent() xdr.ContractEvent { - val := diffSymbolVal() - id := xdr.ContractId{7} - return xdr.ContractEvent{ - ContractId: &id, - Type: xdr.ContractEventTypeContract, - Body: xdr.ContractEventBody{ - V: 0, - V0: &xdr.ContractEventV0{Topics: []xdr.ScVal{val}, Data: val}, - }, - } -} - -// diffClassicEnvelope is a plain (non-Soroban) v1 envelope: Tx.Ext stays at -// discriminant 0, which is what makes it classic. -func diffClassicEnvelope(acctSeq uint32) xdr.TransactionEnvelope { - env, err := xdr.NewTransactionEnvelope(xdr.EnvelopeTypeEnvelopeTypeTx, xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - Fee: 1, - SeqNum: xdr.SequenceNumber(acctSeq), - SourceAccount: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), - }, - }) - if err != nil { - panic(err) - } - return env -} - -// diffFeeBumpEnvelope wraps inner in a fee bump. -func diffFeeBumpEnvelope(inner xdr.TransactionEnvelope) xdr.TransactionEnvelope { - return xdr.TransactionEnvelope{ - Type: xdr.EnvelopeTypeEnvelopeTypeTxFeeBump, - FeeBump: &xdr.FeeBumpTransactionEnvelope{ - Tx: xdr.FeeBumpTransaction{ - FeeSource: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), - Fee: 200, - InnerTx: xdr.FeeBumpTransactionInnerTx{ - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: inner.V1, - }, - }, - }, - } -} - -func diffMetaV1() xdr.TransactionMeta { - return xdr.TransactionMeta{V: 1, V1: &xdr.TransactionMetaV1{Operations: []xdr.OperationMeta{}}} -} - -// diffMetaV3NoSoroban is the straggler corner: a V3 meta with no SorobanMeta -// at all. Paired with a Soroban envelope it is the one shape where the SDK's -// view extractor historically disagreed with the parsed reader on -// operation-slice arity, so the corpus pins it deliberately. -func diffMetaV3NoSoroban() xdr.TransactionMeta { - return xdr.TransactionMeta{V: 3, Operations: &[]xdr.OperationMeta{}, V3: &xdr.TransactionMetaV3{}} -} - -func diffMetaV3WithEvents(events []xdr.ContractEvent, diags []xdr.DiagnosticEvent) xdr.TransactionMeta { - return xdr.TransactionMeta{ - V: 3, - Operations: &[]xdr.OperationMeta{}, - V3: &xdr.TransactionMetaV3{SorobanMeta: &xdr.SorobanTransactionMeta{ - Events: events, - DiagnosticEvents: diags, - ReturnValue: diffSymbolVal(), - }}, - } -} - -func diffMetaV4(ops []xdr.OperationMetaV2, txEvents []xdr.TransactionEvent, - diags []xdr.DiagnosticEvent, -) xdr.TransactionMeta { - return xdr.TransactionMeta{V: 4, V4: &xdr.TransactionMetaV4{ - Operations: ops, - Events: txEvents, - DiagnosticEvents: diags, - }} -} - -// diffResultFor builds the TransactionResultPair for spec: a fee bump carries -// an inner result pair, a plain transaction does not. -func diffResultFor(t *testing.T, spec diffTxSpec) xdr.TransactionResultPair { - t.Helper() - hash, err := network.HashTransactionInEnvelope(spec.envelope, NetworkPassphrase) - require.NoError(t, err) - - code := xdr.TransactionResultCodeTxSuccess - if !spec.succeeds { - code = xdr.TransactionResultCodeTxBadSeq - } - opResults := []xdr.OperationResult{} - res := xdr.TransactionResultResult{Code: code, Results: &opResults} - - if spec.envelope.Type == xdr.EnvelopeTypeEnvelopeTypeTxFeeBump { - innerHash, ierr := network.HashTransactionInEnvelope(xdr.TransactionEnvelope{ - Type: xdr.EnvelopeTypeEnvelopeTypeTx, V1: spec.envelope.FeeBump.Tx.InnerTx.V1, - }, NetworkPassphrase) - require.NoError(t, ierr) - outer := xdr.TransactionResultCodeTxFeeBumpInnerSuccess - if !spec.succeeds { - outer = xdr.TransactionResultCodeTxFeeBumpInnerFailed - } - res = xdr.TransactionResultResult{ - Code: outer, - InnerResultPair: &xdr.InnerTransactionResultPair{ - TransactionHash: innerHash, - Result: xdr.InnerTransactionResult{ - FeeCharged: 100, - Result: xdr.InnerTransactionResultResult{Code: code, Results: &opResults}, - }, - }, - } - } - - return xdr.TransactionResultPair{ - TransactionHash: hash, - Result: xdr.TransactionResult{FeeCharged: 100, Result: res}, - } -} - -// diffLCM assembles a LedgerCloseMeta of the given wire version (1 or 2) at -// sequence seq holding specs. Both versions matter: their TxProcessing arrays -// are different element types, which the view dispatcher walks with different -// code. -func diffLCM(t *testing.T, version int32, seq uint32, specs ...diffTxSpec) xdr.LedgerCloseMeta { - t.Helper() - envs := make([]xdr.TransactionEnvelope, 0, len(specs)) - for _, spec := range specs { - envs = append(envs, spec.envelope) - } - components := []xdr.TxSetComponent{{ - Type: xdr.TxSetComponentTypeTxsetCompTxsMaybeDiscountedFee, - TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{Txs: envs}, - }} - header := xdr.LedgerHeaderHistoryEntry{Header: xdr.LedgerHeader{ - ScpValue: xdr.StellarValue{CloseTime: xdr.TimePoint(ledgerCloseTime(seq))}, - LedgerSeq: xdr.Uint32(seq), - }} - txSet := xdr.GeneralizedTransactionSet{V: 1, V1TxSet: &xdr.TransactionSetV1{ - PreviousLedgerHash: xdr.Hash{1}, - Phases: []xdr.TransactionPhase{{V: 0, V0Components: &components}}, - }} - - if version == 1 { - proc := make([]xdr.TransactionResultMeta, 0, len(specs)) - for _, spec := range specs { - proc = append(proc, xdr.TransactionResultMeta{ - Result: diffResultFor(t, spec), - TxApplyProcessing: spec.meta, - }) - } - return xdr.LedgerCloseMeta{V: 1, V1: &xdr.LedgerCloseMetaV1{ - LedgerHeader: header, TxSet: txSet, TxProcessing: proc, - }} - } - - proc := make([]xdr.TransactionResultMetaV1, 0, len(specs)) - for _, spec := range specs { - proc = append(proc, xdr.TransactionResultMetaV1{ - Result: diffResultFor(t, spec), - TxApplyProcessing: spec.meta, - }) - } - return xdr.LedgerCloseMeta{V: 2, V2: &xdr.LedgerCloseMetaV2{ - LedgerHeader: header, TxSet: txSet, TxProcessing: proc, - }} -} - -// differentialCorpus is the contiguous ledger run [corpusFirstLedger, -// corpusLastLedger] the differential sweeps over. -// Every ledger is a deliberate shape; the comment on each says which axis it -// covers. Account sequence numbers are unique across the corpus so no two -// transactions share an envelope hash (the TxSet is paired to TxProcessing by -// hash, so a collision would hide a mispairing). -func differentialCorpus(t *testing.T) []xdr.LedgerCloseMeta { - t.Helper() - ev := diffContractEvent() - diag := xdr.DiagnosticEvent{InSuccessfulContractCall: true, Event: ev} - failedDiag := xdr.DiagnosticEvent{InSuccessfulContractCall: false, Event: ev} - txEvent := xdr.TransactionEvent{ - Stage: xdr.TransactionEventStageTransactionEventStageAfterAllTxs, - Event: ev, - } - - return []xdr.LedgerCloseMeta{ - // 1: the shape the pre-existing tests use — one TxSet envelope, two - // TxProcessing entries sharing its hash, V3 meta with no SorobanMeta - // on a Soroban envelope (the straggler corner). - createTestLedger(101), - // 2: LCM V1, classic envelopes, V1 meta — no events anywhere. - diffLCM(t, 1, 102, - diffTxSpec{diffClassicEnvelope(200), diffMetaV1(), true}, - diffTxSpec{diffClassicEnvelope(201), diffMetaV1(), false}, - ), - // 3: an empty ledger mid-corpus — the page must walk straight past it. - createEmptyTestLedger(103), - // 4: LCM V2, Soroban envelope, V3 meta WITH SorobanMeta: contract - // events and diagnostic events both present. - diffLCM(t, 2, 104, - diffTxSpec{txEnvelope(202), diffMetaV3WithEvents( - []xdr.ContractEvent{ev, ev}, []xdr.DiagnosticEvent{diag, failedDiag}), true}, - ), - // 5: V3 SorobanMeta present but with no events at all — the "with - // SorobanMeta, empty lists" case, distinct from the absent one. - diffLCM(t, 2, 105, - diffTxSpec{txEnvelope(203), diffMetaV3WithEvents(nil, nil), true}, - ), - // 6: V4 meta — per-operation events across two operations, top-level - // transaction events, and diagnostics. - diffLCM(t, 2, 106, - diffTxSpec{diffClassicEnvelope(204), diffMetaV4( - []xdr.OperationMetaV2{{Events: []xdr.ContractEvent{ev}}, {}}, - []xdr.TransactionEvent{txEvent}, - []xdr.DiagnosticEvent{diag}, - ), true}, - diffTxSpec{diffClassicEnvelope(205), diffMetaV4(nil, nil, nil), false}, - ), - // 7: fee bumps — over a classic inner and over a Soroban inner, one - // succeeding and one failing, on V4 and V3 metas. - diffLCM(t, 2, 107, - diffTxSpec{ - diffFeeBumpEnvelope(diffClassicEnvelope(206)), - diffMetaV4([]xdr.OperationMetaV2{{Events: []xdr.ContractEvent{ev}}}, nil, nil), true, - }, - diffTxSpec{ - diffFeeBumpEnvelope(txEnvelope(207)), - diffMetaV3WithEvents([]xdr.ContractEvent{ev}, []xdr.DiagnosticEvent{diag}), false, - }, - diffTxSpec{diffFeeBumpEnvelope(txEnvelope(208)), diffMetaV3NoSoroban(), true}, - ), - // 8: five transactions in one ledger, so a limit can land anywhere - // inside it and the next page has to resume mid-ledger. - diffLCM(t, 2, 108, - diffTxSpec{diffClassicEnvelope(209), diffMetaV1(), true}, - diffTxSpec{txEnvelope(210), diffMetaV3NoSoroban(), false}, - diffTxSpec{txEnvelope(211), diffMetaV3WithEvents([]xdr.ContractEvent{ev}, nil), true}, - diffTxSpec{diffClassicEnvelope(212), diffMetaV4( - []xdr.OperationMetaV2{{Events: []xdr.ContractEvent{ev, ev}}}, nil, - []xdr.DiagnosticEvent{diag}), true}, - diffTxSpec{diffFeeBumpEnvelope(diffClassicEnvelope(213)), diffMetaV1(), false}, - ), - // 9: LCM V1 carrying a V3 meta — the older ledger envelope with a - // newer transaction meta, which real history contains. - diffLCM(t, 1, 109, - diffTxSpec{txEnvelope(214), diffMetaV3WithEvents( - []xdr.ContractEvent{ev}, []xdr.DiagnosticEvent{diag}), true}, - diffTxSpec{diffClassicEnvelope(215), diffMetaV3NoSoroban(), true}, - ), - // 10: another empty ledger, this time at the tip, so a walk that runs - // off the end of the corpus ends on one. - createEmptyTestLedger(110), - } -} - -// corpusFirstLedger / corpusLastLedger bracket differentialCorpus. The run -// starts at 101 because the shared fixture helpers (createTestLedger, -// createEmptyTestLedger) offset their sequences by 100. -const ( - corpusFirstLedger = 101 - corpusLastLedger = 110 -) - -// setupDifferentialDB writes differentialCorpus into a fresh sqlite store. -func setupDifferentialDB(t *testing.T) *sqlitedb.DB { - t.Helper() - corpus := differentialCorpus(t) - require.Len(t, corpus, corpusLastLedger-corpusFirstLedger+1) - testDB := NewTestDB(t) - daemon := host.MakeNoOpDaemon() - for i, lcm := range corpus { - require.Equal(t, uint32(corpusFirstLedger+i), lcm.LedgerSequence(), - "the corpus must be one contiguous run") - tx, err := sqlitedb.NewReadWriter(log.DefaultLogger, testDB, daemon, 100, passphrase).NewTx(t.Context()) - require.NoError(t, err) - require.NoError(t, tx.LedgerWriter().InsertLedger(lcm)) - require.NoError(t, tx.Commit(lcm, nil)) - } - return testDB -} - -func differentialHandler(testDB *sqlitedb.DB) transactionsRPCHandler { - return transactionsRPCHandler{ - ledgerReader: sqlitedb.NewLedgerReader(testDB), - maxLimit: 100, - defaultLimit: 10, - networkPassphrase: NetworkPassphrase, - } -} - -// assertSameResponse runs both extractions over the same request and asserts -// their responses serialize identically, byte for byte. -func assertSameResponse( - t *testing.T, h transactionsRPCHandler, request protocol.GetTransactionsRequest, -) protocol.GetTransactionsResponse { - t.Helper() - wantResp, wantErr := legacyGetTransactionsByLedgerSequence(context.TODO(), h, request) - gotResp, gotErr := h.getTransactionsByLedgerSequence(context.TODO(), request) - - if wantErr != nil { - require.Error(t, gotErr) - require.Equal(t, wantErr.Error(), gotErr.Error()) - return gotResp - } - require.NoError(t, gotErr) - - wantJSON, err := json.Marshal(wantResp) - require.NoError(t, err) - gotJSON, err := json.Marshal(gotResp) - require.NoError(t, err) - // Byte equality, deliberately, not require.JSONEq's semantic equality: the - // point of the differential is that the wire bytes are unchanged, and - // JSONEq would forgive a field that appeared, vanished, or reordered. - require.Equal(t, string(wantJSON), string(gotJSON)) //nolint:testifylint // see above - return gotResp -} - -// TestGetTransactions_ViewWalkMatchesParsedPath sweeps start ledgers, page -// limits and both response formats over the corpus, asserting byte-identical -// responses. The limits are chosen so pages end inside a ledger as well as on -// its boundary. -func TestGetTransactions_ViewWalkMatchesParsedPath(t *testing.T) { - testDB := setupDifferentialDB(t) - h := differentialHandler(testDB) - - formats := []string{"", protocol.FormatJSON} - limits := []uint{1, 2, 3, 4, 5, 7, 11, 100} - - for _, format := range formats { - for startLedger := corpusFirstLedger; startLedger <= corpusLastLedger; startLedger++ { - for _, limit := range limits { - name := fmt.Sprintf("format=%q/start=%d/limit=%d", format, startLedger, limit) - t.Run(name, func(t *testing.T) { - assertSameResponse(t, h, protocol.GetTransactionsRequest{ - Format: format, - StartLedger: uint32(startLedger), - Pagination: &protocol.LedgerPaginationOptions{Limit: limit}, - }) - }) - } - } - } -} - -// TestGetTransactions_ViewWalkMatchesParsedPath_DefaultPagination covers the -// requests that carry no pagination block at all, so the handler's default -// limit applies. -func TestGetTransactions_ViewWalkMatchesParsedPath_DefaultPagination(t *testing.T) { - testDB := setupDifferentialDB(t) - h := differentialHandler(testDB) - - for _, format := range []string{"", protocol.FormatJSON} { - for startLedger := corpusFirstLedger; startLedger <= corpusLastLedger; startLedger++ { - t.Run(fmt.Sprintf("format=%q/start=%d", format, startLedger), func(t *testing.T) { - assertSameResponse(t, h, protocol.GetTransactionsRequest{ - Format: format, - StartLedger: uint32(startLedger), - }) - }) - } - } -} - -// TestGetTransactions_ViewWalkCursorRoundTrip pages through the whole corpus -// with each extraction driving its OWN cursor chain, so a cursor that differed -// by one would send the two paths down diverging pages. Every page must match -// byte for byte, and both must terminate on the same page count. -func TestGetTransactions_ViewWalkCursorRoundTrip(t *testing.T) { - testDB := setupDifferentialDB(t) - h := differentialHandler(testDB) - - for _, limit := range []uint{1, 2, 3, 5} { - t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) { - request := protocol.GetTransactionsRequest{ - StartLedger: corpusFirstLedger, - Pagination: &protocol.LedgerPaginationOptions{Limit: limit}, - } - legacyReq, viewReq := request, request - - var legacyPages, viewPages int - for { - want, err := legacyGetTransactionsByLedgerSequence(context.TODO(), h, legacyReq) - require.NoError(t, err) - got, err := h.getTransactionsByLedgerSequence(context.TODO(), viewReq) - require.NoError(t, err) - - wantJSON, err := json.Marshal(want) - require.NoError(t, err) - gotJSON, err := json.Marshal(got) - require.NoError(t, err) - //nolint:testifylint // byte equality, not JSONEq's semantic equality - require.Equal(t, string(wantJSON), string(gotJSON), "page %d", legacyPages) - - legacyPages++ - viewPages++ - if len(want.Transactions) == 0 { - break - } - require.Less(t, legacyPages, 200, "paging did not terminate") - - legacyReq = protocol.GetTransactionsRequest{ - Pagination: &protocol.LedgerPaginationOptions{Cursor: want.Cursor, Limit: limit}, - } - viewReq = protocol.GetTransactionsRequest{ - Pagination: &protocol.LedgerPaginationOptions{Cursor: got.Cursor, Limit: limit}, - } - } - require.Equal(t, legacyPages, viewPages) - require.Greater(t, legacyPages, 1, "the corpus must take more than one page") - }) - } -} - -// TestGetTransactions_ViewWalkMatchesParsedPath_Cursors starts each request -// from an explicit cursor, including cursors that land mid-ledger, on a -// ledger's last transaction, past a ledger's last transaction, and past the -// tip (where the request's own cursor has to be echoed back). -func TestGetTransactions_ViewWalkMatchesParsedPath_Cursors(t *testing.T) { - testDB := setupDifferentialDB(t) - h := differentialHandler(testDB) - - for ledger := corpusFirstLedger; ledger <= corpusLastLedger; ledger++ { - for txOrder := range 7 { - for _, limit := range []uint{1, 3, 10} { - cursor := toid.New(int32(ledger), int32(txOrder), 1).String() - name := fmt.Sprintf("cursor=%d.%d/limit=%d", ledger, txOrder, limit) - t.Run(name, func(t *testing.T) { - assertSameResponse(t, h, protocol.GetTransactionsRequest{ - Pagination: &protocol.LedgerPaginationOptions{Cursor: cursor, Limit: limit}, - }) - }) - } - } - } -} - -// TestGetTransactions_ViewWalkMatchesParsedPath_EmptyLedgersOnly pins the -// all-empty corpus separately: with no transaction anywhere, the response is -// driven entirely by the cursor math, which the view walk must not have -// shifted. -func TestGetTransactions_ViewWalkMatchesParsedPath_EmptyLedgersOnly(t *testing.T) { - testDB := setupDBNoTxs(t, 5) - h := differentialHandler(testDB) - - for start := 1; start <= 5; start++ { - for _, limit := range []uint{1, 10} { - t.Run(fmt.Sprintf("start=%d/limit=%d", start, limit), func(t *testing.T) { - assertSameResponse(t, h, protocol.GetTransactionsRequest{ - StartLedger: uint32(start), - Pagination: &protocol.LedgerPaginationOptions{Limit: limit}, - }) - }) - } - } -} - -// TestGetTransactions_ViewWalkCorpusIsNotVacuous guards the differential -// itself: a corpus that silently produced no transactions, or only one shape -// of them, would make every comparison above pass for the wrong reason. -func TestGetTransactions_ViewWalkCorpusIsNotVacuous(t *testing.T) { - testDB := setupDifferentialDB(t) - h := differentialHandler(testDB) - - resp, err := h.getTransactionsByLedgerSequence(context.TODO(), protocol.GetTransactionsRequest{ - StartLedger: corpusFirstLedger, - Pagination: &protocol.LedgerPaginationOptions{Limit: 100}, - }) - require.NoError(t, err) - require.GreaterOrEqual(t, len(resp.Transactions), 15, "the corpus must carry real transactions") - - var feeBumps, failed, withContractEvents, withTxEvents, withDiagnostics int - for _, tx := range resp.Transactions { - if tx.FeeBump { - feeBumps++ - } - if tx.Status == protocol.TransactionStatusFailed { - failed++ - } - for _, op := range tx.Events.ContractEventsXDR { - if len(op) > 0 { - withContractEvents++ - break - } - } - if len(tx.Events.TransactionEventsXDR) > 0 { - withTxEvents++ - } - if len(tx.DiagnosticEventsXDR) > 0 { - withDiagnostics++ - } - } - require.Positive(t, feeBumps, "fee-bump transactions") - require.Positive(t, failed, "failed transactions") - require.Positive(t, withContractEvents, "transactions with contract events") - require.Positive(t, withTxEvents, "transactions with transaction events") - require.Positive(t, withDiagnostics, "transactions with diagnostic events") -} - -// TestTransactionInfo_FieldMapping pins the renderer both extractions share. -// The differential above cannot see a bug in shared code — it would show up -// identically on both sides — so the renderer's field-by-field wiring is -// pinned here instead, with every byte source distinct so that crossing any -// two of them fails. -// -//nolint:funlen // one field-by-field pin; splitting it would scatter the wiring -func TestTransactionInfo_FieldMapping(t *testing.T) { - marshal := func(v interface{ MarshalBinary() ([]byte, error) }) []byte { - raw, err := v.MarshalBinary() - require.NoError(t, err) - return raw - } - symEvent := func(name string) xdr.ContractEvent { - sym := xdr.ScSymbol(name) - val := xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &sym} - id := xdr.ContractId{9} - return xdr.ContractEvent{ - ContractId: &id, - Type: xdr.ContractEventTypeContract, - Body: xdr.ContractEventBody{V: 0, V0: &xdr.ContractEventV0{Topics: []xdr.ScVal{val}, Data: val}}, - } - } - - result := xdr.TransactionResult{ - FeeCharged: 4242, - Result: xdr.TransactionResultResult{ - Code: xdr.TransactionResultCodeTxSuccess, - Results: &[]xdr.OperationResult{}, - }, - } - meta := diffMetaV4(nil, nil, nil) - envelope := diffClassicEnvelope(999) - diagnostic := xdr.DiagnosticEvent{InSuccessfulContractCall: true, Event: symEvent("DIAGNOSTIC")} - txEvent := xdr.TransactionEvent{ - Stage: xdr.TransactionEventStageTransactionEventStageAfterAllTxs, - Event: symEvent("TRANSACTION"), - } - opEventA, opEventB := symEvent("OPERATION_A"), symEvent("OPERATION_B") - - tx := store.Transaction{ - TransactionHash: "deadbeef", - Result: marshal(&result), - Meta: marshal(&meta), - Envelope: marshal(&envelope), - Events: [][]byte{marshal(&diagnostic)}, - TransactionEvents: [][]byte{marshal(&txEvent)}, - ContractEvents: [][][]byte{{marshal(&opEventA)}, {marshal(&opEventB)}}, - FeeBump: true, - ApplicationOrder: 7, - Successful: true, - Ledger: store.LedgerInfo{Sequence: 4242, CloseTime: 987}, - } - - // Every byte source distinct, so any crossed assignment below changes an - // encoding rather than reproducing it. - seen := map[string]string{} - for name, raw := range map[string][]byte{ - "result": tx.Result, "meta": tx.Meta, "envelope": tx.Envelope, - "diagnostic": tx.Events[0], "txEvent": tx.TransactionEvents[0], - "opA": tx.ContractEvents[0][0], "opB": tx.ContractEvents[1][0], - } { - encoded := base64.StdEncoding.EncodeToString(raw) - require.NotContains(t, seen, encoded, "%s collides with %s", name, seen[encoded]) - seen[encoded] = name - } - - t.Run("xdr", func(t *testing.T) { - got, err := transactionInfo(tx, "") - require.NoError(t, err) - - require.Equal(t, "deadbeef", got.TransactionHash) - require.Equal(t, int32(7), got.ApplicationOrder) - require.True(t, got.FeeBump) - require.Equal(t, uint32(4242), got.Ledger) - require.Equal(t, int64(987), got.LedgerCloseTime) - require.Equal(t, protocol.TransactionStatusSuccess, got.Status) - - b64 := base64.StdEncoding.EncodeToString - require.Equal(t, b64(tx.Result), got.ResultXDR) - require.Equal(t, b64(tx.Meta), got.ResultMetaXDR) - require.Equal(t, b64(tx.Envelope), got.EnvelopeXDR) - require.Equal(t, []string{b64(tx.Events[0])}, got.DiagnosticEventsXDR) - require.Equal(t, []string{b64(tx.TransactionEvents[0])}, got.Events.TransactionEventsXDR) - require.Equal(t, [][]string{ - {b64(tx.ContractEvents[0][0])}, - {b64(tx.ContractEvents[1][0])}, - }, got.Events.ContractEventsXDR) - - require.Empty(t, got.ResultJSON) - require.Empty(t, got.EnvelopeJSON) - require.Empty(t, got.ResultMetaJSON) - require.Empty(t, got.DiagnosticEventsJSON) - require.Empty(t, got.Events.ContractEventsJSON) - require.Empty(t, got.Events.TransactionEventsJSON) - }) - - t.Run("json", func(t *testing.T) { - failed := tx - failed.Successful = false - got, err := transactionInfo(failed, protocol.FormatJSON) - require.NoError(t, err) - require.Equal(t, protocol.TransactionStatusFailed, got.Status) - - // Each JSON field must be the conversion of ITS OWN source, so compare - // against the same converter run on that source directly. - wantJSON := func(typ any, raw []byte) string { - converted, cerr := xdr2json.ConvertBytes(typ, raw) - require.NoError(t, cerr) - return string(converted) - } - require.JSONEq(t, wantJSON(xdr.TransactionResult{}, tx.Result), string(got.ResultJSON)) - require.JSONEq(t, wantJSON(xdr.TransactionMeta{}, tx.Meta), string(got.ResultMetaJSON)) - require.JSONEq(t, wantJSON(xdr.TransactionEnvelope{}, tx.Envelope), string(got.EnvelopeJSON)) - require.Len(t, got.DiagnosticEventsJSON, 1) - require.JSONEq(t, wantJSON(xdr.DiagnosticEvent{}, tx.Events[0]), string(got.DiagnosticEventsJSON[0])) - require.Len(t, got.Events.TransactionEventsJSON, 1) - require.JSONEq(t, wantJSON(xdr.TransactionEvent{}, tx.TransactionEvents[0]), - string(got.Events.TransactionEventsJSON[0])) - require.Len(t, got.Events.ContractEventsJSON, 2) - require.JSONEq(t, wantJSON(xdr.ContractEvent{}, tx.ContractEvents[0][0]), - string(got.Events.ContractEventsJSON[0][0])) - require.JSONEq(t, wantJSON(xdr.ContractEvent{}, tx.ContractEvents[1][0]), - string(got.Events.ContractEventsJSON[1][0])) - - require.Empty(t, got.ResultXDR) - require.Empty(t, got.EnvelopeXDR) - require.Empty(t, got.ResultMetaXDR) - require.Empty(t, got.DiagnosticEventsXDR) - require.Empty(t, got.Events.ContractEventsXDR) - require.Empty(t, got.Events.TransactionEventsXDR) - }) -} From 7fe1731e3b7c21af264878b018643e4d665440a0 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 4 Sep 2026 15:06:12 -0400 Subject: [PATCH 23/35] remove stale reference to differential test suite --- cmd/stellar-rpc/internal/methods/get_transactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/stellar-rpc/internal/methods/get_transactions.go b/cmd/stellar-rpc/internal/methods/get_transactions.go index d325c845d..3509b6adb 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions.go @@ -171,7 +171,7 @@ func (h transactionsRPCHandler) processTransactionsInLedger( } // transactionInfo renders one extracted transaction as a page entry. Pure -// formatting — the differential test drives both extractions through it. +// formatting. func transactionInfo(tx store.Transaction, format string) (protocol.TransactionInfo, error) { txInfo := protocol.TransactionInfo{ TransactionDetails: protocol.TransactionDetails{ From 0202a08dea9c5420536e91d534a35c488f5834bb Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 8 Sep 2026 13:30:16 -0400 Subject: [PATCH 24/35] Merge branch feature/full-history into getTxByHash-views --- .../internal/methods/get_events_test.go | 140 +++++++++--------- cmd/stellar-rpc/internal/methods/mocks.go | 7 - .../internal/rpcv1/sqlitedb/ledger.go | 12 -- .../internal/rpcv1/sqlitedb/ledger_test.go | 54 ------- .../internal/rpcv2/adapters/ledger_reader.go | 13 -- go.mod | 23 --- go.sum | 58 ++++---- 7 files changed, 100 insertions(+), 207 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_events_test.go b/cmd/stellar-rpc/internal/methods/get_events_test.go index 2dbd60e36..2286905fe 100644 --- a/cmd/stellar-rpc/internal/methods/get_events_test.go +++ b/cmd/stellar-rpc/internal/methods/get_events_test.go @@ -146,16 +146,16 @@ func TestGetEvents(t *testing.T) { }) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - OpIndex: 0, - TxIndex: uint32(i + 1), + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + OpIndex: 0, + TxIndex: uint32(i + 1), }) } cursor := protocol.MaxCursor @@ -305,16 +305,16 @@ func TestGetEvents(t *testing.T) { require.NoError(t, err) expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{counterXdr, value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), - TxIndex: 5, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{counterXdr, value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), + TxIndex: 5, + OpIndex: 0, }, } @@ -496,16 +496,16 @@ func TestGetEvents(t *testing.T) { // flexible topic length matching expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{counterXdr, value, value, counterXdr}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), - TxIndex: 5, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{counterXdr, value, value, counterXdr}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), + TxIndex: 5, + OpIndex: 0, }, } require.NoError(t, err) @@ -670,16 +670,16 @@ func TestGetEvents(t *testing.T) { require.NoError(t, err) expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr, value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), - TxIndex: 4, - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr, value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), + TxIndex: 4, + OpIndex: 0, }, } cursor := protocol.MaxCursor @@ -758,16 +758,16 @@ func TestGetEvents(t *testing.T) { id := protocol.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 1}.String() expected := []protocol.EventInfo{ { - EventType: protocol.EventTypeSystem, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr}, - ValueXDR: counterXdr, - TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), - TxIndex: 1, - OpIndex: 0, + EventType: protocol.EventTypeSystem, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr}, + ValueXDR: counterXdr, + TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), + TxIndex: 1, + OpIndex: 0, }, } cursor := protocol.MaxCursor @@ -842,16 +842,16 @@ func TestGetEvents(t *testing.T) { value, err := xdr.MarshalBase64(txMeta[i].MustV3().SorobanMeta.Events[0].Body.MustV0().Data) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - TopicXDR: []string{value}, - ValueXDR: value, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - TxIndex: uint32(i + 1), - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 1, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ID: id, + TopicXDR: []string{value}, + ValueXDR: value, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + TxIndex: uint32(i + 1), + OpIndex: 0, }) } cursor := expected[len(expected)-1].ID @@ -954,16 +954,16 @@ func TestGetEvents(t *testing.T) { expectedXdr, err := xdr.MarshalBase64(xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &symbols[i]}) require.NoError(t, err) expected = append(expected, protocol.EventInfo{ - EventType: protocol.EventTypeContract, - Ledger: 5, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - TopicXDR: []string{counterXdr}, - ValueXDR: expectedXdr, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - TxIndex: uint32(i + 1), - OpIndex: 0, + EventType: protocol.EventTypeContract, + Ledger: 5, + LedgerClosedAt: now.Format(time.RFC3339), + ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), + ID: id, + TopicXDR: []string{counterXdr}, + ValueXDR: expectedXdr, + TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), + TxIndex: uint32(i + 1), + OpIndex: 0, }) } cursor := expected[len(expected)-1].ID diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index 8402406d7..de5377481 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -83,13 +83,6 @@ func (m *MockLedgerReaderTx) WithLedgerRaw( return args.Bool(0), args.Error(1) } -func (m *MockLedgerReaderTx) WithLedgerRaw( - ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, -) (bool, error) { - args := m.Called(ctx, sequence, fn) - return args.Bool(0), args.Error(1) -} - func (m *MockLedgerReaderTx) Done() error { args := m.Called() return args.Error(0) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 63072979a..3c7d61a24 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -104,18 +104,6 @@ func (l ledgerReaderTx) WithLedgerRaw( return true, fn(meta) } -// WithLedgerRaw lends the ledger's stored meta blob without decoding it. The -// blob is ours to lend: database/sql clones each BLOB scanned into a *[]byte. -func (l ledgerReaderTx) WithLedgerRaw( - ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, -) (bool, error) { - meta, found, err := getLedgerRawFromDB(ctx, l.tx, sequence) - if err != nil || !found { - return found, err - } - return true, fn(meta) -} - func (l ledgerReaderTx) Done() error { return l.tx.Rollback() } diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index f3cd8d44d..eb81dd7dc 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -3,7 +3,6 @@ package sqlitedb import ( "bytes" "context" - "errors" "io" "path" "testing" @@ -410,56 +409,3 @@ func setupBenchmarkingDB(b *testing.B) (*DB, []xdr.LedgerCloseMeta) { require.NoError(b, write.Commit(lcms[len(lcms)-1], nil)) return testDB, lcms } - -// TestWithLedgerRaw pins the raw accessor against the decoding one: the blob it -// lends must re-marshal to exactly what GetLedger decodes, an absent ledger -// must report found=false without running fn, and a callback error must come -// back verbatim with found=true. -func TestWithLedgerRaw(t *testing.T) { - db := NewTestDB(t) - daemon := host.MakeNoOpDaemon() - write := NewReadWriter(logger, db, daemon, 10, passphrase) - for seq := uint32(1); seq <= 3; seq++ { - tx, err := write.NewTx(t.Context()) - require.NoError(t, err) - lcm := createLedger(seq) - require.NoError(t, tx.LedgerWriter().InsertLedger(lcm)) - require.NoError(t, tx.Commit(lcm, nil)) - } - - reader := NewLedgerReader(db) - readTx, err := reader.NewTx(t.Context()) - require.NoError(t, err) - defer func() { _ = readTx.Done() }() - - for seq := uint32(1); seq <= 3; seq++ { - lcm, ok, err := readTx.GetLedger(t.Context(), seq) - require.NoError(t, err) - require.True(t, ok) - want, err := lcm.MarshalBinary() - require.NoError(t, err) - - var got []byte - found, err := readTx.WithLedgerRaw(t.Context(), seq, func(raw []byte) error { - got = append([]byte(nil), raw...) // the loan forbids retaining raw - return nil - }) - require.NoError(t, err) - require.True(t, found) - assert.Equal(t, want, got, "ledger %d", seq) - } - - ran := false - found, err := readTx.WithLedgerRaw(t.Context(), 99, func([]byte) error { - ran = true - return nil - }) - require.NoError(t, err) - assert.False(t, found) - assert.False(t, ran, "fn must not run for an absent ledger") - - boom := errors.New("boom") - found, err = readTx.WithLedgerRaw(t.Context(), 1, func([]byte) error { return boom }) - assert.ErrorIs(t, err, boom) - assert.True(t, found, "the ledger was there; only the callback failed") -} diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index 0162c10eb..4575c93a1 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -164,19 +164,6 @@ func (tx *ledgerReaderTx) WithLedgerRaw( return true, fn(entry.Bytes) } -// WithLedgerRaw is GetLedger without the decode or the clone: it lends the -// step's bytes straight from the chunk reader's scratch buffer, which the -// next step overwrites — fn must not retain them. -func (tx *ledgerReaderTx) WithLedgerRaw( - ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, -) (bool, error) { - entry, found, err := tx.walk(ctx, sequence) - if err != nil || !found { - return found, err - } - return true, fn(entry.Bytes) -} - func (tx *ledgerReaderTx) GetLedgerRange(_ context.Context) (store.LedgerRange, error) { return getLedgerRange(tx.view) } diff --git a/go.mod b/go.mod index 4edc89931..b1cd757dc 100644 --- a/go.mod +++ b/go.mod @@ -66,10 +66,7 @@ require ( github.com/edsrzf/mmap-go v1.2.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect -<<<<<<< HEAD github.com/go-gorp/gorp/v3 v3.1.0 // indirect -======= ->>>>>>> af569551 (bump sdk to version with SHA d8c8acf) github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect @@ -84,13 +81,8 @@ require ( github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect -<<<<<<< HEAD go.opentelemetry.io/otel/sdk v1.46.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect -======= - go.opentelemetry.io/otel/sdk v1.44.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect ->>>>>>> af569551 (bump sdk to version with SHA d8c8acf) go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect ) @@ -149,25 +141,14 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect -<<<<<<< HEAD go.opentelemetry.io/otel v1.46.0 // indirect go.opentelemetry.io/otel/metric v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect -======= - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect ->>>>>>> af569551 (bump sdk to version with SHA d8c8acf) go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.55.0 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect -<<<<<<< HEAD golang.org/x/mod v0.38.0 golang.org/x/net v0.58.0 // indirect -======= - golang.org/x/mod v0.35.0 - golang.org/x/net v0.55.0 // indirect ->>>>>>> af569551 (bump sdk to version with SHA d8c8acf) golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 @@ -177,11 +158,7 @@ require ( google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect -<<<<<<< HEAD google.golang.org/grpc v1.83.2 // indirect -======= - google.golang.org/grpc v1.83.1 // indirect ->>>>>>> af569551 (bump sdk to version with SHA d8c8acf) google.golang.org/protobuf v1.36.11 // indirect gopkg.in/djherbis/atime.v1 v1.0.0 // indirect gopkg.in/djherbis/stream.v1 v1.3.1 // indirect diff --git a/go.sum b/go.sum index 254f714bc..2828a53be 100644 --- a/go.sum +++ b/go.sum @@ -159,8 +159,8 @@ github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpj github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -364,8 +364,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -402,46 +402,48 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= -go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU= +go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4= +go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= +go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= @@ -459,12 +461,12 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -493,8 +495,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= -google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From c88a6eeb44ab355b2dddd6cd98050cbd5d8c38a8 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 8 Sep 2026 19:12:05 -0400 Subject: [PATCH 25/35] add test pinning legacy v0 meta behavior change --- .../internal/methods/get_transaction_test.go | 143 ++++++++++++++---- 1 file changed, 111 insertions(+), 32 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_transaction_test.go b/cmd/stellar-rpc/internal/methods/get_transaction_test.go index 4ac4e631c..b33fc3b28 100644 --- a/cmd/stellar-rpc/internal/methods/get_transaction_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transaction_test.go @@ -14,6 +14,7 @@ import ( "github.com/stellar/go-stellar-sdk/support/log" "github.com/stellar/go-stellar-sdk/xdr" + "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/host" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/rpcv1/sqlitedb" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" ) @@ -234,19 +235,10 @@ func txHash(acctSeq uint32) xdr.Hash { } func txEnvelope(acctSeq uint32) xdr.TransactionEnvelope { - envelope, err := xdr.NewTransactionEnvelope(xdr.EnvelopeTypeEnvelopeTypeTx, xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - Fee: 1, - SeqNum: xdr.SequenceNumber(acctSeq), - SourceAccount: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), - Ext: xdr.TransactionExt{ - V: 1, - SorobanData: &xdr.SorobanTransactionData{}, - }, - }, - }) - if err != nil { - panic(err) + envelope := classicTxEnvelope(acctSeq) + envelope.V1.Tx.Ext = xdr.TransactionExt{ + V: 1, + SorobanData: &xdr.SorobanTransactionData{}, } return envelope } @@ -267,25 +259,28 @@ func transactionResult(successful bool) xdr.TransactionResult { } func txMeta(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { - envelope := txEnvelope(acctSeq) - - txProcessing := []xdr.TransactionResultMetaV1{ - { - TxApplyProcessing: xdr.TransactionMeta{ - V: 3, - Operations: &[]xdr.OperationMeta{}, - // Soroban envelope with NO SorobanMeta: a Soroban tx charged - // but never executed (real on protocol 20-22 history). Pins - // the [[]] contractEventsXdr arity the view path must serve. - V3: &xdr.TransactionMetaV3{}, - }, - Result: xdr.TransactionResultPair{ - TransactionHash: txHash(acctSeq), - Result: transactionResult(successful), - }, + processing := xdr.TransactionResultMetaV1{ + TxApplyProcessing: xdr.TransactionMeta{ + V: 3, + Operations: &[]xdr.OperationMeta{}, + // Soroban envelope with NO SorobanMeta: a Soroban tx charged + // but never executed (real on protocol 20-22 history). Pins + // the [[]] contractEventsXdr arity the view path must serve. + V3: &xdr.TransactionMetaV3{}, + }, + Result: xdr.TransactionResultPair{ + TransactionHash: txHash(acctSeq), + Result: transactionResult(successful), }, } + return singleTxLedger(acctSeq+100, txEnvelope(acctSeq), processing) +} +// singleTxLedger assembles a one-transaction ledger from an envelope and its +// apply-processing pair. +func singleTxLedger( + ledgerSeq uint32, envelope xdr.TransactionEnvelope, processing xdr.TransactionResultMetaV1, +) xdr.LedgerCloseMeta { components := []xdr.TxSetComponent{ { Type: xdr.TxSetComponentTypeTxsetCompTxsMaybeDiscountedFee, @@ -303,12 +298,12 @@ func txMeta(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { LedgerHeader: xdr.LedgerHeaderHistoryEntry{ Header: xdr.LedgerHeader{ ScpValue: xdr.StellarValue{ - CloseTime: xdr.TimePoint(ledgerCloseTime(acctSeq + 100)), + CloseTime: xdr.TimePoint(ledgerCloseTime(ledgerSeq)), }, - LedgerSeq: xdr.Uint32(acctSeq + 100), + LedgerSeq: xdr.Uint32(ledgerSeq), }, }, - TxProcessing: txProcessing, + TxProcessing: []xdr.TransactionResultMetaV1{processing}, TxSet: xdr.GeneralizedTransactionSet{ V: 1, V1TxSet: &xdr.TransactionSetV1{ @@ -325,6 +320,90 @@ func txMeta(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { } } +// classicTxEnvelope is txEnvelope without SorobanTransactionData — the +// pre-Soroban shape that accompanies a legacy meta. +func classicTxEnvelope(acctSeq uint32) xdr.TransactionEnvelope { + envelope, err := xdr.NewTransactionEnvelope(xdr.EnvelopeTypeEnvelopeTypeTx, xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + Fee: 1, + SeqNum: xdr.SequenceNumber(acctSeq), + SourceAccount: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), + }, + }) + if err != nil { + panic(err) + } + return envelope +} + +// txMetaV0 is a one-transaction ledger whose TxApplyProcessing is legacy +// TransactionMeta V0, as on early pubnet history. +func txMetaV0(acctSeq uint32) (xdr.LedgerCloseMeta, xdr.Hash) { + envelope := classicTxEnvelope(acctSeq) + hash, err := network.HashTransactionInEnvelope(envelope, NetworkPassphrase) + if err != nil { + panic(err) + } + processing := xdr.TransactionResultMetaV1{ + TxApplyProcessing: xdr.TransactionMeta{V: 0, Operations: &[]xdr.OperationMeta{}}, + Result: xdr.TransactionResultPair{ + TransactionHash: hash, + Result: transactionResult(true), + }, + } + return singleTxLedger(acctSeq+100, envelope, processing), hash +} + +// TestTransactionsWithLegacyV0Meta pins an intentional divergence from the +// deleted decode-path extraction: TransactionMeta V0 (early pubnet history) is +// served as an event-free transaction, where GetTransactionEvents errored. +func TestTransactionsWithLegacyV0Meta(t *testing.T) { + testDB := NewTestDB(t) + lcm, hash := txMetaV0(1) // ledger 101 + write, err := sqlitedb.NewReadWriter( + log.DefaultLogger, testDB, host.MakeNoOpDaemon(), 100, NetworkPassphrase, + ).NewTx(t.Context()) + require.NoError(t, err) + require.NoError(t, write.LedgerWriter().InsertLedger(lcm)) + require.NoError(t, write.TransactionWriter().InsertTransactions(lcm)) + require.NoError(t, write.Commit(lcm, nil)) + + eventFree := protocol.Events{ContractEventsXDR: [][]string{}, TransactionEventsXDR: []string{}} + expectedMetaXDR, err := xdr.MarshalBase64(lcm.V2.TxProcessing[0].TxApplyProcessing) + require.NoError(t, err) + + requireServedEmpty := func(status, metaXDR string, diagnosticEventsXDR []string, events protocol.Events) { + require.Equal(t, protocol.TransactionStatusSuccess, status) + require.Equal(t, expectedMetaXDR, metaXDR) + require.Equal(t, []string{}, diagnosticEventsXDR) + require.Equal(t, eventFree, events) + } + + t.Run("getTransaction", func(t *testing.T) { + reader := sqlitedb.NewTransactionReader(log.DefaultLogger, testDB, NetworkPassphrase) + tx, err := GetTransaction(t.Context(), log.DefaultLogger, reader, + sqlitedb.NewLedgerReader(testDB), + protocol.GetTransactionRequest{Hash: hex.EncodeToString(hash[:])}) + require.NoError(t, err) + requireServedEmpty(tx.Status, tx.ResultMetaXDR, tx.DiagnosticEventsXDR, tx.Events) + }) + + t.Run("getTransactions", func(t *testing.T) { + handler := transactionsRPCHandler{ + ledgerReader: sqlitedb.NewLedgerReader(testDB), + maxLimit: 100, + defaultLimit: 10, + networkPassphrase: NetworkPassphrase, + } + response, err := handler.getTransactionsByLedgerSequence(t.Context(), + protocol.GetTransactionsRequest{StartLedger: 101}) + require.NoError(t, err) + require.Len(t, response.Transactions, 1) + got := response.Transactions[0] + requireServedEmpty(got.Status, got.ResultMetaXDR, got.DiagnosticEventsXDR, got.Events) + }) +} + func txMetaWithEvents(acctSeq uint32, successful bool) (xdr.LedgerCloseMeta, xdr.ContractEvent) { meta := txMeta(acctSeq, successful) From 6da5576f02db7083992bc43ac5883a8946828452 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 8 Sep 2026 19:21:36 -0400 Subject: [PATCH 26/35] update unused mock field --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index df6d9a9cf..33600cb79 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -17,7 +17,6 @@ type MockTransactionHandler struct { passphrase string ledgerRange store.LedgerRange - txs map[string]ingest.LedgerTransaction txHashToMeta map[string]*xdr.LedgerCloseMeta ledgerSeqToMeta map[uint32]*xdr.LedgerCloseMeta } @@ -25,7 +24,6 @@ type MockTransactionHandler struct { func NewMockTransactionStore(passphrase string) *MockTransactionHandler { return &MockTransactionHandler{ passphrase: passphrase, - txs: make(map[string]ingest.LedgerTransaction), txHashToMeta: make(map[string]*xdr.LedgerCloseMeta), ledgerSeqToMeta: make(map[uint32]*xdr.LedgerCloseMeta), } @@ -47,9 +45,7 @@ func (txn *MockTransactionHandler) InsertTransactions(lcm xdr.LedgerCloseMeta) e return err } - h := tx.Result.TransactionHash.HexString() - txn.txs[h] = tx - txn.txHashToMeta[h] = &lcm + txn.txHashToMeta[tx.Result.TransactionHash.HexString()] = &lcm } if lcmSeq := lcm.LedgerSequence(); lcmSeq < txn.ledgerRange.FirstLedger.Sequence || From 97c3919ce9c052990c0f397fd0bbe1cc5e575fb9 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 8 Sep 2026 19:24:08 -0400 Subject: [PATCH 27/35] update cursor behavior described in LedgerReaderTx comment --- cmd/stellar-rpc/internal/store/ledger.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index aff11a16b..c6676c898 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -50,10 +50,10 @@ type WithLedgerRawFn func(raw []byte) error // LedgerReaderTx is a read-only snapshot of the ledger store. Call Done to // release it. // -// GetLedger and WithLedgerRaw are one walk, not free-form point reads. Call -// them with ascending, contiguous sequences, starting from the first call's -// sequence, and use only one of the two per Tx: they share a single cursor, -// so interleaving them consumes positions from each other. Read at most +// GetLedger and WithLedgerRaw share one walk, not free-form point reads. Calls +// may use either accessor, but their combined sequence must be ascending and +// contiguous, starting from the first call's sequence, because both consume +// positions from the same cursor. Read at most // methods.LedgerScanLimit ledgers per Tx. The v1 (SQL) backend accepts any // pattern, while the v2 backend only walks from a forward iterator primed on // the first call. From 3799131911756d7ed7be2067875b8acf985ab215 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 11 Sep 2026 12:39:59 -0400 Subject: [PATCH 28/35] fix err when txnViewRange == 0 --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go index 5193f8c73..9c96119ef 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go @@ -208,7 +208,9 @@ func (txn *transactionHandler) getTransactionByHash(ctx context.Context, hash xd txIndex, ledgerSeq, hex.EncodeToString(hash[:]), err) } if len(txnViewRange) == 0 { - return store.Transaction{}, store.ErrNoTransaction + return store.Transaction{}, fmt.Errorf( + "application_order %d does not resolve to a transaction in ledger %d (txhash=%s): index/meta mismatch", + txIndex, ledgerSeq, hex.EncodeToString(hash[:])) } txView := txnViewRange[0] return store.ParseTransactionView(txView), nil From a77a2ed5febca1343678c039efee838b7bfe867d Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 11 Sep 2026 13:01:41 -0400 Subject: [PATCH 29/35] use transactionInfo in GetTransaction --- .../internal/methods/get_transaction.go | 61 +++---------------- 1 file changed, 10 insertions(+), 51 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_transaction.go b/cmd/stellar-rpc/internal/methods/get_transaction.go index d9becbf9c..499a15fa9 100644 --- a/cmd/stellar-rpc/internal/methods/get_transaction.go +++ b/cmd/stellar-rpc/internal/methods/get_transaction.go @@ -2,7 +2,6 @@ package methods import ( "context" - "encoding/base64" "encoding/hex" "errors" "fmt" @@ -16,7 +15,6 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/store" ) -//nolint:funlen func GetTransaction( ctx context.Context, log *log.Entry, @@ -63,15 +61,12 @@ func GetTransaction( LatestLedgerCloseTime: storeRange.LastLedger.CloseTime, OldestLedger: storeRange.FirstLedger.Sequence, OldestLedgerCloseTime: storeRange.FirstLedger.CloseTime, - TransactionDetails: protocol.TransactionDetails{ - TransactionHash: request.Hash, - }, } - if errors.Is(getTxErr, store.ErrNoTransaction) { + switch { + case errors.Is(getTxErr, store.ErrNoTransaction): response.Status = protocol.TransactionStatusNotFound - return response, nil - } else if getTxErr != nil { + case getTxErr != nil: log.WithError(getTxErr). WithField("hash", txHash). Errorf("failed to fetch transaction") @@ -79,54 +74,18 @@ func GetTransaction( Code: jrpc2.InternalError, Message: getTxErr.Error(), } - } - - response.ApplicationOrder = tx.ApplicationOrder - response.FeeBump = tx.FeeBump - response.Ledger = tx.Ledger.Sequence - response.LedgerCloseTime = tx.Ledger.CloseTime - - switch request.Format { - case protocol.FormatJSON: - result, envelope, meta, convErr := transactionToJSON(tx) - if convErr != nil { - return response, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: convErr.Error(), - } - } - diagEvents, convErr := jsonifySlice(xdr.DiagnosticEvent{}, tx.Events) - if convErr != nil { - return response, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: convErr.Error(), - } - } - - response.ResultJSON = result - response.EnvelopeJSON = envelope - response.ResultMetaJSON = meta - response.DiagnosticEventsJSON = diagEvents - - response.Events, convErr = BuildEventsJSONFromTransaction(tx) - if convErr != nil { + default: + txInfo, ferr := transactionInfo(tx, request.Format) + if ferr != nil { return response, &jrpc2.Error{ Code: jrpc2.InternalError, - Message: convErr.Error(), + Message: ferr.Error(), } } - default: - response.ResultXDR = base64.StdEncoding.EncodeToString(tx.Result) - response.EnvelopeXDR = base64.StdEncoding.EncodeToString(tx.Envelope) - response.ResultMetaXDR = base64.StdEncoding.EncodeToString(tx.Meta) - response.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) - response.Events = BuildEventsXDRFromTransaction(tx) - } - - response.Status = protocol.TransactionStatusFailed - if tx.Successful { - response.Status = protocol.TransactionStatusSuccess + response.TransactionDetails = txInfo.TransactionDetails + response.LedgerCloseTime = txInfo.LedgerCloseTime } + response.TransactionHash = request.Hash return response, nil } From 834af4d55d3d70744bb8663240d352e14e7d716a Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 11 Sep 2026 13:13:51 -0400 Subject: [PATCH 30/35] add changelog notes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd86abb64..05a8c0284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ ### Changed * `getTransactions` now scans at most 10,000 ledgers per request. On a sparse range a page can come back short — or even empty — while still carrying a `cursor`, so a response shorter than `limit` no longer implies end-of-data. Every response carries a `cursor`; to tell a sparse scan window from the tip, compare the cursor's ledger part against `latestLedger` and keep paging while it is below ([#908](https://github.com/stellar/stellar-rpc/pull/908)). * `getEvents` now rejects malformed contract IDs in filters with `-32602`: a `C…` string with a valid checksum but wrong-length payload used to decode and silently match nothing, and now errors under the SDK's stricter SEP-23 strkey parsing ([#908](https://github.com/stellar/stellar-rpc/pull/908)). +* `getTransactions` now returns `-32603` (internal error) instead of `-32602` (invalid params) when the node cannot parse a transaction out of its own stored ledger meta in both rpcv1 and rpcv2. Because the request parameters are valid in that case, the previous error was misleading ([#962](https://github.com/stellar/stellar-rpc/pull/962)). +* `getTransaction` and `getTransactions` now serve transactions with legacy `TransactionMeta` V0. This changes wire behavior for BOTH rpcv1 and rpcv2 (though in practice only a full-history node retains ledgers old enough to carry V0 meta). Such a transaction used to fail the request with `-32603` ("unsupported TransactionMeta version: 0"), and in `getTransactions` one V0 transaction failed the whole page. They are now returned like any other pre-Soroban transaction, with no events ([#962](https://github.com/stellar/stellar-rpc/pull/962)). + ## [v28.0.1](https://github.com/stellar/stellar-rpc/compare/v28.0.0...v28.0.1) From 14814e4e71d594249e3d3854ec1c6756b13474e3 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 11 Sep 2026 13:21:22 -0400 Subject: [PATCH 31/35] set cursor before early return with TransactionOrder at 0 --- cmd/stellar-rpc/internal/methods/get_transactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/stellar-rpc/internal/methods/get_transactions.go b/cmd/stellar-rpc/internal/methods/get_transactions.go index 3509b6adb..96c3eea64 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions.go @@ -131,7 +131,7 @@ func (h transactionsRPCHandler) processTransactionsInLedger( if ledgerSeqInt32 == start.LedgerSequence { startTxIdx = int(start.TransactionOrder) } - cursor := toid.New(ledgerSeqInt32, 0, 1) + cursor := toid.New(ledgerSeqInt32, int32(startTxIdx-1), 1) remaining := limitInt - len(*txns) if remaining <= 0 { return cursor, true, nil From 78f0fb5678aa08b8999caa3f795ceaf774cd8061 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 11 Sep 2026 13:27:34 -0400 Subject: [PATCH 32/35] fix transaction.go stale comment --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go index 9c96119ef..be3ba8c5c 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/transaction.go @@ -154,7 +154,8 @@ func (txn *transactionHandler) trimTransactions(latestLedgerSeq uint32, retentio // directly against the RPC handler. // // Errors occur if there are issues with the DB connection or the XDR is -// corrupted somehow. If the transaction is not found, io.EOF is returned. +// corrupted somehow. If the transaction is not found, store.ErrNoTransaction +// is returned. func (txn *transactionHandler) GetTransaction(ctx context.Context, hash xdr.Hash) (store.Transaction, error) { start := time.Now() From 47bcd194854e34380fbbd446973d40659d81baae Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 11 Sep 2026 13:27:59 -0400 Subject: [PATCH 33/35] remove double-blank line in CHANGELOG --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a8c0284..64aaf0af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ * `getTransactions` now returns `-32603` (internal error) instead of `-32602` (invalid params) when the node cannot parse a transaction out of its own stored ledger meta in both rpcv1 and rpcv2. Because the request parameters are valid in that case, the previous error was misleading ([#962](https://github.com/stellar/stellar-rpc/pull/962)). * `getTransaction` and `getTransactions` now serve transactions with legacy `TransactionMeta` V0. This changes wire behavior for BOTH rpcv1 and rpcv2 (though in practice only a full-history node retains ledgers old enough to carry V0 meta). Such a transaction used to fail the request with `-32603` ("unsupported TransactionMeta version: 0"), and in `getTransactions` one V0 transaction failed the whole page. They are now returned like any other pre-Soroban transaction, with no events ([#962](https://github.com/stellar/stellar-rpc/pull/962)). - ## [v28.0.1](https://github.com/stellar/stellar-rpc/compare/v28.0.0...v28.0.1) ### Fixed From a32fcd13fffc23207203fa2185db22e442396876 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 11 Sep 2026 15:26:11 -0400 Subject: [PATCH 34/35] fix incorrect comment about ledgers being loaned --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 3c7d61a24..31df49a5d 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -97,7 +97,7 @@ func (l ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.Led func (l ledgerReaderTx) WithLedgerRaw( ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, ) (bool, error) { - meta, found, err := withLedgerRawFromDB(ctx, l.tx, sequence) + meta, found, err := getLedgerRawFromDB(ctx, l.tx, sequence) if err != nil || !found { return found, err } @@ -163,7 +163,7 @@ func (r ledgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr.Ledge // WithLedgerRaw lends the ledger's stored meta blob without decoding it. func (r ledgerReader) WithLedgerRaw(ctx context.Context, sequence uint32, fn store.WithLedgerRawFn) (bool, error) { - meta, found, err := withLedgerRawFromDB(ctx, r.db, sequence) + meta, found, err := getLedgerRawFromDB(ctx, r.db, sequence) if err != nil || !found { return found, err } @@ -245,7 +245,7 @@ type ledgerRangeRow struct { func ledgerInfoFromRow(ctx context.Context, db readDB, row ledgerRangeRow) (store.LedgerInfo, error) { closeTime, err := xdr.LedgerCloseMetaView(row.MetaPrefix).LedgerCloseTime() if err != nil { - meta, found, dbErr := withLedgerRawFromDB(ctx, db, row.Sequence) + meta, found, dbErr := getLedgerRawFromDB(ctx, db, row.Sequence) if dbErr != nil { return store.LedgerInfo{}, dbErr } @@ -368,7 +368,7 @@ func (l ledgerWriter) trimLedgers(latestLedgerSeq uint32, retentionWindow uint32 // getLedgerFromDB fetches a single ledger from the database. func getLedgerFromDB(ctx context.Context, db readDB, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { - meta, found, err := withLedgerRawFromDB(ctx, db, sequence) + meta, found, err := getLedgerRawFromDB(ctx, db, sequence) if err != nil || !found { return xdr.LedgerCloseMeta{}, false, err } @@ -379,10 +379,9 @@ func getLedgerFromDB(ctx context.Context, db readDB, sequence uint32) (xdr.Ledge return lcm, true, nil } -// withLedgerRawFromDB is a helper function that encapsulates the common logic -// for fetching a single ledger's bytes from the database. The bytes returned -// are lent and shouldn't be reused by a later statement. -func withLedgerRawFromDB(ctx context.Context, db readDB, sequence uint32) ([]byte, bool, error) { +// getLedgerRawFromDB fetches a single ledger's meta blob. The bytes are owned: +// the driver and database/sql each copy the BLOB out of SQLite's memory. +func getLedgerRawFromDB(ctx context.Context, db readDB, sequence uint32) ([]byte, bool, error) { sql := sq.Select("meta").From(ledgerCloseMetaTableName).Where(sq.Eq{"sequence": sequence}) var results [][]byte if err := db.Select(ctx, &results, sql); err != nil { From 735e19b896236c8a9e0e2ae5d84d513f41c7deea Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 11 Sep 2026 15:36:39 -0400 Subject: [PATCH 35/35] remove GetLedger from Tx interface --- cmd/stellar-rpc/internal/methods/mocks.go | 5 -- .../internal/rpcv1/sqlitedb/ledger.go | 5 -- .../internal/rpcv2/adapters/ledger_reader.go | 37 +++------ .../rpcv2/adapters/ledger_reader_test.go | 81 ++++++------------- cmd/stellar-rpc/internal/store/ledger.go | 15 ++-- 5 files changed, 40 insertions(+), 103 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index de5377481..76291e021 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -71,11 +71,6 @@ func (m *MockLedgerReaderTx) BatchGetLedgers(ctx context.Context, start, end uin return args.Get(0).([]store.LedgerMetadataChunk), args.Error(1) //nolint:forcetypeassert } -func (m *MockLedgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { - args := m.Called(ctx, sequence) - return args.Get(0).(xdr.LedgerCloseMeta), args.Bool(1), args.Error(2) //nolint:forcetypeassert -} - func (m *MockLedgerReaderTx) WithLedgerRaw( ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, ) (bool, error) { diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 31df49a5d..b04fa2a0e 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -87,11 +87,6 @@ func (l ledgerReaderTx) BatchGetLedgers( return batch, nil } -// GetLedger fetches a single ledger from the db using a transaction. -func (l ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { - return getLedgerFromDB(ctx, l.tx, sequence) -} - // WithLedgerRaw lends the ledger's stored meta blob without decoding it. The // blob is ours to lend: database/sql clones each BLOB scanned into a *[]byte. func (l ledgerReaderTx) WithLedgerRaw( diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index 4575c93a1..a239d7adb 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -19,11 +19,11 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/store" ) -// walkSpanCap bounds the ledger span ledgerReaderTx.GetLedger primes its walk -// iterator with: one chunk's worth of ledgers, touching at most two chunks -// when the span straddles a boundary, so view.ScanLedgers can resolve every -// reader up front. Handler scan limits (methods.LedgerScanLimit) must stay -// ≤ this cap; the pairing test enforces it. +// walkSpanCap bounds the ledger span ledgerReaderTx.WithLedgerRaw primes its +// walk iterator with: one chunk's worth of ledgers, touching at most two +// chunks when the span straddles a boundary, so view.ScanLedgers can resolve +// every reader up front. Handler scan limits (methods.LedgerScanLimit) must +// stay ≤ this cap; the pairing test enforces it. const walkSpanCap = chunk.LedgersPerChunk // LedgerReader satisfies store.LedgerReader over the query router. Every @@ -125,11 +125,11 @@ func (r *LedgerReader) NewTx(ctx context.Context) (store.LedgerReaderTx, error) } // ledgerReaderTx satisfies store.LedgerReaderTx over the request's read view -// (the serving wrapper owns and releases it — Done does not). GetLedger and -// WithLedgerRaw serve getTransactions' ascending, contiguous per-ledger walk -// by pulling from a single ScanLedgers iterator primed on the first call — -// one iterator between them, see walk; GetLedgerRange and BatchGetLedgers -// read through the same view but never touch that iterator. +// (the serving wrapper owns and releases it — Done does not). WithLedgerRaw +// serves getTransactions' ascending, contiguous per-ledger walk by pulling +// from a ScanLedgers iterator primed on the first call, see walk; +// GetLedgerRange and BatchGetLedgers read through the same view but never +// touch that iterator. type ledgerReaderTx struct { view *query.ReadView @@ -139,21 +139,8 @@ type ledgerReaderTx struct { stop func() } -func (tx *ledgerReaderTx) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { - entry, found, err := tx.walk(ctx, sequence) - if err != nil || !found { - return xdr.LedgerCloseMeta{}, false, err - } - var lcm xdr.LedgerCloseMeta - if err := lcm.UnmarshalBinary(entry.Bytes); err != nil { - return xdr.LedgerCloseMeta{}, false, fmt.Errorf("adapters: unmarshal ledger %d: %w", sequence, err) - } - return lcm, true, nil -} - -// WithLedgerRaw is GetLedger without the decode or the clone: it lends the -// step's bytes straight from the chunk reader's scratch buffer, which the -// next step overwrites — fn must not retain them. +// WithLedgerRaw lends the step's bytes straight from the chunk reader's +// scratch buffer, which the next step overwrites — fn must not retain them. func (tx *ledgerReaderTx) WithLedgerRaw( ctx context.Context, sequence uint32, fn store.WithLedgerRawFn, ) (bool, error) { diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go index 81cd519c5..c41f305a6 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go @@ -181,14 +181,21 @@ func TestStreamLedgerRange_BelowFloorIsRangeError(t *testing.T) { assert.Equal(t, c0.FirstLedger(), rangeErr.Oldest) } -func TestTxGetLedger_WalksContiguously(t *testing.T) { +// txGetLedger is the Tx's retired GetLedger: one WithLedgerRaw step, decoded inside the loan. +func txGetLedger(ctx context.Context, tx store.LedgerReaderTx, seq uint32) (xdr.LedgerCloseMeta, bool, error) { + var lcm xdr.LedgerCloseMeta + found, err := tx.WithLedgerRaw(ctx, seq, lcm.UnmarshalBinary) + return lcm, found, err +} + +func TestTxWalk_Contiguous(t *testing.T) { ctx, reader, c0, _ := sparseFixture(t) tx, err := reader.NewTx(ctx) require.NoError(t, err) defer func() { _ = tx.Done() }() for seq := c0.FirstLedger(); seq <= c0.FirstLedger()+3; seq++ { - lcm, ok, err := tx.GetLedger(context.Background(), seq) + lcm, ok, err := txGetLedger(context.Background(), tx, seq) require.NoError(t, err) require.True(t, ok, "ledger %d", seq) assert.Equal(t, seq, lcm.LedgerSequence()) @@ -196,23 +203,23 @@ func TestTxGetLedger_WalksContiguously(t *testing.T) { } } -func TestTxGetLedger_NonSequentialFailsLoudly(t *testing.T) { +func TestTxWalk_NonSequentialFailsLoudly(t *testing.T) { ctx, reader, c0, _ := sparseFixture(t) tx, err := reader.NewTx(ctx) require.NoError(t, err) defer func() { _ = tx.Done() }() - _, ok, err := tx.GetLedger(context.Background(), c0.FirstLedger()) + _, ok, err := txGetLedger(context.Background(), tx, c0.FirstLedger()) require.NoError(t, err) require.True(t, ok) // Skipping ahead breaks the walk contract; serving the wrong ledger's // data would be worse than an error. - _, _, err = tx.GetLedger(context.Background(), c0.FirstLedger()+2) + _, _, err = txGetLedger(context.Background(), tx, c0.FirstLedger()+2) assert.ErrorContains(t, err, "non-sequential") } -func TestTxGetLedger_GuardsBeforePriming(t *testing.T) { +func TestTxWalk_GuardsBeforePriming(t *testing.T) { ctx, reader, c0, c1 := sparseFixture(t) tx, err := reader.NewTx(ctx) require.NoError(t, err) @@ -221,12 +228,12 @@ func TestTxGetLedger_GuardsBeforePriming(t *testing.T) { // Sub-genesis and out-of-window sequences return clean misses without // consuming the walk iterator... for _, seq := range []uint32{0, 1, c0.FirstLedger() - 1, c1.FirstLedger() + 1} { - _, ok, err := tx.GetLedger(context.Background(), seq) + _, ok, err := txGetLedger(context.Background(), tx, seq) assert.NoError(t, err) assert.False(t, ok) } // ...so a walk primed afterwards still starts at its own sequence. - lcm, ok, err := tx.GetLedger(context.Background(), c0.FirstLedger()) + lcm, ok, err := txGetLedger(context.Background(), tx, c0.FirstLedger()) require.NoError(t, err) require.True(t, ok) assert.Equal(t, c0.FirstLedger(), lcm.LedgerSequence()) @@ -245,7 +252,7 @@ func TestTxGetLedgerRange_DoesNotDisturbTheWalk(t *testing.T) { assert.Equal(t, c0.FirstLedger(), lr.FirstLedger.Sequence) assert.Equal(t, c1.FirstLedger(), lr.LastLedger.Sequence) - lcm, ok, err := tx.GetLedger(context.Background(), c0.FirstLedger()+1) + lcm, ok, err := txGetLedger(context.Background(), tx, c0.FirstLedger()+1) require.NoError(t, err) require.True(t, ok) assert.Equal(t, c0.FirstLedger()+1, lcm.LedgerSequence()) @@ -335,13 +342,13 @@ func TestTxDone_WithAndWithoutPriming(t *testing.T) { tx, err = reader.NewTx(ctx) require.NoError(t, err) - _, _, err = tx.GetLedger(context.Background(), c0.FirstLedger()) + _, _, err = txGetLedger(context.Background(), tx, c0.FirstLedger()) require.NoError(t, err) assert.NoError(t, tx.Done()) assert.NoError(t, tx.Done(), "a second Done must be a no-op, not a double release") } -func TestTxGetLedger_WalkCrossesChunkBorder(t *testing.T) { +func TestTxWalk_CrossesChunkBorder(t *testing.T) { if testing.Short() { t.Skip("seeds a full 10k-ledger chunk") } @@ -358,14 +365,14 @@ func TestTxGetLedger_WalkCrossesChunkBorder(t *testing.T) { defer func() { _ = tx.Done() }() for seq := c0.LastLedger() - 1; seq <= c1.FirstLedger()+1; seq++ { - lcm, ok, err := tx.GetLedger(context.Background(), seq) + lcm, ok, err := txGetLedger(context.Background(), tx, seq) require.NoError(t, err, "ledger %d", seq) require.True(t, ok, "ledger %d", seq) assert.Equal(t, seq, lcm.LedgerSequence()) } } -func TestTxGetLedger_WalkPastSpanCapIsExhaustedNotNonSequential(t *testing.T) { +func TestTxWalk_PastSpanCapIsExhaustedNotNonSequential(t *testing.T) { if testing.Short() { t.Skip("seeds a full 10k-ledger chunk") } @@ -383,12 +390,12 @@ func TestTxGetLedger_WalkPastSpanCapIsExhaustedNotNonSequential(t *testing.T) { start := c0.FirstLedger() for seq := start; seq < start+walkSpanCap; seq++ { - _, ok, err := tx.GetLedger(context.Background(), seq) + _, ok, err := txGetLedger(context.Background(), tx, seq) require.NoError(t, err, "ledger %d", seq) require.True(t, ok, "ledger %d", seq) } - _, _, err = tx.GetLedger(context.Background(), start+walkSpanCap) + _, _, err = txGetLedger(context.Background(), tx, start+walkSpanCap) require.ErrorContains(t, err, "exhausted its primed") assert.NotContains(t, err.Error(), "non-sequential") } @@ -412,22 +419,6 @@ func TestWalkSpanCapCoversTheHandlerScanLimit(t *testing.T) { "methods.LedgerScanLimit cannot derive from it (shared v1 code), so it is pinned here") } -func TestLedgerReaderTx_GetLedgerStopsOnCanceledContext(t *testing.T) { - baseCtx, reader, c0, _ := sparseFixture(t) - tx, err := reader.NewTx(baseCtx) - require.NoError(t, err) - defer func() { _ = tx.Done() }() - - ctx, cancel := context.WithCancel(baseCtx) - _, found, err := tx.GetLedger(ctx, c0.FirstLedger()) - require.NoError(t, err) - require.True(t, found) - - cancel() - _, _, err = tx.GetLedger(ctx, c0.FirstLedger()+1) - require.ErrorIs(t, err, context.Canceled) -} - func TestLedgerReaderTx_BatchGetLedgersStopsOnCanceledContext(t *testing.T) { r, first := sharedViewFixture(t) reader := NewLedgerReader() @@ -521,16 +512,12 @@ func TestWithLedgerRaw_MissDoesNotRunFn(t *testing.T) { func TestTxWithLedgerRaw_LendsTheSameBytesGetLedgerDecodes(t *testing.T) { ctx, reader, c0, _ := sparseFixture(t) - - decodeTx, err := reader.NewTx(ctx) - require.NoError(t, err) - defer func() { _ = decodeTx.Done() }() rawTx, err := reader.NewTx(ctx) require.NoError(t, err) defer func() { _ = rawTx.Done() }() for seq := c0.FirstLedger(); seq <= c0.FirstLedger()+3; seq++ { - lcm, ok, err := decodeTx.GetLedger(context.Background(), seq) + lcm, ok, err := reader.GetLedger(ctx, seq) require.NoError(t, err) require.True(t, ok, "ledger %d", seq) want, err := lcm.MarshalBinary() @@ -589,28 +576,6 @@ func TestTxWithLedgerRaw_CallbackErrorSurfacesAsFound(t *testing.T) { assert.True(t, found) } -func TestTxWithLedgerRaw_SharesTheWalkWithGetLedger(t *testing.T) { - ctx, reader, c0, _ := sparseFixture(t) - tx, err := reader.NewTx(ctx) - require.NoError(t, err) - defer func() { _ = tx.Done() }() - - _, ok, err := tx.GetLedger(context.Background(), c0.FirstLedger()) - require.NoError(t, err) - require.True(t, ok) - - // One cursor: the raw accessor picks up where GetLedger left off, and - // skipping ahead breaks the same contract just as loudly. - found, err := tx.WithLedgerRaw(context.Background(), c0.FirstLedger()+1, - func([]byte) error { return nil }) - require.NoError(t, err) - require.True(t, found) - - _, err = tx.WithLedgerRaw(context.Background(), c0.FirstLedger()+3, - func([]byte) error { return nil }) - assert.ErrorContains(t, err, "non-sequential") -} - func TestTxWithLedgerRaw_StopsOnCanceledContext(t *testing.T) { baseCtx, reader, c0, _ := sparseFixture(t) tx, err := reader.NewTx(baseCtx) diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index c6676c898..c5ad5a4b8 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -50,19 +50,14 @@ type WithLedgerRawFn func(raw []byte) error // LedgerReaderTx is a read-only snapshot of the ledger store. Call Done to // release it. // -// GetLedger and WithLedgerRaw share one walk, not free-form point reads. Calls -// may use either accessor, but their combined sequence must be ascending and -// contiguous, starting from the first call's sequence, because both consume -// positions from the same cursor. Read at most +// WithLedgerRaw is a walk, not a free-form point read: calls must be +// ascending and contiguous from the first call's sequence, and read at most // methods.LedgerScanLimit ledgers per Tx. The v1 (SQL) backend accepts any // pattern, while the v2 backend only walks from a forward iterator primed on -// the first call. -// -// WithLedgerRaw is GetLedger without the decode: fn borrows the marshaled -// LCM under WithLedgerRawFn's loan terms. found=false means fn never ran; -// fn's own error comes back verbatim with found=true. +// the first call. fn borrows the marshaled LCM under WithLedgerRawFn's loan +// terms. found=false means fn never ran; fn's own error comes back verbatim +// with found=true. type LedgerReaderTx interface { - GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) WithLedgerRaw(ctx context.Context, sequence uint32, fn WithLedgerRawFn) (bool, error) GetLedgerRange(ctx context.Context) (LedgerRange, error) BatchGetLedgers(ctx context.Context, start uint32, end uint32) ([]LedgerMetadataChunk, error)