diff --git a/cmd/soroban-rpc/internal/events/events.go b/cmd/soroban-rpc/internal/events/events.go index 39fdc4ce..12e8e765 100644 --- a/cmd/soroban-rpc/internal/events/events.go +++ b/cmd/soroban-rpc/internal/events/events.go @@ -264,3 +264,10 @@ func readEvents(networkPassphrase string, ledgerCloseMeta xdr.LedgerCloseMeta) ( } return events, err } + +// GetLedgerRange returns the first and latest ledger available in the store. +func (m *MemoryStore) GetLedgerRange() ledgerbucketwindow.LedgerRange { + m.lock.RLock() + defer m.lock.RUnlock() + return m.eventsByLedger.GetLedgerRange() +} diff --git a/cmd/soroban-rpc/internal/jsonrpc.go b/cmd/soroban-rpc/internal/jsonrpc.go index d122b9a0..b0ceb372 100644 --- a/cmd/soroban-rpc/internal/jsonrpc.go +++ b/cmd/soroban-rpc/internal/jsonrpc.go @@ -134,6 +134,15 @@ func NewJSONRPCHandler(cfg *config.Config, params HandlerParams) Handler { Logger: func(text string) { params.Logger.Debug(text) }, }, } + + // Get the largest history window + var ledgerRangeGetter methods.LedgerRangeGetter = params.EventStore + var retentionWindow = cfg.EventLedgerRetentionWindow + if cfg.TransactionLedgerRetentionWindow > cfg.EventLedgerRetentionWindow { + retentionWindow = cfg.TransactionLedgerRetentionWindow + ledgerRangeGetter = params.TransactionStore + } + handlers := []struct { methodName string underlyingHandler jrpc2.Handler @@ -143,7 +152,7 @@ func NewJSONRPCHandler(cfg *config.Config, params HandlerParams) Handler { }{ { methodName: "getHealth", - underlyingHandler: methods.NewHealthCheck(params.TransactionStore, cfg.MaxHealthyLedgerLatency), + underlyingHandler: methods.NewHealthCheck(retentionWindow, ledgerRangeGetter, cfg.MaxHealthyLedgerLatency), longName: "get_health", queueLimit: cfg.RequestBacklogGetHealthQueueLimit, requestDurationLimit: cfg.MaxGetHealthExecutionDuration, diff --git a/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go b/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go index 8234b607..7225a6b3 100644 --- a/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go +++ b/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go @@ -65,6 +65,35 @@ func (w *LedgerBucketWindow[T]) Len() uint32 { return uint32(len(w.buckets)) } +type LedgerInfo struct { + Sequence uint32 + CloseTime int64 +} + +type LedgerRange struct { + FirstLedger LedgerInfo + LastLedger LedgerInfo +} + +func (w *LedgerBucketWindow[T]) GetLedgerRange() LedgerRange { + length := w.Len() + if length == 0 { + return LedgerRange{} + } + firstBucket := w.Get(0) + lastBucket := w.Get(length - 1) + return LedgerRange{ + FirstLedger: LedgerInfo{ + Sequence: firstBucket.LedgerSeq, + CloseTime: firstBucket.LedgerCloseTimestamp, + }, + LastLedger: LedgerInfo{ + Sequence: lastBucket.LedgerSeq, + CloseTime: lastBucket.LedgerCloseTimestamp, + }, + } +} + // Get obtains a bucket from the window func (w *LedgerBucketWindow[T]) Get(i uint32) *LedgerBucket[T] { length := w.Len() diff --git a/cmd/soroban-rpc/internal/methods/get_transaction.go b/cmd/soroban-rpc/internal/methods/get_transaction.go index 8b1846f8..1e6ef610 100644 --- a/cmd/soroban-rpc/internal/methods/get_transaction.go +++ b/cmd/soroban-rpc/internal/methods/get_transaction.go @@ -10,6 +10,7 @@ import ( "github.com/creachadair/jrpc2/handler" "github.com/stellar/go/xdr" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/transactions" ) @@ -67,7 +68,7 @@ type GetTransactionRequest struct { } type transactionGetter interface { - GetTransaction(hash xdr.Hash) (transactions.Transaction, bool, transactions.StoreRange) + GetTransaction(hash xdr.Hash) (transactions.Transaction, bool, ledgerbucketwindow.LedgerRange) } func GetTransaction(getter transactionGetter, request GetTransactionRequest) (GetTransactionResponse, error) { diff --git a/cmd/soroban-rpc/internal/methods/health.go b/cmd/soroban-rpc/internal/methods/health.go index ab46d62a..b8f684af 100644 --- a/cmd/soroban-rpc/internal/methods/health.go +++ b/cmd/soroban-rpc/internal/methods/health.go @@ -8,24 +8,32 @@ import ( "github.com/creachadair/jrpc2" "github.com/creachadair/jrpc2/handler" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/transactions" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" ) type HealthCheckResult struct { - Status string `json:"status"` + Status string `json:"status"` + LatestLedger uint32 `json:"latestLedger"` + OldestLedger uint32 `json:"oldestLedger"` + LedgerRetentionWindow uint32 `json:"ledgerRetentionWindow"` +} + +type LedgerRangeGetter interface { + GetLedgerRange() ledgerbucketwindow.LedgerRange } // NewHealthCheck returns a health check json rpc handler -func NewHealthCheck(txStore *transactions.MemoryStore, maxHealthyLedgerLatency time.Duration) jrpc2.Handler { +func NewHealthCheck(retentionWindow uint32, ledgerRangeGetter LedgerRangeGetter, maxHealthyLedgerLatency time.Duration) jrpc2.Handler { return handler.New(func(ctx context.Context) (HealthCheckResult, error) { - ledgerInfo := txStore.GetLatestLedger() - if ledgerInfo.Sequence < 1 { + ledgerRange := ledgerRangeGetter.GetLedgerRange() + if ledgerRange.LastLedger.Sequence < 1 { return HealthCheckResult{}, jrpc2.Error{ Code: jrpc2.InternalError, Message: "data stores are not initialized", } } - lastKnownLedgerCloseTime := time.Unix(ledgerInfo.CloseTime, 0) + + lastKnownLedgerCloseTime := time.Unix(ledgerRange.LastLedger.CloseTime, 0) lastKnownLedgerLatency := time.Since(lastKnownLedgerCloseTime) if lastKnownLedgerLatency > maxHealthyLedgerLatency { roundedLatency := lastKnownLedgerLatency.Round(time.Second) @@ -35,6 +43,12 @@ func NewHealthCheck(txStore *transactions.MemoryStore, maxHealthyLedgerLatency t Message: msg, } } - return HealthCheckResult{Status: "healthy"}, nil + result := HealthCheckResult{ + Status: "healthy", + LatestLedger: ledgerRange.LastLedger.Sequence, + OldestLedger: ledgerRange.FirstLedger.Sequence, + LedgerRetentionWindow: retentionWindow, + } + return result, nil }) } diff --git a/cmd/soroban-rpc/internal/methods/send_transaction.go b/cmd/soroban-rpc/internal/methods/send_transaction.go index c8404c69..215e2998 100644 --- a/cmd/soroban-rpc/internal/methods/send_transaction.go +++ b/cmd/soroban-rpc/internal/methods/send_transaction.go @@ -12,7 +12,6 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/transactions" ) // SendTransactionResponse represents the transaction submission response returned Soroban-RPC @@ -45,14 +44,8 @@ type SendTransactionRequest struct { Transaction string `json:"transaction"` } -// LatestLedgerStore is a store which returns the latest ingested ledger. -type LatestLedgerStore interface { - // GetLatestLedger returns the latest ingested ledger. - GetLatestLedger() transactions.LedgerInfo -} - // NewSendTransactionHandler returns a submit transaction json rpc handler -func NewSendTransactionHandler(daemon interfaces.Daemon, logger *log.Entry, store LatestLedgerStore, passphrase string) jrpc2.Handler { +func NewSendTransactionHandler(daemon interfaces.Daemon, logger *log.Entry, ledgerRangeGetter LedgerRangeGetter, passphrase string) jrpc2.Handler { submitter := daemon.CoreClient() return handler.New(func(ctx context.Context, request SendTransactionRequest) (SendTransactionResponse, error) { var envelope xdr.TransactionEnvelope @@ -74,7 +67,7 @@ func NewSendTransactionHandler(daemon interfaces.Daemon, logger *log.Entry, stor } txHash := hex.EncodeToString(hash[:]) - ledgerInfo := store.GetLatestLedger() + latestLedgerInfo := ledgerRangeGetter.GetLedgerRange().LastLedger resp, err := submitter.SubmitTransaction(ctx, request.Transaction) if err != nil { logger.WithError(err). @@ -110,15 +103,15 @@ func NewSendTransactionHandler(daemon interfaces.Daemon, logger *log.Entry, stor DiagnosticEventsXDR: events, Status: resp.Status, Hash: txHash, - LatestLedger: ledgerInfo.Sequence, - LatestLedgerCloseTime: ledgerInfo.CloseTime, + LatestLedger: latestLedgerInfo.Sequence, + LatestLedgerCloseTime: latestLedgerInfo.CloseTime, }, nil case proto.TXStatusPending, proto.TXStatusDuplicate, proto.TXStatusTryAgainLater: return SendTransactionResponse{ Status: resp.Status, Hash: txHash, - LatestLedger: ledgerInfo.Sequence, - LatestLedgerCloseTime: ledgerInfo.CloseTime, + LatestLedger: latestLedgerInfo.Sequence, + LatestLedgerCloseTime: latestLedgerInfo.CloseTime, }, nil default: logger.WithField("status", resp.Status). diff --git a/cmd/soroban-rpc/internal/test/health_test.go b/cmd/soroban-rpc/internal/test/health_test.go index 46a327fd..75e097d4 100644 --- a/cmd/soroban-rpc/internal/test/health_test.go +++ b/cmd/soroban-rpc/internal/test/health_test.go @@ -6,8 +6,10 @@ import ( "github.com/creachadair/jrpc2" "github.com/creachadair/jrpc2/jhttp" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" "github.com/stretchr/testify/assert" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) func TestHealth(t *testing.T) { @@ -20,5 +22,9 @@ func TestHealth(t *testing.T) { if err := client.CallResult(context.Background(), "getHealth", nil, &result); err != nil { t.Fatalf("rpc call failed: %v", err) } - assert.Equal(t, methods.HealthCheckResult{Status: "healthy"}, result) + assert.Equal(t, "healthy", result.Status) + assert.Equal(t, uint32(ledgerbucketwindow.DefaultEventLedgerRetentionWindow), result.LedgerRetentionWindow) + assert.Greater(t, result.OldestLedger, uint32(0)) + assert.Greater(t, result.LatestLedger, uint32(0)) + assert.GreaterOrEqual(t, result.LatestLedger, result.OldestLedger) } diff --git a/cmd/soroban-rpc/internal/transactions/transactions.go b/cmd/soroban-rpc/internal/transactions/transactions.go index 6b24f429..e5abe55c 100644 --- a/cmd/soroban-rpc/internal/transactions/transactions.go +++ b/cmd/soroban-rpc/internal/transactions/transactions.go @@ -140,11 +140,6 @@ func (m *MemoryStore) IngestTransactions(ledgerCloseMeta xdr.LedgerCloseMeta) er return nil } -type LedgerInfo struct { - Sequence uint32 - CloseTime int64 -} - type Transaction struct { Result []byte // XDR encoded xdr.TransactionResult Meta []byte // XDR encoded xdr.TransactionMeta @@ -153,48 +148,22 @@ type Transaction struct { FeeBump bool ApplicationOrder int32 Successful bool - Ledger LedgerInfo -} - -type StoreRange struct { - FirstLedger LedgerInfo - LastLedger LedgerInfo + Ledger ledgerbucketwindow.LedgerInfo } -// GetLatestLedger returns the latest ledger available in the store. -func (m *MemoryStore) GetLatestLedger() LedgerInfo { +// GetLedgerRange returns the first and latest ledger available in the store. +func (m *MemoryStore) GetLedgerRange() ledgerbucketwindow.LedgerRange { m.lock.RLock() defer m.lock.RUnlock() - if m.transactionsByLedger.Len() > 0 { - lastBucket := m.transactionsByLedger.Get(m.transactionsByLedger.Len() - 1) - return LedgerInfo{ - Sequence: lastBucket.LedgerSeq, - CloseTime: lastBucket.LedgerCloseTimestamp, - } - } - return LedgerInfo{} + return m.transactionsByLedger.GetLedgerRange() } // GetTransaction obtains a transaction from the store and whether it's present and the current store range -func (m *MemoryStore) GetTransaction(hash xdr.Hash) (Transaction, bool, StoreRange) { +func (m *MemoryStore) GetTransaction(hash xdr.Hash) (Transaction, bool, ledgerbucketwindow.LedgerRange) { startTime := time.Now() m.lock.RLock() defer m.lock.RUnlock() - var storeRange StoreRange - if m.transactionsByLedger.Len() > 0 { - firstBucket := m.transactionsByLedger.Get(0) - lastBucket := m.transactionsByLedger.Get(m.transactionsByLedger.Len() - 1) - storeRange = StoreRange{ - FirstLedger: LedgerInfo{ - Sequence: firstBucket.LedgerSeq, - CloseTime: firstBucket.LedgerCloseTimestamp, - }, - LastLedger: LedgerInfo{ - Sequence: lastBucket.LedgerSeq, - CloseTime: lastBucket.LedgerCloseTimestamp, - }, - } - } + storeRange := m.transactionsByLedger.GetLedgerRange() internalTx, ok := m.transactions[hash] if !ok { return Transaction{}, false, storeRange @@ -229,7 +198,7 @@ func (m *MemoryStore) GetTransaction(hash xdr.Hash) (Transaction, bool, StoreRan FeeBump: internalTx.feeBump, Successful: internalTx.successful, ApplicationOrder: internalTx.applicationOrder, - Ledger: LedgerInfo{ + Ledger: ledgerbucketwindow.LedgerInfo{ Sequence: internalTx.bucket.LedgerSeq, CloseTime: internalTx.bucket.LedgerCloseTimestamp, }, diff --git a/cmd/soroban-rpc/internal/transactions/transactions_test.go b/cmd/soroban-rpc/internal/transactions/transactions_test.go index 73ddbaa6..4b801835 100644 --- a/cmd/soroban-rpc/internal/transactions/transactions_test.go +++ b/cmd/soroban-rpc/internal/transactions/transactions_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" ) func expectedTransaction(t *testing.T, ledger uint32, feeBump bool) Transaction { @@ -36,16 +37,16 @@ func expectedTransaction(t *testing.T, ledger uint32, feeBump bool) Transaction return tx } -func expectedLedgerInfo(ledgerSequence uint32) LedgerInfo { - return LedgerInfo{ +func expectedLedgerInfo(ledgerSequence uint32) ledgerbucketwindow.LedgerInfo { + return ledgerbucketwindow.LedgerInfo{ Sequence: ledgerSequence, CloseTime: ledgerCloseTime(ledgerSequence), } } -func expectedStoreRange(startLedger uint32, endLedger uint32) StoreRange { - return StoreRange{ +func expectedStoreRange(startLedger uint32, endLedger uint32) ledgerbucketwindow.LedgerRange { + return ledgerbucketwindow.LedgerRange{ FirstLedger: expectedLedgerInfo(startLedger), LastLedger: expectedLedgerInfo(endLedger), } @@ -295,7 +296,7 @@ func TestIngestTransactions(t *testing.T) { _, ok, storeRange := store.GetTransaction(txHash(1, false)) require.False(t, ok) - require.Equal(t, StoreRange{}, storeRange) + require.Equal(t, ledgerbucketwindow.LedgerRange{}, storeRange) // Insert ledger 1 require.NoError(t, store.IngestTransactions(txMeta(1, false)))