diff --git a/cmd/stellar-rpc/internal/integrationtest/simulate_transaction_test.go b/cmd/stellar-rpc/internal/integrationtest/simulate_transaction_test.go index 802f2cea7..3ca088198 100644 --- a/cmd/stellar-rpc/internal/integrationtest/simulate_transaction_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/simulate_transaction_test.go @@ -267,6 +267,84 @@ func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { require.Equal(t, xdr.ScString("auth"), *event.Event.Body.V0.Topics[0].Str) } +// TestSimulateInvokeContractTransactionUseUpgradedAuth verifies that setting UseUpgradedAuth on the +// simulate request causes recorded authorization entries to use AddressV2 ("v2") +// credentials instead of Address ("v1"). v2 credentials are only emitted by the +// curr soroban-env host, so this requires a protocol that routes to it. +func TestSimulateInvokeContractTransactionUseUpgradedAuth(t *testing.T) { + test := infrastructure.NewTest(t, nil) + if test.GetProtocolVersion() < 27 { + t.Skip("AddressV2 credentials require protocol >= 27 (curr soroban-env host)") + } + + _, contractID, _ := test.CreateHelloWorldContract() + + contractFnParameterSym := xdr.ScSymbol("world") + authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + authAccountIDArg := xdr.MustAddress(authAddrArg) + test.SendMasterOperation(&txnbuild.CreateAccount{ + Destination: authAddrArg, + Amount: "100000", + SourceAccount: test.MasterAccount().GetAccountID(), + }) + params := infrastructure.CreateTransactionParams( + test.MasterAccount(), + infrastructure.CreateInvokeHostOperation( + test.MasterAccount().GetAccountID(), + contractID, + "auth", + xdr.ScVal{ + Type: xdr.ScValTypeScvAddress, + Address: &xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeAccount, + AccountId: &authAccountIDArg, + }, + }, + xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &contractFnParameterSym, + }, + ), + ) + tx, err := txnbuild.NewTransaction(params) + require.NoError(t, err) + txB64, err := tx.Base64() + require.NoError(t, err) + + // Baseline: without UseUpgradedAuth, the recorded credential is v1 (Address). + v1Resp, err := test.GetRPCLient().SimulateTransaction(t.Context(), + protocol.SimulateTransactionRequest{Transaction: txB64}) + require.NoError(t, err) + require.Empty(t, v1Resp.Error) + v1Auth := firstSimulateAuthEntry(t, v1Resp) + require.Equal(t, xdr.SorobanCredentialsTypeSorobanCredentialsAddress, v1Auth.Credentials.Type) + + // With UseUpgradedAuth, the same recorded entry uses v2 (AddressV2) credentials. + v2Resp, err := test.GetRPCLient().SimulateTransaction(t.Context(), + protocol.SimulateTransactionRequest{Transaction: txB64, UseUpgradedAuth: true}) + require.NoError(t, err) + require.Empty(t, v2Resp.Error) + v2Auth := firstSimulateAuthEntry(t, v2Resp) + require.Equal(t, xdr.SorobanCredentialsTypeSorobanCredentialsAddressV2, v2Auth.Credentials.Type) + require.NotNil(t, v2Auth.Credentials.AddressV2) + require.Nil(t, v2Auth.Credentials.Address) + // The authorized invocation itself is unchanged -- only the credential format differs. + require.Equal(t, xdr.ScSymbol("auth"), v2Auth.RootInvocation.Function.ContractFn.FunctionName) +} + +func firstSimulateAuthEntry( + t *testing.T, + resp protocol.SimulateTransactionResponse, +) xdr.SorobanAuthorizationEntry { + t.Helper() + require.Len(t, resp.Results, 1) + require.NotNil(t, resp.Results[0].AuthXDR) + require.Len(t, *resp.Results[0].AuthXDR, 1) + var auth xdr.SorobanAuthorizationEntry + require.NoError(t, xdr.SafeUnmarshalBase64((*resp.Results[0].AuthXDR)[0], &auth)) + return auth +} + func TestSimulateTransactionError(t *testing.T) { test := infrastructure.NewTest(t, nil) diff --git a/cmd/stellar-rpc/internal/methods/simulate_transaction.go b/cmd/stellar-rpc/internal/methods/simulate_transaction.go index ba97c95b2..a30a30acb 100644 --- a/cmd/stellar-rpc/internal/methods/simulate_transaction.go +++ b/cmd/stellar-rpc/internal/methods/simulate_transaction.go @@ -370,6 +370,7 @@ func NewSimulateTransactionHandler(logger *log.Entry, Footprint: footprint, ResourceConfig: resourceConfig, AuthMode: request.AuthMode, + UseUpgradedAuth: request.UseUpgradedAuth, ProtocolVersion: protocolVersion, LedgerEntryGetter: ledgerEntryGetter, LedgerSeq: latestLedger, diff --git a/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go b/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go index 5d7177763..648e5f023 100644 --- a/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go +++ b/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/creachadair/jrpc2" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" @@ -234,3 +235,115 @@ func feeBumpExtendFootprintMissingSorobanData(t *testing.T) xdr.TransactionEnvel FeeBump: &feeBumpEnvelope, } } + +// capturingPreflightGetter records the GetterParameters it receives so tests can +// assert on how the handler populates them, and returns an empty (but valid) +// Preflight result. +type capturingPreflightGetter struct { + called bool + params preflight.GetterParameters +} + +func (c *capturingPreflightGetter) GetPreflight( + _ context.Context, + params preflight.GetterParameters, +) (preflight.Preflight, error) { + c.called = true + c.params = params + return preflight.Preflight{}, nil +} + +func invokeHostFunctionEnvelope(t *testing.T) xdr.TransactionEnvelope { + t.Helper() + + sourceAccountID := xdr.MustAddress("GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON") + source := (&sourceAccountID).ToMuxedAccount() + contractID := xdr.ContractId{0xa, 0xb, 0xc} + argSymbol := xdr.ScSymbol("world") + + op := xdr.Operation{ + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contractID, + }, + FunctionName: "hello", + Args: []xdr.ScVal{{Type: xdr.ScValTypeScvSymbol, Sym: &argSymbol}}, + }, + }, + }, + }, + } + + tx := xdr.Transaction{ + SourceAccount: source, + Fee: xdr.Uint32(100), + SeqNum: xdr.SequenceNumber(1), + Cond: xdr.Preconditions{Type: xdr.PreconditionTypePrecondNone}, + Memo: xdr.Memo{Type: xdr.MemoTypeMemoNone}, + Operations: []xdr.Operation{op}, + Ext: xdr.TransactionExt{V: 0}, + } + + return xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{Tx: tx}, + } +} + +// TestSimulateTransactionThreadsUseUpgradedAuth verifies that the request's useUpgradedAuth flag is +// forwarded into the preflight GetterParameters (and defaults to false when omitted). +func TestSimulateTransactionThreadsUseUpgradedAuth(t *testing.T) { + closeMeta := xdr.LedgerCloseMeta{ + V: 1, + V1: &xdr.LedgerCloseMetaV1{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{LedgerVersion: 23}, + }, + TotalByteSizeOfLiveSorobanState: 100, + }, + } + + txB64, err := xdr.MarshalBase64(invokeHostFunctionEnvelope(t)) + require.NoError(t, err) + + for _, tc := range []struct { + name string + paramsField string + expected bool + }{ + {name: "useUpgradedAuth true is forwarded", paramsField: `, "useUpgradedAuth": true`, expected: true}, + {name: "useUpgradedAuth false is forwarded", paramsField: `, "useUpgradedAuth": false`, expected: false}, + {name: "useUpgradedAuth omitted defaults to false", paramsField: "", expected: false}, + } { + t.Run(tc.name, func(t *testing.T) { + ledgerReader := &MockLedgerReader{} + ledgerReader.On("GetLatestLedgerSequence", mock.Anything).Return(uint32(2), nil) + ledgerReader.On("GetLedger", mock.Anything, uint32(2)).Return(closeMeta, true, nil) + + getter := &capturingPreflightGetter{} + handler := NewSimulateTransactionHandler(log.New(), ledgerReader, nil, getter, xdr.DecodeOptions{}) + + requestJSON := fmt.Sprintf(`{ +"jsonrpc": "2.0", +"id": 1, +"method": "simulateTransaction", +"params": { "transaction": "%s"%s } +}`, txB64, tc.paramsField) + requests, err := jrpc2.ParseRequests([]byte(requestJSON)) + require.NoError(t, err) + require.Len(t, requests, 1) + + _, err = handler(t.Context(), requests[0].ToRequest()) + require.NoError(t, err) + + require.True(t, getter.called, "GetPreflight should have been called") + require.Equal(t, tc.expected, getter.params.UseUpgradedAuth) + }) + } +} diff --git a/cmd/stellar-rpc/internal/preflight/pool.go b/cmd/stellar-rpc/internal/preflight/pool.go index 21456deed..bc5a944a6 100644 --- a/cmd/stellar-rpc/internal/preflight/pool.go +++ b/cmd/stellar-rpc/internal/preflight/pool.go @@ -164,6 +164,7 @@ type GetterParameters struct { Footprint xdr.LedgerFootprint ResourceConfig protocol.ResourceConfig AuthMode string + UseUpgradedAuth bool ProtocolVersion uint32 LedgerEntryGetter ledgerentries.LedgerEntryGetter LedgerSeq uint32 @@ -188,6 +189,7 @@ func (pwp *WorkerPool) GetPreflight(ctx context.Context, params GetterParameters ResourceConfig: params.ResourceConfig, EnableDebug: pwp.enableDebug, AuthMode: params.AuthMode, + UseUpgradedAuth: params.UseUpgradedAuth, ProtocolVersion: params.ProtocolVersion, } resultC := make(chan workerResult) diff --git a/cmd/stellar-rpc/internal/preflight/preflight.go b/cmd/stellar-rpc/internal/preflight/preflight.go index e69935c89..ce85b0816 100644 --- a/cmd/stellar-rpc/internal/preflight/preflight.go +++ b/cmd/stellar-rpc/internal/preflight/preflight.go @@ -107,6 +107,7 @@ type Parameters struct { ResourceConfig protocol.ResourceConfig EnableDebug bool AuthMode string + UseUpgradedAuth bool ProtocolVersion uint32 } @@ -268,6 +269,7 @@ func getInvokeHostFunctionPreflight(ctx context.Context, params Parameters) (Pre resourceConfig, C.bool(params.EnableDebug), C.uint32_t(authMode), + C.bool(params.UseUpgradedAuth), ) return GoPreflight(res), nil diff --git a/cmd/stellar-rpc/internal/preflight/preflight_test.go b/cmd/stellar-rpc/internal/preflight/preflight_test.go index 029b99b28..5218c30cd 100644 --- a/cmd/stellar-rpc/internal/preflight/preflight_test.go +++ b/cmd/stellar-rpc/internal/preflight/preflight_test.go @@ -302,3 +302,19 @@ func BenchmarkGetPreflight(b *testing.B) { require.Empty(b, result.Error) } } + +// TestGetPreflightUseUpgradedAuthSilentlyIgnoredOnPrevProtocol locks in the agreed +// behavior that requesting v2 (AddressV2) credentials on a protocol served by +// the prev soroban-env host -- which predates v2 credentials -- is silently +// ignored rather than rejected: v1 behavior is used and no error is returned. +// It runs at protocol 26, which routes to the prev host. (Asserting the +// credential version itself requires an auth-recording contract on the curr +// host; that is covered by the integration tests.) +func TestGetPreflightUseUpgradedAuthSilentlyIgnoredOnPrevProtocol(t *testing.T) { + const prevHostProtocol = 26 + params := getPreflightParameters(t, prevHostProtocol) + params.UseUpgradedAuth = true + result, err := GetPreflight(t.Context(), params) + require.NoError(t, err) + require.Empty(t, result.Error) +} diff --git a/cmd/stellar-rpc/lib/preflight.h b/cmd/stellar-rpc/lib/preflight.h index 897fe59ec..100c82602 100644 --- a/cmd/stellar-rpc/lib/preflight.h +++ b/cmd/stellar-rpc/lib/preflight.h @@ -53,7 +53,8 @@ preflight_result_t *preflight_invoke_hf_op(uintptr_t handle, // Go Handle to for const ledger_info_t ledger_info, const resource_config_t resource_config, bool enable_debug, - const uint32_t auth_mode); + const uint32_t auth_mode, + bool use_upgraded_auth); // record AddressV2 ("upgraded") credentials preflight_result_t *preflight_footprint_ttl_op(uintptr_t handle, // Go Handle to forward to SnapshotSourceGet const xdr_t op_body, // OperationBody XDR diff --git a/cmd/stellar-rpc/lib/preflight/src/lib.rs b/cmd/stellar-rpc/lib/preflight/src/lib.rs index 376a1f1e7..150523c6c 100644 --- a/cmd/stellar-rpc/lib/preflight/src/lib.rs +++ b/cmd/stellar-rpc/lib/preflight/src/lib.rs @@ -42,18 +42,16 @@ mod curr { pub(crate) const PROTOCOL: u32 = soroban_env_host::meta::INTERFACE_VERSION.protocol; - // Builds the recording auth mode for this protocol version. The shape of - // `RecordingInvocationAuthMode::Recording` differs between soroban versions, - // so each version-specific module provides its own constructor and - // `shared.rs` calls into it via `super::`. From protocol 27 - // the recording params also carry `use_address_v2`, which we leave `false` - // at this layer until v28. - pub(crate) fn recording_auth_mode( + // Constructs the recording auth mode for this protocol version. The current + // protocol's soroban-env can emit either v1 (`Address`) or v2 (`AddressV2`) + // credentials, selected by `use_upgraded_auth`. + pub(crate) fn make_recording_auth_mode( disable_non_root_auth: bool, + use_upgraded_auth: bool, ) -> soroban_env_host::e2e_invoke::RecordingInvocationAuthMode { soroban_env_host::e2e_invoke::RecordingInvocationAuthMode::recording( disable_non_root_auth, - false, + use_upgraded_auth, ) } } @@ -68,10 +66,11 @@ mod prev { pub(crate) const PROTOCOL: u32 = soroban_env_host::meta::INTERFACE_VERSION.protocol; - // See the matching `curr::recording_auth_mode`. The previous soroban version - // models the recording auth mode as a bare `disable_non_root_auth` bool. - pub(crate) fn recording_auth_mode( + // The previous protocol's soroban-env predates v2 Address credentials, so + // `use_upgraded_auth` is ignored and v1 `Address` credentials are always used. + pub(crate) fn make_recording_auth_mode( disable_non_root_auth: bool, + _use_upgraded_auth: bool, ) -> soroban_env_host::e2e_invoke::RecordingInvocationAuthMode { soroban_env_host::e2e_invoke::RecordingInvocationAuthMode::Recording(disable_non_root_auth) } @@ -203,6 +202,7 @@ pub extern "C" fn preflight_invoke_hf_op( resource_config: CResourceConfig, enable_debug: bool, auth_mode: u32, + use_upgraded_auth: bool, ) -> *mut CPreflightResult { let proto = ledger_info.protocol_version; catch_preflight_panic(&move || { @@ -215,6 +215,7 @@ pub extern "C" fn preflight_invoke_hf_op( resource_config, enable_debug, auth_mode.into(), + use_upgraded_auth, ) } else if proto == curr::PROTOCOL { curr::shared::preflight_invoke_hf_op_or_maybe_panic( @@ -225,6 +226,7 @@ pub extern "C" fn preflight_invoke_hf_op( resource_config, enable_debug, auth_mode.into(), + use_upgraded_auth, ) } else { bail!("unsupported protocol version: {proto}") diff --git a/cmd/stellar-rpc/lib/preflight/src/shared.rs b/cmd/stellar-rpc/lib/preflight/src/shared.rs index 6881600cb..2cfc2f868 100644 --- a/cmd/stellar-rpc/lib/preflight/src/shared.rs +++ b/cmd/stellar-rpc/lib/preflight/src/shared.rs @@ -121,6 +121,7 @@ pub(crate) fn preflight_invoke_hf_op_or_maybe_panic( resource_config: CResourceConfig, enable_debug: bool, auth_mode: AuthMode, + use_upgraded_auth: bool, ) -> Result { let invoke_hf_op = InvokeHostFunctionOp::from_xdr(unsafe { from_c_xdr(invoke_hf_op) }, DEFAULT_XDR_RW_LIMITS) @@ -149,8 +150,8 @@ pub(crate) fn preflight_invoke_hf_op_or_maybe_panic( // ignore the list entirely even if it's present. let auth_mode = match auth_mode { AuthMode::Enforce => RecordingInvocationAuthMode::Enforcing(auth_entries), - AuthMode::Record => super::recording_auth_mode(true), - AuthMode::RecordAllowNonroot => super::recording_auth_mode(false), + AuthMode::Record => super::make_recording_auth_mode(true, use_upgraded_auth), + AuthMode::RecordAllowNonroot => super::make_recording_auth_mode(false, use_upgraded_auth), }; preflight_invoke_hf_op_post_autorestore_or_maybe_panic(