diff --git a/cmd/soroban-rpc/internal/methods/simulate_transaction.go b/cmd/soroban-rpc/internal/methods/simulate_transaction.go index 580a5d2f..4cbd523a 100644 --- a/cmd/soroban-rpc/internal/methods/simulate_transaction.go +++ b/cmd/soroban-rpc/internal/methods/simulate_transaction.go @@ -35,6 +35,13 @@ type RestorePreamble struct { MinResourceFee int64 `json:"minResourceFee,string"` } +// LedgerEntryDiff 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 LedgerEntryDiff struct { + Before string `json:"before,omitempty"` // LedgerEntry XDR in base64 + After string `json:"after,omitempty"` // LedgerEntry XDR in base64 +} + type SimulateTransactionResponse struct { Error string `json:"error,omitempty"` TransactionData string `json:"transactionData,omitempty"` // SorobanTransactionData XDR in base64 @@ -43,6 +50,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 + StateDiff []LedgerEntryDiff `json:"stateDiff,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 +157,12 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge } } + stateDiff := make([]LedgerEntryDiff, len(result.LedgerEntryDiff)) + for i := 0; i < len(stateDiff); i++ { + stateDiff[i].Before = base64.StdEncoding.EncodeToString(result.LedgerEntryDiff[i].Before) + stateDiff[i].After = base64.StdEncoding.EncodeToString(result.LedgerEntryDiff[i].After) + } + return SimulateTransactionResponse{ Error: result.Error, Results: results, @@ -161,6 +175,7 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge }, LatestLedger: latestLedger, RestorePreamble: restorePreamble, + StateDiff: stateDiff, } }) } 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..d4627d7e 100644 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go @@ -260,6 +260,14 @@ func TestSimulateTransactionSucceeds(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedXdr, resultXdr) + // Check state diff + assert.Len(t, result.StateDiff, 1) + assert.Empty(t, result.StateDiff[0].Before) + assert.NotEmpty(t, result.StateDiff[0].After) + var after xdr.LedgerEntry + assert.NoError(t, xdr.SafeUnmarshalBase64(result.StateDiff[0].After, &after)) + assert.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type) + // 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()