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/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/cmd/soroban-rpc/internal/methods/simulate_transaction.go b/cmd/soroban-rpc/internal/methods/simulate_transaction.go index 580a5d2f..361022ae 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 + Key string // 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/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/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/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()