From fcd868f59f4c469c181650e9dfaf148f87cff6c9 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 24 Aug 2026 13:18:11 -0400 Subject: [PATCH 01/19] rewrite getLedgerRange functions with views --- .../internal/rpcv1/sqlitedb/ledger.go | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index b50f6bdec..b23bcc616 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -233,19 +233,27 @@ func getLedgerRangeWithCache(ctx context.Context, db readDB, Where( fmt.Sprintf("sequence = (SELECT MIN(sequence) FROM %s)", ledgerCloseMetaTableName), ) - var lcm []xdr.LedgerCloseMeta - if err := db.Select(ctx, &lcm, query); err != nil { + var lcmRaw []xdr.LedgerCloseMetaView + if err := db.Select(ctx, &lcmRaw, query); err != nil { return store.LedgerRange{}, fmt.Errorf("couldn't query ledger range: %w", err) } - if len(lcm) == 0 { + if len(lcmRaw) == 0 { return store.LedgerRange{}, store.ErrEmptyDB } + firstSeq, err := lcmRaw[0].LedgerSequence() + if err != nil { + return store.LedgerRange{}, fmt.Errorf("couldn't get first ledger sequence: %w", err) + } + firstCloseTime, err := lcmRaw[0].LedgerCloseTime() + if err != nil { + return store.LedgerRange{}, fmt.Errorf("couldn't get first ledger close time: %w", err) + } return store.LedgerRange{ FirstLedger: store.LedgerInfo{ - Sequence: lcm[0].LedgerSequence(), - CloseTime: lcm[0].LedgerCloseTime(), + Sequence: firstSeq, + CloseTime: firstCloseTime, }, LastLedger: store.LedgerInfo{ Sequence: latestSeq, @@ -263,7 +271,7 @@ func getLedgerRangeWithoutCache(ctx context.Context, db readDB) (store.LedgerRan sq.Expr("lcm.sequence = (?)", sq.Select("MAX(sequence)").From(ledgerCloseMetaTableName)), }).OrderBy("lcm.sequence ASC") - var lcms []xdr.LedgerCloseMeta + var lcms []xdr.LedgerCloseMetaView if err := db.Select(ctx, &lcms, query); err != nil { return store.LedgerRange{}, fmt.Errorf("couldn't query ledger range: %w", err) } @@ -272,14 +280,31 @@ func getLedgerRangeWithoutCache(ctx context.Context, db readDB) (store.LedgerRan return store.LedgerRange{}, store.ErrEmptyDB } + firstSeq, err := lcms[0].LedgerSequence() + if err != nil { + return store.LedgerRange{}, fmt.Errorf("couldn't get first ledger sequence: %w", err) + } + firstCloseTime, err := lcms[0].LedgerCloseTime() + if err != nil { + return store.LedgerRange{}, fmt.Errorf("couldn't get first ledger close time: %w", err) + } + lastSeq, err := lcms[len(lcms)-1].LedgerSequence() + if err != nil { + return store.LedgerRange{}, fmt.Errorf("couldn't get last ledger sequence: %w", err) + } + lastCloseTime, err := lcms[len(lcms)-1].LedgerCloseTime() + if err != nil { + return store.LedgerRange{}, fmt.Errorf("couldn't get last ledger close time: %w", err) + } + return store.LedgerRange{ FirstLedger: store.LedgerInfo{ - Sequence: lcms[0].LedgerSequence(), - CloseTime: lcms[0].LedgerCloseTime(), + Sequence: firstSeq, + CloseTime: firstCloseTime, }, LastLedger: store.LedgerInfo{ - Sequence: lcms[len(lcms)-1].LedgerSequence(), - CloseTime: lcms[len(lcms)-1].LedgerCloseTime(), + Sequence: lastSeq, + CloseTime: lastCloseTime, }, }, nil } From ce632d0ef6779fc91b3b06a5313e49446d72efcf Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 24 Aug 2026 14:23:08 -0400 Subject: [PATCH 02/19] replace RawLedger usage with xdr.LedgerCloseMetaView --- .../internal/methods/get_latest_ledger.go | 48 +++++++++------- .../methods/get_latest_ledger_test.go | 14 +++++ .../internal/methods/get_transactions_test.go | 9 +++ cmd/stellar-rpc/internal/methods/mocks.go | 8 +++ .../internal/rpcv1/sqlitedb/ledger.go | 55 +++++++++++-------- .../internal/rpcv1/sqlitedb/mocks.go | 12 ++++ .../internal/rpcv2/adapters/ledger_reader.go | 24 ++++++++ .../rpcv2/corestate/corestate_test.go | 4 ++ cmd/stellar-rpc/internal/store/ledger.go | 1 + 9 files changed, 131 insertions(+), 44 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go index 074dea190..fc4c4a9d5 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go @@ -3,11 +3,13 @@ package methods import ( "context" "encoding/base64" + "encoding/hex" "fmt" "github.com/creachadair/jrpc2" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" + "github.com/stellar/go-stellar-sdk/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/store" ) @@ -23,39 +25,43 @@ func NewGetLatestLedgerHandler(ledgerReader store.LedgerReader) jrpc2.Handler { Message: "could not get latest ledger sequence", } } - latestLedger, found, err := ledgerReader.GetLedger(ctx, latestSequence) + latestLedgerView, found, err := ledgerReader.GetLedgerView(ctx, latestSequence) if (err != nil) || (!found) { return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not get latest ledger", } } - header := latestLedger.LedgerHeaderHistoryEntry().Header - headerBytes, err := header.MarshalBinary() + response, err := latestLedgerResponse(latestLedgerView, latestSequence) if err != nil { return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, - Message: fmt.Sprintf("could not marshal latest ledger header: %v", err), + Message: fmt.Sprintf("could not read latest ledger meta: %v", err), } } - response := protocol.GetLatestLedgerResponse{ - Hash: latestLedger.LedgerHash().HexString(), - ProtocolVersion: latestLedger.ProtocolVersion(), - Sequence: latestSequence, - LedgerCloseTime: latestLedger.LedgerCloseTime(), - LedgerHeader: base64.StdEncoding.EncodeToString(headerBytes), - } - - raw, err := latestLedger.MarshalBinary() - if err != nil { - return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: fmt.Sprintf("could not marshal latest ledger metadata: %v", err), - } - } - response.LedgerMetadata = base64.StdEncoding.EncodeToString(raw) - return response, nil } return NewHandler(coreHandler) } + +// latestLedgerResponse extracts the response fields from a ledger close meta view. +func latestLedgerResponse(view xdr.LedgerCloseMetaView, sequence uint32, +) (protocol.GetLatestLedgerResponse, error) { + headerEntry, err := view.LedgerHeader() + if err != nil { + return protocol.GetLatestLedgerResponse{}, err + } + // Must* accessors panic with *ViewError on malformed fields and Try + // recovers it, so the extraction needs a single error check. + return xdr.Try(func() protocol.GetLatestLedgerResponse { + header := headerEntry.MustHeader() + return protocol.GetLatestLedgerResponse{ + Hash: hex.EncodeToString(headerEntry.MustHash().MustRaw()), + ProtocolVersion: header.MustLedgerVersion().MustValue(), + Sequence: sequence, + LedgerCloseTime: int64(header.MustScpValue().MustCloseTime().MustValue()), //nolint:gosec // safe for ~292B years + LedgerHeader: base64.StdEncoding.EncodeToString(header.MustRaw()), + LedgerMetadata: base64.StdEncoding.EncodeToString(view), + } + }) +} diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go index 2efc67e40..285705656 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go @@ -45,6 +45,20 @@ func (ledgerReader *ConstantLedgerReader) GetLedger(_ context.Context, true, nil } +func (ledgerReader *ConstantLedgerReader) GetLedgerView( + _ context.Context, + sequence uint32, +) (xdr.LedgerCloseMetaView, bool, error) { + lcm := createLedger(expectedLatestLedgerHashBytes, + sequence, + expectedLatestLedgerCloseTime) + raw, err := lcm.MarshalBinary() + if err != nil { + return nil, false, err + } + return xdr.LedgerCloseMetaView(raw), true, nil +} + func (ledgerReader *ConstantLedgerReader) StreamLedgerRange( _ context.Context, _ uint32, diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index ba770956c..703f34ebf 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -381,6 +381,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) GetLedgerRange(context.Context) (store.LedgerRange, error) { return store.LedgerRange{ FirstLedger: store.LedgerInfo{Sequence: 1, CloseTime: 100}, diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index f40d487a5..10009ff49 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -27,6 +27,14 @@ func (m *MockLedgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr. return args.Get(0).(xdr.LedgerCloseMeta), args.Bool(1), args.Error(2) //nolint:forcetypeassert } +func (m *MockLedgerReader) 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 *MockLedgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRange, error) { args := m.Called(ctx) return args.Get(0).(store.LedgerRange), args.Error(1) //nolint:forcetypeassert diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index b23bcc616..6031799c4 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -74,28 +74,15 @@ func (l ledgerReaderTx) BatchGetLedgers( batch := make([]store.LedgerMetadataChunk, len(results)) for i, meta := range results { - batch[i] = store.LedgerMetadataChunk{Lcm: meta} - - var v xdr.Int32 - rd := bytes.NewReader(meta) - if _, err := xdr.Unmarshal(rd, &v); err != nil { + headerView, err := xdr.LedgerCloseMetaView(meta).LedgerHeader() + if err != nil { return nil, err } - - if v > 0 { // V0 has no extension - var ext xdr.LedgerCloseMetaExt - if _, err := xdr.Unmarshal(rd, &ext); err != nil { // skipped - return nil, err - } - } - - hdrStart := len(meta) - rd.Len() - var hdr xdr.LedgerHeaderHistoryEntry - n, err := xdr.Unmarshal(rd, &hdr) + headerRaw, err := headerView.Raw() if err != nil { return nil, err } - batch[i].HeaderRaw = meta[hdrStart : hdrStart+n] + batch[i] = store.LedgerMetadataChunk{HeaderRaw: headerRaw, Lcm: meta} } return batch, nil @@ -163,6 +150,15 @@ func (r ledgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr.Ledge return getLedgerFromDB(ctx, r.db, sequence) } +// GetLedgerView fetches a single ledger from the db and returns its view and whether it was found. +func (r ledgerReader) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { + meta, found, err := getLedgerRawFromDB(ctx, r.db, sequence) + if err != nil || !found { + return nil, found, err + } + return xdr.LedgerCloseMetaView(meta), true, nil +} + // GetLedgerRange pulls the min/max ledger sequence numbers from the meta table. func (r ledgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRange, error) { r.db.cache.RLock() @@ -351,21 +347,34 @@ func (l ledgerWriter) trimLedgers(latestLedgerSeq uint32, retentionWindow uint32 return err } -// getLedgerFromDB is a helper function that encapsulates the common logic -// for fetching a single ledger from the database +// getLedgerFromDB fetches a single ledger from the database. func getLedgerFromDB(ctx context.Context, db readDB, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { + raw, found, err := getLedgerRawFromDB(ctx, db, sequence) + if err != nil || !found { + return xdr.LedgerCloseMeta{}, false, err + } + var lcm xdr.LedgerCloseMeta + if _, err := xdr.Unmarshal(bytes.NewReader(raw), &lcm); err != nil { + return xdr.LedgerCloseMeta{}, false, err + } + 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) { sql := sq.Select("meta").From(ledgerCloseMetaTableName).Where(sq.Eq{"sequence": sequence}) - var results []xdr.LedgerCloseMeta + var results [][]byte if err := db.Select(ctx, &results, sql); err != nil { - return xdr.LedgerCloseMeta{}, false, err + return nil, false, err } switch len(results) { case 0: - return xdr.LedgerCloseMeta{}, false, nil + return nil, false, nil case 1: return results[0], true, nil default: - return xdr.LedgerCloseMeta{}, false, fmt.Errorf("multiple lcm entries (%d) for sequence %d in table %q", + return nil, false, fmt.Errorf("multiple lcm entries (%d) for sequence %d in table %q", len(results), sequence, ledgerCloseMetaTableName) } } diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index 8f1c97fc1..25eace379 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -97,6 +97,18 @@ func (m *MockLedgerReader) GetLedger(_ context.Context, sequence uint32) (xdr.Le return *lcm, true, nil } +func (m *MockLedgerReader) GetLedgerView(_ context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { + lcm, ok := m.txn.ledgerSeqToMeta[sequence] + if !ok { + return nil, false, nil + } + rawMeta, err := lcm.MarshalBinary() + if err != nil { + return nil, false, err + } + return xdr.LedgerCloseMetaView(rawMeta), true, nil +} + func (m *MockLedgerReader) StreamLedgerRange(_ context.Context, _ uint32, _ uint32, _ store.StreamLedgerFn) error { return nil } diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index b72bb1c94..1fcf10c54 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -55,6 +55,30 @@ func (r *LedgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr.Ledg return lcm, found, err } +// GetLedgerView is GetLedger without the decode: the raw bytes are copied +// out of the read's loan, so the view may outlive the call. +func (r *LedgerReader) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { + view, err := query.ViewFrom(ctx) + if err != nil { + return nil, false, err + } + if !inWindow(view, sequence) { + return nil, false, nil + } + var raw []byte + err = view.WithLedger(sequence, func(b []byte) error { + raw = bytes.Clone(b) + return nil + }) + if errors.Is(err, stores.ErrNotFound) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + return xdr.LedgerCloseMetaView(raw), true, nil +} + func (r *LedgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRange, error) { view, err := query.ViewFrom(ctx) if err != nil { diff --git a/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go b/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go index 5214f1d47..a6bc258c7 100644 --- a/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go @@ -214,6 +214,10 @@ func (s stubLedgerReader) GetLedger(context.Context, uint32) (xdr.LedgerCloseMet return xdr.LedgerCloseMeta{}, false, errors.New("unused") } +func (s stubLedgerReader) GetLedgerView(context.Context, uint32) (xdr.LedgerCloseMetaView, bool, error) { + return nil, false, errors.New("unused") +} + func (s stubLedgerReader) GetLedgerRange(context.Context) (store.LedgerRange, error) { return store.LedgerRange{}, errors.New("unused") } diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index 69d1c71ba..b1d5b94a7 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -36,6 +36,7 @@ func (lr LedgerRange) ToLedgerSeqRange() protocol.LedgerSeqRange { // implements. Handlers depend on this interface, never on a concrete backend. type LedgerReader 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) StreamLedgerRange(ctx context.Context, startLedger uint32, endLedger uint32, f StreamLedgerFn) error NewTx(ctx context.Context) (LedgerReaderTx, error) From dc9b3efab96b55b99eb037fdda924b524e3c5a3a Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 24 Aug 2026 15:18:57 -0400 Subject: [PATCH 03/19] modify StreamAllLedger/StreamLedgerRange + callers to take/pass a view --- .../methods/get_latest_ledger_test.go | 2 +- .../internal/methods/get_transactions_test.go | 2 +- cmd/stellar-rpc/internal/methods/mocks.go | 2 +- .../internal/rpcv1/daemon/daemon.go | 7 +++++-- .../internal/rpcv1/sqlitedb/ledger.go | 4 ++-- .../internal/rpcv1/sqlitedb/ledger_test.go | 10 +++++---- .../internal/rpcv1/sqlitedb/migration.go | 21 ++++++++++++++----- .../internal/rpcv1/sqlitedb/mocks.go | 2 +- .../internal/rpcv2/adapters/ledger_reader.go | 11 +++++----- .../rpcv2/adapters/ledger_reader_test.go | 14 ++++++++----- .../rpcv2/corestate/corestate_test.go | 2 +- cmd/stellar-rpc/internal/rpcv2/feereplay.go | 8 +++---- cmd/stellar-rpc/internal/store/ledger.go | 8 +++++-- 13 files changed, 57 insertions(+), 36 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go index 285705656..b33f21005 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go @@ -63,7 +63,7 @@ func (ledgerReader *ConstantLedgerReader) StreamLedgerRange( _ context.Context, _ uint32, _ uint32, - _ store.StreamLedgerFn, + _ store.StreamLedgerViewFn, ) error { return nil } diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index 703f34ebf..e7c4caed3 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -401,7 +401,7 @@ func (r *sparseLedgerReader) BatchGetLedgers(context.Context, uint32, uint32) ([ return nil, nil } -func (r *sparseLedgerReader) StreamLedgerRange(context.Context, uint32, uint32, store.StreamLedgerFn) error { +func (r *sparseLedgerReader) StreamLedgerRange(context.Context, uint32, uint32, store.StreamLedgerViewFn) error { return nil } diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index 10009ff49..6fa2f8402 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -41,7 +41,7 @@ func (m *MockLedgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRang } func (m *MockLedgerReader) StreamLedgerRange(ctx context.Context, startLedger, endLedger uint32, - f store.StreamLedgerFn, + f store.StreamLedgerViewFn, ) error { args := m.Called(ctx, startLedger, endLedger, f) return args.Error(0) diff --git a/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go b/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go index abfeb181e..ba9f150d7 100644 --- a/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go +++ b/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go @@ -472,8 +472,11 @@ func (d *Daemon) mustInitializeStorage(cfg *config.Config) *feewindow.FeeWindows readTxMetaCtx, ledgerSeqRange.First, ledgerSeqRange.Last, - func(txMeta xdr.LedgerCloseMeta) error { - currentSeq = txMeta.LedgerSequence() + func(txMeta xdr.LedgerCloseMetaView) error { + currentSeq, err := txMeta.LedgerSequence() + if err != nil { + return err + } if initialSeq == 0 { initialSeq = currentSeq d.logger. diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 6031799c4..8efaac11e 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -121,7 +121,7 @@ func (r ledgerReader) StreamLedgerRange( ctx context.Context, startLedger uint32, endLedger uint32, - f store.StreamLedgerFn, + f store.StreamLedgerViewFn, ) error { sql := sq.Select("meta").From(ledgerCloseMetaTableName). Where(sq.GtOrEq{"sequence": startLedger}). @@ -134,7 +134,7 @@ func (r ledgerReader) StreamLedgerRange( } defer q.Close() for q.Next() { - var closeMeta xdr.LedgerCloseMeta + var closeMeta xdr.LedgerCloseMetaView if err = q.Scan(&closeMeta); err != nil { return err } diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index bb4d2360c..223662eda 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -4,6 +4,7 @@ import ( "context" "io" "path" + "slices" "testing" "github.com/stretchr/testify/assert" @@ -42,9 +43,10 @@ func createLedger(ledgerSequence uint32) xdr.LedgerCloseMeta { func assertLedgerRange(t *testing.T, reader LedgerReader, start, end uint32) { ctx := t.Context() - var allLedgers []xdr.LedgerCloseMeta - err := reader.StreamLedgerRange(ctx, start-1, end+1, func(txmeta xdr.LedgerCloseMeta) error { - allLedgers = append(allLedgers, txmeta) + var allLedgers []xdr.LedgerCloseMetaView + err := reader.StreamLedgerRange(ctx, start-1, end+1, func(txmeta xdr.LedgerCloseMetaView) error { + // the view is only valid for the duration of the callback + allLedgers = append(allLedgers, slices.Clone(txmeta)) return nil }) require.NoError(t, err) @@ -63,7 +65,7 @@ func assertLedgerRange(t *testing.T, reader LedgerReader, start, end uint32) { require.NoError(t, err) assert.Equal(t, expectedBinary, ledgerBinary) - ledgerBinary, err = allLedgers[0].MarshalBinary() + ledgerBinary, err = allLedgers[0].Raw() require.NoError(t, err) assert.Equal(t, expectedBinary, ledgerBinary) allLedgers = allLedgers[1:] diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go index 22a5df906..4c4604ef9 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go @@ -92,19 +92,30 @@ func (mm MultiMigration) ApplicableRange() LedgerSeqRange { return result } -func (mm MultiMigration) Apply(ctx context.Context, meta xdr.LedgerCloseMeta) error { - var err error +func (mm MultiMigration) Apply(ctx context.Context, view xdr.LedgerCloseMetaView) error { + ledgerSeq, err := view.LedgerSequence() + if err != nil { + return errors.Join(err, mm.db.Rollback()) + } + var meta xdr.LedgerCloseMeta + decoded := false + var applyErr error for _, m := range mm.migrations { - ledgerSeq := meta.LedgerSequence() if !m.ApplicableRange().IsLedgerIncluded(ledgerSeq) { // The range of a sub-migration can be smaller than the global range. continue } + if !decoded { + if err := meta.UnmarshalBinary(view); err != nil { + return errors.Join(err, mm.db.Rollback()) + } + decoded = true + } if localErr := m.Apply(ctx, meta); localErr != nil { - err = errors.Join(err, localErr, mm.db.Rollback()) + err = errors.Join(applyErr, localErr, mm.db.Rollback()) } } - return err + return applyErr } func (mm MultiMigration) Commit(ctx context.Context) error { diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index 25eace379..530a8c62f 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -109,7 +109,7 @@ func (m *MockLedgerReader) GetLedgerView(_ context.Context, sequence uint32) (xd return xdr.LedgerCloseMetaView(rawMeta), true, nil } -func (m *MockLedgerReader) StreamLedgerRange(_ context.Context, _ uint32, _ uint32, _ store.StreamLedgerFn) error { +func (m *MockLedgerReader) StreamLedgerRange(_ context.Context, _ uint32, _ uint32, _ store.StreamLedgerViewFn) error { return nil } diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index 1fcf10c54..71e8a5a7b 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -89,7 +89,7 @@ func (r *LedgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRange, e } func (r *LedgerReader) StreamLedgerRange( - ctx context.Context, startLedger, endLedger uint32, f store.StreamLedgerFn, + ctx context.Context, startLedger, endLedger uint32, f store.StreamLedgerViewFn, ) error { view, err := query.ViewFrom(ctx) if err != nil { @@ -107,11 +107,10 @@ func (r *LedgerReader) StreamLedgerRange( if ctx.Err() != nil { return ctx.Err() } - var lcm xdr.LedgerCloseMeta - if err := lcm.UnmarshalBinary(entry.Bytes); err != nil { - return fmt.Errorf("adapters: unmarshal ledger %d: %w", entry.Seq, err) - } - if err := f(lcm); err != nil { + // Entry.Bytes aliases the reader's scratch buffer and is overwritten + // on the next iteration step — exactly the StreamLedgerViewFn + // contract (the view is valid for the duration of the call only). + if err := f(xdr.LedgerCloseMetaView(entry.Bytes)); err != nil { return err } } 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 ae7a4bbf3..73f1377fd 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go @@ -148,9 +148,13 @@ func TestStreamLedgerRange(t *testing.T) { ctx, reader, c0, c1 := sparseFixture(t) var seqs []uint32 err := reader.StreamLedgerRange(ctx, c0.FirstLedger(), c1.FirstLedger()+500, - func(lcm xdr.LedgerCloseMeta) error { - assert.Equal(t, closeTimeFor(lcm.LedgerSequence()), lcm.LedgerCloseTime()) - seqs = append(seqs, lcm.LedgerSequence()) + func(view xdr.LedgerCloseMetaView) error { + seq, err := view.LedgerSequence() + require.NoError(t, err) + closeTime, err := view.LedgerCloseTime() + require.NoError(t, err) + assert.Equal(t, closeTimeFor(seq), closeTime) + seqs = append(seqs, seq) return nil }) require.NoError(t, err) @@ -163,7 +167,7 @@ func TestStreamLedgerRange_CallbackErrorStopsStream(t *testing.T) { boom := errors.New("boom") calls := 0 err := reader.StreamLedgerRange(ctx, c0.FirstLedger(), c0.FirstLedger()+3, - func(xdr.LedgerCloseMeta) error { + func(xdr.LedgerCloseMetaView) error { calls++ return boom }) @@ -175,7 +179,7 @@ func TestStreamLedgerRange_BelowFloorIsRangeError(t *testing.T) { ctx, reader, c0, _ := sparseFixture(t) var rangeErr *query.RangeError err := reader.StreamLedgerRange(ctx, 2, c0.FirstLedger(), - func(xdr.LedgerCloseMeta) error { return nil }) + func(xdr.LedgerCloseMetaView) error { return nil }) require.ErrorAs(t, err, &rangeErr) assert.Equal(t, uint32(2), rangeErr.Requested) assert.Equal(t, c0.FirstLedger(), rangeErr.Oldest) diff --git a/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go b/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go index a6bc258c7..7683f66e0 100644 --- a/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go @@ -223,7 +223,7 @@ func (s stubLedgerReader) GetLedgerRange(context.Context) (store.LedgerRange, er } func (s stubLedgerReader) StreamLedgerRange( - context.Context, uint32, uint32, store.StreamLedgerFn, + context.Context, uint32, uint32, store.StreamLedgerViewFn, ) error { return errors.New("unused") } diff --git a/cmd/stellar-rpc/internal/rpcv2/feereplay.go b/cmd/stellar-rpc/internal/rpcv2/feereplay.go index 89032bed9..fb496c357 100644 --- a/cmd/stellar-rpc/internal/rpcv2/feereplay.go +++ b/cmd/stellar-rpc/internal/rpcv2/feereplay.go @@ -20,11 +20,9 @@ import ( // The replay is the fees-only consumer of the shared walk: raw bytes from the // query router's read view → ExtractLedgerTxParts → FeesFromTxParts, events // never computed. It deliberately does NOT go through store.LedgerReader / -// StreamLedgerFn — that seam exists for the JSON-RPC handlers and hands out -// parsed xdr.LedgerCloseMeta, which would force a full LCM decode per -// replayed ledger (the only full decode anywhere on the v2 path, paid up to -// 1,000 times per restart) just to walk data the view extractor reads for -// free off raw bytes. +// StreamLedgerViewFn — that seam exists for the JSON-RPC handlers and runs +// against a request's read view; the replay walks the registry's read view +// directly and feeds the raw bytes straight to the fee extractor. func replayFeeWindows(registry *query.Registry, windows *feewindow.FeeWindows, lastCommitted uint32) error { if windows == nil { return nil // no fee consumer (tests) → nothing to refill diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index b1d5b94a7..8936b271f 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -11,7 +11,11 @@ import ( // ErrEmptyDB is returned when the storage backend holds no ledgers yet. var ErrEmptyDB = errors.New("DB is empty") -type StreamLedgerFn func(xdr.LedgerCloseMeta) error +// StreamLedgerViewFn receives each streamed ledger as a view over its raw +// bytes. The view is only valid for the duration of the call: a backend may +// hand out a buffer it reuses for the next ledger, so callers that need the +// bytes afterwards must copy them. +type StreamLedgerViewFn func(xdr.LedgerCloseMetaView) error // LedgerInfo identifies one ledger: its sequence number and close time. type LedgerInfo struct { @@ -38,7 +42,7 @@ type LedgerReader 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) - StreamLedgerRange(ctx context.Context, startLedger uint32, endLedger uint32, f StreamLedgerFn) error + StreamLedgerRange(ctx context.Context, startLedger uint32, endLedger uint32, f StreamLedgerViewFn) error NewTx(ctx context.Context) (LedgerReaderTx, error) GetLatestLedgerSequence(ctx context.Context) (uint32, error) } From be6ee92836909c74ff85104e05294e1957d6759e Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 24 Aug 2026 16:47:20 -0400 Subject: [PATCH 04/19] linter/ineffectual err assign --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go index 4c4604ef9..cc930a363 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go @@ -112,7 +112,7 @@ func (mm MultiMigration) Apply(ctx context.Context, view xdr.LedgerCloseMetaView decoded = true } if localErr := m.Apply(ctx, meta); localErr != nil { - err = errors.Join(applyErr, localErr, mm.db.Rollback()) + applyErr = errors.Join(applyErr, localErr, mm.db.Rollback()) } } return applyErr From b514d1a43a016f209d1005f6bdab53af8a6dec09 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 24 Aug 2026 17:23:25 -0400 Subject: [PATCH 05/19] fix shadowing bug in migration --- cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go | 5 +++-- cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go b/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go index ba9f150d7..0b3ea549c 100644 --- a/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go +++ b/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go @@ -473,10 +473,11 @@ func (d *Daemon) mustInitializeStorage(cfg *config.Config) *feewindow.FeeWindows ledgerSeqRange.First, ledgerSeqRange.Last, func(txMeta xdr.LedgerCloseMetaView) error { - currentSeq, err := txMeta.LedgerSequence() + seq, err := txMeta.LedgerSequence() if err != nil { return err } + currentSeq = seq if initialSeq == 0 { initialSeq = currentSeq d.logger. @@ -490,7 +491,7 @@ func (d *Daemon) mustInitializeStorage(cfg *config.Config) *feewindow.FeeWindows Debug("Still initializing in-memory store") } - if err := dataMigrations.Apply(readTxMetaCtx, txMeta); err != nil { + if err := dataMigrations.Apply(readTxMetaCtx, currentSeq, txMeta); err != nil { d.logger.WithError(err).Fatal("could not apply migration for ledger ", currentSeq) } diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go index cc930a363..68c41d840 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go @@ -92,11 +92,7 @@ func (mm MultiMigration) ApplicableRange() LedgerSeqRange { return result } -func (mm MultiMigration) Apply(ctx context.Context, view xdr.LedgerCloseMetaView) error { - ledgerSeq, err := view.LedgerSequence() - if err != nil { - return errors.Join(err, mm.db.Rollback()) - } +func (mm MultiMigration) Apply(ctx context.Context, ledgerSeq uint32, view xdr.LedgerCloseMetaView) error { var meta xdr.LedgerCloseMeta decoded := false var applyErr error From ce58de87cb6c54a1c5e3f885e0eefe9a31b2b4e6 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 24 Aug 2026 17:33:04 -0400 Subject: [PATCH 06/19] optimize getLedgerRange by NOT fetching full blob --- .../internal/methods/get_latest_ledger.go | 3 +- .../internal/rpcv1/sqlitedb/ledger.go | 85 ++++++++++--------- .../internal/rpcv1/sqlitedb/ledger_test.go | 19 +++++ 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go index fc4c4a9d5..fb9a3444a 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go @@ -4,7 +4,6 @@ import ( "context" "encoding/base64" "encoding/hex" - "fmt" "github.com/creachadair/jrpc2" @@ -36,7 +35,7 @@ func NewGetLatestLedgerHandler(ledgerReader store.LedgerReader) jrpc2.Handler { if err != nil { return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, - Message: fmt.Sprintf("could not read latest ledger meta: %v", err), + Message: "could not parse latest ledger header", } } return response, nil diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 8efaac11e..c2b5f5223 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -219,38 +219,61 @@ func (r ledgerReader) GetLatestLedgerSequence(ctx context.Context) (uint32, erro return getLatestLedgerSequence(ctx, r, r.db.cache) } +// ledgerCloseTimePrefixBytes is the fast-path meta prefix fetched for range +// endpoints. Parsing falls back to the full blob if the header extends past it. +const ledgerCloseTimePrefixBytes = 1024 + +// ledgerRangeRow is one endpoint of the stored ledger range. +type ledgerRangeRow struct { + Sequence uint32 `db:"sequence"` + MetaPrefix []byte `db:"meta_prefix"` +} + +// ledgerInfoFromRow reads the close time out of the row's meta prefix, +// refetching the full blob for rare metas whose close time lies beyond it. +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) + if dbErr != nil { + return store.LedgerInfo{}, dbErr + } + if !found { + return store.LedgerInfo{}, fmt.Errorf("couldn't get ledger %d close time: %w", row.Sequence, err) + } + closeTime, err = xdr.LedgerCloseMetaView(meta).LedgerCloseTime() + if err != nil { + return store.LedgerInfo{}, fmt.Errorf("couldn't get ledger %d close time: %w", row.Sequence, err) + } + } + return store.LedgerInfo{Sequence: row.Sequence, CloseTime: closeTime}, nil +} + // getLedgerRangeWithCache uses the latest ledger cache to optimize the query. // It only needs to look up the first ledger since we have the latest cached. func getLedgerRangeWithCache(ctx context.Context, db readDB, latestSeq uint32, latestTime int64, ) (store.LedgerRange, error) { - query := sq.Select("meta"). + query := sq.Select("sequence", fmt.Sprintf("substr(meta, 1, %d) AS meta_prefix", ledgerCloseTimePrefixBytes)). From(ledgerCloseMetaTableName). Where( fmt.Sprintf("sequence = (SELECT MIN(sequence) FROM %s)", ledgerCloseMetaTableName), ) - var lcmRaw []xdr.LedgerCloseMetaView - if err := db.Select(ctx, &lcmRaw, query); err != nil { + var rows []ledgerRangeRow + if err := db.Select(ctx, &rows, query); err != nil { return store.LedgerRange{}, fmt.Errorf("couldn't query ledger range: %w", err) } - if len(lcmRaw) == 0 { + if len(rows) == 0 { return store.LedgerRange{}, store.ErrEmptyDB } - firstSeq, err := lcmRaw[0].LedgerSequence() + firstLedger, err := ledgerInfoFromRow(ctx, db, rows[0]) if err != nil { - return store.LedgerRange{}, fmt.Errorf("couldn't get first ledger sequence: %w", err) - } - firstCloseTime, err := lcmRaw[0].LedgerCloseTime() - if err != nil { - return store.LedgerRange{}, fmt.Errorf("couldn't get first ledger close time: %w", err) + return store.LedgerRange{}, err } return store.LedgerRange{ - FirstLedger: store.LedgerInfo{ - Sequence: firstSeq, - CloseTime: firstCloseTime, - }, + FirstLedger: firstLedger, LastLedger: store.LedgerInfo{ Sequence: latestSeq, CloseTime: latestTime, @@ -260,48 +283,34 @@ func getLedgerRangeWithCache(ctx context.Context, db readDB, // getLedgerRangeWithoutCache queries both the first and last ledger when cache isn't available func getLedgerRangeWithoutCache(ctx context.Context, db readDB) (store.LedgerRange, error) { - query := sq.Select("lcm.meta"). + query := sq.Select("lcm.sequence", fmt.Sprintf("substr(lcm.meta, 1, %d) AS meta_prefix", ledgerCloseTimePrefixBytes)). From(ledgerCloseMetaTableName + " as lcm"). Where(sq.Or{ sq.Expr("lcm.sequence = (?)", sq.Select("MIN(sequence)").From(ledgerCloseMetaTableName)), sq.Expr("lcm.sequence = (?)", sq.Select("MAX(sequence)").From(ledgerCloseMetaTableName)), }).OrderBy("lcm.sequence ASC") - var lcms []xdr.LedgerCloseMetaView - if err := db.Select(ctx, &lcms, query); err != nil { + var rows []ledgerRangeRow + if err := db.Select(ctx, &rows, query); err != nil { return store.LedgerRange{}, fmt.Errorf("couldn't query ledger range: %w", err) } - if len(lcms) == 0 { + if len(rows) == 0 { return store.LedgerRange{}, store.ErrEmptyDB } - firstSeq, err := lcms[0].LedgerSequence() - if err != nil { - return store.LedgerRange{}, fmt.Errorf("couldn't get first ledger sequence: %w", err) - } - firstCloseTime, err := lcms[0].LedgerCloseTime() + firstLedger, err := ledgerInfoFromRow(ctx, db, rows[0]) if err != nil { - return store.LedgerRange{}, fmt.Errorf("couldn't get first ledger close time: %w", err) + return store.LedgerRange{}, err } - lastSeq, err := lcms[len(lcms)-1].LedgerSequence() + lastLedger, err := ledgerInfoFromRow(ctx, db, rows[len(rows)-1]) if err != nil { - return store.LedgerRange{}, fmt.Errorf("couldn't get last ledger sequence: %w", err) - } - lastCloseTime, err := lcms[len(lcms)-1].LedgerCloseTime() - if err != nil { - return store.LedgerRange{}, fmt.Errorf("couldn't get last ledger close time: %w", err) + return store.LedgerRange{}, err } return store.LedgerRange{ - FirstLedger: store.LedgerInfo{ - Sequence: firstSeq, - CloseTime: firstCloseTime, - }, - LastLedger: store.LedgerInfo{ - Sequence: lastSeq, - CloseTime: lastCloseTime, - }, + FirstLedger: firstLedger, + LastLedger: lastLedger, }, nil } diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index 223662eda..e1e71699b 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -115,6 +115,25 @@ func TestLedgers(t *testing.T) { assertLedgerRange(t, reader, 8, 12) } +// TestLedgerInfoFromRow_PrefixFallback covers a meta prefix too short to reach +// the close time, which must fall back to reading the full blob. +func TestLedgerInfoFromRow_PrefixFallback(t *testing.T) { + db := NewTestDB(t) + tx, err := NewReadWriter(logger, db, host.MakeNoOpDaemon(), 15, passphrase).NewTx(t.Context()) + require.NoError(t, err) + lcm := createLedger(42) + require.NoError(t, tx.LedgerWriter().InsertLedger(lcm)) + require.NoError(t, tx.Commit(lcm, nil)) + + raw, err := lcm.MarshalBinary() + require.NoError(t, err) + + info, err := ledgerInfoFromRow(t.Context(), db, ledgerRangeRow{Sequence: 42, MetaPrefix: raw[:8]}) + require.NoError(t, err) + assert.Equal(t, uint32(42), info.Sequence) + assert.Equal(t, lcm.LedgerCloseTime(), info.CloseTime) +} + func TestGetLedgerRange_NonEmptyDB(t *testing.T) { db := NewTestDB(t) ctx := context.TODO() From a8ac17519104d6e3dd1d650371222ad2d55ef125 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Thu, 27 Aug 2026 18:54:24 -0400 Subject: [PATCH 07/19] minor port finalizations --- cmd/stellar-rpc/internal/methods/get_latest_ledger.go | 2 -- cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go | 3 +-- cmd/stellar-rpc/internal/store/ledger.go | 3 +-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go index fb9a3444a..e3271399a 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go @@ -50,8 +50,6 @@ func latestLedgerResponse(view xdr.LedgerCloseMetaView, sequence uint32, if err != nil { return protocol.GetLatestLedgerResponse{}, err } - // Must* accessors panic with *ViewError on malformed fields and Try - // recovers it, so the extraction needs a single error check. return xdr.Try(func() protocol.GetLatestLedgerResponse { header := headerEntry.MustHeader() return protocol.GetLatestLedgerResponse{ diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index 71e8a5a7b..fe1890adc 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -108,8 +108,7 @@ func (r *LedgerReader) StreamLedgerRange( return ctx.Err() } // Entry.Bytes aliases the reader's scratch buffer and is overwritten - // on the next iteration step — exactly the StreamLedgerViewFn - // contract (the view is valid for the duration of the call only). + // on the next iteration, so the view is invalid after the call. if err := f(xdr.LedgerCloseMetaView(entry.Bytes)); err != nil { return err } diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index 8936b271f..8f3fa91c6 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -12,8 +12,7 @@ import ( var ErrEmptyDB = errors.New("DB is empty") // StreamLedgerViewFn receives each streamed ledger as a view over its raw -// bytes. The view is only valid for the duration of the call: a backend may -// hand out a buffer it reuses for the next ledger, so callers that need the +// bytes. The view is only valid for the duration of the call, so callers that // bytes afterwards must copy them. type StreamLedgerViewFn func(xdr.LedgerCloseMetaView) error From 974fe300b1402e41f79087381b83d37b7b9290d4 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 28 Aug 2026 12:57:27 -0400 Subject: [PATCH 08/19] add bench test --- .../internal/rpcv1/sqlitedb/ledger_test.go | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index e1e71699b..e96820a14 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -2,11 +2,13 @@ package sqlitedb import ( "context" + "fmt" "io" "path" "slices" "testing" + sq "github.com/Masterminds/squirrel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -288,6 +290,103 @@ func BenchmarkBatchGetLedgers(b *testing.B) { } } +// fullBlobLedgerRange reproduces the pre-optimization oldest-ledger lookup +// (SELECT meta, i.e. the entire blob) as the benchmark baseline. +func fullBlobLedgerRange(ctx context.Context, db readDB, + latestSeq uint32, latestTime int64, +) (store.LedgerRange, error) { + query := sq.Select("meta"). + From(ledgerCloseMetaTableName). + Where( + fmt.Sprintf("sequence = (SELECT MIN(sequence) FROM %s)", ledgerCloseMetaTableName), + ) + var lcmRaw []xdr.LedgerCloseMetaView + if err := db.Select(ctx, &lcmRaw, query); err != nil { + return store.LedgerRange{}, err + } + if len(lcmRaw) == 0 { + return store.LedgerRange{}, store.ErrEmptyDB + } + firstSeq, err := lcmRaw[0].LedgerSequence() + if err != nil { + return store.LedgerRange{}, err + } + firstCloseTime, err := lcmRaw[0].LedgerCloseTime() + if err != nil { + return store.LedgerRange{}, err + } + return store.LedgerRange{ + FirstLedger: store.LedgerInfo{Sequence: firstSeq, CloseTime: firstCloseTime}, + LastLedger: store.LedgerInfo{Sequence: latestSeq, CloseTime: latestTime}, + }, nil +} + +// padLedger grows a txMeta ledger's meta to roughly size bytes via its soroban return value. +func padLedger(lcm xdr.LedgerCloseMeta, size int) xdr.LedgerCloseMeta { + payload := xdr.ScBytes(make([]byte, size)) + lcm.V2.TxProcessing[0].TxApplyProcessing.V3.SorobanMeta.ReturnValue = xdr.ScVal{ + Type: xdr.ScValTypeScvBytes, + Bytes: &payload, + } + return lcm +} + +// BenchmarkOldestLedgerRangeLookup pits the 1KiB prefix fetch in +// getLedgerRangeWithCache against the full-blob query it replaced. The tx read +// path (getLedgers/getTransactions) runs this lookup once per request; sizes +// are the min/avg/max meta blobs observed on a pubnet 7-day node. +func BenchmarkOldestLedgerRangeLookup(b *testing.B) { + for _, tc := range []struct { + name string + size int + }{ + {"512KiB", 512 << 10}, + {"2MiB", 2 << 20}, + {"4MiB", 4 << 20}, + } { + testDB := NewTestDB(b) + writer := NewReadWriter(logger, testDB, host.MakeNoOpDaemon(), 1_000_000, passphrase) + write, err := writer.NewTx(b.Context()) + require.NoError(b, err) + + lcms := []xdr.LedgerCloseMeta{ + padLedger(txMeta(1000, true), tc.size), + padLedger(txMeta(1001, true), tc.size), + } + ledgerW, txW := write.LedgerWriter(), write.TransactionWriter() + for _, lcm := range lcms { + require.NoError(b, ledgerW.InsertLedger(lcm)) + require.NoError(b, txW.InsertTransactions(lcm)) + } + latest := lcms[len(lcms)-1] + require.NoError(b, write.Commit(latest, nil)) + + ctx := b.Context() + latestSeq, latestTime := latest.LedgerSequence(), latest.LedgerCloseTime() + want, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) + require.NoError(b, err) + got, err := fullBlobLedgerRange(ctx, testDB, latestSeq, latestTime) + require.NoError(b, err) + require.Equal(b, want, got) + require.Equal(b, lcms[0].LedgerSequence(), got.FirstLedger.Sequence) + + b.Run(tc.name+"/prefix", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + _, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) + require.NoError(b, err) + } + }) + b.Run(tc.name+"/fullBlob", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + _, err := fullBlobLedgerRange(ctx, testDB, latestSeq, latestTime) + require.NoError(b, err) + } + }) + } +} + func NewTestDB(tb testing.TB) *DB { tmp := tb.TempDir() dbPath := path.Join(tmp, "db.sqlite") From a813f6bdaf2c777623472561aea1e5468f7d2416 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 28 Aug 2026 12:58:50 -0400 Subject: [PATCH 09/19] minor comment adjustment --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index e96820a14..8649df71f 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -333,13 +333,13 @@ func padLedger(lcm xdr.LedgerCloseMeta, size int) xdr.LedgerCloseMeta { // BenchmarkOldestLedgerRangeLookup pits the 1KiB prefix fetch in // getLedgerRangeWithCache against the full-blob query it replaced. The tx read -// path (getLedgers/getTransactions) runs this lookup once per request; sizes -// are the min/avg/max meta blobs observed on a pubnet 7-day node. +// path (getLedgers/getTransactions) runs this lookup once per request func BenchmarkOldestLedgerRangeLookup(b *testing.B) { for _, tc := range []struct { name string size int }{ + // min/avg/max meta blob sizes observed on a pubnet 7-day node {"512KiB", 512 << 10}, {"2MiB", 2 << 20}, {"4MiB", 4 << 20}, From 0e62cc0ddde4a0a856d8e4f1103781775bd476b6 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 28 Aug 2026 13:15:56 -0400 Subject: [PATCH 10/19] add human-readable comparison --- .../internal/rpcv1/sqlitedb/ledger_test.go | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index 8649df71f..747d5f37f 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -5,7 +5,9 @@ import ( "fmt" "io" "path" + "runtime" "slices" + "strings" "testing" sq "github.com/Masterminds/squirrel" @@ -331,10 +333,33 @@ func padLedger(lcm xdr.LedgerCloseMeta, size int) xdr.LedgerCloseMeta { return lcm } +// benchLookup times fn as a sub-benchmark and returns its result so the parent +// can summarize ratios across variants. +func benchLookup(b *testing.B, name string, fn func() error) testing.BenchmarkResult { + var res testing.BenchmarkResult + b.Run(name, func(b *testing.B) { + b.ReportAllocs() + var before, after runtime.MemStats + runtime.ReadMemStats(&before) + for b.Loop() { + require.NoError(b, fn()) + } + runtime.ReadMemStats(&after) + res = testing.BenchmarkResult{ + N: b.N, + T: b.Elapsed(), + MemAllocs: after.Mallocs - before.Mallocs, + MemBytes: after.TotalAlloc - before.TotalAlloc, + } + }) + return res +} + // BenchmarkOldestLedgerRangeLookup pits the 1KiB prefix fetch in // getLedgerRangeWithCache against the full-blob query it replaced. The tx read // path (getLedgers/getTransactions) runs this lookup once per request func BenchmarkOldestLedgerRangeLookup(b *testing.B) { + var summary strings.Builder for _, tc := range []struct { name string size int @@ -370,21 +395,20 @@ func BenchmarkOldestLedgerRangeLookup(b *testing.B) { require.Equal(b, want, got) require.Equal(b, lcms[0].LedgerSequence(), got.FirstLedger.Sequence) - b.Run(tc.name+"/prefix", func(b *testing.B) { - b.ReportAllocs() - for b.Loop() { - _, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) - require.NoError(b, err) - } + prefix := benchLookup(b, tc.name+"/prefix", func() error { + _, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) + return err }) - b.Run(tc.name+"/fullBlob", func(b *testing.B) { - b.ReportAllocs() - for b.Loop() { - _, err := fullBlobLedgerRange(ctx, testDB, latestSeq, latestTime) - require.NoError(b, err) - } + full := benchLookup(b, tc.name+"/fullBlob", func() error { + _, err := fullBlobLedgerRange(ctx, testDB, latestSeq, latestTime) + return err }) + summary.WriteString(fmt.Sprintf("\n %-8s %.2fx ns/op | %.3fx B/op | %.2fx allocs/op", tc.name+":", + float64(prefix.NsPerOp())/float64(full.NsPerOp()), + float64(prefix.AllocedBytesPerOp())/float64(full.AllocedBytesPerOp()), + float64(prefix.AllocsPerOp())/float64(full.AllocsPerOp()))) } + fmt.Println("\nprefix / fullBlob:" + summary.String()) //nolint:forbidigo // b.Log would only show under -v } func NewTestDB(tb testing.TB) *DB { From 5e517cbae4dcd4bbbdf630eea9e16baf53c9a7e5 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 28 Aug 2026 13:19:29 -0400 Subject: [PATCH 11/19] add explanatory comment --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index 747d5f37f..abff8a0fe 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -386,6 +386,7 @@ func BenchmarkOldestLedgerRangeLookup(b *testing.B) { latest := lcms[len(lcms)-1] require.NoError(b, write.Commit(latest, nil)) + // Verify that both the prefix and full-blob lookups return the same ledger range. ctx := b.Context() latestSeq, latestTime := latest.LedgerSequence(), latest.LedgerCloseTime() want, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) From 2cdc52bba6295e2edd408eafb0f144084550aaf1 Mon Sep 17 00:00:00 2001 From: Christian Jonas <86204361+cjonas9@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:26:20 -0400 Subject: [PATCH 12/19] fix missing verb in comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cmd/stellar-rpc/internal/store/ledger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index 8f3fa91c6..d3f27e518 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -13,7 +13,7 @@ var ErrEmptyDB = errors.New("DB is empty") // StreamLedgerViewFn receives each streamed ledger as a view over its raw // bytes. The view is only valid for the duration of the call, so callers that -// bytes afterwards must copy them. +// retain the bytes afterwards must copy them. type StreamLedgerViewFn func(xdr.LedgerCloseMetaView) error // LedgerInfo identifies one ledger: its sequence number and close time. From c8a45208eec9ffe96b43c14620657e19fc460f77 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 28 Aug 2026 14:04:18 -0400 Subject: [PATCH 13/19] simplify bench test --- .../internal/rpcv1/sqlitedb/ledger_test.go | 94 +++---------------- 1 file changed, 13 insertions(+), 81 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index abff8a0fe..07d1be777 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -2,15 +2,11 @@ package sqlitedb import ( "context" - "fmt" "io" "path" - "runtime" "slices" - "strings" "testing" - sq "github.com/Masterminds/squirrel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -292,37 +288,6 @@ func BenchmarkBatchGetLedgers(b *testing.B) { } } -// fullBlobLedgerRange reproduces the pre-optimization oldest-ledger lookup -// (SELECT meta, i.e. the entire blob) as the benchmark baseline. -func fullBlobLedgerRange(ctx context.Context, db readDB, - latestSeq uint32, latestTime int64, -) (store.LedgerRange, error) { - query := sq.Select("meta"). - From(ledgerCloseMetaTableName). - Where( - fmt.Sprintf("sequence = (SELECT MIN(sequence) FROM %s)", ledgerCloseMetaTableName), - ) - var lcmRaw []xdr.LedgerCloseMetaView - if err := db.Select(ctx, &lcmRaw, query); err != nil { - return store.LedgerRange{}, err - } - if len(lcmRaw) == 0 { - return store.LedgerRange{}, store.ErrEmptyDB - } - firstSeq, err := lcmRaw[0].LedgerSequence() - if err != nil { - return store.LedgerRange{}, err - } - firstCloseTime, err := lcmRaw[0].LedgerCloseTime() - if err != nil { - return store.LedgerRange{}, err - } - return store.LedgerRange{ - FirstLedger: store.LedgerInfo{Sequence: firstSeq, CloseTime: firstCloseTime}, - LastLedger: store.LedgerInfo{Sequence: latestSeq, CloseTime: latestTime}, - }, nil -} - // padLedger grows a txMeta ledger's meta to roughly size bytes via its soroban return value. func padLedger(lcm xdr.LedgerCloseMeta, size int) xdr.LedgerCloseMeta { payload := xdr.ScBytes(make([]byte, size)) @@ -333,33 +298,10 @@ func padLedger(lcm xdr.LedgerCloseMeta, size int) xdr.LedgerCloseMeta { return lcm } -// benchLookup times fn as a sub-benchmark and returns its result so the parent -// can summarize ratios across variants. -func benchLookup(b *testing.B, name string, fn func() error) testing.BenchmarkResult { - var res testing.BenchmarkResult - b.Run(name, func(b *testing.B) { - b.ReportAllocs() - var before, after runtime.MemStats - runtime.ReadMemStats(&before) - for b.Loop() { - require.NoError(b, fn()) - } - runtime.ReadMemStats(&after) - res = testing.BenchmarkResult{ - N: b.N, - T: b.Elapsed(), - MemAllocs: after.Mallocs - before.Mallocs, - MemBytes: after.TotalAlloc - before.TotalAlloc, - } - }) - return res -} - -// BenchmarkOldestLedgerRangeLookup pits the 1KiB prefix fetch in -// getLedgerRangeWithCache against the full-blob query it replaced. The tx read -// path (getLedgers/getTransactions) runs this lookup once per request +// BenchmarkOldestLedgerRangeLookup measures the 1KiB prefix fetch in +// getLedgerRangeWithCache. The tx read path (getLedgers/getTransactions) runs +// this lookup once per request. func BenchmarkOldestLedgerRangeLookup(b *testing.B) { - var summary strings.Builder for _, tc := range []struct { name string size int @@ -369,9 +311,10 @@ func BenchmarkOldestLedgerRangeLookup(b *testing.B) { {"2MiB", 2 << 20}, {"4MiB", 4 << 20}, } { + ctx := b.Context() testDB := NewTestDB(b) writer := NewReadWriter(logger, testDB, host.MakeNoOpDaemon(), 1_000_000, passphrase) - write, err := writer.NewTx(b.Context()) + write, err := writer.NewTx(ctx) require.NoError(b, err) lcms := []xdr.LedgerCloseMeta{ @@ -385,31 +328,20 @@ func BenchmarkOldestLedgerRangeLookup(b *testing.B) { } latest := lcms[len(lcms)-1] require.NoError(b, write.Commit(latest, nil)) - - // Verify that both the prefix and full-blob lookups return the same ledger range. - ctx := b.Context() latestSeq, latestTime := latest.LedgerSequence(), latest.LedgerCloseTime() - want, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) - require.NoError(b, err) - got, err := fullBlobLedgerRange(ctx, testDB, latestSeq, latestTime) + + got, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) require.NoError(b, err) - require.Equal(b, want, got) require.Equal(b, lcms[0].LedgerSequence(), got.FirstLedger.Sequence) - prefix := benchLookup(b, tc.name+"/prefix", func() error { - _, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) - return err - }) - full := benchLookup(b, tc.name+"/fullBlob", func() error { - _, err := fullBlobLedgerRange(ctx, testDB, latestSeq, latestTime) - return err + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + _, err := getLedgerRangeWithCache(ctx, testDB, latestSeq, latestTime) + require.NoError(b, err) + } }) - summary.WriteString(fmt.Sprintf("\n %-8s %.2fx ns/op | %.3fx B/op | %.2fx allocs/op", tc.name+":", - float64(prefix.NsPerOp())/float64(full.NsPerOp()), - float64(prefix.AllocedBytesPerOp())/float64(full.AllocedBytesPerOp()), - float64(prefix.AllocsPerOp())/float64(full.AllocsPerOp()))) } - fmt.Println("\nprefix / fullBlob:" + summary.String()) //nolint:forbidigo // b.Log would only show under -v } func NewTestDB(tb testing.TB) *DB { From 97376c9e138c6f2a21b9f2f688a3e67323559c40 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Fri, 28 Aug 2026 16:58:57 -0400 Subject: [PATCH 14/19] restore migrations/StreamLedgerFn to operate on LCM rather than views --- .../internal/methods/get_latest_ledger_test.go | 2 +- .../internal/methods/get_transactions_test.go | 2 +- cmd/stellar-rpc/internal/methods/mocks.go | 2 +- cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go | 10 +++------- .../internal/rpcv1/sqlitedb/ledger.go | 9 ++++----- .../internal/rpcv1/sqlitedb/ledger_test.go | 10 ++++------ .../internal/rpcv1/sqlitedb/migration.go | 17 +++++------------ .../internal/rpcv1/sqlitedb/mocks.go | 2 +- .../internal/rpcv2/adapters/ledger_reader.go | 10 ++++++---- .../rpcv2/adapters/ledger_reader_test.go | 14 +++++--------- .../internal/rpcv2/corestate/corestate_test.go | 2 +- cmd/stellar-rpc/internal/rpcv2/feereplay.go | 8 +++++--- cmd/stellar-rpc/internal/store/ledger.go | 7 ++----- 13 files changed, 39 insertions(+), 56 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go index b33f21005..285705656 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go @@ -63,7 +63,7 @@ func (ledgerReader *ConstantLedgerReader) StreamLedgerRange( _ context.Context, _ uint32, _ uint32, - _ store.StreamLedgerViewFn, + _ store.StreamLedgerFn, ) error { return nil } diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index e7c4caed3..703f34ebf 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -401,7 +401,7 @@ func (r *sparseLedgerReader) BatchGetLedgers(context.Context, uint32, uint32) ([ return nil, nil } -func (r *sparseLedgerReader) StreamLedgerRange(context.Context, uint32, uint32, store.StreamLedgerViewFn) error { +func (r *sparseLedgerReader) StreamLedgerRange(context.Context, uint32, uint32, store.StreamLedgerFn) error { return nil } diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index 6fa2f8402..10009ff49 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -41,7 +41,7 @@ func (m *MockLedgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRang } func (m *MockLedgerReader) StreamLedgerRange(ctx context.Context, startLedger, endLedger uint32, - f store.StreamLedgerViewFn, + f store.StreamLedgerFn, ) error { args := m.Called(ctx, startLedger, endLedger, f) return args.Error(0) diff --git a/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go b/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go index 0b3ea549c..abfeb181e 100644 --- a/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go +++ b/cmd/stellar-rpc/internal/rpcv1/daemon/daemon.go @@ -472,12 +472,8 @@ func (d *Daemon) mustInitializeStorage(cfg *config.Config) *feewindow.FeeWindows readTxMetaCtx, ledgerSeqRange.First, ledgerSeqRange.Last, - func(txMeta xdr.LedgerCloseMetaView) error { - seq, err := txMeta.LedgerSequence() - if err != nil { - return err - } - currentSeq = seq + func(txMeta xdr.LedgerCloseMeta) error { + currentSeq = txMeta.LedgerSequence() if initialSeq == 0 { initialSeq = currentSeq d.logger. @@ -491,7 +487,7 @@ func (d *Daemon) mustInitializeStorage(cfg *config.Config) *feewindow.FeeWindows Debug("Still initializing in-memory store") } - if err := dataMigrations.Apply(readTxMetaCtx, currentSeq, txMeta); err != nil { + if err := dataMigrations.Apply(readTxMetaCtx, txMeta); err != nil { d.logger.WithError(err).Fatal("could not apply migration for ledger ", currentSeq) } diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index c2b5f5223..15970bef0 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -2,7 +2,6 @@ package sqlitedb import ( - "bytes" "context" "database/sql" "errors" @@ -121,7 +120,7 @@ func (r ledgerReader) StreamLedgerRange( ctx context.Context, startLedger uint32, endLedger uint32, - f store.StreamLedgerViewFn, + f store.StreamLedgerFn, ) error { sql := sq.Select("meta").From(ledgerCloseMetaTableName). Where(sq.GtOrEq{"sequence": startLedger}). @@ -134,7 +133,7 @@ func (r ledgerReader) StreamLedgerRange( } defer q.Close() for q.Next() { - var closeMeta xdr.LedgerCloseMetaView + var closeMeta xdr.LedgerCloseMeta if err = q.Scan(&closeMeta); err != nil { return err } @@ -358,12 +357,12 @@ 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) { - raw, found, err := getLedgerRawFromDB(ctx, db, sequence) + meta, found, err := getLedgerRawFromDB(ctx, db, sequence) if err != nil || !found { return xdr.LedgerCloseMeta{}, false, err } var lcm xdr.LedgerCloseMeta - if _, err := xdr.Unmarshal(bytes.NewReader(raw), &lcm); err != nil { + if err := lcm.UnmarshalBinary(meta); err != nil { return xdr.LedgerCloseMeta{}, false, err } return lcm, true, nil diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index 07d1be777..853f37393 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -4,7 +4,6 @@ import ( "context" "io" "path" - "slices" "testing" "github.com/stretchr/testify/assert" @@ -43,10 +42,9 @@ func createLedger(ledgerSequence uint32) xdr.LedgerCloseMeta { func assertLedgerRange(t *testing.T, reader LedgerReader, start, end uint32) { ctx := t.Context() - var allLedgers []xdr.LedgerCloseMetaView - err := reader.StreamLedgerRange(ctx, start-1, end+1, func(txmeta xdr.LedgerCloseMetaView) error { - // the view is only valid for the duration of the callback - allLedgers = append(allLedgers, slices.Clone(txmeta)) + var allLedgers []xdr.LedgerCloseMeta + err := reader.StreamLedgerRange(ctx, start-1, end+1, func(txmeta xdr.LedgerCloseMeta) error { + allLedgers = append(allLedgers, txmeta) return nil }) require.NoError(t, err) @@ -65,7 +63,7 @@ func assertLedgerRange(t *testing.T, reader LedgerReader, start, end uint32) { require.NoError(t, err) assert.Equal(t, expectedBinary, ledgerBinary) - ledgerBinary, err = allLedgers[0].Raw() + ledgerBinary, err = allLedgers[0].MarshalBinary() require.NoError(t, err) assert.Equal(t, expectedBinary, ledgerBinary) allLedgers = allLedgers[1:] diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go index 68c41d840..22a5df906 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/migration.go @@ -92,26 +92,19 @@ func (mm MultiMigration) ApplicableRange() LedgerSeqRange { return result } -func (mm MultiMigration) Apply(ctx context.Context, ledgerSeq uint32, view xdr.LedgerCloseMetaView) error { - var meta xdr.LedgerCloseMeta - decoded := false - var applyErr error +func (mm MultiMigration) Apply(ctx context.Context, meta xdr.LedgerCloseMeta) error { + var err error for _, m := range mm.migrations { + ledgerSeq := meta.LedgerSequence() if !m.ApplicableRange().IsLedgerIncluded(ledgerSeq) { // The range of a sub-migration can be smaller than the global range. continue } - if !decoded { - if err := meta.UnmarshalBinary(view); err != nil { - return errors.Join(err, mm.db.Rollback()) - } - decoded = true - } if localErr := m.Apply(ctx, meta); localErr != nil { - applyErr = errors.Join(applyErr, localErr, mm.db.Rollback()) + err = errors.Join(err, localErr, mm.db.Rollback()) } } - return applyErr + return err } func (mm MultiMigration) Commit(ctx context.Context) error { diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index 530a8c62f..25eace379 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -109,7 +109,7 @@ func (m *MockLedgerReader) GetLedgerView(_ context.Context, sequence uint32) (xd return xdr.LedgerCloseMetaView(rawMeta), true, nil } -func (m *MockLedgerReader) StreamLedgerRange(_ context.Context, _ uint32, _ uint32, _ store.StreamLedgerViewFn) error { +func (m *MockLedgerReader) StreamLedgerRange(_ context.Context, _ uint32, _ uint32, _ store.StreamLedgerFn) error { return nil } diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index fe1890adc..1fcf10c54 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -89,7 +89,7 @@ func (r *LedgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRange, e } func (r *LedgerReader) StreamLedgerRange( - ctx context.Context, startLedger, endLedger uint32, f store.StreamLedgerViewFn, + ctx context.Context, startLedger, endLedger uint32, f store.StreamLedgerFn, ) error { view, err := query.ViewFrom(ctx) if err != nil { @@ -107,9 +107,11 @@ func (r *LedgerReader) StreamLedgerRange( if ctx.Err() != nil { return ctx.Err() } - // Entry.Bytes aliases the reader's scratch buffer and is overwritten - // on the next iteration, so the view is invalid after the call. - if err := f(xdr.LedgerCloseMetaView(entry.Bytes)); err != nil { + var lcm xdr.LedgerCloseMeta + if err := lcm.UnmarshalBinary(entry.Bytes); err != nil { + return fmt.Errorf("adapters: unmarshal ledger %d: %w", entry.Seq, err) + } + if err := f(lcm); err != nil { return err } } 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 73f1377fd..ae7a4bbf3 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go @@ -148,13 +148,9 @@ func TestStreamLedgerRange(t *testing.T) { ctx, reader, c0, c1 := sparseFixture(t) var seqs []uint32 err := reader.StreamLedgerRange(ctx, c0.FirstLedger(), c1.FirstLedger()+500, - func(view xdr.LedgerCloseMetaView) error { - seq, err := view.LedgerSequence() - require.NoError(t, err) - closeTime, err := view.LedgerCloseTime() - require.NoError(t, err) - assert.Equal(t, closeTimeFor(seq), closeTime) - seqs = append(seqs, seq) + func(lcm xdr.LedgerCloseMeta) error { + assert.Equal(t, closeTimeFor(lcm.LedgerSequence()), lcm.LedgerCloseTime()) + seqs = append(seqs, lcm.LedgerSequence()) return nil }) require.NoError(t, err) @@ -167,7 +163,7 @@ func TestStreamLedgerRange_CallbackErrorStopsStream(t *testing.T) { boom := errors.New("boom") calls := 0 err := reader.StreamLedgerRange(ctx, c0.FirstLedger(), c0.FirstLedger()+3, - func(xdr.LedgerCloseMetaView) error { + func(xdr.LedgerCloseMeta) error { calls++ return boom }) @@ -179,7 +175,7 @@ func TestStreamLedgerRange_BelowFloorIsRangeError(t *testing.T) { ctx, reader, c0, _ := sparseFixture(t) var rangeErr *query.RangeError err := reader.StreamLedgerRange(ctx, 2, c0.FirstLedger(), - func(xdr.LedgerCloseMetaView) error { return nil }) + func(xdr.LedgerCloseMeta) error { return nil }) require.ErrorAs(t, err, &rangeErr) assert.Equal(t, uint32(2), rangeErr.Requested) assert.Equal(t, c0.FirstLedger(), rangeErr.Oldest) diff --git a/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go b/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go index 7683f66e0..a6bc258c7 100644 --- a/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go @@ -223,7 +223,7 @@ func (s stubLedgerReader) GetLedgerRange(context.Context) (store.LedgerRange, er } func (s stubLedgerReader) StreamLedgerRange( - context.Context, uint32, uint32, store.StreamLedgerViewFn, + context.Context, uint32, uint32, store.StreamLedgerFn, ) error { return errors.New("unused") } diff --git a/cmd/stellar-rpc/internal/rpcv2/feereplay.go b/cmd/stellar-rpc/internal/rpcv2/feereplay.go index fb496c357..89032bed9 100644 --- a/cmd/stellar-rpc/internal/rpcv2/feereplay.go +++ b/cmd/stellar-rpc/internal/rpcv2/feereplay.go @@ -20,9 +20,11 @@ import ( // The replay is the fees-only consumer of the shared walk: raw bytes from the // query router's read view → ExtractLedgerTxParts → FeesFromTxParts, events // never computed. It deliberately does NOT go through store.LedgerReader / -// StreamLedgerViewFn — that seam exists for the JSON-RPC handlers and runs -// against a request's read view; the replay walks the registry's read view -// directly and feeds the raw bytes straight to the fee extractor. +// StreamLedgerFn — that seam exists for the JSON-RPC handlers and hands out +// parsed xdr.LedgerCloseMeta, which would force a full LCM decode per +// replayed ledger (the only full decode anywhere on the v2 path, paid up to +// 1,000 times per restart) just to walk data the view extractor reads for +// free off raw bytes. func replayFeeWindows(registry *query.Registry, windows *feewindow.FeeWindows, lastCommitted uint32) error { if windows == nil { return nil // no fee consumer (tests) → nothing to refill diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index d3f27e518..b1d5b94a7 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -11,10 +11,7 @@ import ( // ErrEmptyDB is returned when the storage backend holds no ledgers yet. var ErrEmptyDB = errors.New("DB is empty") -// StreamLedgerViewFn receives each streamed ledger as a view over its raw -// bytes. The view is only valid for the duration of the call, so callers that -// retain the bytes afterwards must copy them. -type StreamLedgerViewFn func(xdr.LedgerCloseMetaView) error +type StreamLedgerFn func(xdr.LedgerCloseMeta) error // LedgerInfo identifies one ledger: its sequence number and close time. type LedgerInfo struct { @@ -41,7 +38,7 @@ type LedgerReader 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) - StreamLedgerRange(ctx context.Context, startLedger uint32, endLedger uint32, f StreamLedgerViewFn) error + StreamLedgerRange(ctx context.Context, startLedger uint32, endLedger uint32, f StreamLedgerFn) error NewTx(ctx context.Context) (LedgerReaderTx, error) GetLatestLedgerSequence(ctx context.Context) (uint32, error) } From 0bc19d80828d4b046f9a4a1eff13ddab36df7773 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Mon, 31 Aug 2026 13:55:34 -0400 Subject: [PATCH 15/19] replace GetLedgerView with loan-shaped WithLedgerRaw on ledgerReader --- .../internal/methods/get_latest_ledger.go | 16 ++++--- .../methods/get_latest_ledger_test.go | 11 +++-- .../internal/methods/get_transactions_test.go | 8 ++-- cmd/stellar-rpc/internal/methods/mocks.go | 11 +++-- .../internal/rpcv1/sqlitedb/ledger.go | 8 ++-- .../internal/rpcv1/sqlitedb/mocks.go | 8 ++-- .../internal/rpcv2/adapters/ledger_reader.go | 28 ++++++------- .../rpcv2/adapters/ledger_reader_test.go | 42 +++++++++++++++++++ .../rpcv2/corestate/corestate_test.go | 4 +- cmd/stellar-rpc/internal/store/ledger.go | 6 ++- 10 files changed, 95 insertions(+), 47 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go index e3271399a..4d774ea92 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go @@ -24,18 +24,22 @@ func NewGetLatestLedgerHandler(ledgerReader store.LedgerReader) jrpc2.Handler { Message: "could not get latest ledger sequence", } } - latestLedgerView, found, err := ledgerReader.GetLedgerView(ctx, latestSequence) - if (err != nil) || (!found) { + var response protocol.GetLatestLedgerResponse + found, err := ledgerReader.WithLedgerRaw(ctx, latestSequence, func(raw []byte) error { + var lerr error + response, lerr = latestLedgerResponse(xdr.LedgerCloseMetaView(raw), latestSequence) + return lerr + }) + if err != nil && found { return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, - Message: "could not get latest ledger", + Message: "could not parse latest ledger header", } } - response, err := latestLedgerResponse(latestLedgerView, latestSequence) - if err != nil { + if err != nil || !found { return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, - Message: "could not parse latest ledger header", + Message: "could not get latest ledger", } } return response, nil diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go index 285705656..cefecd0ff 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go @@ -45,18 +45,17 @@ func (ledgerReader *ConstantLedgerReader) GetLedger(_ context.Context, true, nil } -func (ledgerReader *ConstantLedgerReader) GetLedgerView( - _ context.Context, - sequence uint32, -) (xdr.LedgerCloseMetaView, bool, error) { +func (ledgerReader *ConstantLedgerReader) WithLedgerRaw( + _ context.Context, sequence uint32, fn store.WithLedgerRawFn, +) (bool, error) { lcm := createLedger(expectedLatestLedgerHashBytes, sequence, expectedLatestLedgerCloseTime) raw, err := lcm.MarshalBinary() if err != nil { - return nil, false, err + return false, err } - return xdr.LedgerCloseMetaView(raw), true, nil + return true, fn(raw) } func (ledgerReader *ConstantLedgerReader) StreamLedgerRange( diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index 703f34ebf..40a56fed6 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -381,13 +381,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) { +func (r *sparseLedgerReader) WithLedgerRaw( + _ context.Context, seq uint32, fn store.WithLedgerRawFn, +) (bool, error) { r.gets++ raw, err := createEmptyTestLedger(seq).MarshalBinary() if err != nil { - return nil, false, err + return false, err } - return xdr.LedgerCloseMetaView(raw), true, nil + return true, fn(raw) } func (r *sparseLedgerReader) GetLedgerRange(context.Context) (store.LedgerRange, error) { diff --git a/cmd/stellar-rpc/internal/methods/mocks.go b/cmd/stellar-rpc/internal/methods/mocks.go index 10009ff49..7f9094108 100644 --- a/cmd/stellar-rpc/internal/methods/mocks.go +++ b/cmd/stellar-rpc/internal/methods/mocks.go @@ -27,12 +27,11 @@ func (m *MockLedgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr. return args.Get(0).(xdr.LedgerCloseMeta), args.Bool(1), args.Error(2) //nolint:forcetypeassert } -func (m *MockLedgerReader) 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 *MockLedgerReader) 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 *MockLedgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRange, error) { diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 15970bef0..1e19b098b 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -149,13 +149,13 @@ func (r ledgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr.Ledge return getLedgerFromDB(ctx, r.db, sequence) } -// GetLedgerView fetches a single ledger from the db and returns its view and whether it was found. -func (r ledgerReader) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { +// 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) if err != nil || !found { - return nil, found, err + return found, err } - return xdr.LedgerCloseMetaView(meta), true, nil + return true, fn(meta) } // GetLedgerRange pulls the min/max ledger sequence numbers from the meta table. diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go index 25eace379..5930dd950 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/mocks.go @@ -97,16 +97,16 @@ func (m *MockLedgerReader) GetLedger(_ context.Context, sequence uint32) (xdr.Le return *lcm, true, nil } -func (m *MockLedgerReader) GetLedgerView(_ context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { +func (m *MockLedgerReader) WithLedgerRaw(_ context.Context, sequence uint32, fn store.WithLedgerRawFn) (bool, error) { lcm, ok := m.txn.ledgerSeqToMeta[sequence] if !ok { - return nil, false, nil + return false, nil } rawMeta, err := lcm.MarshalBinary() if err != nil { - return nil, false, err + return false, err } - return xdr.LedgerCloseMetaView(rawMeta), true, nil + return true, fn(rawMeta) } func (m *MockLedgerReader) StreamLedgerRange(_ context.Context, _ uint32, _ uint32, _ store.StreamLedgerFn) error { diff --git a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go index 1fcf10c54..b3efa9023 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader.go @@ -55,28 +55,26 @@ func (r *LedgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr.Ledg return lcm, found, err } -// GetLedgerView is GetLedger without the decode: the raw bytes are copied -// out of the read's loan, so the view may outlive the call. -func (r *LedgerReader) GetLedgerView(ctx context.Context, sequence uint32) (xdr.LedgerCloseMetaView, bool, error) { +// WithLedgerRaw lends the ledger's raw bytes with no copy: the routed point +// read lends the tier's buffer, whose validity ends with fn — exactly the +// loan's terms. +func (r *LedgerReader) WithLedgerRaw(ctx context.Context, sequence uint32, fn store.WithLedgerRawFn) (bool, error) { view, err := query.ViewFrom(ctx) if err != nil { - return nil, false, err + return false, err } if !inWindow(view, sequence) { - return nil, false, nil + return false, nil } - var raw []byte - err = view.WithLedger(sequence, func(b []byte) error { - raw = bytes.Clone(b) - return nil + found := false + err = view.WithLedger(sequence, func(raw []byte) error { + found = true + return fn(raw) }) - if errors.Is(err, stores.ErrNotFound) { - return nil, false, nil - } - if err != nil { - return nil, false, err + if !found && errors.Is(err, stores.ErrNotFound) { + return false, nil } - return xdr.LedgerCloseMetaView(raw), true, nil + return found, err } func (r *LedgerReader) GetLedgerRange(ctx context.Context) (store.LedgerRange, 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 ae7a4bbf3..70f9ef6d2 100644 --- a/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/adapters/ledger_reader_test.go @@ -485,3 +485,45 @@ func TestGetLedgerRange_SeededWindowReadsNoLedgers(t *testing.T) { assert.Less(t, perCall, uint64(2048), "a seeded GetLedgerRange allocated %d bytes per call; it should not be reading ledgers", perCall) } + +func TestWithLedgerRaw_LendsTheSameBytesGetLedgerDecodes(t *testing.T) { + ctx, reader, c0, _ := sparseFixture(t) + lcm, ok, err := reader.GetLedger(ctx, c0.FirstLedger()) + require.NoError(t, err) + require.True(t, ok) + want, err := lcm.MarshalBinary() + require.NoError(t, err) + + var got []byte + found, err := reader.WithLedgerRaw(ctx, c0.FirstLedger(), func(raw []byte) error { + got = bytes.Clone(raw) // the loan forbids retaining raw + return nil + }) + require.NoError(t, err) + require.True(t, found) + assert.Equal(t, want, got) +} + +func TestWithLedgerRaw_MissDoesNotRunFn(t *testing.T) { + ctx, reader, c0, c1 := sparseFixture(t) + // below the floor, in an in-window gap, and above latest + for _, seq := range []uint32{c0.FirstLedger() - 1, c0.FirstLedger() + 10, c1.FirstLedger() + 1} { + ran := false + found, err := reader.WithLedgerRaw(ctx, seq, func([]byte) error { + ran = true + return nil + }) + assert.NoError(t, err, "ledger %d", seq) + 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") + // found stays true: the ledger WAS there, the caller's own callback failed. + found, err := reader.WithLedgerRaw(ctx, c0.FirstLedger(), func([]byte) error { return boom }) + assert.ErrorIs(t, err, boom) + assert.True(t, found) +} diff --git a/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go b/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go index a6bc258c7..9596fb217 100644 --- a/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go +++ b/cmd/stellar-rpc/internal/rpcv2/corestate/corestate_test.go @@ -214,8 +214,8 @@ func (s stubLedgerReader) GetLedger(context.Context, uint32) (xdr.LedgerCloseMet return xdr.LedgerCloseMeta{}, false, errors.New("unused") } -func (s stubLedgerReader) GetLedgerView(context.Context, uint32) (xdr.LedgerCloseMetaView, bool, error) { - return nil, false, errors.New("unused") +func (s stubLedgerReader) WithLedgerRaw(context.Context, uint32, store.WithLedgerRawFn) (bool, error) { + return false, errors.New("unused") } func (s stubLedgerReader) GetLedgerRange(context.Context) (store.LedgerRange, error) { diff --git a/cmd/stellar-rpc/internal/store/ledger.go b/cmd/stellar-rpc/internal/store/ledger.go index b1d5b94a7..e589ae752 100644 --- a/cmd/stellar-rpc/internal/store/ledger.go +++ b/cmd/stellar-rpc/internal/store/ledger.go @@ -36,13 +36,17 @@ func (lr LedgerRange) ToLedgerSeqRange() protocol.LedgerSeqRange { // implements. Handlers depend on this interface, never on a concrete backend. type LedgerReader 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) (found bool, err error) GetLedgerRange(ctx context.Context) (LedgerRange, error) StreamLedgerRange(ctx context.Context, startLedger uint32, endLedger uint32, f StreamLedgerFn) error NewTx(ctx context.Context) (LedgerReaderTx, error) GetLatestLedgerSequence(ctx context.Context) (uint32, error) } +// WithLedgerRawFn receives one ledger's marshaled LCM on loan and the bytes +// are valid only inside the call, read-only. Copy whatever outlives fn. +type WithLedgerRawFn func(raw []byte) error + // LedgerReaderTx is a read-only snapshot of the ledger store. Call Done to // release it. // From 62c442eee96bf94ed38e50ef1e9ae6956677f8b4 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 1 Sep 2026 17:00:00 -0400 Subject: [PATCH 16/19] error optimization on ledgerInfoFromRow --- cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go index 1e19b098b..8a235c89b 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger.go @@ -237,10 +237,9 @@ func ledgerInfoFromRow(ctx context.Context, db readDB, row ledgerRangeRow) (stor if dbErr != nil { return store.LedgerInfo{}, dbErr } - if !found { - return store.LedgerInfo{}, fmt.Errorf("couldn't get ledger %d close time: %w", row.Sequence, err) + if found { + closeTime, err = xdr.LedgerCloseMetaView(meta).LedgerCloseTime() } - closeTime, err = xdr.LedgerCloseMetaView(meta).LedgerCloseTime() if err != nil { return store.LedgerInfo{}, fmt.Errorf("couldn't get ledger %d close time: %w", row.Sequence, err) } From f9e2905660eb9a7d033efc2377a841ef29d963f7 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 1 Sep 2026 17:05:17 -0400 Subject: [PATCH 17/19] fix parse failure error overreporting --- .../internal/methods/get_latest_ledger.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go index 4d774ea92..58a4497f7 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go @@ -25,21 +25,19 @@ func NewGetLatestLedgerHandler(ledgerReader store.LedgerReader) jrpc2.Handler { } } var response protocol.GetLatestLedgerResponse + var parseErr error found, err := ledgerReader.WithLedgerRaw(ctx, latestSequence, func(raw []byte) error { - var lerr error - response, lerr = latestLedgerResponse(xdr.LedgerCloseMetaView(raw), latestSequence) - return lerr + response, parseErr = latestLedgerResponse(xdr.LedgerCloseMetaView(raw), latestSequence) + return parseErr }) - if err != nil && found { - return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not parse latest ledger header", - } - } if err != nil || !found { + msg := "could not get latest ledger" + if parseErr != nil { + msg = "could not parse latest ledger header" + } return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, - Message: "could not get latest ledger", + Message: msg, } } return response, nil From 3e397d6dfc839a1f2b6cbe3a0f5b47fd0fd604f9 Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 1 Sep 2026 18:01:04 -0400 Subject: [PATCH 18/19] add WithLedgerRaw test --- .../internal/rpcv1/sqlitedb/ledger_test.go | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go index 853f37393..eb81dd7dc 100644 --- a/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go +++ b/cmd/stellar-rpc/internal/rpcv1/sqlitedb/ledger_test.go @@ -1,6 +1,7 @@ package sqlitedb import ( + "bytes" "context" "io" "path" @@ -252,6 +253,38 @@ func TestGetLedgerRange_EmptyDB(t *testing.T) { assert.Equal(t, int64(0), ledgerRange.LastLedger.CloseTime) } +// TestWithLedgerRaw covers both lend outcomes: a hit lends the stored meta +// blob verbatim, and a miss reports found=false without running fn. +func TestWithLedgerRaw(t *testing.T) { + db := NewTestDB(t) + tx, err := NewReadWriter(logger, db, host.MakeNoOpDaemon(), 15, passphrase).NewTx(t.Context()) + require.NoError(t, err) + lcm := createLedger(42) + require.NoError(t, tx.LedgerWriter().InsertLedger(lcm)) + require.NoError(t, tx.Commit(lcm, nil)) + want, err := lcm.MarshalBinary() + require.NoError(t, err) + + reader := NewLedgerReader(db) + var got []byte + found, err := reader.WithLedgerRaw(t.Context(), 42, func(raw []byte) error { + got = bytes.Clone(raw) + return nil + }) + require.NoError(t, err) + assert.True(t, found) + assert.Equal(t, want, got) + + ran := false + found, err = reader.WithLedgerRaw(t.Context(), 43, func([]byte) error { + ran = true + return nil + }) + require.NoError(t, err) + assert.False(t, found) + assert.False(t, ran) +} + func BenchmarkGetLedgerRange(b *testing.B) { testDB, lcms := setupBenchmarkingDB(b) reader := NewLedgerReader(testDB) From cb10f5f001dfbd87285267356ebc90fc3a52172a Mon Sep 17 00:00:00 2001 From: Christian Jonas Date: Tue, 1 Sep 2026 18:24:12 -0400 Subject: [PATCH 19/19] fix getLatestLedger error reporting --- .../internal/methods/get_latest_ledger.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go index 58a4497f7..a646ce39d 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/hex" + "fmt" "github.com/creachadair/jrpc2" @@ -31,9 +32,14 @@ func NewGetLatestLedgerHandler(ledgerReader store.LedgerReader) jrpc2.Handler { return parseErr }) if err != nil || !found { - msg := "could not get latest ledger" - if parseErr != nil { - msg = "could not parse latest ledger header" + var msg string + switch { + case parseErr != nil: + msg = fmt.Sprintf("could not parse latest ledger header: %v", parseErr) + case err != nil: + msg = fmt.Sprintf("could not get latest ledger: %v", err) + default: // clean miss: no underlying error to report + msg = "could not get latest ledger" } return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError,