diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 71e57e11..2e458a80 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,7 +37,7 @@ jobs: # resolution options, using npm release or a gh ref: # # option #1, set the version of stellar-sdk based on a npm release version - SYSTEM_TEST_JS_STELLAR_SDK_NPM_VERSION: 11.1.0 + SYSTEM_TEST_JS_STELLAR_SDK_NPM_VERSION: 11.3.0 # option #2, set the version of stellar-sdk used as a ref to a gh repo if # a value is set on SYSTEM_TEST_JS_STELLAR_SDK_GH_REPO, it takes # precedence over any SYSTEM_TEST_JS_STELLAR_SDK_NPM_VERSION diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d880061c..bdaa4c9d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: runs-on: ${{ matrix.os }} env: VERSION: '${{ github.event.release.name }}' - NAME: 'soroban-rpc-${{ github.event.release.name }}-${{ matrix.target }}' + NAME: 'stellar-rpc-client-${{ github.event.release.name }}-${{ matrix.target }}' steps: - uses: actions/checkout@v3 - run: rustup update @@ -49,8 +49,8 @@ jobs: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc run: | cd target/package - tar xvfz soroban-rpc-$VERSION.crate - cd soroban-rpc-$VERSION + tar xvfz stellar-rpc-client-$VERSION.crate + cd stellar-rpc-client-$VERSION cargo build --target-dir=../.. --release --target ${{ matrix.target }} - name: Compress run: | diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c38742b7..b7230b8c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -109,7 +109,7 @@ jobs: cargo-hack-feature-options: --features opt --ignore-unknown-features uses: stellar/actions/.github/workflows/rust-publish-dry-run-v2.yml@main with: - crates: soroban-rpc + crates: stellar-rpc-client runs-on: ${{ matrix.os }} target: ${{ matrix.target }} cargo-hack-feature-options: ${{ matrix.cargo-hack-feature-options }} diff --git a/Cargo.lock b/Cargo.lock index d683e64a..900fe3e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1187,7 +1187,7 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "preflight" -version = "20.3.3" +version = "20.3.5" dependencies = [ "anyhow", "base64 0.21.7", @@ -1758,7 +1758,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stellar-rpc-client" -version = "20.3.3" +version = "20.3.5" dependencies = [ "base64 0.21.7", "clap", diff --git a/Cargo.toml b/Cargo.toml index 9f7c138f..51f595fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ default-members = ["cmd/crates/stellar-rpc-client"] #exclude = ["cmd/crates/soroban-test/tests/fixtures/hello"] [workspace.package] -version = "20.3.3" +version = "20.3.5" rust-version = "1.74.0" [workspace.dependencies.soroban-env-host] @@ -61,7 +61,7 @@ version = "=20.3.2" # rev = "4aef54ff9295c2fca4c5b9fbd2c92d0ff99f67de" [workspace.dependencies.stellar-rpc-client] -version = "20.3.3" +version = "20.3.5" path = "cmd/crates/stellar-rpc-client" [workspace.dependencies.stellar-xdr] diff --git a/cmd/crates/stellar-rpc-client/src/lib.rs b/cmd/crates/stellar-rpc-client/src/lib.rs index 2e0be210..ca861e1b 100644 --- a/cmd/crates/stellar-rpc-client/src/lib.rs +++ b/cmd/crates/stellar-rpc-client/src/lib.rs @@ -1,7 +1,7 @@ use http::{uri::Authority, Uri}; use itertools::Itertools; use jsonrpsee_core::params::ObjectParams; -use jsonrpsee_core::{self, client::ClientT, rpc_params}; +use jsonrpsee_core::{self, client::ClientT}; use jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder}; use serde_aux::prelude::{ deserialize_default_from_null, deserialize_number_from_string, @@ -279,6 +279,21 @@ pub struct SimulateHostFunctionResult { pub xdr: xdr::ScVal, } +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)] +#[serde(tag = "type")] +pub enum LedgerEntryChange { + #[serde(rename = "created")] + Created { key: String, after: String }, + #[serde(rename = "deleted")] + Deleted { key: String, before: String }, + #[serde(rename = "updated")] + Updated { + key: String, + before: String, + after: String, + }, +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone)] pub struct SimulateTransactionResponse { #[serde( @@ -305,6 +320,12 @@ pub struct SimulateTransactionResponse { default )] pub restore_preamble: Option, + #[serde( + rename = "stateChanges", + skip_serializing_if = "Option::is_none", + default + )] + pub state_changes: Option>, #[serde(rename = "latestLedger")] pub latest_ledger: u32, #[serde(skip_serializing_if = "Option::is_none", default)] @@ -637,7 +658,10 @@ impl Client { /// # Errors pub async fn get_network(&self) -> Result { tracing::trace!("Getting network"); - Ok(self.client()?.request("getNetwork", rpc_params![]).await?) + Ok(self + .client()? + .request("getNetwork", ObjectParams::new()) + .await?) } /// @@ -646,7 +670,7 @@ impl Client { tracing::trace!("Getting latest ledger"); Ok(self .client()? - .request("getLatestLedger", rpc_params![]) + .request("getLatestLedger", ObjectParams::new()) .await?) } @@ -691,16 +715,15 @@ soroban config identity fund {address} --helper-url "# ) -> Result { let client = self.client()?; tracing::trace!("Sending:\n{tx:#?}"); + let mut oparams = ObjectParams::new(); + oparams.insert("transaction", tx.to_xdr_base64(Limits::none())?)?; let SendTransactionResponse { hash, error_result_xdr, status, .. } = client - .request( - "sendTransaction", - rpc_params![tx.to_xdr_base64(Limits::none())?], - ) + .request("sendTransaction", oparams) .await .map_err(|err| { Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}")) @@ -761,11 +784,11 @@ soroban config identity fund {address} --helper-url "# ) -> Result { tracing::trace!("Simulating:\n{tx:#?}"); let base64_tx = tx.to_xdr_base64(Limits::none())?; - let mut builder = ObjectParams::new(); - builder.insert("transaction", base64_tx)?; + let mut oparams = ObjectParams::new(); + oparams.insert("transaction", base64_tx)?; let response: SimulateTransactionResponse = self .client()? - .request("simulateTransaction", builder) + .request("simulateTransaction", oparams) .await?; tracing::trace!("Simulation response:\n {response:#?}"); match response.error { @@ -835,10 +858,9 @@ soroban config identity fund {address} --helper-url "# /// /// # Errors pub async fn get_transaction(&self, tx_id: &str) -> Result { - Ok(self - .client()? - .request("getTransaction", rpc_params![tx_id]) - .await?) + let mut oparams = ObjectParams::new(); + oparams.insert("hash", tx_id)?; + Ok(self.client()?.request("getTransaction", oparams).await?) } /// @@ -855,10 +877,9 @@ soroban config identity fund {address} --helper-url "# } base64_keys.push(k.to_xdr_base64(Limits::none())?); } - Ok(self - .client()? - .request("getLedgerEntries", rpc_params![base64_keys]) - .await?) + let mut oparams = ObjectParams::new(); + oparams.insert("keys", base64_keys)?; + Ok(self.client()?.request("getLedgerEntries", oparams).await?) } /// @@ -1070,10 +1091,20 @@ mod tests { "minResourceFee": "100000000", "cost": { "cpuInsns": "1000", "memBytes": "1000" }, "transactionData": "", - "latestLedger": 1234 - }"#; + "latestLedger": 1234, + "stateChanges": [{ + "type": "created", + "key": "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==", + "before": null, + "after": "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }] + }"#; let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap(); + assert_eq!( + resp.state_changes.unwrap()[0], + LedgerEntryChange::Created { key: "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==".to_string(), after: "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string() }, + ); assert_eq!(resp.min_resource_fee, 100_000_000); } diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index 9e21f97a..c2c224de 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -34,10 +34,11 @@ import ( ) const ( - prometheusNamespace = "soroban_rpc" - maxLedgerEntryWriteBatchSize = 150 - defaultReadTimeout = 5 * time.Second - defaultShutdownGracePeriod = 10 * time.Second + prometheusNamespace = "soroban_rpc" + maxLedgerEntryWriteBatchSize = 150 + defaultReadTimeout = 5 * time.Second + defaultShutdownGracePeriod = 10 * time.Second + inMemoryInitializationLedgerLogPeriod = 1_000_000 ) type Daemon struct { @@ -137,6 +138,11 @@ func MustNew(cfg *config.Config) *Daemon { logger.UseJSONFormatter() } + logger.WithFields(supportlog.F{ + "version": config.Version, + "commit": config.CommitHash, + }).Info("starting Soroban RPC") + core, err := newCaptiveCore(cfg, logger) if err != nil { logger.WithError(err).Fatal("could not create captive core") @@ -196,7 +202,20 @@ func MustNew(cfg *config.Config) *Daemon { // NOTE: We could optimize this to avoid unnecessary ingestion calls // (the range of txmetads can be larger than the store retention windows) // but it's probably not worth the pain. + var initialSeq uint32 + var currentSeq uint32 err = db.NewLedgerReader(dbConn).StreamAllLedgers(readTxMetaCtx, func(txmeta xdr.LedgerCloseMeta) error { + currentSeq = txmeta.LedgerSequence() + if initialSeq == 0 { + initialSeq = currentSeq + logger.WithFields(supportlog.F{ + "seq": currentSeq, + }).Info("initializing in-memory store") + } else if (currentSeq-initialSeq)%inMemoryInitializationLedgerLogPeriod == 0 { + logger.WithFields(supportlog.F{ + "seq": currentSeq, + }).Debug("still initializing in-memory store") + } if err := eventStore.IngestEvents(txmeta); err != nil { logger.WithError(err).Fatal("could not initialize event memory store") } @@ -205,6 +224,11 @@ func MustNew(cfg *config.Config) *Daemon { } return nil }) + if currentSeq != 0 { + logger.WithFields(supportlog.F{ + "seq": currentSeq, + }).Info("finished initializing in-memory store") + } if err != nil { logger.WithError(err).Fatal("could not obtain txmeta cache from the database") } @@ -285,10 +309,8 @@ func MustNew(cfg *config.Config) *Daemon { func (d *Daemon) Run() { d.logger.WithFields(supportlog.F{ - "version": config.Version, - "commit": config.CommitHash, - "addr": d.server.Addr, - }).Info("starting Soroban JSON RPC server") + "addr": d.server.Addr, + }).Info("starting HTTP server") panicGroup := util.UnrecoverablePanicGroup.Log(d.logger) panicGroup.Go(func() { diff --git a/cmd/soroban-rpc/internal/events/events.go b/cmd/soroban-rpc/internal/events/events.go index f2abf063..3b570602 100644 --- a/cmd/soroban-rpc/internal/events/events.go +++ b/cmd/soroban-rpc/internal/events/events.go @@ -265,3 +265,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 709a31a1..1ec4c958 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_events.go b/cmd/soroban-rpc/internal/methods/get_events.go index 73105760..533603bd 100644 --- a/cmd/soroban-rpc/internal/methods/get_events.go +++ b/cmd/soroban-rpc/internal/methods/get_events.go @@ -8,7 +8,6 @@ import ( "time" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" "github.com/stellar/go/strkey" "github.com/stellar/go/support/errors" @@ -429,7 +428,7 @@ func NewGetEventsHandler(eventsStore *events.MemoryStore, maxLimit, defaultLimit maxLimit: maxLimit, defaultLimit: defaultLimit, } - return handler.New(func(ctx context.Context, request GetEventsRequest) (GetEventsResponse, error) { + return NewHandler(func(ctx context.Context, request GetEventsRequest) (GetEventsResponse, error) { return eventsHandler.getEvents(request) }) } diff --git a/cmd/soroban-rpc/internal/methods/get_latest_ledger.go b/cmd/soroban-rpc/internal/methods/get_latest_ledger.go index 2f933304..3617e70d 100644 --- a/cmd/soroban-rpc/internal/methods/get_latest_ledger.go +++ b/cmd/soroban-rpc/internal/methods/get_latest_ledger.go @@ -4,7 +4,6 @@ import ( "context" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" ) @@ -20,7 +19,7 @@ type GetLatestLedgerResponse struct { // NewGetLatestLedgerHandler returns a JSON RPC handler to retrieve the latest ledger entry from Stellar core. func NewGetLatestLedgerHandler(ledgerEntryReader db.LedgerEntryReader, ledgerReader db.LedgerReader) jrpc2.Handler { - return handler.New(func(ctx context.Context) (GetLatestLedgerResponse, error) { + return NewHandler(func(ctx context.Context) (GetLatestLedgerResponse, error) { tx, err := ledgerEntryReader.NewTx(ctx) if err != nil { return GetLatestLedgerResponse{}, &jrpc2.Error{ diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go index e9e58868..c3f2b617 100644 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go +++ b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" @@ -40,7 +39,7 @@ const getLedgerEntriesMaxKeys = 200 // NewGetLedgerEntriesHandler returns a JSON RPC handler to retrieve the specified ledger entries from Stellar Core. func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader) jrpc2.Handler { - return handler.New(func(ctx context.Context, request GetLedgerEntriesRequest) (GetLedgerEntriesResponse, error) { + return NewHandler(func(ctx context.Context, request GetLedgerEntriesRequest) (GetLedgerEntriesResponse, error) { if len(request.Keys) > getLedgerEntriesMaxKeys { return GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go index e457a3b1..910d2155 100644 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go +++ b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go @@ -5,8 +5,6 @@ import ( "fmt" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" @@ -33,7 +31,7 @@ type GetLedgerEntryResponse struct { // Deprecated. use NewGetLedgerEntriesHandler instead. // TODO(https://github.com/stellar/soroban-tools/issues/374) remove after getLedgerEntries is deployed. func NewGetLedgerEntryHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader) jrpc2.Handler { - return handler.New(func(ctx context.Context, request GetLedgerEntryRequest) (GetLedgerEntryResponse, error) { + return NewHandler(func(ctx context.Context, request GetLedgerEntryRequest) (GetLedgerEntryResponse, error) { var key xdr.LedgerKey if err := xdr.SafeUnmarshalBase64(request.Key, &key); err != nil { logger.WithError(err).WithField("request", request). diff --git a/cmd/soroban-rpc/internal/methods/get_network.go b/cmd/soroban-rpc/internal/methods/get_network.go index 5c990c41..f706e7c6 100644 --- a/cmd/soroban-rpc/internal/methods/get_network.go +++ b/cmd/soroban-rpc/internal/methods/get_network.go @@ -4,7 +4,6 @@ import ( "context" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" ) @@ -20,7 +19,7 @@ type GetNetworkResponse struct { // NewGetNetworkHandler returns a json rpc handler to for the getNetwork method func NewGetNetworkHandler(daemon interfaces.Daemon, networkPassphrase, friendbotURL string) jrpc2.Handler { coreClient := daemon.CoreClient() - return handler.New(func(ctx context.Context, request GetNetworkRequest) (GetNetworkResponse, error) { + return NewHandler(func(ctx context.Context, request GetNetworkRequest) (GetNetworkResponse, error) { info, err := coreClient.Info(ctx) if err != nil { return GetNetworkResponse{}, (&jrpc2.Error{ diff --git a/cmd/soroban-rpc/internal/methods/get_transaction.go b/cmd/soroban-rpc/internal/methods/get_transaction.go index 8b1846f8..166cb152 100644 --- a/cmd/soroban-rpc/internal/methods/get_transaction.go +++ b/cmd/soroban-rpc/internal/methods/get_transaction.go @@ -7,9 +7,9 @@ import ( "fmt" "github.com/creachadair/jrpc2" - "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 +67,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) { @@ -120,7 +120,7 @@ func GetTransaction(getter transactionGetter, request GetTransactionRequest) (Ge // NewGetTransactionHandler returns a get transaction json rpc handler func NewGetTransactionHandler(getter transactionGetter) jrpc2.Handler { - return handler.New(func(ctx context.Context, request GetTransactionRequest) (GetTransactionResponse, error) { + return NewHandler(func(ctx context.Context, request GetTransactionRequest) (GetTransactionResponse, error) { return GetTransaction(getter, request) }) } diff --git a/cmd/soroban-rpc/internal/methods/handler.go b/cmd/soroban-rpc/internal/methods/handler.go new file mode 100644 index 00000000..2dc4d99b --- /dev/null +++ b/cmd/soroban-rpc/internal/methods/handler.go @@ -0,0 +1,17 @@ +package methods + +import ( + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/handler" +) + +func NewHandler(fn any) jrpc2.Handler { + fi, err := handler.Check(fn) + if err != nil { + panic(err) + } + // explicitly disable array arguments since otherwise we cannot add + // new method arguments without breaking backwards compatibility with clients + fi.AllowArray(false) + return fi.Wrap() +} diff --git a/cmd/soroban-rpc/internal/methods/handler_test.go b/cmd/soroban-rpc/internal/methods/handler_test.go new file mode 100644 index 00000000..564f7f51 --- /dev/null +++ b/cmd/soroban-rpc/internal/methods/handler_test.go @@ -0,0 +1,60 @@ +package methods + +import ( + "context" + "testing" + + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/handler" + "github.com/stretchr/testify/assert" +) + +type Request struct { + Parameter string `json:"parameter"` +} + +func TestNewHandlerNoArrayParameters(t *testing.T) { + callCount := 0 + f := func(ctx context.Context, request Request) error { + callCount++ + assert.Equal(t, "bar", request.Parameter) + return nil + } + objectRequest := `{ +"jsonrpc": "2.0", +"id": 1, +"method": "foo", +"params": { "parameter": "bar" } +}` + requests, err := jrpc2.ParseRequests([]byte(objectRequest)) + assert.NoError(t, err) + assert.Len(t, requests, 1) + finalObjectRequest := requests[0].ToRequest() + + // object parameters should work with our handlers + customHandler := NewHandler(f) + _, err = customHandler(context.Background(), finalObjectRequest) + assert.NoError(t, err) + assert.Equal(t, 1, callCount) + + arrayRequest := `{ +"jsonrpc": "2.0", +"id": 1, +"method": "foo", +"params": ["bar"] +}` + requests, err = jrpc2.ParseRequests([]byte(arrayRequest)) + assert.NoError(t, err) + assert.Len(t, requests, 1) + finalArrayRequest := requests[0].ToRequest() + + // Array requests should work with the normal handler, but not with our handlers + stdHandler := handler.New(f) + _, err = stdHandler(context.Background(), finalArrayRequest) + assert.NoError(t, err) + assert.Equal(t, 2, callCount) + + _, err = customHandler(context.Background(), finalArrayRequest) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid parameters") +} diff --git a/cmd/soroban-rpc/internal/methods/health.go b/cmd/soroban-rpc/internal/methods/health.go index ab46d62a..f9b1c50e 100644 --- a/cmd/soroban-rpc/internal/methods/health.go +++ b/cmd/soroban-rpc/internal/methods/health.go @@ -6,26 +6,33 @@ import ( "time" "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 { - return handler.New(func(ctx context.Context) (HealthCheckResult, error) { - ledgerInfo := txStore.GetLatestLedger() - if ledgerInfo.Sequence < 1 { +func NewHealthCheck(retentionWindow uint32, ledgerRangeGetter LedgerRangeGetter, maxHealthyLedgerLatency time.Duration) jrpc2.Handler { + return NewHandler(func(ctx context.Context) (HealthCheckResult, error) { + 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 +42,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..82d014d1 100644 --- a/cmd/soroban-rpc/internal/methods/send_transaction.go +++ b/cmd/soroban-rpc/internal/methods/send_transaction.go @@ -5,14 +5,12 @@ import ( "encoding/hex" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" "github.com/stellar/go/network" proto "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/support/log" "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,16 +43,10 @@ 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) { + return NewHandler(func(ctx context.Context, request SendTransactionRequest) (SendTransactionResponse, error) { var envelope xdr.TransactionEnvelope err := xdr.SafeUnmarshalBase64(request.Transaction, &envelope) if err != nil { @@ -74,7 +66,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 +102,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/methods/simulate_transaction.go b/cmd/soroban-rpc/internal/methods/simulate_transaction.go index 361022ae..6edc37d7 100644 --- a/cmd/soroban-rpc/internal/methods/simulate_transaction.go +++ b/cmd/soroban-rpc/internal/methods/simulate_transaction.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" @@ -134,10 +133,10 @@ func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff) error { // LedgerEntryChange designates a change in a ledger entry. Before and After cannot be be omitted at the same time. // If Before is omitted, it constitutes a creation, if After is omitted, it constitutes a delation. type LedgerEntryChange struct { - Type LedgerEntryChangeType - Key string // LedgerEntryKey in base64 - Before *string `json:"before"` // LedgerEntry XDR in base64 - After *string `json:"after"` // LedgerEntry XDR in base64 + Type LedgerEntryChangeType `json:"type"` + Key string `json:"key"` // LedgerEntryKey in base64 + Before *string `json:"before"` // LedgerEntry XDR in base64 + After *string `json:"after"` // LedgerEntry XDR in base64 } type SimulateTransactionResponse struct { @@ -159,7 +158,7 @@ type PreflightGetter interface { // NewSimulateTransactionHandler returns a json rpc handler to run preflight simulations func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader, ledgerReader db.LedgerReader, getter PreflightGetter) jrpc2.Handler { - return handler.New(func(ctx context.Context, request SimulateTransactionRequest) SimulateTransactionResponse { + return NewHandler(func(ctx context.Context, request SimulateTransactionRequest) SimulateTransactionResponse { var txEnvelope xdr.TransactionEnvelope if err := xdr.SafeUnmarshalBase64(request.Transaction, &txEnvelope); err != nil { logger.WithError(err).WithField("request", request). 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))) diff --git a/cmd/soroban-rpc/lib/preflight/Cargo.toml b/cmd/soroban-rpc/lib/preflight/Cargo.toml index 5c7d7648..1ec43429 100644 --- a/cmd/soroban-rpc/lib/preflight/Cargo.toml +++ b/cmd/soroban-rpc/lib/preflight/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "preflight" -version = "20.3.3" +version = "20.3.5" publish = false [lib] @@ -15,4 +15,4 @@ sha2 = { workspace = true } soroban-env-host = { workspace = true, features = ["recording_mode", "testutils", "unstable-next-api"]} soroban-simulation = { workspace = true } anyhow = "1.0.75" -rand = { version = "0.8.5", features = [] } \ No newline at end of file +rand = { version = "0.8.5", features = [] } diff --git a/go.mod b/go.mod index b81f67ed..793453c6 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.1 require ( github.com/Masterminds/squirrel v1.5.4 github.com/cenkalti/backoff/v4 v4.2.1 - github.com/creachadair/jrpc2 v1.1.2 + github.com/creachadair/jrpc2 v1.2.0 github.com/go-chi/chi v4.1.2+incompatible github.com/go-git/go-git/v5 v5.9.0 github.com/mattn/go-sqlite3 v1.14.17 @@ -74,7 +74,7 @@ require ( github.com/aws/aws-sdk-go v1.45.27 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/creachadair/mds v0.3.0 // indirect + github.com/creachadair/mds v0.13.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -109,7 +109,7 @@ require ( golang.org/x/crypto v0.16.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.19.0 // indirect - golang.org/x/sync v0.5.0 // indirect + golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.32.0 // indirect diff --git a/go.sum b/go.sum index 4cd67af2..a5083ee2 100644 --- a/go.sum +++ b/go.sum @@ -92,10 +92,10 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creachadair/jrpc2 v1.1.2 h1:UOYMipEFYlwd5qmcvs9GZBurn3oXt1UDIX5JLjWWFzo= -github.com/creachadair/jrpc2 v1.1.2/go.mod h1:JcCe2Eny3lIvVwZLm92WXyU+tNUgTBWFCLMsfNkjEGk= -github.com/creachadair/mds v0.3.0 h1:uKbCKVtd3iOKVv3uviOm13fFNfe9qoCXJh1Vo7y3Kr0= -github.com/creachadair/mds v0.3.0/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= +github.com/creachadair/jrpc2 v1.2.0 h1:SXr0OgnwM0X18P+HccJP0uT3KGSDk/BCSRlJBvE2bMY= +github.com/creachadair/jrpc2 v1.2.0/go.mod h1:66uKSdr6tR5ZeNvkIjDSbbVUtOv0UhjS/vcd8ECP7Iw= +github.com/creachadair/mds v0.13.4 h1:RgU0MhiVqkzp6/xtNWhK6Pw7tDeaVuGFtA0UA2RBYvY= +github.com/creachadair/mds v0.13.4/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -543,8 +543,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=