diff --git a/.github/actions/setup-integration-tests/action.yml b/.github/actions/setup-integration-tests/action.yml index 938acb6d..bfdd5c3b 100644 --- a/.github/actions/setup-integration-tests/action.yml +++ b/.github/actions/setup-integration-tests/action.yml @@ -53,6 +53,9 @@ runs: sudo apt-get remove -y moby-compose sudo apt-get install -y docker-compose-plugin + # add alias for docker compose + ln -f -s /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose + echo "Docker Compose Version:" docker-compose 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/.github/workflows/soroban-rpc.yml b/.github/workflows/soroban-rpc.yml index 5c7ddf4f..9fbdfc30 100644 --- a/.github/workflows/soroban-rpc.yml +++ b/.github/workflows/soroban-rpc.yml @@ -107,7 +107,6 @@ jobs: matrix: os: [ubuntu-20.04, ubuntu-22.04] go: [1.22] - test: ['.*CLI.*', '^Test(([^C])|(C[^L])|(CL[^I])).*$'] runs-on: ${{ matrix.os }} env: SOROBAN_RPC_INTEGRATION_TESTS_ENABLED: true @@ -127,4 +126,4 @@ jobs: - name: Run Soroban RPC Integration Tests run: | make install_rust - go test -race -run '${{ matrix.test }}' -timeout 60m -v ./cmd/soroban-rpc/internal/test/... + go test -race -timeout 60m -v ./cmd/soroban-rpc/internal/test/... 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 6eb42c01..12e8e765 100644 --- a/cmd/soroban-rpc/internal/events/events.go +++ b/cmd/soroban-rpc/internal/events/events.go @@ -99,18 +99,25 @@ type ScanFunction func(xdr.DiagnosticEvent, Cursor, int64, *xdr.Hash) bool // remaining events in the range). Note that a read lock is held for the // entire duration of the Scan function so f should be written in a way // to minimize latency. -func (m *MemoryStore) Scan(eventRange Range, f ScanFunction) (uint32, error) { +func (m *MemoryStore) Scan(eventRange Range, f ScanFunction) (lastLedgerInWindow uint32, err error) { startTime := time.Now() + defer func() { + if err == nil { + m.eventsDurationMetric.With(prometheus.Labels{"operation": "scan"}). + Observe(time.Since(startTime).Seconds()) + } + }() + m.lock.RLock() defer m.lock.RUnlock() - if err := m.validateRange(&eventRange); err != nil { - return 0, err + if err = m.validateRange(&eventRange); err != nil { + return } firstLedgerInRange := eventRange.Start.Ledger firstLedgerInWindow := m.eventsByLedger.Get(0).LedgerSeq - lastLedgerInWindow := firstLedgerInWindow + (m.eventsByLedger.Len() - 1) + lastLedgerInWindow = firstLedgerInWindow + (m.eventsByLedger.Len() - 1) for i := firstLedgerInRange - firstLedgerInWindow; i < m.eventsByLedger.Len(); i++ { bucket := m.eventsByLedger.Get(i) events := bucket.BucketContent @@ -122,21 +129,19 @@ func (m *MemoryStore) Scan(eventRange Range, f ScanFunction) (uint32, error) { for _, event := range events { cur := event.cursor(bucket.LedgerSeq) if eventRange.End.Cmp(cur) <= 0 { - return lastLedgerInWindow, nil + return } var diagnosticEvent xdr.DiagnosticEvent - err := xdr.SafeUnmarshal(event.diagnosticEventXDR, &diagnosticEvent) + err = xdr.SafeUnmarshal(event.diagnosticEventXDR, &diagnosticEvent) if err != nil { - return 0, err + return } if !f(diagnosticEvent, cur, timestamp, event.txHash) { - return lastLedgerInWindow, nil + return } } } - m.eventsDurationMetric.With(prometheus.Labels{"operation": "scan"}). - Observe(time.Since(startTime).Seconds()) - return lastLedgerInWindow, nil + return } // validateRange checks if the range falls within the bounds @@ -259,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/events/events_test.go b/cmd/soroban-rpc/internal/events/events_test.go index 55145fba..c5fda34c 100644 --- a/cmd/soroban-rpc/internal/events/events_test.go +++ b/cmd/soroban-rpc/internal/events/events_test.go @@ -4,6 +4,8 @@ import ( "bytes" "testing" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" "github.com/stellar/go/xdr" "github.com/stretchr/testify/require" @@ -240,9 +242,16 @@ func concat(slices ...[]event) []event { return result } -func TestScan(t *testing.T) { - m := createStore(t) +func getMetricValue(metric prometheus.Metric) *dto.Metric { + value := &dto.Metric{} + err := metric.Write(value) + if err != nil { + panic(err) + } + return value +} +func TestScan(t *testing.T) { genEquivalentInputs := func(input Range) []Range { results := []Range{input} if !input.ClampStart { @@ -360,6 +369,7 @@ func TestScan(t *testing.T) { }, } { for _, input := range genEquivalentInputs(testCase.input) { + m := createStore(t) var events []event iterateAll := true f := func(contractEvent xdr.DiagnosticEvent, cursor Cursor, ledgerCloseTimestamp int64, hash *xdr.Hash) bool { @@ -378,11 +388,17 @@ func TestScan(t *testing.T) { require.NoError(t, err) require.Equal(t, uint32(8), latest) eventsAreEqual(t, testCase.expected, events) + metric, err := m.eventsDurationMetric.MetricVec.GetMetricWith(prometheus.Labels{ + "operation": "scan", + }) + require.NoError(t, err) + require.Equal(t, uint64(1), getMetricValue(metric).GetSummary().GetSampleCount()) if len(events) > 0 { events = nil iterateAll = false latest, err := m.Scan(input, f) require.NoError(t, err) + require.Equal(t, uint64(2), getMetricValue(metric).GetSummary().GetSampleCount()) require.Equal(t, uint32(8), latest) eventsAreEqual(t, []event{testCase.expected[0]}, events) } diff --git a/cmd/soroban-rpc/internal/ingest/service.go b/cmd/soroban-rpc/internal/ingest/service.go index d2c00b7b..931abfe8 100644 --- a/cmd/soroban-rpc/internal/ingest/service.go +++ b/cmd/soroban-rpc/internal/ingest/service.go @@ -249,12 +249,13 @@ func (s *Service) fillEntriesFromCheckpoint(ctx context.Context, archive history } func (s *Service) ingest(ctx context.Context, sequence uint32) error { - startTime := time.Now() s.logger.Infof("Ingesting ledger %d", sequence) ledgerCloseMeta, err := s.ledgerBackend.GetLedger(ctx, sequence) if err != nil { return err } + + startTime := time.Now() reader, err := ingest.NewLedgerChangeReaderFromLedgerCloseMeta(s.networkPassPhrase, ledgerCloseMeta) if err != nil { return err diff --git a/cmd/soroban-rpc/internal/jsonrpc.go b/cmd/soroban-rpc/internal/jsonrpc.go index 84585510..825a2093 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/methods/simulate_transaction.go b/cmd/soroban-rpc/internal/methods/simulate_transaction.go index 580a5d2f..6ad3c63b 100644 --- a/cmd/soroban-rpc/internal/methods/simulate_transaction.go +++ b/cmd/soroban-rpc/internal/methods/simulate_transaction.go @@ -3,7 +3,10 @@ package methods import ( "context" "encoding/base64" + "encoding/json" + "errors" "fmt" + "strings" "github.com/creachadair/jrpc2" "github.com/creachadair/jrpc2/handler" @@ -34,6 +37,108 @@ type RestorePreamble struct { TransactionData string `json:"transactionData"` // SorobanTransactionData XDR in base64 MinResourceFee int64 `json:"minResourceFee,string"` } +type LedgerEntryChangeType int + +const ( + LedgerEntryChangeTypeCreated LedgerEntryChangeType = iota + 1 + LedgerEntryChangeTypeUpdated + LedgerEntryChangeTypeDeleted +) + +var ( + LedgerEntryChangeTypeName = map[LedgerEntryChangeType]string{ + LedgerEntryChangeTypeCreated: "created", + LedgerEntryChangeTypeUpdated: "updated", + LedgerEntryChangeTypeDeleted: "deleted", + } + LedgerEntryChangeTypeValue = map[string]LedgerEntryChangeType{ + "created": LedgerEntryChangeTypeCreated, + "updated": LedgerEntryChangeTypeUpdated, + "deleted": LedgerEntryChangeTypeDeleted, + } +) + +func (l LedgerEntryChangeType) String() string { + return LedgerEntryChangeTypeName[l] +} + +func (l LedgerEntryChangeType) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +func (l *LedgerEntryChangeType) Parse(s string) error { + s = strings.TrimSpace(strings.ToLower(s)) + value, ok := LedgerEntryChangeTypeValue[s] + if !ok { + return fmt.Errorf("%q is not a valid ledger entry change type", s) + } + *l = value + return nil +} + +func (l *LedgerEntryChangeType) UnmarshalJSON(data []byte) (err error) { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + return l.Parse(s) +} + +func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff) error { + beforePresent := len(diff.Before) > 0 + afterPresent := len(diff.After) > 0 + var ( + entryXDR []byte + changeType LedgerEntryChangeType + ) + switch { + case beforePresent: + entryXDR = diff.Before + if afterPresent { + changeType = LedgerEntryChangeTypeUpdated + } else { + changeType = LedgerEntryChangeTypeDeleted + } + case afterPresent: + entryXDR = diff.After + changeType = LedgerEntryChangeTypeCreated + default: + return errors.New("missing before and after") + } + var entry xdr.LedgerEntry + + if err := xdr.SafeUnmarshal(entryXDR, &entry); err != nil { + return err + } + key, err := entry.LedgerKey() + if err != nil { + return err + } + keyB64, err := xdr.MarshalBase64(key) + if err != nil { + return err + } + l.Type = changeType + l.Key = keyB64 + if beforePresent { + before := base64.StdEncoding.EncodeToString(diff.Before) + l.Before = &before + } + if afterPresent { + after := base64.StdEncoding.EncodeToString(diff.After) + l.After = &after + } + return nil +} + +// 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 `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 { Error string `json:"error,omitempty"` @@ -43,6 +148,7 @@ type SimulateTransactionResponse struct { Results []SimulateHostFunctionResult `json:"results,omitempty"` // an array of the individual host function call results Cost SimulateTransactionCost `json:"cost,omitempty"` // the effective cpu and memory cost of the invoked transaction execution. RestorePreamble *RestorePreamble `json:"restorePreamble,omitempty"` // If present, it indicates that a prior RestoreFootprint is required + StateChanges []LedgerEntryChange `json:"stateChanges,omitempty"` // If present, it indicates how the state (ledger entries) will change as a result of the transaction execution. LatestLedger uint32 `json:"latestLedger"` } @@ -149,6 +255,11 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge } } + stateChanges := make([]LedgerEntryChange, len(result.LedgerEntryDiff)) + for i := 0; i < len(stateChanges); i++ { + stateChanges[i].FromXDRDiff(result.LedgerEntryDiff[i]) + } + return SimulateTransactionResponse{ Error: result.Error, Results: results, @@ -161,6 +272,7 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge }, LatestLedger: latestLedger, RestorePreamble: restorePreamble, + StateChanges: stateChanges, } }) } diff --git a/cmd/soroban-rpc/internal/methods/simulate_transaction_test.go b/cmd/soroban-rpc/internal/methods/simulate_transaction_test.go new file mode 100644 index 00000000..0c3184b1 --- /dev/null +++ b/cmd/soroban-rpc/internal/methods/simulate_transaction_test.go @@ -0,0 +1,94 @@ +package methods + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/preflight" +) + +func TestLedgerEntryChange(t *testing.T) { + entry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 100, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON"), + Balance: 100, + SeqNum: 1, + }, + }, + } + + entryXDR, err := entry.MarshalBinary() + require.NoError(t, err) + entryB64 := base64.StdEncoding.EncodeToString(entryXDR) + + key, err := entry.LedgerKey() + require.NoError(t, err) + keyXDR, err := key.MarshalBinary() + require.NoError(t, err) + keyB64 := base64.StdEncoding.EncodeToString(keyXDR) + + for _, test := range []struct { + name string + input preflight.XDRDiff + expectedOutput LedgerEntryChange + }{ + { + name: "creation", + input: preflight.XDRDiff{ + Before: nil, + After: entryXDR, + }, + expectedOutput: LedgerEntryChange{ + Type: LedgerEntryChangeTypeCreated, + Key: keyB64, + Before: nil, + After: &entryB64, + }, + }, + { + name: "deletion", + input: preflight.XDRDiff{ + Before: entryXDR, + After: nil, + }, + expectedOutput: LedgerEntryChange{ + Type: LedgerEntryChangeTypeDeleted, + Key: keyB64, + Before: &entryB64, + After: nil, + }, + }, + { + name: "update", + input: preflight.XDRDiff{ + Before: entryXDR, + After: entryXDR, + }, + expectedOutput: LedgerEntryChange{ + Type: LedgerEntryChangeTypeUpdated, + Key: keyB64, + Before: &entryB64, + After: &entryB64, + }, + }, + } { + var change LedgerEntryChange + require.NoError(t, change.FromXDRDiff(test.input), test.name) + assert.Equal(t, test.expectedOutput, change) + + // test json roundtrip + changeJSON, err := json.Marshal(change) + require.NoError(t, err, test.name) + var change2 LedgerEntryChange + require.NoError(t, json.Unmarshal(changeJSON, &change2)) + assert.Equal(t, change, change2, test.name) + } +} diff --git a/cmd/soroban-rpc/internal/preflight/preflight.go b/cmd/soroban-rpc/internal/preflight/preflight.go index 59c15152..9a0fb94f 100644 --- a/cmd/soroban-rpc/internal/preflight/preflight.go +++ b/cmd/soroban-rpc/internal/preflight/preflight.go @@ -105,6 +105,11 @@ type PreflightParameters struct { EnableDebug bool } +type XDRDiff struct { + Before []byte // optional before XDR + After []byte // optional after XDR +} + type Preflight struct { Error string Events [][]byte // DiagnosticEvents XDR @@ -116,6 +121,7 @@ type Preflight struct { MemoryBytes uint64 PreRestoreTransactionData []byte // SorobanTransactionData XDR PreRestoreMinFee int64 + LedgerEntryDiff []XDRDiff } func CXDR(xdr []byte) C.xdr_t { @@ -138,6 +144,16 @@ func GoXDRVector(xdrVector C.xdr_vector_t) [][]byte { return result } +func GoXDRDiffVector(xdrDiffVector C.xdr_diff_vector_t) []XDRDiff { + result := make([]XDRDiff, xdrDiffVector.len) + inputSlice := unsafe.Slice(xdrDiffVector.array, xdrDiffVector.len) + for i, v := range inputSlice { + result[i].Before = GoXDR(v.before) + result[i].After = GoXDR(v.after) + } + return result +} + func GetPreflight(ctx context.Context, params PreflightParameters) (Preflight, error) { switch params.OpBody.Type { case xdr.OperationTypeInvokeHostFunction: @@ -259,6 +275,7 @@ func GoPreflight(result *C.preflight_result_t) Preflight { MemoryBytes: uint64(result.memory_bytes), PreRestoreTransactionData: GoXDR(result.pre_restore_transaction_data), PreRestoreMinFee: int64(result.pre_restore_min_fee), + LedgerEntryDiff: GoXDRDiffVector(result.ledger_entry_diff), } return preflight } 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/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go index 0b55c729..d414f315 100644 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go @@ -260,6 +260,20 @@ func TestSimulateTransactionSucceeds(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedXdr, resultXdr) + // Check state diff + assert.Len(t, result.StateChanges, 1) + assert.Nil(t, result.StateChanges[0].Before) + assert.NotNil(t, result.StateChanges[0].After) + assert.Equal(t, methods.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type) + var after xdr.LedgerEntry + assert.NoError(t, xdr.SafeUnmarshalBase64(*result.StateChanges[0].After, &after)) + assert.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type) + entryKey, err := after.LedgerKey() + assert.NoError(t, err) + entryKeyB64, err := xdr.MarshalBase64(entryKey) + assert.NoError(t, err) + assert.Equal(t, entryKeyB64, result.StateChanges[0].Key) + // test operation which does not have a source account withoutSourceAccountOp := createInstallContractCodeOperation("", contractBinary) params = txnbuild.TransactionParams{ 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.h b/cmd/soroban-rpc/lib/preflight.h index f52d56c1..40587ad4 100644 --- a/cmd/soroban-rpc/lib/preflight.h +++ b/cmd/soroban-rpc/lib/preflight.h @@ -22,21 +22,32 @@ typedef struct xdr_vector_t { size_t len; } xdr_vector_t; +typedef struct xdr_diff_t { + xdr_t before; + xdr_t after; +} xdr_diff_t; + +typedef struct xdr_diff_vector_t { + xdr_diff_t *array; + size_t len; +} xdr_diff_vector_t; + typedef struct resource_config_t { uint64_t instruction_leeway; // Allow this many extra instructions when budgeting } resource_config_t; typedef struct preflight_result_t { - char *error; // Error string in case of error, otherwise null - xdr_vector_t auth; // array of SorobanAuthorizationEntries - xdr_t result; // XDR SCVal - xdr_t transaction_data; - int64_t min_fee; // Minimum recommended resource fee - xdr_vector_t events; // array of XDR DiagnosticEvents - uint64_t cpu_instructions; - uint64_t memory_bytes; - xdr_t pre_restore_transaction_data; // SorobanTransactionData XDR for a prerequired RestoreFootprint operation - int64_t pre_restore_min_fee; // Minimum recommended resource fee for a prerequired RestoreFootprint operation + char *error; // Error string in case of error, otherwise null + xdr_vector_t auth; // array of SorobanAuthorizationEntries + xdr_t result; // XDR SCVal + xdr_t transaction_data; + int64_t min_fee; // Minimum recommended resource fee + xdr_vector_t events; // array of XDR DiagnosticEvents + uint64_t cpu_instructions; + uint64_t memory_bytes; + xdr_t pre_restore_transaction_data; // SorobanTransactionData XDR for a prerequired RestoreFootprint operation + int64_t pre_restore_min_fee; // Minimum recommended resource fee for a prerequired RestoreFootprint operation + xdr_diff_vector_t ledger_entry_diff; // Contains the ledger entry changes which would be caused by the transaction execution } preflight_result_t; preflight_result_t *preflight_invoke_hf_op(uintptr_t handle, // Go Handle to forward to SnapshotSourceGet 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/cmd/soroban-rpc/lib/preflight/src/lib.rs b/cmd/soroban-rpc/lib/preflight/src/lib.rs index fd80b018..200bd7a6 100644 --- a/cmd/soroban-rpc/lib/preflight/src/lib.rs +++ b/cmd/soroban-rpc/lib/preflight/src/lib.rs @@ -16,7 +16,8 @@ use soroban_env_host::xdr::{ use soroban_env_host::{HostError, LedgerInfo, DEFAULT_XDR_RW_LIMITS}; use soroban_simulation::simulation::{ simulate_extend_ttl_op, simulate_invoke_host_function_op, simulate_restore_op, - InvokeHostFunctionSimulationResult, RestoreOpSimulationResult, SimulationAdjustmentConfig, + InvokeHostFunctionSimulationResult, LedgerEntryDiff, RestoreOpSimulationResult, + SimulationAdjustmentConfig, }; use soroban_simulation::{AutoRestoringSnapshotSource, NetworkConfig, SnapshotSourceWithArchive}; use std::cell::RefCell; @@ -59,10 +60,12 @@ pub struct CXDR { // It would be nicer to derive Default, but we can't. It errors with: // The trait bound `*mut u8: std::default::Default` is not satisfied -fn get_default_c_xdr() -> CXDR { - CXDR { - xdr: null_mut(), - len: 0, +impl Default for CXDR { + fn default() -> Self { + CXDR { + xdr: null_mut(), + len: 0, + } } } @@ -73,10 +76,35 @@ pub struct CXDRVector { pub len: libc::size_t, } -fn get_default_c_xdr_vector() -> CXDRVector { - CXDRVector { - array: null_mut(), - len: 0, +impl Default for CXDRVector { + fn default() -> Self { + CXDRVector { + array: null_mut(), + len: 0, + } + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct CXDRDiff { + pub before: CXDR, + pub after: CXDR, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct CXDRDiffVector { + pub array: *mut CXDRDiff, + pub len: libc::size_t, +} + +impl Default for CXDRDiffVector { + fn default() -> Self { + CXDRDiffVector { + array: null_mut(), + len: 0, + } } } @@ -107,21 +135,24 @@ pub struct CPreflightResult { pub pre_restore_transaction_data: CXDR, // Minimum recommended resource fee for a prerequired RestoreFootprint operation pub pre_restore_min_fee: i64, + // Contains the ledger entry changes which would be caused by the transaction execution + pub ledger_entry_diff: CXDRDiffVector, } impl Default for CPreflightResult { fn default() -> Self { Self { error: CString::new(String::new()).unwrap().into_raw(), - auth: get_default_c_xdr_vector(), - result: get_default_c_xdr(), - transaction_data: get_default_c_xdr(), + auth: Default::default(), + result: Default::default(), + transaction_data: Default::default(), min_fee: 0, - events: get_default_c_xdr_vector(), + events: Default::default(), cpu_instructions: 0, memory_bytes: 0, - pre_restore_transaction_data: get_default_c_xdr(), + pre_restore_transaction_data: Default::default(), pre_restore_min_fee: 0, + ledger_entry_diff: Default::default(), } } } @@ -149,6 +180,7 @@ impl CPreflightResult { events: xdr_vec_to_c(invoke_hf_result.diagnostic_events), cpu_instructions: invoke_hf_result.simulated_instructions as u64, memory_bytes: invoke_hf_result.simulated_memory as u64, + ledger_entry_diff: ledger_entry_diff_vec_to_c(invoke_hf_result.modified_entries), ..Default::default() }; if let Some(p) = restore_preamble { @@ -272,6 +304,7 @@ pub extern "C" fn preflight_footprint_ttl_op( preflight_footprint_ttl_op_or_maybe_panic(handle, op_body, footprint, ledger_info) })) } + fn preflight_footprint_ttl_op_or_maybe_panic( handle: libc::uintptr_t, op_body: CXDR, @@ -289,7 +322,7 @@ fn preflight_footprint_ttl_op_or_maybe_panic( match op_body { OperationBody::ExtendFootprintTtl(extend_op) => { preflight_extend_ttl_op(extend_op, footprint.read_only.as_slice(), go_storage, &network_config, &ledger_info) - }, + } OperationBody::RestoreFootprint(_) => { preflight_restore_op(footprint.read_write.as_slice(), go_storage, &network_config, &ledger_info) } @@ -297,6 +330,7 @@ fn preflight_footprint_ttl_op_or_maybe_panic( op_body.discriminant()).into()) } } + fn preflight_extend_ttl_op( extend_op: ExtendFootprintTtlOp, keys_to_extend: &[LedgerKey], @@ -382,6 +416,10 @@ fn catch_preflight_panic(op: Box Result>) -> *mut Box::into_raw(Box::new(c_preflight_result)) } +// TODO: We could use something like https://github.com/sonos/ffi-convert-rs +// to replace all the free_* , *_to_c and from_c_* functions by implementations of CDrop, +// CReprOf and AsRust + fn xdr_to_c(v: impl WriteXdr) -> CXDR { let (xdr, len) = vec_to_c_array(v.to_xdr(DEFAULT_XDR_RW_LIMITS).unwrap()); CXDR { xdr, len } @@ -397,6 +435,13 @@ fn option_xdr_to_c(v: Option) -> CXDR { ) } +fn ledger_entry_diff_to_c(v: LedgerEntryDiff) -> CXDRDiff { + CXDRDiff { + before: option_xdr_to_c(v.state_before), + after: option_xdr_to_c(v.state_after), + } +} + fn xdr_vec_to_c(v: Vec) -> CXDRVector { let c_v = v.into_iter().map(xdr_to_c).collect(); let (array, len) = vec_to_c_array(c_v); @@ -422,6 +467,15 @@ fn vec_to_c_array(mut v: Vec) -> (*mut T, libc::size_t) { (ptr, len) } +fn ledger_entry_diff_vec_to_c(modified_entries: Vec) -> CXDRDiffVector { + let c_diffs = modified_entries + .into_iter() + .map(ledger_entry_diff_to_c) + .collect(); + let (array, len) = vec_to_c_array(c_diffs); + CXDRDiffVector { array, len } +} + /// . /// /// # Safety @@ -439,6 +493,7 @@ pub unsafe extern "C" fn free_preflight_result(result: *mut CPreflightResult) { free_c_xdr(boxed.transaction_data); free_c_xdr_array(boxed.events); free_c_xdr(boxed.pre_restore_transaction_data); + free_c_xdr_diff_array(boxed.ledger_entry_diff); } fn free_c_string(str: *mut libc::c_char) { @@ -471,6 +526,19 @@ fn free_c_xdr_array(xdr_array: CXDRVector) { } } +fn free_c_xdr_diff_array(xdr_array: CXDRDiffVector) { + if xdr_array.array.is_null() { + return; + } + unsafe { + let v = Vec::from_raw_parts(xdr_array.array, xdr_array.len, xdr_array.len); + for diff in v { + free_c_xdr(diff.before); + free_c_xdr(diff.after); + } + } +} + fn from_c_string(str: *const libc::c_char) -> String { let c_str = unsafe { CStr::from_ptr(str) }; c_str.to_str().unwrap().to_string()