From ec6d05f30a6ec80f99dd2b34e4546c6a959dbb4e Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 21 Apr 2026 22:02:58 +0200 Subject: [PATCH 01/34] Accounts Refactor PR 2: Adding target_account --- .../sov-rollup-apis/src/endpoints/simulate.rs | 1 + .../stf_blueprint/operator/auth_eip712.rs | 16 +- .../sov-accounts/tests/integration/main.rs | 251 +++++++++++++++++- .../sov-evm/src/authenticate.rs | 1 + .../tests/integration/hooks_tests.rs | 2 +- .../module-system/sov-capabilities/src/lib.rs | 27 ++ .../src/runtime/capabilities/authorization.rs | 7 + .../src/transaction/types/v0.rs | 1 + .../src/transaction/types/v1.rs | 7 + .../src/transaction/unsigned/v0.rs | 6 + .../src/transaction/unsigned/v1.rs | 6 + .../src/authentication/mod.rs | 2 + .../src/authentication/payload.rs | 1 + .../tests/resync/data/mock_da.sqlite | Bin 49152 -> 49152 bytes 14 files changed, 320 insertions(+), 8 deletions(-) diff --git a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs index 4be0dae6e6..9d15da4870 100644 --- a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs +++ b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs @@ -320,6 +320,7 @@ impl> SovereignSimulate { credential_id, default_address: credential_id.into(), credentials: Credentials::new(credential_id), + address: None, } } diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs index e8c7a369ed..25bb9e2d0f 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs @@ -175,10 +175,13 @@ pub fn sign_utx_in_place>( } /// Signs a V1 (multisig) unsigned transaction with the given private key using EIP712. -/// The credential_address is computed from the provided multisig. +/// The credential_address is computed from the provided multisig. `target_address` is +/// forwarded into the signed `UnsignedTransactionV1`; pass `None` to match pre-target +/// routing or `Some(X)` to sign for a specific target account. pub fn sign_utx_v1_in_place>( utx: &UnsignedTransactionV0, multisig: &Multisig<::PublicKey>, + target_address: Option, private_key: &::PrivateKey, ) -> ::Signature { let schema = TestSchemaProvider::get_schema(); @@ -197,6 +200,7 @@ pub fn sign_utx_v1_in_place>( uniqueness: utx.uniqueness, details: utx.details.clone(), credential_address, + target_address, }); let utx_bytes = borsh::to_vec(&utx_enum).expect("Failed to serialize unsigned transaction"); let eip712_signing_data = schema @@ -303,16 +307,20 @@ fn test_multisig_signature_verification() { // Generate a signature from a random private key that's not part of the multisig. We'll use this in some of the test cases. let random_private_key = TestPrivateKey::generate(); + // The multisig credential is authorized for `admin.address()` by the + // InsertCredentialId tx above. Target that address so the multisig tx pays + // gas from admin's balance; the multisig's own default address is unfunded. + let target = Some(admin.address()); let make_multisig_tx = |generation| { let utx = create_utx_with_generation::(encode_message::<_, RT>(), generation); let signatures = multisig_keys .iter() - .map(|key| sign_utx_v1_in_place(&utx, &multisig, key)) + .map(|key| sign_utx_v1_in_place(&utx, &multisig, target, key)) .collect::>(); - let random_signature = sign_utx_v1_in_place(&utx, &multisig, &random_private_key); + let random_signature = sign_utx_v1_in_place(&utx, &multisig, target, &random_private_key); ( - utx.to_multisig_tx(multisig.clone()), + utx.to_multisig_tx(multisig.clone(), target), signatures, random_signature, ) diff --git a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs index de534a4773..61adc4b0e6 100644 --- a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs +++ b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs @@ -1,4 +1,4 @@ -use sov_accounts::{Accounts, CallMessage}; +use sov_accounts::{AccountData, Accounts, CallMessage}; use sov_modules_api::transaction::{UnsignedTransactionV0, Version1}; use sov_modules_api::{ CryptoSpec, PrivateKey, PublicKey, RawTx, Runtime, SkippedTxContents, Spec, TxEffect, @@ -180,7 +180,8 @@ fn test_setup_multisig_and_act() { let genesis_config = HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![multisig_user.clone()]); let genesis = GenesisConfig::from_minimal_config(genesis_config.into()); - let mut runner = TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); // Define utilities for... // - Generating a valid multisig (version 1) transaction @@ -196,7 +197,7 @@ fn test_setup_multisig_and_act() { sov_modules_api::capabilities::UniquenessData::Generation(0), default_test_tx_details::(), ) - .to_multisig_tx(multisig.clone()) + .to_multisig_tx(multisig.clone(), None) }; let sign = |tx: &mut Version1, key: &TestPrivateKey| { @@ -485,3 +486,247 @@ fn test_disable_custom_account_mappings() { }), }); } + +/// A multisig test fixture: keys, envelope, credential id, and a funded `TestUser` +/// registered at the multisig's default address in genesis. +struct MultisigEnv { + keys: [TestPrivateKey; 3], + multisig: sov_modules_api::Multisig<<::CryptoSpec as CryptoSpec>::PublicKey>, + credential_id: sov_modules_api::CredentialId, + user: TestUser, +} + +fn make_multisig_env() -> MultisigEnv { + let keys = [ + TestPrivateKey::generate(), + TestPrivateKey::generate(), + TestPrivateKey::generate(), + ]; + let multisig = sov_modules_api::Multisig::new(2, keys.iter().map(|k| k.pub_key()).collect()); + let credential_id = multisig.credential_id::<<::CryptoSpec as CryptoSpec>::Hasher>(); + let user = TestUser::generate_with_default_balance().add_credential_id(credential_id); + MultisigEnv { + keys, + multisig, + credential_id, + user, + } +} + +/// Builds an unsigned V1 tx carrying an `InsertCredentialId` call. +fn make_v1_tx( + multisig: &sov_modules_api::Multisig<<::CryptoSpec as CryptoSpec>::PublicKey>, + inner_credential: sov_modules_api::CredentialId, + target_address: Option<::Address>, +) -> Version1 { + UnsignedTransactionV0::::new_with_details( + TestAccountsRuntimeCall::Accounts(CallMessage::InsertCredentialId(inner_credential)), + sov_modules_api::capabilities::UniquenessData::Generation(0), + default_test_tx_details::(), + ) + .to_multisig_tx(multisig.clone(), target_address) +} + +fn sign_v1(tx: &mut Version1, key: &TestPrivateKey) { + tx.sign(key, &>::CHAIN_HASH).unwrap(); +} + +fn submit_v1(tx: Version1) -> TransactionType { + let tx = Transaction::::from(tx); + TransactionType::::PreSigned(RawTx { + data: borsh::to_vec(&tx).unwrap(), + }) +} + +/// V1 tx with `target_address = None` routes through `resolve_sender_address` +/// and executes as the multisig's default address. Evidence: the `InsertCredentialId` +/// call writes `(multisig_default, inner_credential)` to `account_owners`. +#[test] +fn test_v1_target_none_uses_default_resolver() { + let MultisigEnv { + keys, + multisig, + user: multisig_user, + .. + } = make_multisig_env(); + let multisig_default_address = multisig_user.address(); + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![multisig_user]); + let genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + let mut tx = make_v1_tx(&multisig, inner_credential, None); + sign_v1(&mut tx, &keys[0]); + sign_v1(&mut tx, &keys[1]); + + runner.execute_transaction(TransactionTestCase { + input: submit_v1(tx), + assert: Box::new(move |result, state| { + assert!(result.tx_receipt.is_successful()); + let accounts = Accounts::::default(); + assert!( + accounts + .is_authorized(&multisig_default_address, &inner_credential, state) + .unwrap(), + "target=None should route to multisig default; the InsertCredentialId \ + call must write the new credential under that address" + ); + }), + }); +} + +/// V1 tx with `target_address = Some(X)` where `(X, multisig_credential_id)` is +/// authorized resolves context as `X`. Evidence: `InsertCredentialId` writes +/// `(X, inner_credential)` — not `(multisig_default, inner_credential)`. +#[test] +fn test_v1_target_some_authorized_succeeds() { + let MultisigEnv { + keys, + multisig, + credential_id: multisig_credential_id, + user: multisig_user, + } = make_multisig_env(); + + // Alice is a regular funded user; we authorize the multisig for her address. + let alice = TestUser::::generate_with_default_balance(); + let alice_address = alice.address(); + + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![multisig_user, alice]); + let mut genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + genesis.accounts.accounts.push(AccountData { + credential_id: multisig_credential_id, + address: alice_address, + }); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + let mut tx = make_v1_tx(&multisig, inner_credential, Some(alice_address)); + sign_v1(&mut tx, &keys[0]); + sign_v1(&mut tx, &keys[1]); + + runner.execute_transaction(TransactionTestCase { + input: submit_v1(tx), + assert: Box::new(move |result, state| { + assert!( + result.tx_receipt.is_successful(), + "V1 target=Some(authorized) should succeed, got {:?}", + result.tx_receipt + ); + let accounts = Accounts::::default(); + assert!( + accounts + .is_authorized(&alice_address, &inner_credential, state) + .unwrap(), + "InsertCredentialId should write under target_address, not multisig default" + ); + }), + }); +} + +/// V1 tx with `target_address = Some(Y)` where `(Y, credential_id) ∉ account_owners` +/// is skipped, not reverted. No state is mutated — in particular, no auto-register +/// on the `Some` path. +#[test] +fn test_v1_target_some_unauthorized_skipped() { + let MultisigEnv { + keys, + multisig, + user: multisig_user, + .. + } = make_multisig_env(); + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![multisig_user]); + let genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + // Unowned: nothing in `account_owners` points to this address. + let unowned_address = TestUser::::generate_with_default_balance().address(); + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + + let mut tx = make_v1_tx(&multisig, inner_credential, Some(unowned_address)); + sign_v1(&mut tx, &keys[0]); + sign_v1(&mut tx, &keys[1]); + + runner.execute_transaction(TransactionTestCase { + input: submit_v1(tx), + assert: Box::new(move |result, state| { + match result.tx_receipt { + TxEffect::Skipped(SkippedTxContents { error, .. }) => { + let msg = error.to_string(); + assert!( + msg.contains("not authorized for target address"), + "unexpected skip reason: {msg}" + ); + } + other => panic!("expected skipped tx, got {other:?}"), + } + // No auto-register on the `Some` path: the unauthorized lookup must + // not have created an entry. + let accounts = Accounts::::default(); + assert!( + !accounts + .is_authorized(&unowned_address, &inner_credential, state) + .unwrap(), + "unauthorized target path must not auto-register any tuple" + ); + }), + }); +} + +/// `target_address` is part of the signed bytes: tampering with it after signing +/// invalidates the signature. +#[test] +fn test_v1_target_tamper_breaks_signature() { + let MultisigEnv { + keys, + multisig, + credential_id: multisig_credential_id, + user: multisig_user, + } = make_multisig_env(); + let alice = TestUser::::generate_with_default_balance(); + let alice_address = alice.address(); + + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![multisig_user, alice]); + let mut genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + // Authorize the multisig for both alice and a second, unrelated address so + // the "tampered-to" address is also in the account_owners map. This isolates + // the failure to signature verification rather than authorization. + let tampered_address = TestUser::::generate_with_default_balance().address(); + genesis.accounts.accounts.push(AccountData { + credential_id: multisig_credential_id, + address: alice_address, + }); + genesis.accounts.accounts.push(AccountData { + credential_id: multisig_credential_id, + address: tampered_address, + }); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + let mut tx = make_v1_tx(&multisig, inner_credential, Some(alice_address)); + sign_v1(&mut tx, &keys[0]); + sign_v1(&mut tx, &keys[1]); + // Mutate after signing. + tx.target_address = Some(tampered_address); + + runner.execute_transaction(TransactionTestCase { + input: submit_v1(tx), + assert: Box::new(move |result, _state| match result.tx_receipt { + TxEffect::Skipped(SkippedTxContents { error, .. }) => { + let msg = error.to_string(); + assert!( + msg.contains("Verification equation was not satisfied"), + "expected signature failure after target_address tamper, got: {msg}" + ); + } + other => panic!("expected skipped tx, got {other:?}"), + }), + }); +} diff --git a/crates/module-system/module-implementations/sov-evm/src/authenticate.rs b/crates/module-system/module-implementations/sov-evm/src/authenticate.rs index 2caceecc71..4e55b205ee 100644 --- a/crates/module-system/module-implementations/sov-evm/src/authenticate.rs +++ b/crates/module-system/module-implementations/sov-evm/src/authenticate.rs @@ -220,6 +220,7 @@ where credential_id, credentials, default_address: S::Address::from_vm_address(ethereum_address), + address: None, } } diff --git a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/hooks_tests.rs b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/hooks_tests.rs index 06c5e3e92f..0e67bdaedd 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/hooks_tests.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/hooks_tests.rs @@ -161,7 +161,7 @@ fn send_tx_bad_generation_duplicate_with_malleated_v1_envelope() { UniquenessData::Generation(0), default_test_tx_details::(), ) - .to_multisig_tx(multisig); + .to_multisig_tx(multisig, None); original_tx .sign(&multisig_keys[0], &RT::CHAIN_HASH) .unwrap(); diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index c0e1274958..5abcb16f72 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -60,6 +60,33 @@ impl<'a, S: Spec, T> StandardProvenRollupCapabilities<'a, S, T> { rewarded_token_holder } + + fn resolve_sender( + &mut self, + auth_data: &AuthorizationData, + state: &mut impl StateAccessor, + ) -> anyhow::Result { + match auth_data.address { + Some(requested) => { + if !self + .accounts + .is_authorized(&requested, &auth_data.credential_id, state)? + { + anyhow::bail!( + "credential {} not authorized for target address {}", + auth_data.credential_id, + requested, + ); + } + Ok(requested) + } + None => Ok(self.accounts.resolve_sender_address( + &auth_data.default_address, + &auth_data.credential_id, + state, + )?), + } + } } trait HasGasPayer { diff --git a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs index a5eef7f565..61bf0eef48 100644 --- a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs +++ b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs @@ -102,4 +102,11 @@ pub struct AuthorizationData { /// The default address. pub default_address: S::Address, + + /// Signer-declared target address for execution. `None` routes the transaction + /// through the default resolver (default address + legacy fallback). `Some(X)` + /// requires `(X, credential_id) ∈ account_owners`; the transaction is skipped + /// otherwise. Populated from V1 transactions' signed `target_address` field; + /// always `None` for V0 and non-sov-tx authenticators. + pub address: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs index c34734bbac..47fcebcb47 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs @@ -71,6 +71,7 @@ impl Version0 { credential_id, credentials: Credentials::new(pub_key), default_address: credential_id.into(), + address: None, }) } } diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs index 2efba818cb..701e83e756 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs @@ -87,6 +87,11 @@ pub struct Version1, + /// Signer-declared target address for execution. `None` routes through + /// `resolve_sender_address` (default-address / legacy fallback). `Some(X)` requires + /// the multisig credential to be authorized for `X` in `account_owners`; the tx + /// is skipped otherwise. This field is part of the signed bytes. + pub target_address: Option, } impl Version1 { @@ -111,6 +116,7 @@ impl Version1 { uniqueness: self.uniqueness, details: self.details.clone(), credential_address: self.credential_address(), + target_address: self.target_address, }) } } @@ -188,6 +194,7 @@ impl Version1 { credential_id, credentials: Credentials::new(multisig), default_address: credential_id.into(), + address: self.target_address, }) } } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs index b794137e14..e0909ad942 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs @@ -114,9 +114,14 @@ impl UnsignedTransactionV0 { } /// Creates a new `V1` transaction from this unsigned transaction. + /// + /// `target_address = None` routes the tx through `resolve_sender_address` + /// (default address / legacy fallback). `target_address = Some(X)` requires + /// `(X, credential_id) ∈ account_owners`, else the tx is skipped at authorization. pub fn to_multisig_tx( self, multisig: Multisig<::PublicKey>, + target_address: Option, ) -> Version1 { Version1 { signatures: SafeVec::new(), @@ -128,6 +133,7 @@ impl UnsignedTransactionV0 { runtime_call: self.runtime_call, uniqueness: self.uniqueness, details: self.details, + target_address, } } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs index 362affe05a..c1bc851427 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs @@ -31,6 +31,10 @@ pub struct UnsignedTransactionV1 { /// and prevent credential malleability from reusing signed bytes in a different /// multisig envelope. pub credential_address: S::Address, + /// Signer-declared target address. `None` routes through `resolve_sender_address` + /// (default-address resolver). `Some(X)` requires the multisig credential to be + /// authorized for `X` via `account_owners`; otherwise the tx is skipped. + pub target_address: Option, } impl Clone for UnsignedTransactionV1 { @@ -40,6 +44,7 @@ impl Clone for UnsignedTransactionV1 { uniqueness: self.uniqueness, details: self.details.clone(), credential_address: self.credential_address, + target_address: self.target_address, } } } @@ -49,6 +54,7 @@ impl PartialEq for UnsignedTransactionV1 && self.uniqueness == other.uniqueness && self.details == other.details && self.credential_address == other.credential_address + && self.target_address == other.target_address } } impl Eq for UnsignedTransactionV1 {} diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs index 6cd39bed10..304559427d 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs @@ -154,6 +154,7 @@ fn build_auth_data( credential_id, credentials: Credentials::new(pub_key.clone()), default_address: credential_id.into(), + address: None, }) } UnpackedSolanaMessage::V1 { @@ -182,6 +183,7 @@ fn build_auth_data( credential_id, credentials: Credentials::new(multisig), default_address: credential_id.into(), + address: None, }) } } diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index 4df63589ba..c3e5061768 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -107,6 +107,7 @@ where uniqueness: self.uniqueness, details: self.details, credential_address: self.multisig_id, + target_address: None, }) } diff --git a/examples/demo-rollup/tests/resync/data/mock_da.sqlite b/examples/demo-rollup/tests/resync/data/mock_da.sqlite index bf8d66eb1aa112c89aa8f3b754ca799e2954e13b..ab0373e36540ab9ba725c72dd31e8530ff84497d 100644 GIT binary patch literal 49152 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 literal 49152 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 UQn9CX4?Haw)Kt(o^8WI_0F-b-P5=M^ From 3d2d08af94c44b69bd2013864f73ef9b8ea2b357 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 22 Apr 2026 11:31:48 +0200 Subject: [PATCH 02/34] Updating typescript to match new transaction --- crates/full-node/sov-api-spec/openapi-v3.yaml | 4 + .../sov-rollup-apis/src/endpoints/simulate.rs | 24 +- .../tests/integration/rest_api.rs | 93 ++++++- .../src/authentication/mod.rs | 24 +- .../src/authentication/payload.rs | 10 +- .../tests/integration/main.rs | 229 +++++++++++++++++- .../packages/multisig/src/index.test.ts | 27 +++ typescript/packages/multisig/src/index.ts | 3 +- typescript/packages/types/src/index.ts | 2 + .../src/rollup/solana-signable-rollup.test.ts | 224 +++++++++++++++++ .../web3/src/rollup/solana-signable-rollup.ts | 44 ++++ .../web3/src/rollup/standard-rollup.test.ts | 39 +++ .../web3/src/rollup/standard-rollup.ts | 17 +- 13 files changed, 712 insertions(+), 28 deletions(-) diff --git a/crates/full-node/sov-api-spec/openapi-v3.yaml b/crates/full-node/sov-api-spec/openapi-v3.yaml index d0ab3ba33a..b90b9651f1 100644 --- a/crates/full-node/sov-api-spec/openapi-v3.yaml +++ b/crates/full-node/sov-api-spec/openapi-v3.yaml @@ -1218,6 +1218,10 @@ components: type: object description: Optional uniqueness data for the transaction additionalProperties: true + target_address: + type: string + nullable: true + description: Optional target address for execution; null or omission uses default routing required: - sender - call diff --git a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs index 9d15da4870..df0523ff3e 100644 --- a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs +++ b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs @@ -304,8 +304,18 @@ impl> SovereignSimulate { &self, params: &SimulateParameters, state: &mut StateCheckpoint, - ) -> AuthorizationData { - let credential_id = CredentialId::from_str(¶ms.sender).unwrap(); + ) -> Result, SimulateError> { + let credential_id = CredentialId::from_str(¶ms.sender).map_err(|_| { + SimulateError::InvalidInput("failed to parse sender credential id".to_owned()) + })?; + let address = params + .target_address + .as_deref() + .map(S::Address::from_str) + .transpose() + .map_err(|_| { + SimulateError::InvalidInput("failed to parse target address".to_owned()) + })?; let uniqueness = params.uniqueness.unwrap_or_else(|| { let generation = Uniqueness::::default() .next_generation(&credential_id, state) @@ -313,15 +323,15 @@ impl> SovereignSimulate { UniquenessData::Generation(generation) }); - AuthorizationData { + Ok(AuthorizationData { tx_hash: NULL_TX_HASH, non_malleable_hash: NULL_TX_HASH, uniqueness, credential_id, default_address: credential_id.into(), credentials: Credentials::new(credential_id), - address: None, - } + address, + }) } fn outcome(&self, result: ApplyTxResult) -> SimulateOutcome { @@ -403,6 +413,8 @@ pub struct SimulateParameters { /// Optional uniqueness data for the transaction. /// If not provided a valid uniqueness will be used. pub uniqueness: Option, + /// Optional target address for execution. If not provided, default routing is used. + pub target_address: Option, } impl> SimulateEndpoint for SovereignSimulate { @@ -428,7 +440,7 @@ impl> SimulateEndpoint for SovereignSimulate { .chain_state() .base_fee_per_gas(&mut accessor) .ok_or(SimulateError::GasPriceRetrieval)?; - let auth_data = state.authorization_data(¶ms, &mut accessor); + let auth_data = state.authorization_data(¶ms, &mut accessor)?; let sequencer = state.sequencer(params.sequencer.unwrap_or_default())?; let auth_tx_data = AuthenticatedTransactionData(state.tx_details(params.tx_details.unwrap_or_default())?); diff --git a/crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs b/crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs index 6adfefc8c5..7dffa2bc48 100644 --- a/crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs +++ b/crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs @@ -1,10 +1,15 @@ use sov_api_spec::types; use sov_bank::{config_gas_token_id, Bank}; use sov_modules_api::prelude::tokio::{self}; -use sov_modules_api::{Amount, Gas, GasArray, GasSpec, Spec}; +use sov_modules_api::sov_universal_wallet::schema::RollupRoots; +use sov_modules_api::{ + get_runtime_schema, Amount, CredentialId, Gas, GasArray, GasSpec, Runtime, Spec, +}; use sov_rest_utils::json_obj; use sov_rollup_interface::node::SyncStatus; -use sov_test_utils::{AsUser, TestUser, TransactionTestCase}; +use sov_test_utils::{ + default_test_tx_details, AsUser, TestUser, TransactionTestCase, TransactionType, +}; use crate::{TestData, RT, S}; @@ -167,6 +172,90 @@ async fn test_simulation_success() { ); } +#[tokio::test(flavor = "multi_thread")] +async fn test_simulation_success_with_target_address() { + let mut data = TestData::setup().await; + let delegated_credential: CredentialId = [9u8; 32].into(); + let registration_call = json_obj!({ + "accounts": { + "insert_credential_id": delegated_credential, + }, + }); + let schema = get_runtime_schema::().unwrap(); + let registration_call_bytes = schema + .json_to_borsh( + schema + .rollup_expected_index(RollupRoots::RuntimeCall) + .unwrap(), + &serde_json::to_string(®istration_call).unwrap(), + ) + .unwrap(); + + data.runner.execute_transaction(TransactionTestCase { + input: TransactionType::Plain { + message: RT::decode_call(®istration_call_bytes).unwrap(), + key: data.user.private_key().clone(), + details: default_test_tx_details::(), + }, + assert: Box::new(move |result, _state| { + assert!( + result.tx_receipt.is_successful(), + "The credential registration should have succeeded" + ); + }), + }); + data.send_storage(); + + let target_address = data.user.address(); + let receiver = TestUser::::generate_with_default_balance().address(); + let call = sov_bank::CallMessage::::Transfer { + to: receiver, + coins: sov_bank::Coins { + amount: Amount::new(1000), + token_id: config_gas_token_id(), + }, + }; + let params = json_obj!({ + "sender": delegated_credential.to_string(), + "target_address": target_address.to_string(), + "call": { + "bank": call, + }, + }); + let client = reqwest::Client::new(); + + let response = client + .post(format!("http://{}/rollup/simulate", data.axum_addr)) + .json(¶ms) + .send() + .await + .unwrap(); + let actual = response.json::().await.unwrap(); + + assert_eq!(actual["outcome"], "success"); + assert_eq!( + actual["events"][0], + serde_json::json!({ + "key": "Bank/TokenTransferred", + "module": "Bank", + "value": { + "token_transferred": { + "from": { + "user": target_address + }, + "to": { + "user": receiver + }, + "coins": { + "amount": "1000", + "token_id": "token_1nyl0e0yweragfsatygt24zmd8jrr2vqtvdfptzjhxkguz2xxx3vs0y07u7" + } + } + } + }) + ); +} + #[tokio::test(flavor = "multi_thread")] async fn test_simulation_fail() { let data = TestData::setup().await; diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs index 304559427d..362b3a597d 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs @@ -122,9 +122,15 @@ fn verify_signatures( } /// Builds authorization data for either single-sig or multisig transactions. +/// +/// `target_address` is the signer-declared target extracted from the deserialized V1 JSON payload. +/// It must always be `None` for V0; for V1, it is forwarded from +/// `SolanaOffchainUnsignedTransactionV1::target_address` so that the authorization layer can route +/// execution to a pre-authorized account instead of the multisig's default address. fn build_auth_data( unpacked: &UnpackedSolanaMessage, uniqueness: UniquenessData, + target_address: Option, raw_tx_hash: TxHash, meter: &mut impl GasMeter, ) -> Result, AuthenticationError> { @@ -147,6 +153,10 @@ fn build_auth_data( sov_modules_api::metered_credential::(pub_key, meter) .map_err(|e| AuthenticationError::OutOfGas(e.to_string()))?; + debug_assert!( + target_address.is_none(), + "V0 Solana transactions cannot carry a target_address" + ); Ok(AuthorizationData { uniqueness, tx_hash: raw_tx_hash, @@ -183,7 +193,7 @@ fn build_auth_data( credential_id, credentials: Credentials::new(multisig), default_address: credential_id.into(), - address: None, + address: target_address, }) } } @@ -280,11 +290,11 @@ where raw_tx_hash, ) }; - let (provided_chain_name, unsigned_tx) = match &unpacked_message { + let (provided_chain_name, target_address, unsigned_tx) = match &unpacked_message { UnpackedSolanaMessage::V0 { .. } => { let tx = SolanaOffchainUnsignedTransactionV0::::unmetered_deserialize(json_slice) .map_err(deser_err)?; - (tx.chain_name.to_string(), tx.into_unsigned_tx()) + (tx.chain_name.to_string(), None, tx.into_unsigned_tx()) } UnpackedSolanaMessage::V1 { signatures, @@ -301,7 +311,12 @@ where *min_signers, raw_tx_hash, )?; - (tx.chain_name.to_string(), tx.into_unsigned_tx()) + let target_address = tx.target_address; + ( + tx.chain_name.to_string(), + target_address, + tx.into_unsigned_tx(), + ) } }; @@ -335,6 +350,7 @@ where let authorization_data = build_auth_data::( &unpacked_message, unsigned_tx.uniqueness(), + target_address, raw_tx_hash, state, )?; diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index c3e5061768..4bd2df658c 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -78,6 +78,14 @@ pub struct SolanaOffchainUnsignedTransactionV1 /// This is the "multisig address" except if the credential is mapped to another address in /// `sov-accounts`. pub multisig_id: S::Address, + /// Signer-declared target address. `None` routes through `resolve_sender_address` + /// (default-address resolver). `Some(X)` requires the multisig credential to be authorized + /// for `X` via `account_owners`; otherwise the tx is skipped. + /// + /// Omitted from the serialized JSON when `None`, so pre-change signed messages (which have no + /// `target_address` field) remain byte-identical and verify against the same signature. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target_address: Option, /// Message format version. Must be `1` for this struct. #[serde(deserialize_with = "deserialize_version_1")] pub version: u8, @@ -107,7 +115,7 @@ where uniqueness: self.uniqueness, details: self.details, credential_address: self.multisig_id, - target_address: None, + target_address: self.target_address, }) } diff --git a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs index 4e27cd1fd3..65c1563839 100644 --- a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs +++ b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs @@ -93,6 +93,27 @@ type TestHasher = <::CryptoSpec as CryptoSpec>::Hasher; async fn create_test_rollup() -> anyhow::Result<( TestRollup>, TestUser, +)> { + create_test_rollup_with_extra_account_owners(|_| vec![]).await +} + +/// Variant of [`create_test_rollup`] that seeds additional `(credential_id, address)` pairs +/// into `sov-accounts` at genesis. Callers use this to authorize a multisig credential for a +/// specific target address so V1 transactions carrying `target_address = Some(X)` can resolve +/// their sender to `X` instead of the multisig's default address. +/// +/// `extra_account_owners_for` is a closure invoked with the generated admin user so the caller +/// can bind extra `AccountData` entries to admin's address without needing to materialize admin +/// before calling this helper. +async fn create_test_rollup_with_extra_account_owners( + extra_account_owners_for: impl FnOnce( + &TestUser, + ) -> Vec< + sov_test_utils::runtime::sov_accounts::AccountData<::Address>, + >, +) -> anyhow::Result<( + TestRollup>, + TestUser, )> { // Create genesis config let genesis_config = HighLevelOptimisticGenesisConfig::::generate() @@ -100,7 +121,7 @@ async fn create_test_rollup() -> anyhow::Result<( let sequencer = genesis_config.initial_sequencer.clone(); let admin = genesis_config.additional_accounts()[0].clone(); - let rt_genesis_config = >::GenesisConfig::from_minimal_config( + let mut rt_genesis_config = >::GenesisConfig::from_minimal_config( genesis_config.clone().into(), ValueSetterConfig { admin: admin.address(), @@ -126,6 +147,10 @@ async fn create_test_rollup() -> anyhow::Result<( .unwrap(), }, ); + rt_genesis_config + .accounts + .accounts + .extend(extra_account_owners_for(&admin)); let genesis_params = GenesisParams { runtime: rt_genesis_config, @@ -442,6 +467,7 @@ fn create_multisig_transfer_tx_json( amount: Amount, recipient: &str, multisig_id: ::Address, + target_address: Option<::Address>, ) -> String { let msg: TestRuntimeCall = TestRuntimeCall::Bank(BankCallMessage::Transfer { to: ::Address::from_str(recipient).unwrap(), @@ -464,6 +490,7 @@ fn create_multisig_transfer_tx_json( details: unsigned_tx.details, chain_name: config_value!("CHAIN_NAME").to_string().try_into().unwrap(), multisig_id, + target_address, version: 1, }; serde_json::to_string(&solana_unsigned_tx).unwrap() @@ -528,7 +555,7 @@ async fn test_submit_multisig_simple_message_transaction() { // Build a transfer from the multisig to the recipient, using V1 format which commits // to the credential_id in the signed message. let transfer_json = - create_multisig_transfer_tx_json(Amount(7_000), RECIPIENT_ADDRESS, multisig_address); + create_multisig_transfer_tx_json(Amount(7_000), RECIPIENT_ADDRESS, multisig_address, None); let json_bytes = transfer_json.as_bytes(); let wire_bytes = create_multisig_simple_wire_bytes(&transfer_json); @@ -624,7 +651,7 @@ async fn test_submit_multisig_insufficient_signatures() { // Only 1 signer for a 2-of-3 multisig — should fail let transfer_json = - create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address); + create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address, None); let json_bytes = transfer_json.as_bytes(); let wire_bytes = create_multisig_simple_wire_bytes(&transfer_json); let sig1 = key1.sign(json_bytes); @@ -689,7 +716,7 @@ async fn test_submit_multisig_invalid_signature() { // Corrupt the second signature let transfer_json = - create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address); + create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address, None); let json_bytes = transfer_json.as_bytes(); let wire_bytes = create_multisig_simple_wire_bytes(&transfer_json); let sig1 = key1.sign(json_bytes); @@ -776,7 +803,7 @@ async fn test_submit_multisig_spec_compliant_message_transaction() { // Build a transfer from the multisig using spec-compliant format. let transfer_json = - create_multisig_transfer_tx_json(Amount(7_000), RECIPIENT_ADDRESS, multisig_address); + create_multisig_transfer_tx_json(Amount(7_000), RECIPIENT_ADDRESS, multisig_address, None); let json_bytes = transfer_json.as_bytes(); // Build the multisig preamble with a canonical signer ordering so the emitted payload @@ -884,7 +911,7 @@ async fn test_submit_spec_compliant_multisig_insufficient_signatures() { // Only 1 signer when 2 are required let transfer_json = - create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address); + create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address, None); let json_bytes = transfer_json.as_bytes(); let preamble = make_multisig_preamble_for_message( @@ -952,7 +979,7 @@ async fn test_submit_spec_compliant_multisig_invalid_signature() { } let transfer_json = - create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address); + create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address, None); let json_bytes = transfer_json.as_bytes(); let preamble = make_multisig_preamble_for_message( @@ -1045,7 +1072,7 @@ async fn test_submit_ledger_signed_multisig_transaction() { // Build the V1 multisig transfer transaction let transfer_json_tx = - create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address); + create_multisig_transfer_tx_json(Amount(5_000), RECIPIENT_ADDRESS, multisig_address, None); let encoded_tx = transfer_json_tx.as_bytes(); // Build the multisig preamble with all 3 pubkeys (Ledger at index 0) @@ -1109,3 +1136,189 @@ async fn test_submit_ledger_signed_multisig_transaction() { "Expected recipient to have received 5,000 tokens" ); } + +/// End-to-end plumbing test: a V1 transaction carrying `target_address = Some(admin)` — where +/// genesis authorizes `(admin.address(), multisig_credential_id)` in `account_owners` — routes +/// execution as `admin`, not as the multisig's default address. Proves that `target_address` +/// flows from the signed JSON through `authenticate`, `build_auth_data`, `AuthorizationData`, +/// and into `resolve_sender`. +#[tokio::test(flavor = "multi_thread")] +async fn test_submit_multisig_with_authorized_target_address() { + // Build a 2-of-3 multisig (its default address is never funded). + let key1 = Ed25519PrivateKey::generate(); + let key2 = Ed25519PrivateKey::generate(); + let key3 = Ed25519PrivateKey::generate(); + let pub1 = key1.pub_key(); + let pub2 = key2.pub_key(); + let pub3 = key3.pub_key(); + let min_signers: u8 = 2; + let multisig = + sov_modules_api::Multisig::new(min_signers, vec![pub1.clone(), pub2.clone(), pub3.clone()]); + let multisig_credential_id = multisig.credential_id::(); + let multisig_default_address: ::Address = multisig_credential_id.into(); + + // Bring up a rollup where admin's address is authorized for the multisig's credential. + let (test_rollup, admin) = create_test_rollup_with_extra_account_owners(|admin| { + vec![sov_test_utils::runtime::sov_accounts::AccountData { + credential_id: multisig_credential_id, + address: admin.address(), + }] + }) + .await + .expect("Failed to create rollup"); + + let admin_address_str = admin.address().to_string(); + let admin_balance_before = query_balance(&test_rollup.client, &admin_address_str).await; + + // Build a V1 multisig transfer targeting admin's address. + let transfer_json = create_multisig_transfer_tx_json( + Amount(7_000), + RECIPIENT_ADDRESS, + multisig_default_address, + Some(admin.address()), + ); + let json_bytes = transfer_json.as_bytes(); + let wire_bytes = create_multisig_simple_wire_bytes(&transfer_json); + + // Two of three signers sign. + let sig1 = key1.sign(json_bytes); + let sig2 = key2.sign(json_bytes); + let multisig_msg = SolanaOffchainSimpleMultisigMessage:: { + wire_bytes, + chain_hash: RT::CHAIN_HASH, + signatures: vec![ + PubKeyAndSignature { + signature: sig1, + pub_key: pub1, + }, + PubKeyAndSignature { + signature: sig2, + pub_key: pub2, + }, + ] + .try_into() + .unwrap(), + unused_pub_keys: vec![pub3].try_into().unwrap(), + min_signers, + }; + + let raw_tx_bytes = borsh::to_vec(&multisig_msg).unwrap(); + let response = submit_tx(test_rollup.api_client(), raw_tx_bytes).await; + assert!( + response.status().is_success(), + "Expected multisig-with-target_address transaction to succeed. Response: {response:?}" + ); + + // Recipient got the transfer. + let recipient_balance = query_balance(&test_rollup.client, RECIPIENT_ADDRESS).await; + assert_eq!( + recipient_balance, + Some(Amount::new(7_000)), + "Expected recipient to have received 7,000 tokens via target-routed multisig tx" + ); + + // Admin's balance decreased (fees + transferred amount). We only check the ordering + // invariant: post < pre. A strict equality is fragile because paymaster gas is deducted. + let admin_balance_after = query_balance(&test_rollup.client, &admin_address_str).await; + assert!( + admin_balance_after < admin_balance_before, + "Expected admin balance to decrease after target-routed tx; before={admin_balance_before:?}, after={admin_balance_after:?}" + ); + + // The multisig's default address was never funded and should still have no balance — + // proving that the tx did NOT route through the default resolver. + let multisig_default_balance = + query_balance(&test_rollup.client, &multisig_default_address.to_string()).await; + assert!( + multisig_default_balance.is_none() || multisig_default_balance == Some(Amount::new(0)), + "Multisig default address must not have been used as sender; got balance {multisig_default_balance:?}" + ); +} + +/// Helper: builds a `SolanaOffchainUnsignedTransactionV1` with an arbitrary recipient for the +/// serde round-trip / byte-equivalence tests below. +fn build_v1_payload( + recipient: &str, + multisig_id: ::Address, + target_address: Option<::Address>, +) -> SolanaOffchainUnsignedTransactionV1 { + let call: TestRuntimeCall = TestRuntimeCall::Bank(BankCallMessage::Transfer { + to: ::Address::from_str(recipient).unwrap(), + coins: Coins { + amount: Amount(1_000), + token_id: config_value!("GAS_TOKEN_ID"), + }, + }); + let unsigned_tx = UnsignedTransactionV0::::new( + call, + config_value!("CHAIN_ID"), + TEST_DEFAULT_MAX_PRIORITY_FEE, + TEST_DEFAULT_MAX_FEE, + UniquenessData::Nonce(0), + Some(TEST_DEFAULT_GAS_LIMIT.into()), + ); + SolanaOffchainUnsignedTransactionV1:: { + runtime_call: unsigned_tx.runtime_call, + uniqueness: unsigned_tx.uniqueness, + details: unsigned_tx.details, + chain_name: config_value!("CHAIN_NAME").to_string().try_into().unwrap(), + multisig_id, + target_address, + version: 1, + } +} + +/// `target_address: None` is omitted from the serialized JSON, so pre-change signed messages +/// remain byte-identical and existing signatures verify unchanged. +#[test] +fn test_v1_payload_omits_target_address_when_none() { + let multisig_address: ::Address = [0xABu8; 32].into(); + let payload = build_v1_payload(RECIPIENT_ADDRESS, multisig_address, None); + + let json = serde_json::to_string(&payload).expect("serialize"); + assert!( + !json.contains("target_address"), + "target_address must not appear in JSON when it is None; got: {json}" + ); +} + +/// Pre-change JSON (no `target_address` key) deserializes into a payload with `target_address: +/// None`. Pins the `#[serde(default)]` promise: future refactors cannot silently break deployed +/// Solana signers. +#[test] +fn test_v1_payload_without_target_address_field_deserializes_as_none() { + let multisig_address: ::Address = [0xABu8; 32].into(); + let expected = build_v1_payload(RECIPIENT_ADDRESS, multisig_address, None); + let canonical_json = serde_json::to_string(&expected).expect("serialize baseline"); + assert!( + !canonical_json.contains("target_address"), + "guard: the canonical None-payload already omits target_address" + ); + + let parsed: SolanaOffchainUnsignedTransactionV1 = + serde_json::from_str(&canonical_json).expect("deserialize"); + assert_eq!( + parsed.target_address, None, + "missing target_address field must default to None" + ); +} + +/// A `target_address: Some(X)` round-trips through serialize/deserialize unchanged. +#[test] +fn test_v1_payload_with_target_address_round_trips() { + let multisig_address: ::Address = [0xABu8; 32].into(); + let target: ::Address = + ::Address::from_str(RECIPIENT_ADDRESS).expect("parse recipient"); + let original = build_v1_payload(RECIPIENT_ADDRESS, multisig_address, Some(target)); + + let json = serde_json::to_string(&original).expect("serialize"); + assert!( + json.contains("target_address"), + "target_address must appear in JSON when it is Some; got: {json}" + ); + + let parsed: SolanaOffchainUnsignedTransactionV1 = + serde_json::from_str(&json).expect("deserialize"); + assert_eq!(parsed.target_address, Some(target)); + assert_eq!(parsed.multisig_id, multisig_address); +} diff --git a/typescript/packages/multisig/src/index.test.ts b/typescript/packages/multisig/src/index.test.ts index 0c645ecda8..73bd3a4373 100644 --- a/typescript/packages/multisig/src/index.test.ts +++ b/typescript/packages/multisig/src/index.test.ts @@ -67,6 +67,7 @@ describe("MultisigTransaction", () => { unused_pub_keys: ["pubkey2"], signatures: [{ pub_key: "pubkey1", signature: "sig1" }], min_signers: 1, + target_address: null, ...unsignedTx, }, }; @@ -245,6 +246,32 @@ describe("MultisigTransaction", () => { }); }); + describe("asTransaction", () => { + it("should include a null target address by default", () => { + const multisig = MultisigTransaction.empty(createUnsignedTx(), 1, [ + "pubkey1", + ]); + + const result = multisig.asTransaction() as TransactionV1; + + expect(result.V1.target_address).toBeNull(); + }); + + it("should include the provided target address", () => { + const multisig = MultisigTransaction.empty(createUnsignedTx(), 1, [ + "pubkey1", + ]); + + const result = multisig.asTransaction( + "sov1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq5k2jj4", + ) as TransactionV1; + + expect(result.V1.target_address).toBe( + "sov1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq5k2jj4", + ); + }); + }); + describe("getMultisigAddress", () => { it("should match Rust implementation output for known test vector", () => { const multisig = MultisigTransaction.empty(createUnsignedTx(), 2, [ diff --git a/typescript/packages/multisig/src/index.ts b/typescript/packages/multisig/src/index.ts index b333e94ab8..ab21c4899b 100644 --- a/typescript/packages/multisig/src/index.ts +++ b/typescript/packages/multisig/src/index.ts @@ -258,7 +258,7 @@ export class MultisigTransaction { * Converts the multisig to a V1 transaction that can be submitted to the network. * @returns A V1 transaction containing all collected signatures and unused public keys */ - asTransaction(): Transaction { + asTransaction(targetAddress: string | null = null): Transaction { return { V1: { runtime_call: this.unsignedTx.runtime_call, @@ -267,6 +267,7 @@ export class MultisigTransaction { unused_pub_keys: Array.from(this.unusedPubKeys), signatures: this.signatures, min_signers: this.minSigners, + target_address: targetAddress, }, }; } diff --git a/typescript/packages/types/src/index.ts b/typescript/packages/types/src/index.ts index 8265d1c96e..8f1b97d72f 100644 --- a/typescript/packages/types/src/index.ts +++ b/typescript/packages/types/src/index.ts @@ -73,6 +73,8 @@ export type TransactionV1 = { signatures: SignatureAndPubKey[]; /** Minimum number of signatures required for transaction validity */ min_signers: number; + /** Optional target address for execution; null uses default routing */ + target_address: string | null; } & UnsignedTransaction; }; diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts index 49295a0848..8abdf0043a 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts @@ -4,6 +4,7 @@ import { JsSerializer } from "@sovereign-sdk/serializers"; import { Ed25519Signer } from "@sovereign-sdk/signers"; import { LedgerSolanaSigner } from "@sovereign-sdk/signers/ledger-solana"; import { bytesToHex, hexToBytes } from "@sovereign-sdk/utils"; +import bs58 from "bs58"; import { describe, expect, it, vi } from "vitest"; import demoRollupSchema from "../../../__fixtures__/demo-rollup-schema.json"; import { @@ -785,4 +786,227 @@ describe("SolanaSignableRollup", () => { expect(decodedBody[0]).not.toBe(0xff); }); }); + + describe("V1 target_address", () => { + // Shared fixture: builds a rollup + multisig context the same way the byte-compatibility + // tests above do, then exposes the pieces each test needs. + async function setupMultisigContext(): Promise<{ + rollup: SolanaSignableRollup; + capturedPayloadRef: { current: any }; + multisigAddress: Uint8Array; + multisigPubkeys: Uint8Array[]; + pubHexes: { pub1: string; pub2: string; pub3: string }; + signers: { + signer1: Ed25519Signer; + signer2: Ed25519Signer; + signer3: Ed25519Signer; + }; + unsignedTx: any; + }> { + const key1PrivHex = + "09817894bf1e858df8d9bb3b931646c558ec4cacb9e4f9c05e91d0d788ec1142"; + const key2PrivHex = + "71d81253990513758c7014bec174b4405988c133fc616d6f3d633170858f3dc9"; + const key3PrivHex = + "90f1cca556a78435468bb17f116a923c8eb5c6074619a9bf39f28eb673a22a50"; + + const mockClient = createMockClient({ + chainId: 4321, + chainName: "TestChain", + chainHash: + "0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + }); + const capturedPayloadRef: { current: any } = { current: undefined }; + mockClient.post = vi + .fn() + .mockImplementation((_path: string, options: any) => { + capturedPayloadRef.current = options; + return Promise.resolve({ id: "test-tx-hash" }); + }); + + const rollup = await createSolanaSignableRollup({ + client: mockClient, + getSerializer: (schema: any) => ({ schema }) as any, + }); + + const signer1 = new Ed25519Signer(key1PrivHex); + const signer2 = new Ed25519Signer(key2PrivHex); + const signer3 = new Ed25519Signer(key3PrivHex); + const pub1 = await signer1.publicKey(); + const pub2 = await signer2.publicKey(); + const pub3 = await signer3.publicKey(); + const pub1Hex = bytesToHex(pub1); + const pub2Hex = bytesToHex(pub2); + const pub3Hex = bytesToHex(pub3); + const minSigners = 2; + const multisigPubkeys = [pub1, pub2, pub3]; + + const sortedPubKeys = [pub1Hex, pub2Hex, pub3Hex].sort(); + const pubKeyBytes = sortedPubKeys.map((pk) => + Array.from(hexToBytes(pk)), + ); + const borshData = new Uint8Array(1 + 4 + 3 * 32); + const dv = new DataView(borshData.buffer); + borshData[0] = minSigners; + dv.setUint32(1, 3, true); + pubKeyBytes.forEach((pk, i) => borshData.set(pk, 5 + i * 32)); + const multisigAddress = sha256(borshData); + + const unsignedTx = { + runtime_call: { + bank: { + transfer: { + to: "4zdwHNaEa5npHtRtaZ3RL1m6rptuQZ6RBLHG6cAyVHjL", + coins: { + amount: "7000", + token_id: + "token_1nyl0e0yweragfsatygt24zmd8jrr2vqtvdfptzjhxkguz2xxx3vs0y07u7", + }, + }, + }, + }, + uniqueness: { nonce: 0 }, + details: { + max_priority_fee_bips: 0, + max_fee: "100000000000", + gas_limit: [1000000000, 1000000000], + chain_id: 4321, + }, + }; + + return { + rollup, + capturedPayloadRef, + multisigAddress, + multisigPubkeys, + pubHexes: { pub1: pub1Hex, pub2: pub2Hex, pub3: pub3Hex }, + signers: { signer1, signer2, signer3 }, + unsignedTx, + }; + } + + /** + * Extracts the JSON payload a signer signed. The `solanaSimple` flow has each signer sign + * the JSON bytes directly (no preamble), so the signer's `sign()` call receives exactly + * what was JSON-serialized. + */ + function captureSignerInput(signer: Ed25519Signer): { + mock: Ed25519Signer; + captured: { bytes?: Uint8Array }; + } { + const captured: { bytes?: Uint8Array } = {}; + const originalSign = signer.sign.bind(signer); + const mock = { + publicKey: () => signer.publicKey(), + sign: async (data: Uint8Array) => { + captured.bytes = new Uint8Array(data); + return originalSign(data); + }, + } as unknown as Ed25519Signer; + return { mock, captured }; + } + + it("omits target_address from the signed JSON when no targetAddress is provided (backward compat)", async () => { + const { rollup, multisigAddress, multisigPubkeys, signers, unsignedTx } = + await setupMultisigContext(); + + const { mock, captured } = captureSignerInput(signers.signer1); + await rollup.signTransactionForMultisig(unsignedTx, { + signer: mock, + authenticator: "solanaSimple", + multisigAddress, + multisigPubkeys, + }); + + expect(captured.bytes).toBeDefined(); + const signedJson = new TextDecoder().decode(captured.bytes!); + expect(signedJson).not.toContain("target_address"); + const parsed = JSON.parse(signedJson); + expect(parsed).not.toHaveProperty("target_address"); + }); + + it("includes target_address in the signed JSON when targetAddress is provided (solanaSimple)", async () => { + const { rollup, multisigAddress, multisigPubkeys, signers, unsignedTx } = + await setupMultisigContext(); + + const targetAddress = new Uint8Array(32); + targetAddress.fill(0x42); + + const { mock, captured } = captureSignerInput(signers.signer1); + await rollup.signTransactionForMultisig(unsignedTx, { + signer: mock, + authenticator: "solanaSimple", + multisigAddress, + multisigPubkeys, + targetAddress, + }); + + expect(captured.bytes).toBeDefined(); + const signedJson = new TextDecoder().decode(captured.bytes!); + const parsed = JSON.parse(signedJson); + expect(parsed.target_address).toBe(bs58.encode(targetAddress)); + }); + + it("forwards tx.target_address into the submitted JSON via submitMultisigTransaction", async () => { + const { + rollup, + capturedPayloadRef, + multisigAddress, + multisigPubkeys, + pubHexes, + signers, + unsignedTx, + } = await setupMultisigContext(); + + const targetAddress = new Uint8Array(32); + targetAddress.fill(0x99); + const targetAddressBs58 = bs58.encode(targetAddress); + + // Each signer signs with the same target_address — the signatures cover the full JSON + // including the target_address field. + const signedTx3 = await rollup.signTransactionForMultisig(unsignedTx, { + signer: signers.signer3, + authenticator: "solanaSimple", + multisigAddress, + multisigPubkeys, + targetAddress, + }); + const signedTx1 = await rollup.signTransactionForMultisig(unsignedTx, { + signer: signers.signer1, + authenticator: "solanaSimple", + multisigAddress, + multisigPubkeys, + targetAddress, + }); + + const v0_3 = (signedTx3 as any).V0; + const v0_1 = (signedTx1 as any).V0; + const multisigV1 = { + V1: { + ...unsignedTx, + signatures: [ + { pub_key: v0_3.pub_key, signature: v0_3.signature }, + { pub_key: v0_1.pub_key, signature: v0_1.signature }, + ], + unused_pub_keys: [pubHexes.pub2], + min_signers: 2, + target_address: targetAddressBs58, + }, + }; + + await rollup.submitMultisigTransaction(multisigV1 as any, { + authenticator: "solanaSimple", + multisigAddress, + multisigPubkeys, + }); + + // The endpoint payload is a base64-wrapped borsh blob whose tail is the signed JSON. We + // don't need to re-parse the borsh layout — it's sufficient to check that the target + // address bytes appear in the submitted blob (via its base58 ASCII representation). + const submittedBody = capturedPayloadRef.current.body.body as string; + const submittedBytes = Buffer.from(submittedBody, "base64"); + const submittedText = new TextDecoder().decode(submittedBytes); + expect(submittedText).toContain(`"target_address":"${targetAddressBs58}"`); + }); + }); }); diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts index 27a9740eba..6f8c01ec66 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts @@ -27,6 +27,14 @@ export type SolanaOffchainUnsignedTransaction = export type SolanaOffchainUnsignedTransactionV1 = SolanaOffchainUnsignedTransaction & { multisig_id: string; + /** + * Signer-declared target address. Omitted (or `null`) routes through + * `resolve_sender_address` on-chain. A concrete value requires the multisig credential to be + * authorized for that address in `account_owners`; otherwise the transaction is skipped. + * Omitted from the serialized JSON when not set, preserving byte equivalence with + * pre-change signed messages so existing signatures continue to verify. + */ + target_address?: string; version: number; }; @@ -79,6 +87,15 @@ export type SolanaMultisigSubmitParams = export type SolanaMultisigSignParams = SolanaMultisigSubmitParams & { signer: Signer; + /** + * Optional target address to embed in the signed V1 payload. When omitted, the signed message + * does not carry a `target_address` field, matching pre-change byte layout. When provided, the + * multisig credential must be authorized for this address in `account_owners` on-chain or the + * transaction will be skipped at authorization time. + * + * Unused by the `"standard"` authenticator. + */ + targetAddress?: Uint8Array; }; /** @@ -490,10 +507,15 @@ export class SolanaSignableRollup { /** * Creates V1 JSON bytes for a multisig transaction, including multisig_id and version. * The resulting JSON is what each signer signs directly (no discriminator prefix). + * + * When `targetAddress` is omitted the JSON has no `target_address` key — this preserves + * byte equivalence with pre-change signed messages, so signatures produced by older clients + * keep verifying. */ private async createMultisigJsonBytes( unsignedTx: UnsignedTransaction, multisigAddress: Uint8Array, + targetAddress?: Uint8Array, ): Promise { const serializer = await this.inner.serializer(); const schema = serializer.schema; @@ -511,6 +533,10 @@ export class SolanaSignableRollup { version: 1, }; + if (targetAddress !== undefined) { + solanaUnsignedTx.target_address = bs58.encode(targetAddress); + } + return new TextEncoder().encode(JSON.stringify(solanaUnsignedTx)); } @@ -521,10 +547,12 @@ export class SolanaSignableRollup { unsignedTx: UnsignedTransaction, multisigAddress: Uint8Array, multisigPubkeys: Uint8Array[], + targetAddress?: Uint8Array, ): Promise { const jsonBytes = await this.createMultisigJsonBytes( unsignedTx, multisigAddress, + targetAddress, ); const chainHash = await this.inner.chainHash(); const preamble = createSolanaPreamble( @@ -544,6 +572,7 @@ export class SolanaSignableRollup { signer: Signer, multisigAddress: Uint8Array, multisigPubkeys: Uint8Array[], + targetAddress?: Uint8Array, ): Promise> { const pubkey = await signer.publicKey(); const signerPubkeyHex = bytesToHex(pubkey); @@ -558,6 +587,7 @@ export class SolanaSignableRollup { const jsonBytes = await this.createMultisigJsonBytes( unsignedTx, multisigAddress, + targetAddress, ); const signature = await signer.sign(jsonBytes); @@ -578,6 +608,7 @@ export class SolanaSignableRollup { signer: Signer, multisigAddress: Uint8Array, multisigPubkeys: Uint8Array[], + targetAddress?: Uint8Array, ): Promise> { const pubkey = await signer.publicKey(); const signerPubkeyHex = bytesToHex(pubkey); @@ -594,6 +625,7 @@ export class SolanaSignableRollup { unsignedTx, multisigAddress, multisigPubkeys, + targetAddress, ); const signature = await signer.sign(signedMessageWithPreamble); @@ -627,6 +659,7 @@ export class SolanaSignableRollup { params.signer, params.multisigAddress, this.canonicalizeMultisigPubkeys(params.multisigPubkeys), + params.targetAddress, ); case "solana": return this.signForSolanaSpecMultisig( @@ -634,6 +667,7 @@ export class SolanaSignableRollup { params.signer, params.multisigAddress, this.canonicalizeMultisigPubkeys(params.multisigPubkeys), + params.targetAddress, ); } } @@ -728,6 +762,14 @@ export class SolanaSignableRollup { details: tx.details, }; + // Decode the signed `target_address` back to bytes so we can pass it through the same + // `createMultisigJsonBytes` path used by signers. Re-encoding is idempotent under base58, so + // the produced JSON bytes match what the signer signed over. + const targetAddress: Uint8Array | undefined = + tx.target_address !== null && tx.target_address !== undefined + ? bs58.decode(tx.target_address) + : undefined; + switch (params.authenticator) { case "standard": return this.inner.submitTransaction( @@ -739,6 +781,7 @@ export class SolanaSignableRollup { const jsonBytes = await this.createMultisigJsonBytes( unsignedTx, params.multisigAddress, + targetAddress, ); const wireBytes = new Uint8Array(1 + jsonBytes.length); wireBytes[0] = MULTISIG_SIMPLE_DISCRIMINATOR; @@ -767,6 +810,7 @@ export class SolanaSignableRollup { unsignedTx, params.multisigAddress, multisigPubkeys, + targetAddress, ); const message = this.buildSpecCompliantMultisigEnvelope( tx, diff --git a/typescript/packages/web3/src/rollup/standard-rollup.test.ts b/typescript/packages/web3/src/rollup/standard-rollup.test.ts index 307ae1344e..35ad31aa6b 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.test.ts @@ -254,4 +254,43 @@ describe("createStandardRollup", () => { }, }); }); + + it("should pass optional simulation parameters to the client", async () => { + const client = new SovereignClient({ fetch: vi.fn() }); + client.rollup.simulate = vi.fn().mockResolvedValue({ outcome: "success" }); + const rollup = await createStandardRollup({ + ...mockConfig, + client, + }); + const signer = { + publicKey: vi.fn().mockResolvedValue(new Uint8Array([0xab, 0xcd])), + }; + const runtimeCall = { + bank: { + transfer: { + to: "receiver", + coins: { amount: "1", token_id: "token" }, + }, + }, + }; + const txDetails = { + max_fee: "1234", + gas_limit: null, + }; + + await rollup.simulate(runtimeCall, { + signer: signer as any, + target_address: "sov1target", + tx_details: txDetails, + uniqueness: { nonce: 7 }, + }); + + expect(client.rollup.simulate).toHaveBeenCalledWith({ + sender: "abcd", + call: runtimeCall, + target_address: "sov1target", + tx_details: txDetails, + uniqueness: { nonce: 7 }, + }); + }); }); diff --git a/typescript/packages/web3/src/rollup/standard-rollup.ts b/typescript/packages/web3/src/rollup/standard-rollup.ts index ef16cadca2..09e1948505 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.ts @@ -84,10 +84,11 @@ export function standardTypeBuilder< /** * The parameters for simulating a runtime call transaction. */ -export type SimulateParams = Omit< - SovereignClient.RollupSimulateParams, - "call" | "sender" -> & +type RollupSimulateParams = SovereignClient.RollupSimulateParams & { + target_address?: string | null; +}; + +export type SimulateParams = Omit & SignerParams; export class StandardRollup extends Rollup< @@ -103,13 +104,17 @@ export class StandardRollup extends Rollup< */ async simulate( runtimeMessage: StandardRollupSpec["RuntimeCall"], - { signer }: SimulateParams, + { signer, ...params }: SimulateParams, ): Promise { const publicKey = await signer.publicKey(); const sender = bytesToHex(publicKey); const call = runtimeMessage as { [key: string]: unknown }; - return this.rollup.simulate({ sender, call }); + return this.rollup.simulate({ + ...params, + sender, + call, + } as SovereignClient.RollupSimulateParams); } } From 7bd74c90a5b3321015f16723f884a100b88e0cb7 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 22 Apr 2026 12:09:09 +0200 Subject: [PATCH 03/34] Fixing typescript --- typescript/packages/web3/src/rollup/standard-rollup.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typescript/packages/web3/src/rollup/standard-rollup.test.ts b/typescript/packages/web3/src/rollup/standard-rollup.test.ts index 35ad31aa6b..6c717c2781 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.test.ts @@ -275,7 +275,6 @@ describe("createStandardRollup", () => { }; const txDetails = { max_fee: "1234", - gas_limit: null, }; await rollup.simulate(runtimeCall, { From 3c7a41bf93edc1d680be3af101b6869496c68f34 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 22 Apr 2026 14:11:12 +0200 Subject: [PATCH 04/34] Continue fixing typescript --- .../web3/src/rollup/solana-signable-rollup.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts index 8abdf0043a..efc8272540 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts @@ -842,9 +842,7 @@ describe("SolanaSignableRollup", () => { const multisigPubkeys = [pub1, pub2, pub3]; const sortedPubKeys = [pub1Hex, pub2Hex, pub3Hex].sort(); - const pubKeyBytes = sortedPubKeys.map((pk) => - Array.from(hexToBytes(pk)), - ); + const pubKeyBytes = sortedPubKeys.map((pk) => Array.from(hexToBytes(pk))); const borshData = new Uint8Array(1 + 4 + 3 * 32); const dv = new DataView(borshData.buffer); borshData[0] = minSigners; @@ -1006,7 +1004,9 @@ describe("SolanaSignableRollup", () => { const submittedBody = capturedPayloadRef.current.body.body as string; const submittedBytes = Buffer.from(submittedBody, "base64"); const submittedText = new TextDecoder().decode(submittedBytes); - expect(submittedText).toContain(`"target_address":"${targetAddressBs58}"`); + expect(submittedText).toContain( + `"target_address":"${targetAddressBs58}"`, + ); }); }); }); From c0db317201d1afa269b1b65975e13a2ec444b737 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 22 Apr 2026 14:44:22 +0200 Subject: [PATCH 05/34] Fixing lint --- .../tests/stf_blueprint/operator/auth_eip712.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs index 25bb9e2d0f..cd4e152d2c 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs @@ -307,10 +307,7 @@ fn test_multisig_signature_verification() { // Generate a signature from a random private key that's not part of the multisig. We'll use this in some of the test cases. let random_private_key = TestPrivateKey::generate(); - // The multisig credential is authorized for `admin.address()` by the - // InsertCredentialId tx above. Target that address so the multisig tx pays - // gas from admin's balance; the multisig's own default address is unfunded. - let target = Some(admin.address()); + let target = None; let make_multisig_tx = |generation| { let utx = create_utx_with_generation::(encode_message::<_, RT>(), generation); let signatures = multisig_keys From 6f7486a8285f8ee50e8771104eaca11f70283d04 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 22 Apr 2026 15:40:15 +0200 Subject: [PATCH 06/34] Updating README tests part 1 --- examples/demo-rollup/README.md | 6 +++--- examples/demo-rollup/README_CELESTIA.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/demo-rollup/README.md b/examples/demo-rollup/README.md index 19c6980573..0919259c92 100644 --- a/examples/demo-rollup/README.md +++ b/examples/demo-rollup/README.md @@ -120,7 +120,7 @@ this case have the TokenCreated Event ```sh,test-ci,bashtestmd:compare-output $ sleep 5 -$ curl -sS http://127.0.0.1:12346/ledger/txs/0x4d5d2b90fefa8511c8d87384d7ead3d1113c9df0f3b7cf08fc7a87497cef91d4/events | jq +$ curl -sS http://127.0.0.1:12346/ledger/txs/0xbe16e6f31255f2f5f0a1e39530f91fd7f6480d73a3fbd3d3dcf7265962417db7/events | jq [ { "type": "event", @@ -154,7 +154,7 @@ $ curl -sS http://127.0.0.1:12346/ledger/txs/0x4d5d2b90fefa8511c8d87384d7ead3d11 "type": "moduleRef", "name": "Bank" }, - "tx_hash": "0x4d5d2b90fefa8511c8d87384d7ead3d1113c9df0f3b7cf08fc7a87497cef91d4" + "tx_hash": "0xbe16e6f31255f2f5f0a1e39530f91fd7f6480d73a3fbd3d3dcf7265962417db7" } ] ``` @@ -333,7 +333,7 @@ Adding the following transaction to batch: } } }, - "chain_hash": "0x01ae20b6512a6adf179acdcfe9340c5ee2bf77f02405c77f7ace574010adcf64", + "chain_hash": "0x65746dac667c302c76553208e916ba9a2f5738780f789014f8a900d6c2aeb8e5", "details": { "max_priority_fee_bips": 0, "max_fee": "100000000", diff --git a/examples/demo-rollup/README_CELESTIA.md b/examples/demo-rollup/README_CELESTIA.md index 6ac1a548a0..ca83aaca54 100644 --- a/examples/demo-rollup/README_CELESTIA.md +++ b/examples/demo-rollup/README_CELESTIA.md @@ -290,7 +290,7 @@ Adding the following transaction to batch: } } }, - "chain_hash": "0x01ae20b6512a6adf179acdcfe9340c5ee2bf77f02405c77f7ace574010adcf64", + "chain_hash": "0x65746dac667c302c76553208e916ba9a2f5738780f789014f8a900d6c2aeb8e5", "details": { "max_priority_fee_bips": 0, "max_fee": "100000000", From 9c916aef457e7a5161caac0a7aae7276bc3b5d66 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 22 Apr 2026 15:56:29 +0200 Subject: [PATCH 07/34] Documentation formatting --- .../tests/stf_blueprint/operator/auth_eip712.rs | 6 +++--- .../src/runtime/capabilities/authorization.rs | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs index cd4e152d2c..5b6940971e 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs @@ -175,9 +175,9 @@ pub fn sign_utx_in_place>( } /// Signs a V1 (multisig) unsigned transaction with the given private key using EIP712. -/// The credential_address is computed from the provided multisig. `target_address` is -/// forwarded into the signed `UnsignedTransactionV1`; pass `None` to match pre-target -/// routing or `Some(X)` to sign for a specific target account. +/// The credential_address is computed from the provided multisig. +/// `target_address` is forwarded into the signed `UnsignedTransactionV1`; +/// pass `None` to match pre-target routing or `Some(X)` to sign for a specific target account. pub fn sign_utx_v1_in_place>( utx: &UnsignedTransactionV0, multisig: &Multisig<::PublicKey>, diff --git a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs index 61bf0eef48..fce2e7e364 100644 --- a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs +++ b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs @@ -103,10 +103,12 @@ pub struct AuthorizationData { /// The default address. pub default_address: S::Address, - /// Signer-declared target address for execution. `None` routes the transaction - /// through the default resolver (default address + legacy fallback). `Some(X)` - /// requires `(X, credential_id) ∈ account_owners`; the transaction is skipped - /// otherwise. Populated from V1 transactions' signed `target_address` field; + /// Signer-declared target address for execution. + /// `None` routes the transaction through the default resolver + /// (default address + legacy fallback). + /// `Some(X)` requires `(X, credential_id) ∈ account_owners`; + /// the transaction is skipped otherwise. + /// Populated from V1 transactions' signed `target_address` field; /// always `None` for V0 and non-sov-tx authenticators. pub address: Option, } From e440353a0727a1df2296a3e1e468c65c8a449527 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 22 Apr 2026 16:34:40 +0200 Subject: [PATCH 08/34] Addressing the feedback --- .../sov-rollup-apis/src/endpoints/simulate.rs | 4 +-- .../module-system/sov-capabilities/src/lib.rs | 4 +++ .../src/rollup/solana-signable-rollup.test.ts | 28 +++++++++++++++++++ .../web3/src/rollup/solana-signable-rollup.ts | 11 +++++--- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs index df0523ff3e..a5a9097a5c 100644 --- a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs +++ b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs @@ -313,8 +313,8 @@ impl> SovereignSimulate { .as_deref() .map(S::Address::from_str) .transpose() - .map_err(|_| { - SimulateError::InvalidInput("failed to parse target address".to_owned()) + .map_err(|e| { + SimulateError::InvalidInput(format!("failed to parse target address: {e:?}")) })?; let uniqueness = params.uniqueness.unwrap_or_else(|| { let generation = Uniqueness::::default() diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index 5abcb16f72..656104ce96 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -354,6 +354,10 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' )) } + /// V1 `target_address` semantics apply uniformly here: when `auth_data.address` is + /// `Some(X)`, `resolve_sender` enforces `is_authorized(X, credential_id)` and uses `X` as both + /// the sender and the sequencer's rollup address. V0 transactions set `address = None` and + /// hit the legacy `resolve_sender_address` path unchanged. fn resolve_unregistered_context( &mut self, auth_data: &AuthorizationData, diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts index efc8272540..8f1a76816c 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts @@ -945,6 +945,34 @@ describe("SolanaSignableRollup", () => { expect(parsed.target_address).toBe(bs58.encode(targetAddress)); }); + it("places target_address before version in the signed JSON to match Rust field order", async () => { + // The Rust `SolanaOffchainUnsignedTransactionV1` struct declares `target_address` before + // `version`. Multisig signers sign the raw JSON bytes, so TS and Rust must serialize the + // fields in the same order — otherwise signatures produced in one language won't verify + // against JSON produced in the other. + const { rollup, multisigAddress, multisigPubkeys, signers, unsignedTx } = + await setupMultisigContext(); + + const targetAddress = new Uint8Array(32); + targetAddress.fill(0x42); + + const { mock, captured } = captureSignerInput(signers.signer1); + await rollup.signTransactionForMultisig(unsignedTx, { + signer: mock, + authenticator: "solanaSimple", + multisigAddress, + multisigPubkeys, + targetAddress, + }); + + const signedJson = new TextDecoder().decode(captured.bytes!); + const targetIdx = signedJson.indexOf('"target_address"'); + const versionIdx = signedJson.indexOf('"version"'); + expect(targetIdx).toBeGreaterThanOrEqual(0); + expect(versionIdx).toBeGreaterThanOrEqual(0); + expect(targetIdx).toBeLessThan(versionIdx); + }); + it("forwards tx.target_address into the submitted JSON via submitMultisigTransaction", async () => { const { rollup, diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts index 6f8c01ec66..21c3b3fb48 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts @@ -521,6 +521,10 @@ export class SolanaSignableRollup { const schema = serializer.schema; const chainName = schema.chain_data.chain_name || ""; + // Field order matches the Rust `SolanaOffchainUnsignedTransactionV1` struct + // (`crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs`): + // `target_address` must sit between `multisig_id` and `version` so TS- and Rust-generated + // JSON bytes are byte-identical — the multisig signatures cover these bytes directly. const solanaUnsignedTx: SolanaOffchainUnsignedTransactionV1 = { runtime_call: unsignedTx.runtime_call, uniqueness: unsignedTx.uniqueness, @@ -530,13 +534,12 @@ export class SolanaSignableRollup { // primary address type. Will be replaced with rollup-aware address formatting once the // SDK supports flexible address encoding (see #2673). multisig_id: bs58.encode(multisigAddress), + ...(targetAddress !== undefined && { + target_address: bs58.encode(targetAddress), + }), version: 1, }; - if (targetAddress !== undefined) { - solanaUnsignedTx.target_address = bs58.encode(targetAddress); - } - return new TextEncoder().encode(JSON.stringify(solanaUnsignedTx)); } From 56dac928a91b76462278f32d1ec2a784b505ac51 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Mon, 4 May 2026 16:24:24 +0200 Subject: [PATCH 09/34] Fixes after rebase --- .../sov-accounts/tests/integration/main.rs | 6 ++-- .../module-system/sov-capabilities/src/lib.rs | 16 ++++----- .../src/transaction/types/v1.rs | 6 ++-- .../src/transaction/unsigned/v0.rs | 5 +-- .../src/transaction/unsigned/v1.rs | 5 ++- .../src/authentication/mod.rs | 4 --- .../src/authentication/payload.rs | 5 ++- .../src/rollup/solana-signable-rollup.test.ts | 34 ++++--------------- .../web3/src/rollup/solana-signable-rollup.ts | 17 ++++------ .../web3/src/rollup/standard-rollup.ts | 11 +++--- 10 files changed, 36 insertions(+), 73 deletions(-) diff --git a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs index 61adc4b0e6..e73c24452a 100644 --- a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs +++ b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs @@ -568,7 +568,7 @@ fn test_v1_target_none_uses_default_resolver() { let accounts = Accounts::::default(); assert!( accounts - .is_authorized(&multisig_default_address, &inner_credential, state) + .is_explicitly_authorized(&multisig_default_address, &inner_credential, state) .unwrap(), "target=None should route to multisig default; the InsertCredentialId \ call must write the new credential under that address" @@ -619,7 +619,7 @@ fn test_v1_target_some_authorized_succeeds() { let accounts = Accounts::::default(); assert!( accounts - .is_authorized(&alice_address, &inner_credential, state) + .is_explicitly_authorized(&alice_address, &inner_credential, state) .unwrap(), "InsertCredentialId should write under target_address, not multisig default" ); @@ -670,7 +670,7 @@ fn test_v1_target_some_unauthorized_skipped() { let accounts = Accounts::::default(); assert!( !accounts - .is_authorized(&unowned_address, &inner_credential, state) + .is_explicitly_authorized(&unowned_address, &inner_credential, state) .unwrap(), "unauthorized target path must not auto-register any tuple" ); diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index 656104ce96..1ec18eeadb 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -61,6 +61,9 @@ impl<'a, S: Spec, T> StandardProvenRollupCapabilities<'a, S, T> { rewarded_token_holder } + /// Resolves the sender address. When `auth_data.address` is `Some(X)`, requires + /// `is_explicitly_authorized(X, credential_id)`. When `None`, falls through to + /// `resolve_sender_address` (default-address / legacy mapping). fn resolve_sender( &mut self, auth_data: &AuthorizationData, @@ -68,10 +71,11 @@ impl<'a, S: Spec, T> StandardProvenRollupCapabilities<'a, S, T> { ) -> anyhow::Result { match auth_data.address { Some(requested) => { - if !self - .accounts - .is_authorized(&requested, &auth_data.credential_id, state)? - { + if !self.accounts.is_explicitly_authorized( + &requested, + &auth_data.credential_id, + state, + )? { anyhow::bail!( "credential {} not authorized for target address {}", auth_data.credential_id, @@ -354,10 +358,6 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' )) } - /// V1 `target_address` semantics apply uniformly here: when `auth_data.address` is - /// `Some(X)`, `resolve_sender` enforces `is_authorized(X, credential_id)` and uses `X` as both - /// the sender and the sequencer's rollup address. V0 transactions set `address = None` and - /// hit the legacy `resolve_sender_address` path unchanged. fn resolve_unregistered_context( &mut self, auth_data: &AuthorizationData, diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs index 701e83e756..ef826f6f97 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs @@ -87,10 +87,8 @@ pub struct Version1, - /// Signer-declared target address for execution. `None` routes through - /// `resolve_sender_address` (default-address / legacy fallback). `Some(X)` requires - /// the multisig credential to be authorized for `X` in `account_owners`; the tx - /// is skipped otherwise. This field is part of the signed bytes. + /// Signer-declared target address; part of the signed bytes. + /// See [`crate::capabilities::AuthorizationData::address`] for routing semantics. pub target_address: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs index e0909ad942..7ac44e1135 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs @@ -114,10 +114,7 @@ impl UnsignedTransactionV0 { } /// Creates a new `V1` transaction from this unsigned transaction. - /// - /// `target_address = None` routes the tx through `resolve_sender_address` - /// (default address / legacy fallback). `target_address = Some(X)` requires - /// `(X, credential_id) ∈ account_owners`, else the tx is skipped at authorization. + /// See [`crate::capabilities::AuthorizationData::address`] for `target_address` routing. pub fn to_multisig_tx( self, multisig: Multisig<::PublicKey>, diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs index c1bc851427..04f02b2242 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs @@ -31,9 +31,8 @@ pub struct UnsignedTransactionV1 { /// and prevent credential malleability from reusing signed bytes in a different /// multisig envelope. pub credential_address: S::Address, - /// Signer-declared target address. `None` routes through `resolve_sender_address` - /// (default-address resolver). `Some(X)` requires the multisig credential to be - /// authorized for `X` via `account_owners`; otherwise the tx is skipped. + /// Signer-declared target address. + /// See [`crate::capabilities::AuthorizationData::address`] for routing semantics. pub target_address: Option, } diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs index 362b3a597d..b04f201a8c 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs @@ -153,10 +153,6 @@ fn build_auth_data( sov_modules_api::metered_credential::(pub_key, meter) .map_err(|e| AuthenticationError::OutOfGas(e.to_string()))?; - debug_assert!( - target_address.is_none(), - "V0 Solana transactions cannot carry a target_address" - ); Ok(AuthorizationData { uniqueness, tx_hash: raw_tx_hash, diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index 4bd2df658c..b4cc72032e 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -78,9 +78,8 @@ pub struct SolanaOffchainUnsignedTransactionV1 /// This is the "multisig address" except if the credential is mapped to another address in /// `sov-accounts`. pub multisig_id: S::Address, - /// Signer-declared target address. `None` routes through `resolve_sender_address` - /// (default-address resolver). `Some(X)` requires the multisig credential to be authorized - /// for `X` via `account_owners`; otherwise the tx is skipped. + /// Signer-declared target address; routing semantics in + /// [`sov_modules_api::capabilities::AuthorizationData::address`]. /// /// Omitted from the serialized JSON when `None`, so pre-change signed messages (which have no /// `target_address` field) remain byte-identical and verify against the same signature. diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts index 8f1a76816c..9a0071401e 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts @@ -923,7 +923,10 @@ describe("SolanaSignableRollup", () => { expect(parsed).not.toHaveProperty("target_address"); }); - it("includes target_address in the signed JSON when targetAddress is provided (solanaSimple)", async () => { + it("includes target_address in the signed JSON before version when targetAddress is provided", async () => { + // Field order matches Rust's `SolanaOffchainUnsignedTransactionV1`: signers sign the raw + // JSON bytes, so TS and Rust must emit fields in the same order or signatures produced + // in one language won't verify against JSON produced in the other. const { rollup, multisigAddress, multisigPubkeys, signers, unsignedTx } = await setupMultisigContext(); @@ -941,35 +944,12 @@ describe("SolanaSignableRollup", () => { expect(captured.bytes).toBeDefined(); const signedJson = new TextDecoder().decode(captured.bytes!); - const parsed = JSON.parse(signedJson); - expect(parsed.target_address).toBe(bs58.encode(targetAddress)); - }); - - it("places target_address before version in the signed JSON to match Rust field order", async () => { - // The Rust `SolanaOffchainUnsignedTransactionV1` struct declares `target_address` before - // `version`. Multisig signers sign the raw JSON bytes, so TS and Rust must serialize the - // fields in the same order — otherwise signatures produced in one language won't verify - // against JSON produced in the other. - const { rollup, multisigAddress, multisigPubkeys, signers, unsignedTx } = - await setupMultisigContext(); - - const targetAddress = new Uint8Array(32); - targetAddress.fill(0x42); - - const { mock, captured } = captureSignerInput(signers.signer1); - await rollup.signTransactionForMultisig(unsignedTx, { - signer: mock, - authenticator: "solanaSimple", - multisigAddress, - multisigPubkeys, - targetAddress, - }); - - const signedJson = new TextDecoder().decode(captured.bytes!); + expect(JSON.parse(signedJson).target_address).toBe( + bs58.encode(targetAddress), + ); const targetIdx = signedJson.indexOf('"target_address"'); const versionIdx = signedJson.indexOf('"version"'); expect(targetIdx).toBeGreaterThanOrEqual(0); - expect(versionIdx).toBeGreaterThanOrEqual(0); expect(targetIdx).toBeLessThan(versionIdx); }); diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts index 21c3b3fb48..80f597e2c9 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts @@ -28,11 +28,9 @@ export type SolanaOffchainUnsignedTransactionV1 = SolanaOffchainUnsignedTransaction & { multisig_id: string; /** - * Signer-declared target address. Omitted (or `null`) routes through - * `resolve_sender_address` on-chain. A concrete value requires the multisig credential to be - * authorized for that address in `account_owners`; otherwise the transaction is skipped. - * Omitted from the serialized JSON when not set, preserving byte equivalence with - * pre-change signed messages so existing signatures continue to verify. + * Signer-declared target address. Omitted from the serialized JSON when not set, + * preserving byte equivalence with pre-change signed messages so existing signatures + * continue to verify. See `AuthorizationData::address` (Rust) for routing semantics. */ target_address?: string; version: number; @@ -88,12 +86,9 @@ export type SolanaMultisigSubmitParams = export type SolanaMultisigSignParams = SolanaMultisigSubmitParams & { signer: Signer; /** - * Optional target address to embed in the signed V1 payload. When omitted, the signed message - * does not carry a `target_address` field, matching pre-change byte layout. When provided, the - * multisig credential must be authorized for this address in `account_owners` on-chain or the - * transaction will be skipped at authorization time. - * - * Unused by the `"standard"` authenticator. + * Optional target address to embed in the signed V1 payload. Omitted from the JSON when + * unset to preserve byte equivalence with pre-change signed messages. Unused by the + * `"standard"` authenticator. See `AuthorizationData::address` (Rust) for routing semantics. */ targetAddress?: Uint8Array; }; diff --git a/typescript/packages/web3/src/rollup/standard-rollup.ts b/typescript/packages/web3/src/rollup/standard-rollup.ts index 09e1948505..02d41cef98 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.ts @@ -84,12 +84,11 @@ export function standardTypeBuilder< /** * The parameters for simulating a runtime call transaction. */ -type RollupSimulateParams = SovereignClient.RollupSimulateParams & { - target_address?: string | null; -}; - -export type SimulateParams = Omit & - SignerParams; +export type SimulateParams = Omit< + SovereignClient.RollupSimulateParams, + "call" | "sender" +> & + SignerParams & { target_address?: string | null }; export class StandardRollup extends Rollup< StandardRollupSpec, From acc4169452e6543ec72a14c4573df59286d4388c Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 5 May 2026 13:01:19 +0200 Subject: [PATCH 10/34] Fix after rebase --- .../module-system/sov-capabilities/src/lib.rs | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index 1ec18eeadb..c0e1274958 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -60,37 +60,6 @@ impl<'a, S: Spec, T> StandardProvenRollupCapabilities<'a, S, T> { rewarded_token_holder } - - /// Resolves the sender address. When `auth_data.address` is `Some(X)`, requires - /// `is_explicitly_authorized(X, credential_id)`. When `None`, falls through to - /// `resolve_sender_address` (default-address / legacy mapping). - fn resolve_sender( - &mut self, - auth_data: &AuthorizationData, - state: &mut impl StateAccessor, - ) -> anyhow::Result { - match auth_data.address { - Some(requested) => { - if !self.accounts.is_explicitly_authorized( - &requested, - &auth_data.credential_id, - state, - )? { - anyhow::bail!( - "credential {} not authorized for target address {}", - auth_data.credential_id, - requested, - ); - } - Ok(requested) - } - None => Ok(self.accounts.resolve_sender_address( - &auth_data.default_address, - &auth_data.credential_id, - state, - )?), - } - } } trait HasGasPayer { From b008969407ca2c1eef4dda6058391de0946db4f6 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 5 May 2026 16:25:42 +0200 Subject: [PATCH 11/34] Simlify and fix --- .../tests/stf_blueprint/mod.rs | 17 +- .../stf_blueprint/operator/auth_eip712.rs | 7 +- .../stf_blueprint/optimistic/da_simulation.rs | 21 +- .../sov-accounts/tests/integration/main.rs | 178 ++++++++++++- .../module-system/sov-capabilities/src/lib.rs | 19 +- .../src/runtime/capabilities/authorization.rs | 11 +- .../sov-modules-api/src/transaction/mod.rs | 18 -- .../src/transaction/types/v0.rs | 16 +- .../src/transaction/types/v1.rs | 4 +- .../src/transaction/unsigned/v0.rs | 24 +- .../src/transaction/unsigned/v1.rs | 4 +- .../tests/integration/transaction.rs | 17 +- .../src/authentication/mod.rs | 16 +- .../src/authentication/payload.rs | 12 +- .../tests/integration/main.rs | 234 ++++++++++++++++++ crates/web3/src/rust.rs | 14 +- crates/web3/src/schema.rs | 14 ++ python/py_sovereign_web3/rust/src/lib.rs | 4 +- .../src/sovereign_web3/sovereign_web3.pyi | 2 + .../packages/multisig/src/index.test.ts | 3 +- typescript/packages/multisig/src/index.ts | 8 +- .../tests/multisig.integration-test.ts | 1 + typescript/packages/types/src/index.ts | 4 +- .../src/rollup/solana-signable-rollup.test.ts | 10 + .../web3/src/rollup/solana-signable-rollup.ts | 27 +- .../web3/src/rollup/standard-rollup.test.ts | 5 + .../web3/src/rollup/standard-rollup.ts | 1 + 27 files changed, 595 insertions(+), 96 deletions(-) diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs index c5fcbd69cf..e4a65b40b7 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs @@ -19,7 +19,7 @@ use sov_test_utils::runtime::{config_gas_token_id, Payable, TestRunner}; type S = sov_test_utils::TestSpec; -use sov_modules_api::transaction::{Transaction, TxDetails, UnsignedTransactionV0}; +use sov_modules_api::transaction::{Transaction, TxDetails, UnsignedTransactionV0, Version0}; use sov_modules_api::{PrivateKey, RawTx}; use sov_test_utils::{EncodeCall, TestUser, TEST_DEFAULT_MAX_FEE}; use sov_value_setter::ValueSetter; @@ -251,18 +251,19 @@ pub fn create_tx_bad_sig>( let bad_signature = signer.private_key.sign(&[1, 2, 3]); match signed_tx { - Transaction::V0(inner) => Transaction::new_with_details_v0( - inner.pub_key, - inner.runtime_call, - bad_signature, - inner.uniqueness, - TxDetails { + Transaction::V0(inner) => Transaction::V0(Version0 { + signature: bad_signature, + pub_key: inner.pub_key, + runtime_call: inner.runtime_call, + uniqueness: inner.uniqueness, + details: TxDetails { max_priority_fee_bips, max_fee: Amount::new(200_000), gas_limit: None, chain_id, }, - ), + target_address: inner.target_address, + }), Transaction::V1(_inner) => { todo!("Bad signature generation for multisig transactions is not yet supported"); } diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs index 5b6940971e..46eb57f649 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs @@ -277,10 +277,9 @@ fn correct_signature_is_accepted() { #[test] fn test_multisig_signature_verification() { // Build the multisig first so we can seed genesis with its canonical address - // funded. After the accounts refactor, `resolve_sender_address` no longer - // writes an `accounts` entry, so the multisig must either be a canonical- - // address owner with gas, or be covered by a legacy mapping. Here we pick - // the canonical path. + // funded. After the accounts refactor, the sender resolver no longer auto- + // creates an `accounts` entry, so the multisig must be funded at its + // canonical address. Here we pick the canonical path. let multisig_keys = [ TestPrivateKey::generate(), TestPrivateKey::generate(), diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs index 48435c1a2b..de1c77af8a 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use sov_bank::Bank; use sov_modules_api::capabilities::{TransactionAuthenticator, UniquenessData}; -use sov_modules_api::transaction::{Transaction, UnsignedTransactionV0}; +use sov_modules_api::transaction::{Transaction, UnsignedTransactionV0, Version0}; use sov_modules_api::{Amount, EncodeCall, FullyBakedTx, PrivateKey, RawTx, Runtime}; use sov_test_utils::generators::bank::BankMessageGenerator; use sov_test_utils::generators::sequencer_registry::SequencerRegistryMessageGenerator; @@ -45,15 +45,16 @@ pub fn simulate_da_with_revert_msg(admin: TestPrivateKey) -> Vec { pub fn simulate_da_with_bad_sig(key: TestPrivateKey) -> Vec { let bank_generator: BankMessageGenerator = BankMessageGenerator::with_minter(key.clone()); let create_token_message = bank_generator.create_default_messages().remove(0); - let tx = Transaction::, S>::new_with_details_v0( - create_token_message.sender_key.pub_key(), - as EncodeCall>>::to_decodable(create_token_message.content), - // Use the signature of an empty message - key.sign(&[]), - UniquenessData::Generation(create_token_message.generation), - create_token_message.details, - ); - // Overwrite the signature with the signature of the empty message + let tx = Transaction::, S>::V0(Version0 { + signature: key.sign(&[]), + pub_key: create_token_message.sender_key.pub_key(), + runtime_call: as EncodeCall>>::to_decodable( + create_token_message.content, + ), + uniqueness: UniquenessData::Generation(create_token_message.generation), + details: create_token_message.details, + target_address: None, + }); vec![encode_with_auth(tx)] } diff --git a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs index e73c24452a..e3252a73e6 100644 --- a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs +++ b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs @@ -527,10 +527,30 @@ fn make_v1_tx( .to_multisig_tx(multisig.clone(), target_address) } +fn make_v0_tx( + sender: &TestUser, + inner_credential: sov_modules_api::CredentialId, + target_address: Option<::Address>, +) -> Transaction { + let mut utx = UnsignedTransactionV0::::new_with_details( + TestAccountsRuntimeCall::Accounts(CallMessage::InsertCredentialId(inner_credential)), + sov_modules_api::capabilities::UniquenessData::Generation(0), + default_test_tx_details::(), + ); + utx.target_address = target_address; + utx.sign(sender.private_key(), &>::CHAIN_HASH) +} + fn sign_v1(tx: &mut Version1, key: &TestPrivateKey) { tx.sign(key, &>::CHAIN_HASH).unwrap(); } +fn submit_v0(tx: Transaction) -> TransactionType { + TransactionType::::PreSigned(RawTx { + data: borsh::to_vec(&tx).unwrap(), + }) +} + fn submit_v1(tx: Version1) -> TransactionType { let tx = Transaction::::from(tx); TransactionType::::PreSigned(RawTx { @@ -538,9 +558,9 @@ fn submit_v1(tx: Version1) -> TransactionType { }) } -/// V1 tx with `target_address = None` routes through `resolve_sender_address` -/// and executes as the multisig's default address. Evidence: the `InsertCredentialId` -/// call writes `(multisig_default, inner_credential)` to `account_owners`. +/// V1 tx with `target_address = None` executes as the multisig's default +/// address. Evidence: the `InsertCredentialId` call writes +/// `(multisig_default, inner_credential)` to `account_owners`. #[test] fn test_v1_target_none_uses_default_resolver() { let MultisigEnv { @@ -730,3 +750,155 @@ fn test_v1_target_tamper_breaks_signature() { }), }); } + +/// V0 tx with `target_address = None` executes as the signer's default address. +#[test] +fn test_v0_target_none_uses_default_resolver() { + let sender = TestUser::::generate_with_default_balance(); + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![sender.clone()]); + let genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + let tx = make_v0_tx(&sender, inner_credential, None); + + runner.execute_transaction(TransactionTestCase { + input: submit_v0(tx), + assert: Box::new(move |result, state| { + assert!(result.tx_receipt.is_successful()); + let accounts = Accounts::::default(); + assert!( + accounts + .is_explicitly_authorized(&sender.address(), &inner_credential, state) + .unwrap(), + "target=None should route to the signer's default address" + ); + }), + }); +} + +/// V0 tx with `target_address = Some(X)` where `(X, credential_id)` is +/// authorized resolves context as `X`. +#[test] +fn test_v0_target_some_authorized_succeeds() { + let sender = TestUser::::generate_with_default_balance(); + let alice = TestUser::::generate_with_default_balance(); + let alice_address = alice.address(); + + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![sender.clone(), alice]); + let mut genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + genesis.accounts.accounts.push(AccountData { + credential_id: sender.credential_id(), + address: alice_address, + }); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + let tx = make_v0_tx(&sender, inner_credential, Some(alice_address)); + + runner.execute_transaction(TransactionTestCase { + input: submit_v0(tx), + assert: Box::new(move |result, state| { + assert!( + result.tx_receipt.is_successful(), + "V0 target=Some(authorized) should succeed, got {:?}", + result.tx_receipt + ); + let accounts = Accounts::::default(); + assert!( + accounts + .is_explicitly_authorized(&alice_address, &inner_credential, state) + .unwrap(), + "InsertCredentialId should write under target_address, not sender default" + ); + }), + }); +} + +/// V0 tx with `target_address = Some(Y)` where `(Y, credential_id) ∉ account_owners` +/// is skipped, not reverted. +#[test] +fn test_v0_target_some_unauthorized_skipped() { + let sender = TestUser::::generate_with_default_balance(); + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![sender.clone()]); + let genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + let unowned_address = TestUser::::generate_with_default_balance().address(); + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + let tx = make_v0_tx(&sender, inner_credential, Some(unowned_address)); + + runner.execute_transaction(TransactionTestCase { + input: submit_v0(tx), + assert: Box::new(move |result, state| { + match result.tx_receipt { + TxEffect::Skipped(SkippedTxContents { error, .. }) => { + let msg = error.to_string(); + assert!( + msg.contains("not authorized for target address"), + "unexpected skip reason: {msg}" + ); + } + other => panic!("expected skipped tx, got {other:?}"), + } + let accounts = Accounts::::default(); + assert!( + !accounts + .is_explicitly_authorized(&unowned_address, &inner_credential, state) + .unwrap(), + "unauthorized target path must not auto-register any tuple" + ); + }), + }); +} + +/// `target_address` is part of the signed bytes for V0 as well: tampering with it +/// after signing invalidates the signature. +#[test] +fn test_v0_target_tamper_breaks_signature() { + let sender = TestUser::::generate_with_default_balance(); + let alice = TestUser::::generate_with_default_balance(); + let alice_address = alice.address(); + let tampered_address = TestUser::::generate_with_default_balance().address(); + + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![sender.clone(), alice]); + let mut genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + genesis.accounts.accounts.push(AccountData { + credential_id: sender.credential_id(), + address: alice_address, + }); + genesis.accounts.accounts.push(AccountData { + credential_id: sender.credential_id(), + address: tampered_address, + }); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + let mut tx = make_v0_tx(&sender, inner_credential, Some(alice_address)); + let Transaction::V0(inner) = &mut tx else { + panic!("expected v0 tx"); + }; + inner.target_address = Some(tampered_address); + + runner.execute_transaction(TransactionTestCase { + input: submit_v0(tx), + assert: Box::new(move |result, _state| match result.tx_receipt { + TxEffect::Skipped(SkippedTxContents { error, .. }) => { + let msg = error.to_string(); + assert!( + msg.contains("Verification equation was not satisfied"), + "expected signature failure after target_address tamper, got: {msg}" + ); + } + other => panic!("expected skipped tx, got {other:?}"), + }), + }); +} diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index c0e1274958..fa33b36436 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -311,13 +311,28 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' auth_data: &AuthorizationData, sequencer: &::Address, sequencer_rollup_address: S::Address, - _state: &mut impl StateAccessor, + state: &mut impl StateAccessor, sequencing_data: Option, execution_context: ExecutionContext, sequencer_type: SequencerType, ) -> anyhow::Result> { + let sender = match auth_data.address { + Some(target_address) => { + anyhow::ensure!( + self.accounts.is_explicitly_authorized( + &target_address, + &auth_data.credential_id, + state, + )?, + "not authorized for target address" + ); + target_address + } + None => auth_data.default_address, + }; + Ok(Context::new( - auth_data.default_address, + sender, auth_data.credentials.clone(), sequencer_rollup_address, *sequencer, diff --git a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs index fce2e7e364..9d26ed4164 100644 --- a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs +++ b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs @@ -104,11 +104,12 @@ pub struct AuthorizationData { pub default_address: S::Address, /// Signer-declared target address for execution. - /// `None` routes the transaction through the default resolver - /// (default address + legacy fallback). - /// `Some(X)` requires `(X, credential_id) ∈ account_owners`; + /// + /// `None` ⇒ resolve to the credential's default address. + /// `Some(X)` ⇒ requires an explicit `(X, credential_id)` entry in `account_owners`; /// the transaction is skipped otherwise. - /// Populated from V1 transactions' signed `target_address` field; - /// always `None` for V0 and non-sov-tx authenticators. + /// + /// Set by authenticators that carry a signed target_address (sov-tx V0/V1, + /// Solana off-chain auth). Other authenticators leave it `None`. pub address: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/mod.rs b/crates/module-system/sov-modules-api/src/transaction/mod.rs index c9557a9c34..a099922d3e 100644 --- a/crates/module-system/sov-modules-api/src/transaction/mod.rs +++ b/crates/module-system/sov-modules-api/src/transaction/mod.rs @@ -23,7 +23,6 @@ pub use types::{ }; pub use unsigned::{UnsignedTransaction, UnsignedTransactionV0, UnsignedTransactionV1}; -use crate::capabilities::UniquenessData; use crate::{ CryptoSpecExt, DispatchCall, Gas, GasMeter, GasMeteringError, GasSpec, MeteredBorshDeserialize, MeteredBorshDeserializeError, MeteredSigVerificationError, MeteredSignature, Spec, @@ -219,23 +218,6 @@ impl Transaction { } } - /// Creates a new transaction with the provided metadata. - pub fn new_with_details_v0( - pub_key: C::PublicKey, - runtime_call: R::Call, - signature: C::Signature, - uniqueness: UniquenessData, - details: TxDetails, - ) -> Self { - Self::V0(Version0 { - signature, - pub_key, - runtime_call, - uniqueness, - details, - }) - } - /// Serialize the transaction, appending the runtime's chain_hash. /// This is the standard serialization for Sovereign signature signing. pub fn serialized_with_chain_hash( diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs index 47fcebcb47..295f060bbe 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs @@ -41,16 +41,20 @@ pub struct Version0, + /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + #[serde(default)] + pub target_address: Option, } impl Version0 { /// Extracts the versioned unsigned transaction data from this signed envelope. pub fn as_unsigned(&self) -> UnsignedTransaction { - UnsignedTransaction::V0(UnsignedTransactionV0::new_with_details( - self.runtime_call.clone(), - self.uniqueness, - self.details.clone(), - )) + UnsignedTransaction::V0(UnsignedTransactionV0 { + runtime_call: self.runtime_call.clone(), + uniqueness: self.uniqueness, + details: self.details.clone(), + target_address: self.target_address, + }) } /// Extracts authorization data from this transaction. @@ -71,7 +75,7 @@ impl Version0 { credential_id, credentials: Credentials::new(pub_key), default_address: credential_id.into(), - address: None, + address: self.target_address, }) } } diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs index ef826f6f97..aacf0a5a71 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs @@ -87,8 +87,8 @@ pub struct Version1, - /// Signer-declared target address; part of the signed bytes. - /// See [`crate::capabilities::AuthorizationData::address`] for routing semantics. + /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + #[serde(default)] pub target_address: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs index 7ac44e1135..ef2ca1e308 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs @@ -6,7 +6,9 @@ use sov_universal_wallet::UniversalWallet; use crate::{ capabilities::UniquenessData, - transaction::{PriorityFeeBips, Transaction, TransactionCallable, TxDetails, Version1}, + transaction::{ + PriorityFeeBips, Transaction, TransactionCallable, TxDetails, Version0, Version1, + }, Amount, CryptoSpecExt, Multisig, Spec, }; @@ -27,6 +29,9 @@ pub struct UnsignedTransactionV0 { pub uniqueness: UniquenessData, /// Data related to fees and gas handling. pub details: TxDetails, + /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + #[serde(default)] + pub target_address: Option, } // Manually implemented to ensure correct trait bounds (derive would require R: Clone/PartialEq) @@ -36,6 +41,7 @@ impl Clone for UnsignedTransactionV0 { runtime_call: self.runtime_call.clone(), uniqueness: self.uniqueness, details: self.details.clone(), + target_address: self.target_address, } } } @@ -44,6 +50,7 @@ impl PartialEq for UnsignedTransactionV0 self.runtime_call == other.runtime_call && self.uniqueness == other.uniqueness && self.details == other.details + && self.target_address == other.target_address } } impl Eq for UnsignedTransactionV0 {} @@ -81,6 +88,7 @@ impl UnsignedTransactionV0 { gas_limit, chain_id, }, + target_address: None, } } @@ -94,6 +102,7 @@ impl UnsignedTransactionV0 { runtime_call, uniqueness, details, + target_address: None, } } @@ -104,13 +113,14 @@ impl UnsignedTransactionV0 { pub_key: C::PublicKey, signature: C::Signature, ) -> Transaction { - Transaction::new_with_details_v0( - pub_key, - self.runtime_call, + Transaction::V0(Version0 { signature, - self.uniqueness, - self.details, - ) + pub_key, + runtime_call: self.runtime_call, + uniqueness: self.uniqueness, + details: self.details, + target_address: self.target_address, + }) } /// Creates a new `V1` transaction from this unsigned transaction. diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs index 04f02b2242..0ae3c653b9 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs @@ -31,8 +31,8 @@ pub struct UnsignedTransactionV1 { /// and prevent credential malleability from reusing signed bytes in a different /// multisig envelope. pub credential_address: S::Address, - /// Signer-declared target address. - /// See [`crate::capabilities::AuthorizationData::address`] for routing semantics. + /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + #[serde(default)] pub target_address: Option, } diff --git a/crates/module-system/sov-modules-api/tests/integration/transaction.rs b/crates/module-system/sov-modules-api/tests/integration/transaction.rs index edf8319a13..888bc385f7 100644 --- a/crates/module-system/sov-modules-api/tests/integration/transaction.rs +++ b/crates/module-system/sov-modules-api/tests/integration/transaction.rs @@ -37,6 +37,7 @@ fn test_serde_serialize_tx() { runtime_call: TestOptimisticRuntimeCall::ValueSetter(native_call), uniqueness: uniq, details, + target_address: None, }; let native = Transaction::::V0(native_tx); let native_json = serde_json::to_value(&native).unwrap(); @@ -75,7 +76,8 @@ fn test_schema_and_native_serialization_consistency() { "max_fee": 10000, "gas_limit": [500, 500], "chain_id": 1337 - } + }, + "target_address": null } }"#; let schema = Schema::of_single_type::>().unwrap(); @@ -104,6 +106,7 @@ fn test_schema_and_native_serialization_consistency() { runtime_call: TestOptimisticRuntimeCall::ValueSetter(native_call), uniqueness: uniq, details, + target_address: None, }; let native = Transaction::::V0(native_tx); let native_bytes = borsh::to_vec(&native).unwrap(); @@ -157,7 +160,8 @@ mod web3_compatibility { "max_fee": 10000, "gas_limit": null, "chain_id": 1337 - } + }, + "target_address": null }}"#; let schema = Schema::of_single_type::>().unwrap(); @@ -183,7 +187,8 @@ mod web3_compatibility { "max_fee": 10000, "gas_limit": [500, 500], "chain_id": 1337 - } + }, + "target_address": null }}"#; let schema = Schema::of_single_type::>().unwrap(); @@ -213,7 +218,8 @@ mod web3_compatibility { "max_fee": 10000, "gas_limit": [500, 500], "chain_id": 1337 - } + }, + "target_address": null } }"#; let schema = Schema::of_single_type::>().unwrap(); @@ -244,7 +250,8 @@ mod web3_compatibility { "max_fee": 10000, "gas_limit": null, "chain_id": 1337 - } + }, + "target_address": null } }"#; let schema = Schema::of_single_type::>().unwrap(); diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs index b04f201a8c..1f49121462 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs @@ -122,11 +122,8 @@ fn verify_signatures( } /// Builds authorization data for either single-sig or multisig transactions. -/// -/// `target_address` is the signer-declared target extracted from the deserialized V1 JSON payload. -/// It must always be `None` for V0; for V1, it is forwarded from -/// `SolanaOffchainUnsignedTransactionV1::target_address` so that the authorization layer can route -/// execution to a pre-authorized account instead of the multisig's default address. +/// `target_address` is forwarded from the signed payload — see +/// [`sov_modules_api::capabilities::AuthorizationData::address`] for routing semantics. fn build_auth_data( unpacked: &UnpackedSolanaMessage, uniqueness: UniquenessData, @@ -160,7 +157,7 @@ fn build_auth_data( credential_id, credentials: Credentials::new(pub_key.clone()), default_address: credential_id.into(), - address: None, + address: target_address, }) } UnpackedSolanaMessage::V1 { @@ -290,7 +287,12 @@ where UnpackedSolanaMessage::V0 { .. } => { let tx = SolanaOffchainUnsignedTransactionV0::::unmetered_deserialize(json_slice) .map_err(deser_err)?; - (tx.chain_name.to_string(), None, tx.into_unsigned_tx()) + let target_address = tx.target_address; + ( + tx.chain_name.to_string(), + target_address, + tx.into_unsigned_tx(), + ) } UnpackedSolanaMessage::V1 { signatures, diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index b4cc72032e..e2dc866f60 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -33,6 +33,10 @@ pub struct SolanaOffchainUnsignedTransactionV0 /// from malicious chains (if the chain name matches some other chain the use but didn't expect /// to be signing for right now). pub chain_name: SafeString, + /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + /// Skipped from JSON when `None` so pre-change signed messages stay byte-identical. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target_address: Option, } impl SolanaOffchainUnsignedTransactionV0 @@ -46,6 +50,7 @@ where runtime_call: self.runtime_call, uniqueness: self.uniqueness, details: self.details, + target_address: self.target_address, }) } @@ -78,11 +83,8 @@ pub struct SolanaOffchainUnsignedTransactionV1 /// This is the "multisig address" except if the credential is mapped to another address in /// `sov-accounts`. pub multisig_id: S::Address, - /// Signer-declared target address; routing semantics in - /// [`sov_modules_api::capabilities::AuthorizationData::address`]. - /// - /// Omitted from the serialized JSON when `None`, so pre-change signed messages (which have no - /// `target_address` field) remain byte-identical and verify against the same signature. + /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + /// Skipped from JSON when `None` so pre-change signed messages stay byte-identical. #[serde(default, skip_serializing_if = "Option::is_none")] pub target_address: Option, /// Message format version. Must be `1` for this struct. diff --git a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs index 65c1563839..8d1ce366e3 100644 --- a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs +++ b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs @@ -198,6 +198,14 @@ async fn create_test_rollup_with_extra_account_owners( } fn create_transfer_tx_json(amount: Amount, recipient: &str) -> String { + create_transfer_tx_json_with_target(amount, recipient, None) +} + +fn create_transfer_tx_json_with_target( + amount: Amount, + recipient: &str, + target_address: Option<::Address>, +) -> String { let msg: TestRuntimeCall = TestRuntimeCall::Bank(BankCallMessage::Transfer { to: ::Address::from_str(recipient).unwrap(), coins: Coins { @@ -218,11 +226,27 @@ fn create_transfer_tx_json(amount: Amount, recipient: &str) -> String { uniqueness: unsigned_tx.uniqueness, details: unsigned_tx.details, chain_name: config_value!("CHAIN_NAME").to_string().try_into().unwrap(), + target_address, }; serde_json::to_string(&solana_unsigned_tx).unwrap() } +async fn submit_simple_json_tx( + client: &sov_api_spec::client::Client, + json: String, + signer: &Ed25519PrivateKey, +) -> reqwest::Response { + let signed_message = json.into_bytes(); + let message = SolanaOffchainSimpleMessage:: { + signed_message: signed_message.clone(), + chain_hash: RT::CHAIN_HASH, + pubkey: signer.pub_key(), + signature: signer.sign(&signed_message), + }; + submit_tx(client, borsh::to_vec(&message).unwrap()).await +} + async fn submit_tx( client: &sov_api_spec::client::Client, raw_tx_bytes: Vec, @@ -1322,3 +1346,213 @@ fn test_v1_payload_with_target_address_round_trips() { assert_eq!(parsed.target_address, Some(target)); assert_eq!(parsed.multisig_id, multisig_address); } + +/// End-to-end plumbing test for single-sig V0: admin's credential is explicitly +/// authorized for a delegated address at genesis, that delegated address is funded, +/// and a V0 tx carrying `target_address = Some(delegated)` spends from it. +#[tokio::test(flavor = "multi_thread")] +async fn test_submit_single_sig_with_authorized_target_address() { + let delegated_user = TestUser::::generate_with_default_balance(); + let delegated_address = delegated_user.address(); + let delegated_address_str = delegated_address.to_string(); + let (test_rollup, admin) = create_test_rollup_with_extra_account_owners(|admin| { + vec![sov_test_utils::runtime::sov_accounts::AccountData { + credential_id: admin.credential_id(), + address: delegated_address, + }] + }) + .await + .expect("Failed to create rollup"); + + let response = submit_simple_json_tx( + test_rollup.api_client(), + create_transfer_tx_json(Amount(8_000), &delegated_address_str), + admin.private_key(), + ) + .await; + assert!( + response.status().is_success(), + "Expected delegated funding tx to succeed. Response: {response:?}" + ); + + let delegated_balance_before = query_balance(&test_rollup.client, &delegated_address_str).await; + assert_eq!( + delegated_balance_before, + Some(Amount::new(8_000)), + "Expected delegated address to be funded before target-routed transfer" + ); + + let response = submit_simple_json_tx( + test_rollup.api_client(), + create_transfer_tx_json_with_target( + Amount(7_000), + RECIPIENT_ADDRESS, + Some(delegated_address), + ), + admin.private_key(), + ) + .await; + assert!( + response.status().is_success(), + "Expected single-sig V0 target-routed tx to succeed. Response: {response:?}" + ); + + let recipient_balance = query_balance(&test_rollup.client, RECIPIENT_ADDRESS).await; + assert_eq!( + recipient_balance, + Some(Amount::new(7_000)), + "Expected recipient to receive 7,000 tokens via target-routed V0 tx" + ); + + let delegated_balance_after = query_balance(&test_rollup.client, &delegated_address_str).await; + assert_eq!( + delegated_balance_after, + Some(Amount::new(1_000)), + "Expected target-routed V0 tx to spend from delegated address" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_submit_single_sig_with_unauthorized_target_address_fails() { + let (test_rollup, admin) = create_test_rollup().await.expect("Failed to create rollup"); + let unowned_address = TestUser::::generate_with_default_balance().address(); + + let response = submit_simple_json_tx( + test_rollup.api_client(), + create_transfer_tx_json_with_target( + Amount(1_000), + RECIPIENT_ADDRESS, + Some(unowned_address), + ), + admin.private_key(), + ) + .await; + + assert_eq!( + response.status(), + 400, + "Expected 400 status for unauthorized target address" + ); + let response_text = response.text().await.expect("Failed to read response body"); + assert!( + response_text.contains("not authorized for target address"), + "Expected unauthorized target error, got: {response_text}" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_submit_single_sig_with_tampered_target_address_fails_signature() { + let delegated_user = TestUser::::generate_with_default_balance(); + let delegated_address = delegated_user.address(); + let (test_rollup, admin) = create_test_rollup_with_extra_account_owners(|admin| { + vec![sov_test_utils::runtime::sov_accounts::AccountData { + credential_id: admin.credential_id(), + address: delegated_address, + }] + }) + .await + .expect("Failed to create rollup"); + + let json = create_transfer_tx_json_with_target( + Amount(1_000), + RECIPIENT_ADDRESS, + Some(delegated_address), + ); + let signature = admin.private_key().sign(json.as_bytes()); + let mut payload: SolanaOffchainUnsignedTransactionV0 = + serde_json::from_str(&json).expect("deserialize"); + payload.target_address = Some(admin.address()); + let tampered_json = serde_json::to_string(&payload).expect("serialize"); + let message = SolanaOffchainSimpleMessage:: { + signed_message: tampered_json.into_bytes(), + chain_hash: RT::CHAIN_HASH, + pubkey: admin.private_key().pub_key(), + signature, + }; + + let response = submit_tx(test_rollup.api_client(), borsh::to_vec(&message).unwrap()).await; + assert_eq!( + response.status(), + 400, + "Expected 400 status for tampered target address" + ); + let response_text = response.text().await.expect("Failed to read response body"); + assert!( + response_text.contains("Signature verification failed") + || response_text.contains("Verification equation was not satisfied"), + "Expected signature verification error, got: {response_text}" + ); +} + +fn build_v0_payload( + recipient: &str, + target_address: Option<::Address>, +) -> SolanaOffchainUnsignedTransactionV0 { + let call: TestRuntimeCall = TestRuntimeCall::Bank(BankCallMessage::Transfer { + to: ::Address::from_str(recipient).unwrap(), + coins: Coins { + amount: Amount(1_000), + token_id: config_value!("GAS_TOKEN_ID"), + }, + }); + let unsigned_tx = UnsignedTransactionV0::::new( + call, + config_value!("CHAIN_ID"), + TEST_DEFAULT_MAX_PRIORITY_FEE, + TEST_DEFAULT_MAX_FEE, + UniquenessData::Nonce(0), + Some(TEST_DEFAULT_GAS_LIMIT.into()), + ); + SolanaOffchainUnsignedTransactionV0:: { + runtime_call: unsigned_tx.runtime_call, + uniqueness: unsigned_tx.uniqueness, + details: unsigned_tx.details, + chain_name: config_value!("CHAIN_NAME").to_string().try_into().unwrap(), + target_address, + } +} + +#[test] +fn test_v0_payload_omits_target_address_when_none() { + let payload = build_v0_payload(RECIPIENT_ADDRESS, None); + + let json = serde_json::to_string(&payload).expect("serialize"); + assert!( + !json.contains("target_address"), + "target_address must not appear in JSON when it is None; got: {json}" + ); +} + +#[test] +fn test_v0_payload_without_target_address_field_deserializes_as_none() { + let expected = build_v0_payload(RECIPIENT_ADDRESS, None); + let canonical_json = serde_json::to_string(&expected).expect("serialize baseline"); + assert!( + !canonical_json.contains("target_address"), + "guard: the canonical None-payload already omits target_address" + ); + + let parsed: SolanaOffchainUnsignedTransactionV0 = + serde_json::from_str(&canonical_json).expect("deserialize"); + assert_eq!( + parsed.target_address, None, + "missing target_address field must default to None" + ); +} + +#[test] +fn test_v0_payload_with_target_address_round_trips() { + let target: ::Address = + ::Address::from_str(RECIPIENT_ADDRESS).expect("parse recipient"); + let original = build_v0_payload(RECIPIENT_ADDRESS, Some(target)); + + let json = serde_json::to_string(&original).expect("serialize"); + assert!( + json.contains("target_address"), + "target_address must appear in JSON when it is Some; got: {json}" + ); + + let parsed: SolanaOffchainUnsignedTransactionV0 = + serde_json::from_str(&json).expect("deserialize"); + assert_eq!(parsed.target_address, Some(target)); +} diff --git a/crates/web3/src/rust.rs b/crates/web3/src/rust.rs index 53e58e0795..20289a7e48 100644 --- a/crates/web3/src/rust.rs +++ b/crates/web3/src/rust.rs @@ -112,6 +112,7 @@ pub struct TransactionBuilder, max_fee: Option, gas_limit: Option>, + target_address: Option, _phantom: std::marker::PhantomData, } @@ -131,6 +132,7 @@ impl TransactionBui priority_fee_bips: None, max_fee: None, gas_limit: None, + target_address: None, _phantom: Default::default(), } } @@ -188,6 +190,12 @@ impl TransactionBui self } + /// Sets the explicit target address for the transaction. + pub fn target_address(mut self, target_address: S::Address) -> Self { + self.target_address = Some(target_address); + self + } + /// Builds an unsigned transaction with the configured parameters. /// /// Uses default values for any parameters that were not explicitly set: @@ -210,14 +218,16 @@ impl TransactionBui let gas_limit = self.gas_limit.unwrap_or(None); let uniqueness = self.uniqueness.unwrap_or_else(default_uniqueness); - Ok(UnsignedTransactionV0::new( + let mut tx = UnsignedTransactionV0::new( self.call, config_chain_id(), priority_fee, max_fee, uniqueness, gas_limit, - )) + ); + tx.target_address = self.target_address; + Ok(tx) } /// Builds and signs a transaction in one step. diff --git a/crates/web3/src/schema.rs b/crates/web3/src/schema.rs index c7cb390591..fa2bfcf775 100644 --- a/crates/web3/src/schema.rs +++ b/crates/web3/src/schema.rs @@ -359,6 +359,8 @@ pub struct UnsignedTransactionV0 { pub uniqueness: UniquenessData, /// Transaction execution details including fees and gas limits. pub details: TxDetails, + /// Optional target address for execution; null uses default routing. + pub target_address: Option, } /// A versioned unsigned transaction envelope. @@ -392,6 +394,7 @@ impl UnsignedTransactionV0 { runtime_call: self.runtime_call.clone(), uniqueness: self.uniqueness, details: self.details.clone(), + target_address: self.target_address.clone(), }) } } @@ -417,6 +420,8 @@ pub struct TransactionV0 { pub uniqueness: UniquenessData, /// Transaction execution details including fees and gas limits. pub details: TxDetails, + /// Optional target address for execution; null uses default routing. + pub target_address: Option, } /// A versioned transaction envelope supporting different transaction formats. @@ -456,6 +461,7 @@ pub struct TransactionBuilder { max_fee: Option, gas_limit: Option>>, chain_id: Option, + target_address: Option, } impl TransactionBuilder { @@ -476,6 +482,7 @@ impl TransactionBuilder { max_fee: None, gas_limit: None, chain_id: None, + target_address: None, } } @@ -546,6 +553,12 @@ impl TransactionBuilder { self } + /// Sets the explicit target address for the transaction. + pub fn target_address(mut self, target_address: impl Into) -> Self { + self.target_address = Some(target_address.into()); + self + } + /// Builds an unsigned transaction with the configured parameters. /// /// Uses default values for any parameters that were not explicitly set: @@ -587,6 +600,7 @@ impl TransactionBuilder { gas_limit, chain_id, }, + target_address: self.target_address, }) } } diff --git a/python/py_sovereign_web3/rust/src/lib.rs b/python/py_sovereign_web3/rust/src/lib.rs index c8d5b2f7a3..baed3a26ed 100644 --- a/python/py_sovereign_web3/rust/src/lib.rs +++ b/python/py_sovereign_web3/rust/src/lib.rs @@ -118,11 +118,12 @@ struct PyUnsignedTransactionV0 { #[pymethods] impl PyUnsignedTransactionV0 { #[new] - #[pyo3(signature = (runtime_call, details, uniqueness=None))] + #[pyo3(signature = (runtime_call, details, uniqueness=None, target_address=None))] fn new( runtime_call: &Bound<'_, PyDict>, details: &PyTxDetails, uniqueness: Option<&PyUniquenessData>, + target_address: Option, ) -> PyResult { let call: serde_json::Value = pythonize::depythonize(runtime_call).map_err(|e| { PyValueError::new_err(format!("Failed to convert runtime_call dict to JSON: {e}")) @@ -139,6 +140,7 @@ impl PyUnsignedTransactionV0 { runtime_call: call, uniqueness, details: details.inner.clone(), + target_address, }; Ok(PyUnsignedTransactionV0 { inner: unsigned_tx }) diff --git a/python/py_sovereign_web3/src/sovereign_web3/sovereign_web3.pyi b/python/py_sovereign_web3/src/sovereign_web3/sovereign_web3.pyi index 2910d32135..b0280a1949 100644 --- a/python/py_sovereign_web3/src/sovereign_web3/sovereign_web3.pyi +++ b/python/py_sovereign_web3/src/sovereign_web3/sovereign_web3.pyi @@ -167,6 +167,7 @@ class UnsignedTransactionV0: runtime_call: Dict[str, Any], details: TxDetails, uniqueness: Optional[UniquenessData] = None, + target_address: Optional[str] = None, ) -> None: """Create V0 unsigned transaction. @@ -174,6 +175,7 @@ class UnsignedTransactionV0: runtime_call: Runtime call data as dictionary details: Transaction details uniqueness: Optional uniqueness data (defaults to default uniqueness) + target_address: Optional explicit target address Raises: ValueError: If runtime_call cannot be converted to JSON or uniqueness creation fails diff --git a/typescript/packages/multisig/src/index.test.ts b/typescript/packages/multisig/src/index.test.ts index 73bd3a4373..6c478c51f2 100644 --- a/typescript/packages/multisig/src/index.test.ts +++ b/typescript/packages/multisig/src/index.test.ts @@ -22,6 +22,7 @@ const createUnsignedTx = (nonce = 1): UnsignedTransaction => ({ gas_limit: null, chain_id: 1, }, + target_address: null, }); const createTransactionV0 = ( @@ -48,6 +49,7 @@ describe("MultisigTransaction", () => { gas_limit: null, chain_id: 1, }, + target_address: null, }; const tx = createTransactionV0(unsignedTx, "pubkey1", "sig1"); @@ -67,7 +69,6 @@ describe("MultisigTransaction", () => { unused_pub_keys: ["pubkey2"], signatures: [{ pub_key: "pubkey1", signature: "sig1" }], min_signers: 1, - target_address: null, ...unsignedTx, }, }; diff --git a/typescript/packages/multisig/src/index.ts b/typescript/packages/multisig/src/index.ts index ab21c4899b..3f0d3f36a9 100644 --- a/typescript/packages/multisig/src/index.ts +++ b/typescript/packages/multisig/src/index.ts @@ -258,7 +258,9 @@ export class MultisigTransaction { * Converts the multisig to a V1 transaction that can be submitted to the network. * @returns A V1 transaction containing all collected signatures and unused public keys */ - asTransaction(targetAddress: string | null = null): Transaction { + asTransaction(targetAddress?: string | null): Transaction { + const resolvedTargetAddress = + targetAddress ?? this.unsignedTx.target_address; return { V1: { runtime_call: this.unsignedTx.runtime_call, @@ -267,7 +269,7 @@ export class MultisigTransaction { unused_pub_keys: Array.from(this.unusedPubKeys), signatures: this.signatures, min_signers: this.minSigners, - target_address: targetAddress, + target_address: resolvedTargetAddress, }, }; } @@ -291,6 +293,7 @@ function asUnsignedTransaction( runtime_call: tx.V0.runtime_call, uniqueness: tx.V0.uniqueness, details: tx.V0.details, + target_address: tx.V0.target_address, }; } @@ -299,6 +302,7 @@ function asUnsignedTransaction( runtime_call: tx.V1.runtime_call, uniqueness: tx.V1.uniqueness, details: tx.V1.details, + target_address: tx.V1.target_address, }; } diff --git a/typescript/packages/multisig/tests/multisig.integration-test.ts b/typescript/packages/multisig/tests/multisig.integration-test.ts index 52cd33be00..9a62235e28 100644 --- a/typescript/packages/multisig/tests/multisig.integration-test.ts +++ b/typescript/packages/multisig/tests/multisig.integration-test.ts @@ -59,6 +59,7 @@ describe("multisig", async () => { runtime_call, uniqueness: { nonce: 0 }, details: { ...DEFAULT_TX_DETAILS, chain_id }, + target_address: null, }; const requiredSigners = 3; diff --git a/typescript/packages/types/src/index.ts b/typescript/packages/types/src/index.ts index 8f1b97d72f..2eca3e6fda 100644 --- a/typescript/packages/types/src/index.ts +++ b/typescript/packages/types/src/index.ts @@ -35,6 +35,8 @@ export type UnsignedTransaction = { uniqueness: Uniqueness; /** Transaction execution details including fees and gas limits */ details: TxDetails; + /** Optional target address for execution; null uses default routing */ + target_address: string | null; }; /** @@ -73,8 +75,6 @@ export type TransactionV1 = { signatures: SignatureAndPubKey[]; /** Minimum number of signatures required for transaction validity */ min_signers: number; - /** Optional target address for execution; null uses default routing */ - target_address: string | null; } & UnsignedTransaction; }; diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts index 9a0071401e..f08363d673 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts @@ -160,6 +160,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: 1, }, + target_address: null, } as any, { signer: createMockSigner(), @@ -205,6 +206,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: fixtureChainId, }, + target_address: null, } as any, { signer: createMockSigner(), @@ -287,6 +289,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, + target_address: null, }; await rollup.signAndSubmitTransaction(unsignedTx, { @@ -373,6 +376,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, + target_address: null, }; await rollup.signAndSubmitTransaction(unsignedTx, { @@ -468,6 +472,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, + target_address: null, }; // Each signer signs independently (same order as Rust: key3, key1) @@ -539,6 +544,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: 1, }, + target_address: null, }; const signedTx = await rollup.signTransactionForMultisig( @@ -643,6 +649,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, + target_address: null, }; const signedTx3 = await rollup.signTransactionForMultisig(unsignedTx, { @@ -719,6 +726,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: 1, }, + target_address: null, } as any, { signer: ledgerSigner, @@ -770,6 +778,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: 1, }, + target_address: null, } as any, { signer: ed25519Signer, @@ -870,6 +879,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, + target_address: null, }; return { diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts index 80f597e2c9..e8118fcedd 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts @@ -3,7 +3,9 @@ import { type Signer, isLedgerSolanaSigner } from "@sovereign-sdk/signers"; import type { Transaction, TransactionV1, + TxDetails, UnsignedTransaction, + Uniqueness, } from "@sovereign-sdk/types"; import { bytesToHex, hexToBytes } from "@sovereign-sdk/utils"; import bs58 from "bs58"; @@ -19,9 +21,21 @@ import { standardTypeBuilder, } from "./standard-rollup"; -export type SolanaOffchainUnsignedTransaction = - UnsignedTransaction & { - chain_name: string; +export type SolanaOffchainUnsignedTransaction = { + runtime_call: RuntimeCall; + uniqueness: Uniqueness; + details: TxDetails; + chain_name: string; +}; + +export type SolanaOffchainUnsignedTransactionV0 = + SolanaOffchainUnsignedTransaction & { + /** + * Signer-declared target address. Omitted from the serialized JSON when not set, + * preserving byte equivalence with pre-change signed messages so existing signatures + * continue to verify. See `AuthorizationData::address` (Rust) for routing semantics. + */ + target_address?: string; }; export type SolanaOffchainUnsignedTransactionV1 = @@ -338,11 +352,15 @@ export class SolanaSignableRollup { const schema = serializer.schema; const chainName = schema.chain_data.chain_name || ""; - const solanaUnsignedTx: SolanaOffchainUnsignedTransaction = { + const solanaUnsignedTx: SolanaOffchainUnsignedTransactionV0 = { runtime_call: unsignedTx.runtime_call, uniqueness: unsignedTx.uniqueness, details: unsignedTx.details, chain_name: chainName, + ...(unsignedTx.target_address !== null && + unsignedTx.target_address !== undefined && { + target_address: unsignedTx.target_address, + }), }; // JSON serialize the Solana unsigned transaction @@ -758,6 +776,7 @@ export class SolanaSignableRollup { runtime_call: tx.runtime_call, uniqueness: tx.uniqueness, details: tx.details, + target_address: tx.target_address, }; // Decode the signed `target_address` back to bytes so we can pass it through the same diff --git a/typescript/packages/web3/src/rollup/standard-rollup.test.ts b/typescript/packages/web3/src/rollup/standard-rollup.test.ts index 6c717c2781..1aeb76229d 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.test.ts @@ -54,6 +54,7 @@ describe("standardTypeBuilder", () => { max_fee: "1000", chain_id: 1, }, + target_address: null, }); }); @@ -74,6 +75,7 @@ describe("standardTypeBuilder", () => { max_fee: "1000", chain_id: 1, }, + target_address: null, }); }); @@ -100,6 +102,7 @@ describe("standardTypeBuilder", () => { gas_limit: [1000000, 1000000], chain_id: 1, }, + target_address: null, }); }); }); @@ -118,6 +121,7 @@ describe("standardTypeBuilder", () => { chain_id: 1, gas_limit: null, }, + target_address: null, }, sender: new Uint8Array([4, 5, 6]), signature: new Uint8Array([7, 8, 9]), @@ -138,6 +142,7 @@ describe("standardTypeBuilder", () => { chain_id: 1, gas_limit: null, }, + target_address: null, }, }); }); diff --git a/typescript/packages/web3/src/rollup/standard-rollup.ts b/typescript/packages/web3/src/rollup/standard-rollup.ts index 02d41cef98..aab9016787 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.ts @@ -63,6 +63,7 @@ export function standardTypeBuilder< runtime_call: runtimeCall, uniqueness, details, + target_address: overrides.target_address ?? null, } as S["UnsignedTransaction"]; }, async transaction({ From 93392db7ec616823ac518f1b0437cc416081a8db Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 5 May 2026 17:09:34 +0200 Subject: [PATCH 12/34] Rename target_address to address_override --- crates/full-node/sov-api-spec/openapi-v3.yaml | 4 +- .../sov-rollup-apis/src/endpoints/simulate.rs | 12 +- .../tests/integration/rest_api.rs | 8 +- .../tests/stf_blueprint/mod.rs | 2 +- .../stf_blueprint/operator/auth_eip712.rs | 17 +-- .../stf_blueprint/optimistic/da_simulation.rs | 2 +- .../sov-accounts/tests/integration/main.rs | 70 +++++----- .../sov-evm/src/authenticate.rs | 2 +- .../module-system/sov-capabilities/src/lib.rs | 10 +- .../src/runtime/capabilities/authorization.rs | 6 +- .../src/transaction/types/v0.rs | 8 +- .../src/transaction/types/v1.rs | 8 +- .../src/transaction/unsigned/v0.rs | 20 +-- .../src/transaction/unsigned/v1.rs | 8 +- .../tests/integration/transaction.rs | 14 +- .../src/authentication/mod.rs | 22 ++-- .../src/authentication/payload.rs | 12 +- .../tests/integration/main.rs | 120 +++++++++--------- crates/web3/src/rust.rs | 12 +- crates/web3/src/schema.rs | 22 ++-- python/py_sovereign_web3/rust/src/lib.rs | 6 +- .../src/sovereign_web3/sovereign_web3.pyi | 4 +- .../packages/multisig/src/index.test.ts | 12 +- typescript/packages/multisig/src/index.ts | 12 +- .../tests/multisig.integration-test.ts | 2 +- typescript/packages/types/src/index.ts | 4 +- .../src/rollup/solana-signable-rollup.test.ts | 66 +++++----- .../web3/src/rollup/solana-signable-rollup.ts | 64 +++++----- .../web3/src/rollup/standard-rollup.test.ts | 14 +- .../web3/src/rollup/standard-rollup.ts | 4 +- 30 files changed, 284 insertions(+), 283 deletions(-) diff --git a/crates/full-node/sov-api-spec/openapi-v3.yaml b/crates/full-node/sov-api-spec/openapi-v3.yaml index b90b9651f1..708e431240 100644 --- a/crates/full-node/sov-api-spec/openapi-v3.yaml +++ b/crates/full-node/sov-api-spec/openapi-v3.yaml @@ -1218,10 +1218,10 @@ components: type: object description: Optional uniqueness data for the transaction additionalProperties: true - target_address: + address_override: type: string nullable: true - description: Optional target address for execution; null or omission uses default routing + description: Optional address override for execution; null or omission uses default routing required: - sender - call diff --git a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs index a5a9097a5c..90f4a1f415 100644 --- a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs +++ b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs @@ -308,13 +308,13 @@ impl> SovereignSimulate { let credential_id = CredentialId::from_str(¶ms.sender).map_err(|_| { SimulateError::InvalidInput("failed to parse sender credential id".to_owned()) })?; - let address = params - .target_address + let address_override = params + .address_override .as_deref() .map(S::Address::from_str) .transpose() .map_err(|e| { - SimulateError::InvalidInput(format!("failed to parse target address: {e:?}")) + SimulateError::InvalidInput(format!("failed to parse address override: {e:?}")) })?; let uniqueness = params.uniqueness.unwrap_or_else(|| { let generation = Uniqueness::::default() @@ -330,7 +330,7 @@ impl> SovereignSimulate { credential_id, default_address: credential_id.into(), credentials: Credentials::new(credential_id), - address, + address_override, }) } @@ -413,8 +413,8 @@ pub struct SimulateParameters { /// Optional uniqueness data for the transaction. /// If not provided a valid uniqueness will be used. pub uniqueness: Option, - /// Optional target address for execution. If not provided, default routing is used. - pub target_address: Option, + /// Optional address override for execution. If not provided, default routing is used. + pub address_override: Option, } impl> SimulateEndpoint for SovereignSimulate { diff --git a/crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs b/crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs index 7dffa2bc48..7650450324 100644 --- a/crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs +++ b/crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs @@ -173,7 +173,7 @@ async fn test_simulation_success() { } #[tokio::test(flavor = "multi_thread")] -async fn test_simulation_success_with_target_address() { +async fn test_simulation_success_with_address_override() { let mut data = TestData::setup().await; let delegated_credential: CredentialId = [9u8; 32].into(); let registration_call = json_obj!({ @@ -206,7 +206,7 @@ async fn test_simulation_success_with_target_address() { }); data.send_storage(); - let target_address = data.user.address(); + let address_override = data.user.address(); let receiver = TestUser::::generate_with_default_balance().address(); let call = sov_bank::CallMessage::::Transfer { to: receiver, @@ -217,7 +217,7 @@ async fn test_simulation_success_with_target_address() { }; let params = json_obj!({ "sender": delegated_credential.to_string(), - "target_address": target_address.to_string(), + "address_override": address_override.to_string(), "call": { "bank": call, }, @@ -241,7 +241,7 @@ async fn test_simulation_success_with_target_address() { "value": { "token_transferred": { "from": { - "user": target_address + "user": address_override }, "to": { "user": receiver diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs index e4a65b40b7..bbab80f10a 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs @@ -262,7 +262,7 @@ pub fn create_tx_bad_sig>( gas_limit: None, chain_id, }, - target_address: inner.target_address, + address_override: inner.address_override, }), Transaction::V1(_inner) => { todo!("Bad signature generation for multisig transactions is not yet supported"); diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs index 46eb57f649..2143856c34 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs @@ -176,12 +176,12 @@ pub fn sign_utx_in_place>( /// Signs a V1 (multisig) unsigned transaction with the given private key using EIP712. /// The credential_address is computed from the provided multisig. -/// `target_address` is forwarded into the signed `UnsignedTransactionV1`; -/// pass `None` to match pre-target routing or `Some(X)` to sign for a specific target account. +/// `address_override` is forwarded into the signed `UnsignedTransactionV1`; +/// pass `None` to match default routing or `Some(X)` to sign for a specific override account. pub fn sign_utx_v1_in_place>( utx: &UnsignedTransactionV0, multisig: &Multisig<::PublicKey>, - target_address: Option, + address_override: Option, private_key: &::PrivateKey, ) -> ::Signature { let schema = TestSchemaProvider::get_schema(); @@ -200,7 +200,7 @@ pub fn sign_utx_v1_in_place>( uniqueness: utx.uniqueness, details: utx.details.clone(), credential_address, - target_address, + address_override, }); let utx_bytes = borsh::to_vec(&utx_enum).expect("Failed to serialize unsigned transaction"); let eip712_signing_data = schema @@ -306,17 +306,18 @@ fn test_multisig_signature_verification() { // Generate a signature from a random private key that's not part of the multisig. We'll use this in some of the test cases. let random_private_key = TestPrivateKey::generate(); - let target = None; + let address_override = None; let make_multisig_tx = |generation| { let utx = create_utx_with_generation::(encode_message::<_, RT>(), generation); let signatures = multisig_keys .iter() - .map(|key| sign_utx_v1_in_place(&utx, &multisig, target, key)) + .map(|key| sign_utx_v1_in_place(&utx, &multisig, address_override, key)) .collect::>(); - let random_signature = sign_utx_v1_in_place(&utx, &multisig, target, &random_private_key); + let random_signature = + sign_utx_v1_in_place(&utx, &multisig, address_override, &random_private_key); ( - utx.to_multisig_tx(multisig.clone(), target), + utx.to_multisig_tx(multisig.clone(), address_override), signatures, random_signature, ) diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs index de1c77af8a..bc982a51d1 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs @@ -53,7 +53,7 @@ pub fn simulate_da_with_bad_sig(key: TestPrivateKey) -> Vec { ), uniqueness: UniquenessData::Generation(create_token_message.generation), details: create_token_message.details, - target_address: None, + address_override: None, }); vec![encode_with_auth(tx)] diff --git a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs index e3252a73e6..6b7e62babf 100644 --- a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs +++ b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs @@ -22,7 +22,7 @@ type RT = TestAccountsRuntime; struct TestData { account_1: TestUser, // `account_2` is intentionally kept in the fixture for the upcoming - // `target_address` PR. Until then it has no readers — silence the lint. + // `address_override` PR. Until then it has no readers — silence the lint. #[allow(dead_code)] account_2: TestUser, non_registered_account: TestUser, @@ -517,27 +517,27 @@ fn make_multisig_env() -> MultisigEnv { fn make_v1_tx( multisig: &sov_modules_api::Multisig<<::CryptoSpec as CryptoSpec>::PublicKey>, inner_credential: sov_modules_api::CredentialId, - target_address: Option<::Address>, + address_override: Option<::Address>, ) -> Version1 { UnsignedTransactionV0::::new_with_details( TestAccountsRuntimeCall::Accounts(CallMessage::InsertCredentialId(inner_credential)), sov_modules_api::capabilities::UniquenessData::Generation(0), default_test_tx_details::(), ) - .to_multisig_tx(multisig.clone(), target_address) + .to_multisig_tx(multisig.clone(), address_override) } fn make_v0_tx( sender: &TestUser, inner_credential: sov_modules_api::CredentialId, - target_address: Option<::Address>, + address_override: Option<::Address>, ) -> Transaction { let mut utx = UnsignedTransactionV0::::new_with_details( TestAccountsRuntimeCall::Accounts(CallMessage::InsertCredentialId(inner_credential)), sov_modules_api::capabilities::UniquenessData::Generation(0), default_test_tx_details::(), ); - utx.target_address = target_address; + utx.address_override = address_override; utx.sign(sender.private_key(), &>::CHAIN_HASH) } @@ -558,11 +558,11 @@ fn submit_v1(tx: Version1) -> TransactionType { }) } -/// V1 tx with `target_address = None` executes as the multisig's default +/// V1 tx with `address_override = None` executes as the multisig's default /// address. Evidence: the `InsertCredentialId` call writes /// `(multisig_default, inner_credential)` to `account_owners`. #[test] -fn test_v1_target_none_uses_default_resolver() { +fn test_v1_address_override_none_uses_default_resolver() { let MultisigEnv { keys, multisig, @@ -590,18 +590,18 @@ fn test_v1_target_none_uses_default_resolver() { accounts .is_explicitly_authorized(&multisig_default_address, &inner_credential, state) .unwrap(), - "target=None should route to multisig default; the InsertCredentialId \ + "address_override=None should route to multisig default; the InsertCredentialId \ call must write the new credential under that address" ); }), }); } -/// V1 tx with `target_address = Some(X)` where `(X, multisig_credential_id)` is +/// V1 tx with `address_override = Some(X)` where `(X, multisig_credential_id)` is /// authorized resolves context as `X`. Evidence: `InsertCredentialId` writes /// `(X, inner_credential)` — not `(multisig_default, inner_credential)`. #[test] -fn test_v1_target_some_authorized_succeeds() { +fn test_v1_address_override_some_authorized_succeeds() { let MultisigEnv { keys, multisig, @@ -633,7 +633,7 @@ fn test_v1_target_some_authorized_succeeds() { assert: Box::new(move |result, state| { assert!( result.tx_receipt.is_successful(), - "V1 target=Some(authorized) should succeed, got {:?}", + "V1 address_override=Some(authorized) should succeed, got {:?}", result.tx_receipt ); let accounts = Accounts::::default(); @@ -641,17 +641,17 @@ fn test_v1_target_some_authorized_succeeds() { accounts .is_explicitly_authorized(&alice_address, &inner_credential, state) .unwrap(), - "InsertCredentialId should write under target_address, not multisig default" + "InsertCredentialId should write under address_override, not multisig default" ); }), }); } -/// V1 tx with `target_address = Some(Y)` where `(Y, credential_id) ∉ account_owners` +/// V1 tx with `address_override = Some(Y)` where `(Y, credential_id) ∉ account_owners` /// is skipped, not reverted. No state is mutated — in particular, no auto-register /// on the `Some` path. #[test] -fn test_v1_target_some_unauthorized_skipped() { +fn test_v1_address_override_some_unauthorized_skipped() { let MultisigEnv { keys, multisig, @@ -679,7 +679,7 @@ fn test_v1_target_some_unauthorized_skipped() { TxEffect::Skipped(SkippedTxContents { error, .. }) => { let msg = error.to_string(); assert!( - msg.contains("not authorized for target address"), + msg.contains("not authorized for address override"), "unexpected skip reason: {msg}" ); } @@ -692,16 +692,16 @@ fn test_v1_target_some_unauthorized_skipped() { !accounts .is_explicitly_authorized(&unowned_address, &inner_credential, state) .unwrap(), - "unauthorized target path must not auto-register any tuple" + "unauthorized address_override path must not auto-register any tuple" ); }), }); } -/// `target_address` is part of the signed bytes: tampering with it after signing +/// `address_override` is part of the signed bytes: tampering with it after signing /// invalidates the signature. #[test] -fn test_v1_target_tamper_breaks_signature() { +fn test_v1_address_override_tamper_breaks_signature() { let MultisigEnv { keys, multisig, @@ -734,7 +734,7 @@ fn test_v1_target_tamper_breaks_signature() { sign_v1(&mut tx, &keys[0]); sign_v1(&mut tx, &keys[1]); // Mutate after signing. - tx.target_address = Some(tampered_address); + tx.address_override = Some(tampered_address); runner.execute_transaction(TransactionTestCase { input: submit_v1(tx), @@ -743,7 +743,7 @@ fn test_v1_target_tamper_breaks_signature() { let msg = error.to_string(); assert!( msg.contains("Verification equation was not satisfied"), - "expected signature failure after target_address tamper, got: {msg}" + "expected signature failure after address_override tamper, got: {msg}" ); } other => panic!("expected skipped tx, got {other:?}"), @@ -751,9 +751,9 @@ fn test_v1_target_tamper_breaks_signature() { }); } -/// V0 tx with `target_address = None` executes as the signer's default address. +/// V0 tx with `address_override = None` executes as the signer's default address. #[test] -fn test_v0_target_none_uses_default_resolver() { +fn test_v0_address_override_none_uses_default_resolver() { let sender = TestUser::::generate_with_default_balance(); let genesis_config = HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![sender.clone()]); @@ -773,16 +773,16 @@ fn test_v0_target_none_uses_default_resolver() { accounts .is_explicitly_authorized(&sender.address(), &inner_credential, state) .unwrap(), - "target=None should route to the signer's default address" + "address_override=None should route to the signer's default address" ); }), }); } -/// V0 tx with `target_address = Some(X)` where `(X, credential_id)` is +/// V0 tx with `address_override = Some(X)` where `(X, credential_id)` is /// authorized resolves context as `X`. #[test] -fn test_v0_target_some_authorized_succeeds() { +fn test_v0_address_override_some_authorized_succeeds() { let sender = TestUser::::generate_with_default_balance(); let alice = TestUser::::generate_with_default_balance(); let alice_address = alice.address(); @@ -805,7 +805,7 @@ fn test_v0_target_some_authorized_succeeds() { assert: Box::new(move |result, state| { assert!( result.tx_receipt.is_successful(), - "V0 target=Some(authorized) should succeed, got {:?}", + "V0 address_override=Some(authorized) should succeed, got {:?}", result.tx_receipt ); let accounts = Accounts::::default(); @@ -813,16 +813,16 @@ fn test_v0_target_some_authorized_succeeds() { accounts .is_explicitly_authorized(&alice_address, &inner_credential, state) .unwrap(), - "InsertCredentialId should write under target_address, not sender default" + "InsertCredentialId should write under address_override, not sender default" ); }), }); } -/// V0 tx with `target_address = Some(Y)` where `(Y, credential_id) ∉ account_owners` +/// V0 tx with `address_override = Some(Y)` where `(Y, credential_id) ∉ account_owners` /// is skipped, not reverted. #[test] -fn test_v0_target_some_unauthorized_skipped() { +fn test_v0_address_override_some_unauthorized_skipped() { let sender = TestUser::::generate_with_default_balance(); let genesis_config = HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![sender.clone()]); @@ -841,7 +841,7 @@ fn test_v0_target_some_unauthorized_skipped() { TxEffect::Skipped(SkippedTxContents { error, .. }) => { let msg = error.to_string(); assert!( - msg.contains("not authorized for target address"), + msg.contains("not authorized for address override"), "unexpected skip reason: {msg}" ); } @@ -852,16 +852,16 @@ fn test_v0_target_some_unauthorized_skipped() { !accounts .is_explicitly_authorized(&unowned_address, &inner_credential, state) .unwrap(), - "unauthorized target path must not auto-register any tuple" + "unauthorized address_override path must not auto-register any tuple" ); }), }); } -/// `target_address` is part of the signed bytes for V0 as well: tampering with it +/// `address_override` is part of the signed bytes for V0 as well: tampering with it /// after signing invalidates the signature. #[test] -fn test_v0_target_tamper_breaks_signature() { +fn test_v0_address_override_tamper_breaks_signature() { let sender = TestUser::::generate_with_default_balance(); let alice = TestUser::::generate_with_default_balance(); let alice_address = alice.address(); @@ -886,7 +886,7 @@ fn test_v0_target_tamper_breaks_signature() { let Transaction::V0(inner) = &mut tx else { panic!("expected v0 tx"); }; - inner.target_address = Some(tampered_address); + inner.address_override = Some(tampered_address); runner.execute_transaction(TransactionTestCase { input: submit_v0(tx), @@ -895,7 +895,7 @@ fn test_v0_target_tamper_breaks_signature() { let msg = error.to_string(); assert!( msg.contains("Verification equation was not satisfied"), - "expected signature failure after target_address tamper, got: {msg}" + "expected signature failure after address_override tamper, got: {msg}" ); } other => panic!("expected skipped tx, got {other:?}"), diff --git a/crates/module-system/module-implementations/sov-evm/src/authenticate.rs b/crates/module-system/module-implementations/sov-evm/src/authenticate.rs index 4e55b205ee..2904241d96 100644 --- a/crates/module-system/module-implementations/sov-evm/src/authenticate.rs +++ b/crates/module-system/module-implementations/sov-evm/src/authenticate.rs @@ -220,7 +220,7 @@ where credential_id, credentials, default_address: S::Address::from_vm_address(ethereum_address), - address: None, + address_override: None, } } diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index fa33b36436..7306e7ad7d 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -316,17 +316,17 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' execution_context: ExecutionContext, sequencer_type: SequencerType, ) -> anyhow::Result> { - let sender = match auth_data.address { - Some(target_address) => { + let sender = match auth_data.address_override { + Some(address_override) => { anyhow::ensure!( self.accounts.is_explicitly_authorized( - &target_address, + &address_override, &auth_data.credential_id, state, )?, - "not authorized for target address" + "not authorized for address override" ); - target_address + address_override } None => auth_data.default_address, }; diff --git a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs index 9d26ed4164..786522d83f 100644 --- a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs +++ b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs @@ -103,13 +103,13 @@ pub struct AuthorizationData { /// The default address. pub default_address: S::Address, - /// Signer-declared target address for execution. + /// Signer-declared override of the default execution address. /// /// `None` ⇒ resolve to the credential's default address. /// `Some(X)` ⇒ requires an explicit `(X, credential_id)` entry in `account_owners`; /// the transaction is skipped otherwise. /// - /// Set by authenticators that carry a signed target_address (sov-tx V0/V1, + /// Set by authenticators that carry a signed `address_override` (sov-tx V0/V1, /// Solana off-chain auth). Other authenticators leave it `None`. - pub address: Option, + pub address_override: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs index 295f060bbe..dea7ed7ed8 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs @@ -41,9 +41,9 @@ pub struct Version0, - /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. #[serde(default)] - pub target_address: Option, + pub address_override: Option, } impl Version0 { @@ -53,7 +53,7 @@ impl Version0 { runtime_call: self.runtime_call.clone(), uniqueness: self.uniqueness, details: self.details.clone(), - target_address: self.target_address, + address_override: self.address_override, }) } @@ -75,7 +75,7 @@ impl Version0 { credential_id, credentials: Credentials::new(pub_key), default_address: credential_id.into(), - address: self.target_address, + address_override: self.address_override, }) } } diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs index aacf0a5a71..e2e1c7faf5 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs @@ -87,9 +87,9 @@ pub struct Version1, - /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. #[serde(default)] - pub target_address: Option, + pub address_override: Option, } impl Version1 { @@ -114,7 +114,7 @@ impl Version1 { uniqueness: self.uniqueness, details: self.details.clone(), credential_address: self.credential_address(), - target_address: self.target_address, + address_override: self.address_override, }) } } @@ -192,7 +192,7 @@ impl Version1 { credential_id, credentials: Credentials::new(multisig), default_address: credential_id.into(), - address: self.target_address, + address_override: self.address_override, }) } } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs index ef2ca1e308..cd6d59533d 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs @@ -29,9 +29,9 @@ pub struct UnsignedTransactionV0 { pub uniqueness: UniquenessData, /// Data related to fees and gas handling. pub details: TxDetails, - /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. #[serde(default)] - pub target_address: Option, + pub address_override: Option, } // Manually implemented to ensure correct trait bounds (derive would require R: Clone/PartialEq) @@ -41,7 +41,7 @@ impl Clone for UnsignedTransactionV0 { runtime_call: self.runtime_call.clone(), uniqueness: self.uniqueness, details: self.details.clone(), - target_address: self.target_address, + address_override: self.address_override, } } } @@ -50,7 +50,7 @@ impl PartialEq for UnsignedTransactionV0 self.runtime_call == other.runtime_call && self.uniqueness == other.uniqueness && self.details == other.details - && self.target_address == other.target_address + && self.address_override == other.address_override } } impl Eq for UnsignedTransactionV0 {} @@ -88,7 +88,7 @@ impl UnsignedTransactionV0 { gas_limit, chain_id, }, - target_address: None, + address_override: None, } } @@ -102,7 +102,7 @@ impl UnsignedTransactionV0 { runtime_call, uniqueness, details, - target_address: None, + address_override: None, } } @@ -119,16 +119,16 @@ impl UnsignedTransactionV0 { runtime_call: self.runtime_call, uniqueness: self.uniqueness, details: self.details, - target_address: self.target_address, + address_override: self.address_override, }) } /// Creates a new `V1` transaction from this unsigned transaction. - /// See [`crate::capabilities::AuthorizationData::address`] for `target_address` routing. + /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. pub fn to_multisig_tx( self, multisig: Multisig<::PublicKey>, - target_address: Option, + address_override: Option, ) -> Version1 { Version1 { signatures: SafeVec::new(), @@ -140,7 +140,7 @@ impl UnsignedTransactionV0 { runtime_call: self.runtime_call, uniqueness: self.uniqueness, details: self.details, - target_address, + address_override, } } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs index 0ae3c653b9..1df3f23c9a 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs @@ -31,9 +31,9 @@ pub struct UnsignedTransactionV1 { /// and prevent credential malleability from reusing signed bytes in a different /// multisig envelope. pub credential_address: S::Address, - /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. #[serde(default)] - pub target_address: Option, + pub address_override: Option, } impl Clone for UnsignedTransactionV1 { @@ -43,7 +43,7 @@ impl Clone for UnsignedTransactionV1 { uniqueness: self.uniqueness, details: self.details.clone(), credential_address: self.credential_address, - target_address: self.target_address, + address_override: self.address_override, } } } @@ -53,7 +53,7 @@ impl PartialEq for UnsignedTransactionV1 && self.uniqueness == other.uniqueness && self.details == other.details && self.credential_address == other.credential_address - && self.target_address == other.target_address + && self.address_override == other.address_override } } impl Eq for UnsignedTransactionV1 {} diff --git a/crates/module-system/sov-modules-api/tests/integration/transaction.rs b/crates/module-system/sov-modules-api/tests/integration/transaction.rs index 888bc385f7..651d433b1a 100644 --- a/crates/module-system/sov-modules-api/tests/integration/transaction.rs +++ b/crates/module-system/sov-modules-api/tests/integration/transaction.rs @@ -37,7 +37,7 @@ fn test_serde_serialize_tx() { runtime_call: TestOptimisticRuntimeCall::ValueSetter(native_call), uniqueness: uniq, details, - target_address: None, + address_override: None, }; let native = Transaction::::V0(native_tx); let native_json = serde_json::to_value(&native).unwrap(); @@ -77,7 +77,7 @@ fn test_schema_and_native_serialization_consistency() { "gas_limit": [500, 500], "chain_id": 1337 }, - "target_address": null + "address_override": null } }"#; let schema = Schema::of_single_type::>().unwrap(); @@ -106,7 +106,7 @@ fn test_schema_and_native_serialization_consistency() { runtime_call: TestOptimisticRuntimeCall::ValueSetter(native_call), uniqueness: uniq, details, - target_address: None, + address_override: None, }; let native = Transaction::::V0(native_tx); let native_bytes = borsh::to_vec(&native).unwrap(); @@ -161,7 +161,7 @@ mod web3_compatibility { "gas_limit": null, "chain_id": 1337 }, - "target_address": null + "address_override": null }}"#; let schema = Schema::of_single_type::>().unwrap(); @@ -188,7 +188,7 @@ mod web3_compatibility { "gas_limit": [500, 500], "chain_id": 1337 }, - "target_address": null + "address_override": null }}"#; let schema = Schema::of_single_type::>().unwrap(); @@ -219,7 +219,7 @@ mod web3_compatibility { "gas_limit": [500, 500], "chain_id": 1337 }, - "target_address": null + "address_override": null } }"#; let schema = Schema::of_single_type::>().unwrap(); @@ -251,7 +251,7 @@ mod web3_compatibility { "gas_limit": null, "chain_id": 1337 }, - "target_address": null + "address_override": null } }"#; let schema = Schema::of_single_type::>().unwrap(); diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs index 1f49121462..d860713dd7 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs @@ -122,12 +122,12 @@ fn verify_signatures( } /// Builds authorization data for either single-sig or multisig transactions. -/// `target_address` is forwarded from the signed payload — see -/// [`sov_modules_api::capabilities::AuthorizationData::address`] for routing semantics. +/// `address_override` is forwarded from the signed payload — see +/// [`sov_modules_api::capabilities::AuthorizationData::address_override`] for routing semantics. fn build_auth_data( unpacked: &UnpackedSolanaMessage, uniqueness: UniquenessData, - target_address: Option, + address_override: Option, raw_tx_hash: TxHash, meter: &mut impl GasMeter, ) -> Result, AuthenticationError> { @@ -157,7 +157,7 @@ fn build_auth_data( credential_id, credentials: Credentials::new(pub_key.clone()), default_address: credential_id.into(), - address: target_address, + address_override, }) } UnpackedSolanaMessage::V1 { @@ -186,7 +186,7 @@ fn build_auth_data( credential_id, credentials: Credentials::new(multisig), default_address: credential_id.into(), - address: target_address, + address_override, }) } } @@ -283,14 +283,14 @@ where raw_tx_hash, ) }; - let (provided_chain_name, target_address, unsigned_tx) = match &unpacked_message { + let (provided_chain_name, address_override, unsigned_tx) = match &unpacked_message { UnpackedSolanaMessage::V0 { .. } => { let tx = SolanaOffchainUnsignedTransactionV0::::unmetered_deserialize(json_slice) .map_err(deser_err)?; - let target_address = tx.target_address; + let address_override = tx.address_override; ( tx.chain_name.to_string(), - target_address, + address_override, tx.into_unsigned_tx(), ) } @@ -309,10 +309,10 @@ where *min_signers, raw_tx_hash, )?; - let target_address = tx.target_address; + let address_override = tx.address_override; ( tx.chain_name.to_string(), - target_address, + address_override, tx.into_unsigned_tx(), ) } @@ -348,7 +348,7 @@ where let authorization_data = build_auth_data::( &unpacked_message, unsigned_tx.uniqueness(), - target_address, + address_override, raw_tx_hash, state, )?; diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index e2dc866f60..159d3ce6ae 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -33,10 +33,10 @@ pub struct SolanaOffchainUnsignedTransactionV0 /// from malicious chains (if the chain name matches some other chain the use but didn't expect /// to be signing for right now). pub chain_name: SafeString, - /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. /// Skipped from JSON when `None` so pre-change signed messages stay byte-identical. #[serde(default, skip_serializing_if = "Option::is_none")] - pub target_address: Option, + pub address_override: Option, } impl SolanaOffchainUnsignedTransactionV0 @@ -50,7 +50,7 @@ where runtime_call: self.runtime_call, uniqueness: self.uniqueness, details: self.details, - target_address: self.target_address, + address_override: self.address_override, }) } @@ -83,10 +83,10 @@ pub struct SolanaOffchainUnsignedTransactionV1 /// This is the "multisig address" except if the credential is mapped to another address in /// `sov-accounts`. pub multisig_id: S::Address, - /// Signer-declared target address. See [`AuthorizationData::address`] for routing semantics. + /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. /// Skipped from JSON when `None` so pre-change signed messages stay byte-identical. #[serde(default, skip_serializing_if = "Option::is_none")] - pub target_address: Option, + pub address_override: Option, /// Message format version. Must be `1` for this struct. #[serde(deserialize_with = "deserialize_version_1")] pub version: u8, @@ -116,7 +116,7 @@ where uniqueness: self.uniqueness, details: self.details, credential_address: self.multisig_id, - target_address: self.target_address, + address_override: self.address_override, }) } diff --git a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs index 8d1ce366e3..bd019b0bc5 100644 --- a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs +++ b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs @@ -99,7 +99,7 @@ async fn create_test_rollup() -> anyhow::Result<( /// Variant of [`create_test_rollup`] that seeds additional `(credential_id, address)` pairs /// into `sov-accounts` at genesis. Callers use this to authorize a multisig credential for a -/// specific target address so V1 transactions carrying `target_address = Some(X)` can resolve +/// specific address override so V1 transactions carrying `address_override = Some(X)` can resolve /// their sender to `X` instead of the multisig's default address. /// /// `extra_account_owners_for` is a closure invoked with the generated admin user so the caller @@ -198,13 +198,13 @@ async fn create_test_rollup_with_extra_account_owners( } fn create_transfer_tx_json(amount: Amount, recipient: &str) -> String { - create_transfer_tx_json_with_target(amount, recipient, None) + create_transfer_tx_json_with_address_override(amount, recipient, None) } -fn create_transfer_tx_json_with_target( +fn create_transfer_tx_json_with_address_override( amount: Amount, recipient: &str, - target_address: Option<::Address>, + address_override: Option<::Address>, ) -> String { let msg: TestRuntimeCall = TestRuntimeCall::Bank(BankCallMessage::Transfer { to: ::Address::from_str(recipient).unwrap(), @@ -226,7 +226,7 @@ fn create_transfer_tx_json_with_target( uniqueness: unsigned_tx.uniqueness, details: unsigned_tx.details, chain_name: config_value!("CHAIN_NAME").to_string().try_into().unwrap(), - target_address, + address_override, }; serde_json::to_string(&solana_unsigned_tx).unwrap() @@ -491,7 +491,7 @@ fn create_multisig_transfer_tx_json( amount: Amount, recipient: &str, multisig_id: ::Address, - target_address: Option<::Address>, + address_override: Option<::Address>, ) -> String { let msg: TestRuntimeCall = TestRuntimeCall::Bank(BankCallMessage::Transfer { to: ::Address::from_str(recipient).unwrap(), @@ -514,7 +514,7 @@ fn create_multisig_transfer_tx_json( details: unsigned_tx.details, chain_name: config_value!("CHAIN_NAME").to_string().try_into().unwrap(), multisig_id, - target_address, + address_override, version: 1, }; serde_json::to_string(&solana_unsigned_tx).unwrap() @@ -1161,13 +1161,13 @@ async fn test_submit_ledger_signed_multisig_transaction() { ); } -/// End-to-end plumbing test: a V1 transaction carrying `target_address = Some(admin)` — where +/// End-to-end plumbing test: a V1 transaction carrying `address_override = Some(admin)` — where /// genesis authorizes `(admin.address(), multisig_credential_id)` in `account_owners` — routes -/// execution as `admin`, not as the multisig's default address. Proves that `target_address` +/// execution as `admin`, not as the multisig's default address. Proves that `address_override` /// flows from the signed JSON through `authenticate`, `build_auth_data`, `AuthorizationData`, /// and into `resolve_sender`. #[tokio::test(flavor = "multi_thread")] -async fn test_submit_multisig_with_authorized_target_address() { +async fn test_submit_multisig_with_authorized_address_override() { // Build a 2-of-3 multisig (its default address is never funded). let key1 = Ed25519PrivateKey::generate(); let key2 = Ed25519PrivateKey::generate(); @@ -1230,7 +1230,7 @@ async fn test_submit_multisig_with_authorized_target_address() { let response = submit_tx(test_rollup.api_client(), raw_tx_bytes).await; assert!( response.status().is_success(), - "Expected multisig-with-target_address transaction to succeed. Response: {response:?}" + "Expected multisig-with-address_override transaction to succeed. Response: {response:?}" ); // Recipient got the transfer. @@ -1238,7 +1238,7 @@ async fn test_submit_multisig_with_authorized_target_address() { assert_eq!( recipient_balance, Some(Amount::new(7_000)), - "Expected recipient to have received 7,000 tokens via target-routed multisig tx" + "Expected recipient to have received 7,000 tokens via override-routed multisig tx" ); // Admin's balance decreased (fees + transferred amount). We only check the ordering @@ -1246,7 +1246,7 @@ async fn test_submit_multisig_with_authorized_target_address() { let admin_balance_after = query_balance(&test_rollup.client, &admin_address_str).await; assert!( admin_balance_after < admin_balance_before, - "Expected admin balance to decrease after target-routed tx; before={admin_balance_before:?}, after={admin_balance_after:?}" + "Expected admin balance to decrease after override-routed tx; before={admin_balance_before:?}, after={admin_balance_after:?}" ); // The multisig's default address was never funded and should still have no balance — @@ -1264,7 +1264,7 @@ async fn test_submit_multisig_with_authorized_target_address() { fn build_v1_payload( recipient: &str, multisig_id: ::Address, - target_address: Option<::Address>, + address_override: Option<::Address>, ) -> SolanaOffchainUnsignedTransactionV1 { let call: TestRuntimeCall = TestRuntimeCall::Bank(BankCallMessage::Transfer { to: ::Address::from_str(recipient).unwrap(), @@ -1287,49 +1287,49 @@ fn build_v1_payload( details: unsigned_tx.details, chain_name: config_value!("CHAIN_NAME").to_string().try_into().unwrap(), multisig_id, - target_address, + address_override, version: 1, } } -/// `target_address: None` is omitted from the serialized JSON, so pre-change signed messages +/// `address_override: None` is omitted from the serialized JSON, so pre-change signed messages /// remain byte-identical and existing signatures verify unchanged. #[test] -fn test_v1_payload_omits_target_address_when_none() { +fn test_v1_payload_omits_address_override_when_none() { let multisig_address: ::Address = [0xABu8; 32].into(); let payload = build_v1_payload(RECIPIENT_ADDRESS, multisig_address, None); let json = serde_json::to_string(&payload).expect("serialize"); assert!( - !json.contains("target_address"), - "target_address must not appear in JSON when it is None; got: {json}" + !json.contains("address_override"), + "address_override must not appear in JSON when it is None; got: {json}" ); } -/// Pre-change JSON (no `target_address` key) deserializes into a payload with `target_address: +/// Pre-change JSON (no `address_override` key) deserializes into a payload with `address_override: /// None`. Pins the `#[serde(default)]` promise: future refactors cannot silently break deployed /// Solana signers. #[test] -fn test_v1_payload_without_target_address_field_deserializes_as_none() { +fn test_v1_payload_without_address_override_field_deserializes_as_none() { let multisig_address: ::Address = [0xABu8; 32].into(); let expected = build_v1_payload(RECIPIENT_ADDRESS, multisig_address, None); let canonical_json = serde_json::to_string(&expected).expect("serialize baseline"); assert!( - !canonical_json.contains("target_address"), - "guard: the canonical None-payload already omits target_address" + !canonical_json.contains("address_override"), + "guard: the canonical None-payload already omits address_override" ); let parsed: SolanaOffchainUnsignedTransactionV1 = serde_json::from_str(&canonical_json).expect("deserialize"); assert_eq!( - parsed.target_address, None, - "missing target_address field must default to None" + parsed.address_override, None, + "missing address_override field must default to None" ); } -/// A `target_address: Some(X)` round-trips through serialize/deserialize unchanged. +/// A `address_override: Some(X)` round-trips through serialize/deserialize unchanged. #[test] -fn test_v1_payload_with_target_address_round_trips() { +fn test_v1_payload_with_address_override_round_trips() { let multisig_address: ::Address = [0xABu8; 32].into(); let target: ::Address = ::Address::from_str(RECIPIENT_ADDRESS).expect("parse recipient"); @@ -1337,21 +1337,21 @@ fn test_v1_payload_with_target_address_round_trips() { let json = serde_json::to_string(&original).expect("serialize"); assert!( - json.contains("target_address"), - "target_address must appear in JSON when it is Some; got: {json}" + json.contains("address_override"), + "address_override must appear in JSON when it is Some; got: {json}" ); let parsed: SolanaOffchainUnsignedTransactionV1 = serde_json::from_str(&json).expect("deserialize"); - assert_eq!(parsed.target_address, Some(target)); + assert_eq!(parsed.address_override, Some(target)); assert_eq!(parsed.multisig_id, multisig_address); } /// End-to-end plumbing test for single-sig V0: admin's credential is explicitly /// authorized for a delegated address at genesis, that delegated address is funded, -/// and a V0 tx carrying `target_address = Some(delegated)` spends from it. +/// and a V0 tx carrying `address_override = Some(delegated)` spends from it. #[tokio::test(flavor = "multi_thread")] -async fn test_submit_single_sig_with_authorized_target_address() { +async fn test_submit_single_sig_with_authorized_address_override() { let delegated_user = TestUser::::generate_with_default_balance(); let delegated_address = delegated_user.address(); let delegated_address_str = delegated_address.to_string(); @@ -1379,12 +1379,12 @@ async fn test_submit_single_sig_with_authorized_target_address() { assert_eq!( delegated_balance_before, Some(Amount::new(8_000)), - "Expected delegated address to be funded before target-routed transfer" + "Expected delegated address to be funded before override-routed transfer" ); let response = submit_simple_json_tx( test_rollup.api_client(), - create_transfer_tx_json_with_target( + create_transfer_tx_json_with_address_override( Amount(7_000), RECIPIENT_ADDRESS, Some(delegated_address), @@ -1394,32 +1394,32 @@ async fn test_submit_single_sig_with_authorized_target_address() { .await; assert!( response.status().is_success(), - "Expected single-sig V0 target-routed tx to succeed. Response: {response:?}" + "Expected single-sig V0 override-routed tx to succeed. Response: {response:?}" ); let recipient_balance = query_balance(&test_rollup.client, RECIPIENT_ADDRESS).await; assert_eq!( recipient_balance, Some(Amount::new(7_000)), - "Expected recipient to receive 7,000 tokens via target-routed V0 tx" + "Expected recipient to receive 7,000 tokens via override-routed V0 tx" ); let delegated_balance_after = query_balance(&test_rollup.client, &delegated_address_str).await; assert_eq!( delegated_balance_after, Some(Amount::new(1_000)), - "Expected target-routed V0 tx to spend from delegated address" + "Expected override-routed V0 tx to spend from delegated address" ); } #[tokio::test(flavor = "multi_thread")] -async fn test_submit_single_sig_with_unauthorized_target_address_fails() { +async fn test_submit_single_sig_with_unauthorized_address_override_fails() { let (test_rollup, admin) = create_test_rollup().await.expect("Failed to create rollup"); let unowned_address = TestUser::::generate_with_default_balance().address(); let response = submit_simple_json_tx( test_rollup.api_client(), - create_transfer_tx_json_with_target( + create_transfer_tx_json_with_address_override( Amount(1_000), RECIPIENT_ADDRESS, Some(unowned_address), @@ -1431,17 +1431,17 @@ async fn test_submit_single_sig_with_unauthorized_target_address_fails() { assert_eq!( response.status(), 400, - "Expected 400 status for unauthorized target address" + "Expected 400 status for unauthorized address override" ); let response_text = response.text().await.expect("Failed to read response body"); assert!( - response_text.contains("not authorized for target address"), - "Expected unauthorized target error, got: {response_text}" + response_text.contains("not authorized for address override"), + "Expected unauthorized address override error, got: {response_text}" ); } #[tokio::test(flavor = "multi_thread")] -async fn test_submit_single_sig_with_tampered_target_address_fails_signature() { +async fn test_submit_single_sig_with_tampered_address_override_fails_signature() { let delegated_user = TestUser::::generate_with_default_balance(); let delegated_address = delegated_user.address(); let (test_rollup, admin) = create_test_rollup_with_extra_account_owners(|admin| { @@ -1453,7 +1453,7 @@ async fn test_submit_single_sig_with_tampered_target_address_fails_signature() { .await .expect("Failed to create rollup"); - let json = create_transfer_tx_json_with_target( + let json = create_transfer_tx_json_with_address_override( Amount(1_000), RECIPIENT_ADDRESS, Some(delegated_address), @@ -1461,7 +1461,7 @@ async fn test_submit_single_sig_with_tampered_target_address_fails_signature() { let signature = admin.private_key().sign(json.as_bytes()); let mut payload: SolanaOffchainUnsignedTransactionV0 = serde_json::from_str(&json).expect("deserialize"); - payload.target_address = Some(admin.address()); + payload.address_override = Some(admin.address()); let tampered_json = serde_json::to_string(&payload).expect("serialize"); let message = SolanaOffchainSimpleMessage:: { signed_message: tampered_json.into_bytes(), @@ -1474,7 +1474,7 @@ async fn test_submit_single_sig_with_tampered_target_address_fails_signature() { assert_eq!( response.status(), 400, - "Expected 400 status for tampered target address" + "Expected 400 status for tampered address override" ); let response_text = response.text().await.expect("Failed to read response body"); assert!( @@ -1486,7 +1486,7 @@ async fn test_submit_single_sig_with_tampered_target_address_fails_signature() { fn build_v0_payload( recipient: &str, - target_address: Option<::Address>, + address_override: Option<::Address>, ) -> SolanaOffchainUnsignedTransactionV0 { let call: TestRuntimeCall = TestRuntimeCall::Bank(BankCallMessage::Transfer { to: ::Address::from_str(recipient).unwrap(), @@ -1508,51 +1508,51 @@ fn build_v0_payload( uniqueness: unsigned_tx.uniqueness, details: unsigned_tx.details, chain_name: config_value!("CHAIN_NAME").to_string().try_into().unwrap(), - target_address, + address_override, } } #[test] -fn test_v0_payload_omits_target_address_when_none() { +fn test_v0_payload_omits_address_override_when_none() { let payload = build_v0_payload(RECIPIENT_ADDRESS, None); let json = serde_json::to_string(&payload).expect("serialize"); assert!( - !json.contains("target_address"), - "target_address must not appear in JSON when it is None; got: {json}" + !json.contains("address_override"), + "address_override must not appear in JSON when it is None; got: {json}" ); } #[test] -fn test_v0_payload_without_target_address_field_deserializes_as_none() { +fn test_v0_payload_without_address_override_field_deserializes_as_none() { let expected = build_v0_payload(RECIPIENT_ADDRESS, None); let canonical_json = serde_json::to_string(&expected).expect("serialize baseline"); assert!( - !canonical_json.contains("target_address"), - "guard: the canonical None-payload already omits target_address" + !canonical_json.contains("address_override"), + "guard: the canonical None-payload already omits address_override" ); let parsed: SolanaOffchainUnsignedTransactionV0 = serde_json::from_str(&canonical_json).expect("deserialize"); assert_eq!( - parsed.target_address, None, - "missing target_address field must default to None" + parsed.address_override, None, + "missing address_override field must default to None" ); } #[test] -fn test_v0_payload_with_target_address_round_trips() { +fn test_v0_payload_with_address_override_round_trips() { let target: ::Address = ::Address::from_str(RECIPIENT_ADDRESS).expect("parse recipient"); let original = build_v0_payload(RECIPIENT_ADDRESS, Some(target)); let json = serde_json::to_string(&original).expect("serialize"); assert!( - json.contains("target_address"), - "target_address must appear in JSON when it is Some; got: {json}" + json.contains("address_override"), + "address_override must appear in JSON when it is Some; got: {json}" ); let parsed: SolanaOffchainUnsignedTransactionV0 = serde_json::from_str(&json).expect("deserialize"); - assert_eq!(parsed.target_address, Some(target)); + assert_eq!(parsed.address_override, Some(target)); } diff --git a/crates/web3/src/rust.rs b/crates/web3/src/rust.rs index 20289a7e48..19bff661c4 100644 --- a/crates/web3/src/rust.rs +++ b/crates/web3/src/rust.rs @@ -112,7 +112,7 @@ pub struct TransactionBuilder, max_fee: Option, gas_limit: Option>, - target_address: Option, + address_override: Option, _phantom: std::marker::PhantomData, } @@ -132,7 +132,7 @@ impl TransactionBui priority_fee_bips: None, max_fee: None, gas_limit: None, - target_address: None, + address_override: None, _phantom: Default::default(), } } @@ -190,9 +190,9 @@ impl TransactionBui self } - /// Sets the explicit target address for the transaction. - pub fn target_address(mut self, target_address: S::Address) -> Self { - self.target_address = Some(target_address); + /// Sets the explicit address override for the transaction. + pub fn address_override(mut self, address_override: S::Address) -> Self { + self.address_override = Some(address_override); self } @@ -226,7 +226,7 @@ impl TransactionBui uniqueness, gas_limit, ); - tx.target_address = self.target_address; + tx.address_override = self.address_override; Ok(tx) } diff --git a/crates/web3/src/schema.rs b/crates/web3/src/schema.rs index fa2bfcf775..f75cb82578 100644 --- a/crates/web3/src/schema.rs +++ b/crates/web3/src/schema.rs @@ -359,8 +359,8 @@ pub struct UnsignedTransactionV0 { pub uniqueness: UniquenessData, /// Transaction execution details including fees and gas limits. pub details: TxDetails, - /// Optional target address for execution; null uses default routing. - pub target_address: Option, + /// Optional address override for execution; null uses default routing. + pub address_override: Option, } /// A versioned unsigned transaction envelope. @@ -394,7 +394,7 @@ impl UnsignedTransactionV0 { runtime_call: self.runtime_call.clone(), uniqueness: self.uniqueness, details: self.details.clone(), - target_address: self.target_address.clone(), + address_override: self.address_override.clone(), }) } } @@ -420,8 +420,8 @@ pub struct TransactionV0 { pub uniqueness: UniquenessData, /// Transaction execution details including fees and gas limits. pub details: TxDetails, - /// Optional target address for execution; null uses default routing. - pub target_address: Option, + /// Optional address override for execution; null uses default routing. + pub address_override: Option, } /// A versioned transaction envelope supporting different transaction formats. @@ -461,7 +461,7 @@ pub struct TransactionBuilder { max_fee: Option, gas_limit: Option>>, chain_id: Option, - target_address: Option, + address_override: Option, } impl TransactionBuilder { @@ -482,7 +482,7 @@ impl TransactionBuilder { max_fee: None, gas_limit: None, chain_id: None, - target_address: None, + address_override: None, } } @@ -553,9 +553,9 @@ impl TransactionBuilder { self } - /// Sets the explicit target address for the transaction. - pub fn target_address(mut self, target_address: impl Into) -> Self { - self.target_address = Some(target_address.into()); + /// Sets the explicit address override for the transaction. + pub fn address_override(mut self, address_override: impl Into) -> Self { + self.address_override = Some(address_override.into()); self } @@ -600,7 +600,7 @@ impl TransactionBuilder { gas_limit, chain_id, }, - target_address: self.target_address, + address_override: self.address_override, }) } } diff --git a/python/py_sovereign_web3/rust/src/lib.rs b/python/py_sovereign_web3/rust/src/lib.rs index baed3a26ed..484477fbe1 100644 --- a/python/py_sovereign_web3/rust/src/lib.rs +++ b/python/py_sovereign_web3/rust/src/lib.rs @@ -118,12 +118,12 @@ struct PyUnsignedTransactionV0 { #[pymethods] impl PyUnsignedTransactionV0 { #[new] - #[pyo3(signature = (runtime_call, details, uniqueness=None, target_address=None))] + #[pyo3(signature = (runtime_call, details, uniqueness=None, address_override=None))] fn new( runtime_call: &Bound<'_, PyDict>, details: &PyTxDetails, uniqueness: Option<&PyUniquenessData>, - target_address: Option, + address_override: Option, ) -> PyResult { let call: serde_json::Value = pythonize::depythonize(runtime_call).map_err(|e| { PyValueError::new_err(format!("Failed to convert runtime_call dict to JSON: {e}")) @@ -140,7 +140,7 @@ impl PyUnsignedTransactionV0 { runtime_call: call, uniqueness, details: details.inner.clone(), - target_address, + address_override, }; Ok(PyUnsignedTransactionV0 { inner: unsigned_tx }) diff --git a/python/py_sovereign_web3/src/sovereign_web3/sovereign_web3.pyi b/python/py_sovereign_web3/src/sovereign_web3/sovereign_web3.pyi index b0280a1949..79a5cd84ca 100644 --- a/python/py_sovereign_web3/src/sovereign_web3/sovereign_web3.pyi +++ b/python/py_sovereign_web3/src/sovereign_web3/sovereign_web3.pyi @@ -167,7 +167,7 @@ class UnsignedTransactionV0: runtime_call: Dict[str, Any], details: TxDetails, uniqueness: Optional[UniquenessData] = None, - target_address: Optional[str] = None, + address_override: Optional[str] = None, ) -> None: """Create V0 unsigned transaction. @@ -175,7 +175,7 @@ class UnsignedTransactionV0: runtime_call: Runtime call data as dictionary details: Transaction details uniqueness: Optional uniqueness data (defaults to default uniqueness) - target_address: Optional explicit target address + address_override: Optional explicit address override Raises: ValueError: If runtime_call cannot be converted to JSON or uniqueness creation fails diff --git a/typescript/packages/multisig/src/index.test.ts b/typescript/packages/multisig/src/index.test.ts index 6c478c51f2..a78b5205b5 100644 --- a/typescript/packages/multisig/src/index.test.ts +++ b/typescript/packages/multisig/src/index.test.ts @@ -22,7 +22,7 @@ const createUnsignedTx = (nonce = 1): UnsignedTransaction => ({ gas_limit: null, chain_id: 1, }, - target_address: null, + address_override: null, }); const createTransactionV0 = ( @@ -49,7 +49,7 @@ describe("MultisigTransaction", () => { gas_limit: null, chain_id: 1, }, - target_address: null, + address_override: null, }; const tx = createTransactionV0(unsignedTx, "pubkey1", "sig1"); @@ -248,17 +248,17 @@ describe("MultisigTransaction", () => { }); describe("asTransaction", () => { - it("should include a null target address by default", () => { + it("should include a null address override by default", () => { const multisig = MultisigTransaction.empty(createUnsignedTx(), 1, [ "pubkey1", ]); const result = multisig.asTransaction() as TransactionV1; - expect(result.V1.target_address).toBeNull(); + expect(result.V1.address_override).toBeNull(); }); - it("should include the provided target address", () => { + it("should include the provided address override", () => { const multisig = MultisigTransaction.empty(createUnsignedTx(), 1, [ "pubkey1", ]); @@ -267,7 +267,7 @@ describe("MultisigTransaction", () => { "sov1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq5k2jj4", ) as TransactionV1; - expect(result.V1.target_address).toBe( + expect(result.V1.address_override).toBe( "sov1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq5k2jj4", ); }); diff --git a/typescript/packages/multisig/src/index.ts b/typescript/packages/multisig/src/index.ts index 3f0d3f36a9..e7ebeff578 100644 --- a/typescript/packages/multisig/src/index.ts +++ b/typescript/packages/multisig/src/index.ts @@ -258,9 +258,9 @@ export class MultisigTransaction { * Converts the multisig to a V1 transaction that can be submitted to the network. * @returns A V1 transaction containing all collected signatures and unused public keys */ - asTransaction(targetAddress?: string | null): Transaction { - const resolvedTargetAddress = - targetAddress ?? this.unsignedTx.target_address; + asTransaction(addressOverride?: string | null): Transaction { + const resolvedAddressOverride = + addressOverride ?? this.unsignedTx.address_override; return { V1: { runtime_call: this.unsignedTx.runtime_call, @@ -269,7 +269,7 @@ export class MultisigTransaction { unused_pub_keys: Array.from(this.unusedPubKeys), signatures: this.signatures, min_signers: this.minSigners, - target_address: resolvedTargetAddress, + address_override: resolvedAddressOverride, }, }; } @@ -293,7 +293,7 @@ function asUnsignedTransaction( runtime_call: tx.V0.runtime_call, uniqueness: tx.V0.uniqueness, details: tx.V0.details, - target_address: tx.V0.target_address, + address_override: tx.V0.address_override, }; } @@ -302,7 +302,7 @@ function asUnsignedTransaction( runtime_call: tx.V1.runtime_call, uniqueness: tx.V1.uniqueness, details: tx.V1.details, - target_address: tx.V1.target_address, + address_override: tx.V1.address_override, }; } diff --git a/typescript/packages/multisig/tests/multisig.integration-test.ts b/typescript/packages/multisig/tests/multisig.integration-test.ts index 9a62235e28..51ec947b9b 100644 --- a/typescript/packages/multisig/tests/multisig.integration-test.ts +++ b/typescript/packages/multisig/tests/multisig.integration-test.ts @@ -59,7 +59,7 @@ describe("multisig", async () => { runtime_call, uniqueness: { nonce: 0 }, details: { ...DEFAULT_TX_DETAILS, chain_id }, - target_address: null, + address_override: null, }; const requiredSigners = 3; diff --git a/typescript/packages/types/src/index.ts b/typescript/packages/types/src/index.ts index 2eca3e6fda..40bb8758e3 100644 --- a/typescript/packages/types/src/index.ts +++ b/typescript/packages/types/src/index.ts @@ -35,8 +35,8 @@ export type UnsignedTransaction = { uniqueness: Uniqueness; /** Transaction execution details including fees and gas limits */ details: TxDetails; - /** Optional target address for execution; null uses default routing */ - target_address: string | null; + /** Optional address override for execution; null uses default routing */ + address_override: string | null; }; /** diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts index f08363d673..e150b5528d 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts @@ -160,7 +160,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: 1, }, - target_address: null, + address_override: null, } as any, { signer: createMockSigner(), @@ -206,7 +206,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: fixtureChainId, }, - target_address: null, + address_override: null, } as any, { signer: createMockSigner(), @@ -289,7 +289,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, - target_address: null, + address_override: null, }; await rollup.signAndSubmitTransaction(unsignedTx, { @@ -376,7 +376,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, - target_address: null, + address_override: null, }; await rollup.signAndSubmitTransaction(unsignedTx, { @@ -472,7 +472,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, - target_address: null, + address_override: null, }; // Each signer signs independently (same order as Rust: key3, key1) @@ -544,7 +544,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: 1, }, - target_address: null, + address_override: null, }; const signedTx = await rollup.signTransactionForMultisig( @@ -649,7 +649,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, - target_address: null, + address_override: null, }; const signedTx3 = await rollup.signTransactionForMultisig(unsignedTx, { @@ -726,7 +726,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: 1, }, - target_address: null, + address_override: null, } as any, { signer: ledgerSigner, @@ -778,7 +778,7 @@ describe("SolanaSignableRollup", () => { gas_limit: null, chain_id: 1, }, - target_address: null, + address_override: null, } as any, { signer: ed25519Signer, @@ -796,7 +796,7 @@ describe("SolanaSignableRollup", () => { }); }); - describe("V1 target_address", () => { + describe("V1 address_override", () => { // Shared fixture: builds a rollup + multisig context the same way the byte-compatibility // tests above do, then exposes the pieces each test needs. async function setupMultisigContext(): Promise<{ @@ -879,7 +879,7 @@ describe("SolanaSignableRollup", () => { gas_limit: [1000000000, 1000000000], chain_id: 4321, }, - target_address: null, + address_override: null, }; return { @@ -914,7 +914,7 @@ describe("SolanaSignableRollup", () => { return { mock, captured }; } - it("omits target_address from the signed JSON when no targetAddress is provided (backward compat)", async () => { + it("omits address_override from the signed JSON when no addressOverride is provided (backward compat)", async () => { const { rollup, multisigAddress, multisigPubkeys, signers, unsignedTx } = await setupMultisigContext(); @@ -928,20 +928,20 @@ describe("SolanaSignableRollup", () => { expect(captured.bytes).toBeDefined(); const signedJson = new TextDecoder().decode(captured.bytes!); - expect(signedJson).not.toContain("target_address"); + expect(signedJson).not.toContain("address_override"); const parsed = JSON.parse(signedJson); - expect(parsed).not.toHaveProperty("target_address"); + expect(parsed).not.toHaveProperty("address_override"); }); - it("includes target_address in the signed JSON before version when targetAddress is provided", async () => { + it("includes address_override in the signed JSON before version when addressOverride is provided", async () => { // Field order matches Rust's `SolanaOffchainUnsignedTransactionV1`: signers sign the raw // JSON bytes, so TS and Rust must emit fields in the same order or signatures produced // in one language won't verify against JSON produced in the other. const { rollup, multisigAddress, multisigPubkeys, signers, unsignedTx } = await setupMultisigContext(); - const targetAddress = new Uint8Array(32); - targetAddress.fill(0x42); + const addressOverride = new Uint8Array(32); + addressOverride.fill(0x42); const { mock, captured } = captureSignerInput(signers.signer1); await rollup.signTransactionForMultisig(unsignedTx, { @@ -949,21 +949,21 @@ describe("SolanaSignableRollup", () => { authenticator: "solanaSimple", multisigAddress, multisigPubkeys, - targetAddress, + addressOverride, }); expect(captured.bytes).toBeDefined(); const signedJson = new TextDecoder().decode(captured.bytes!); - expect(JSON.parse(signedJson).target_address).toBe( - bs58.encode(targetAddress), + expect(JSON.parse(signedJson).address_override).toBe( + bs58.encode(addressOverride), ); - const targetIdx = signedJson.indexOf('"target_address"'); + const overrideIdx = signedJson.indexOf('"address_override"'); const versionIdx = signedJson.indexOf('"version"'); - expect(targetIdx).toBeGreaterThanOrEqual(0); - expect(targetIdx).toBeLessThan(versionIdx); + expect(overrideIdx).toBeGreaterThanOrEqual(0); + expect(overrideIdx).toBeLessThan(versionIdx); }); - it("forwards tx.target_address into the submitted JSON via submitMultisigTransaction", async () => { + it("forwards tx.address_override into the submitted JSON via submitMultisigTransaction", async () => { const { rollup, capturedPayloadRef, @@ -974,25 +974,25 @@ describe("SolanaSignableRollup", () => { unsignedTx, } = await setupMultisigContext(); - const targetAddress = new Uint8Array(32); - targetAddress.fill(0x99); - const targetAddressBs58 = bs58.encode(targetAddress); + const addressOverride = new Uint8Array(32); + addressOverride.fill(0x99); + const addressOverrideBs58 = bs58.encode(addressOverride); - // Each signer signs with the same target_address — the signatures cover the full JSON - // including the target_address field. + // Each signer signs with the same address_override — the signatures cover the full JSON + // including the address_override field. const signedTx3 = await rollup.signTransactionForMultisig(unsignedTx, { signer: signers.signer3, authenticator: "solanaSimple", multisigAddress, multisigPubkeys, - targetAddress, + addressOverride, }); const signedTx1 = await rollup.signTransactionForMultisig(unsignedTx, { signer: signers.signer1, authenticator: "solanaSimple", multisigAddress, multisigPubkeys, - targetAddress, + addressOverride, }); const v0_3 = (signedTx3 as any).V0; @@ -1006,7 +1006,7 @@ describe("SolanaSignableRollup", () => { ], unused_pub_keys: [pubHexes.pub2], min_signers: 2, - target_address: targetAddressBs58, + address_override: addressOverrideBs58, }, }; @@ -1023,7 +1023,7 @@ describe("SolanaSignableRollup", () => { const submittedBytes = Buffer.from(submittedBody, "base64"); const submittedText = new TextDecoder().decode(submittedBytes); expect(submittedText).toContain( - `"target_address":"${targetAddressBs58}"`, + `"address_override":"${addressOverrideBs58}"`, ); }); }); diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts index e8118fcedd..d49b56cada 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts @@ -31,22 +31,22 @@ export type SolanaOffchainUnsignedTransaction = { export type SolanaOffchainUnsignedTransactionV0 = SolanaOffchainUnsignedTransaction & { /** - * Signer-declared target address. Omitted from the serialized JSON when not set, + * Signer-declared address override. Omitted from the serialized JSON when not set, * preserving byte equivalence with pre-change signed messages so existing signatures - * continue to verify. See `AuthorizationData::address` (Rust) for routing semantics. + * continue to verify. See `AuthorizationData::address_override` (Rust) for routing semantics. */ - target_address?: string; + address_override?: string; }; export type SolanaOffchainUnsignedTransactionV1 = SolanaOffchainUnsignedTransaction & { multisig_id: string; /** - * Signer-declared target address. Omitted from the serialized JSON when not set, + * Signer-declared address override. Omitted from the serialized JSON when not set, * preserving byte equivalence with pre-change signed messages so existing signatures - * continue to verify. See `AuthorizationData::address` (Rust) for routing semantics. + * continue to verify. See `AuthorizationData::address_override` (Rust) for routing semantics. */ - target_address?: string; + address_override?: string; version: number; }; @@ -100,11 +100,11 @@ export type SolanaMultisigSubmitParams = export type SolanaMultisigSignParams = SolanaMultisigSubmitParams & { signer: Signer; /** - * Optional target address to embed in the signed V1 payload. Omitted from the JSON when + * Optional address override to embed in the signed V1 payload. Omitted from the JSON when * unset to preserve byte equivalence with pre-change signed messages. Unused by the - * `"standard"` authenticator. See `AuthorizationData::address` (Rust) for routing semantics. + * `"standard"` authenticator. See `AuthorizationData::address_override` (Rust) for routing semantics. */ - targetAddress?: Uint8Array; + addressOverride?: Uint8Array; }; /** @@ -357,9 +357,9 @@ export class SolanaSignableRollup { uniqueness: unsignedTx.uniqueness, details: unsignedTx.details, chain_name: chainName, - ...(unsignedTx.target_address !== null && - unsignedTx.target_address !== undefined && { - target_address: unsignedTx.target_address, + ...(unsignedTx.address_override !== null && + unsignedTx.address_override !== undefined && { + address_override: unsignedTx.address_override, }), }; @@ -521,14 +521,14 @@ export class SolanaSignableRollup { * Creates V1 JSON bytes for a multisig transaction, including multisig_id and version. * The resulting JSON is what each signer signs directly (no discriminator prefix). * - * When `targetAddress` is omitted the JSON has no `target_address` key — this preserves + * When `addressOverride` is omitted the JSON has no `address_override` key — this preserves * byte equivalence with pre-change signed messages, so signatures produced by older clients * keep verifying. */ private async createMultisigJsonBytes( unsignedTx: UnsignedTransaction, multisigAddress: Uint8Array, - targetAddress?: Uint8Array, + addressOverride?: Uint8Array, ): Promise { const serializer = await this.inner.serializer(); const schema = serializer.schema; @@ -536,7 +536,7 @@ export class SolanaSignableRollup { // Field order matches the Rust `SolanaOffchainUnsignedTransactionV1` struct // (`crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs`): - // `target_address` must sit between `multisig_id` and `version` so TS- and Rust-generated + // `address_override` must sit between `multisig_id` and `version` so TS- and Rust-generated // JSON bytes are byte-identical — the multisig signatures cover these bytes directly. const solanaUnsignedTx: SolanaOffchainUnsignedTransactionV1 = { runtime_call: unsignedTx.runtime_call, @@ -547,8 +547,8 @@ export class SolanaSignableRollup { // primary address type. Will be replaced with rollup-aware address formatting once the // SDK supports flexible address encoding (see #2673). multisig_id: bs58.encode(multisigAddress), - ...(targetAddress !== undefined && { - target_address: bs58.encode(targetAddress), + ...(addressOverride !== undefined && { + address_override: bs58.encode(addressOverride), }), version: 1, }; @@ -563,12 +563,12 @@ export class SolanaSignableRollup { unsignedTx: UnsignedTransaction, multisigAddress: Uint8Array, multisigPubkeys: Uint8Array[], - targetAddress?: Uint8Array, + addressOverride?: Uint8Array, ): Promise { const jsonBytes = await this.createMultisigJsonBytes( unsignedTx, multisigAddress, - targetAddress, + addressOverride, ); const chainHash = await this.inner.chainHash(); const preamble = createSolanaPreamble( @@ -588,7 +588,7 @@ export class SolanaSignableRollup { signer: Signer, multisigAddress: Uint8Array, multisigPubkeys: Uint8Array[], - targetAddress?: Uint8Array, + addressOverride?: Uint8Array, ): Promise> { const pubkey = await signer.publicKey(); const signerPubkeyHex = bytesToHex(pubkey); @@ -603,7 +603,7 @@ export class SolanaSignableRollup { const jsonBytes = await this.createMultisigJsonBytes( unsignedTx, multisigAddress, - targetAddress, + addressOverride, ); const signature = await signer.sign(jsonBytes); @@ -624,7 +624,7 @@ export class SolanaSignableRollup { signer: Signer, multisigAddress: Uint8Array, multisigPubkeys: Uint8Array[], - targetAddress?: Uint8Array, + addressOverride?: Uint8Array, ): Promise> { const pubkey = await signer.publicKey(); const signerPubkeyHex = bytesToHex(pubkey); @@ -641,7 +641,7 @@ export class SolanaSignableRollup { unsignedTx, multisigAddress, multisigPubkeys, - targetAddress, + addressOverride, ); const signature = await signer.sign(signedMessageWithPreamble); @@ -675,7 +675,7 @@ export class SolanaSignableRollup { params.signer, params.multisigAddress, this.canonicalizeMultisigPubkeys(params.multisigPubkeys), - params.targetAddress, + params.addressOverride, ); case "solana": return this.signForSolanaSpecMultisig( @@ -683,7 +683,7 @@ export class SolanaSignableRollup { params.signer, params.multisigAddress, this.canonicalizeMultisigPubkeys(params.multisigPubkeys), - params.targetAddress, + params.addressOverride, ); } } @@ -776,15 +776,15 @@ export class SolanaSignableRollup { runtime_call: tx.runtime_call, uniqueness: tx.uniqueness, details: tx.details, - target_address: tx.target_address, + address_override: tx.address_override, }; - // Decode the signed `target_address` back to bytes so we can pass it through the same + // Decode the signed `address_override` back to bytes so we can pass it through the same // `createMultisigJsonBytes` path used by signers. Re-encoding is idempotent under base58, so // the produced JSON bytes match what the signer signed over. - const targetAddress: Uint8Array | undefined = - tx.target_address !== null && tx.target_address !== undefined - ? bs58.decode(tx.target_address) + const addressOverride: Uint8Array | undefined = + tx.address_override !== null && tx.address_override !== undefined + ? bs58.decode(tx.address_override) : undefined; switch (params.authenticator) { @@ -798,7 +798,7 @@ export class SolanaSignableRollup { const jsonBytes = await this.createMultisigJsonBytes( unsignedTx, params.multisigAddress, - targetAddress, + addressOverride, ); const wireBytes = new Uint8Array(1 + jsonBytes.length); wireBytes[0] = MULTISIG_SIMPLE_DISCRIMINATOR; @@ -827,7 +827,7 @@ export class SolanaSignableRollup { unsignedTx, params.multisigAddress, multisigPubkeys, - targetAddress, + addressOverride, ); const message = this.buildSpecCompliantMultisigEnvelope( tx, diff --git a/typescript/packages/web3/src/rollup/standard-rollup.test.ts b/typescript/packages/web3/src/rollup/standard-rollup.test.ts index 1aeb76229d..332156932c 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.test.ts @@ -54,7 +54,7 @@ describe("standardTypeBuilder", () => { max_fee: "1000", chain_id: 1, }, - target_address: null, + address_override: null, }); }); @@ -75,7 +75,7 @@ describe("standardTypeBuilder", () => { max_fee: "1000", chain_id: 1, }, - target_address: null, + address_override: null, }); }); @@ -102,7 +102,7 @@ describe("standardTypeBuilder", () => { gas_limit: [1000000, 1000000], chain_id: 1, }, - target_address: null, + address_override: null, }); }); }); @@ -121,7 +121,7 @@ describe("standardTypeBuilder", () => { chain_id: 1, gas_limit: null, }, - target_address: null, + address_override: null, }, sender: new Uint8Array([4, 5, 6]), signature: new Uint8Array([7, 8, 9]), @@ -142,7 +142,7 @@ describe("standardTypeBuilder", () => { chain_id: 1, gas_limit: null, }, - target_address: null, + address_override: null, }, }); }); @@ -284,7 +284,7 @@ describe("createStandardRollup", () => { await rollup.simulate(runtimeCall, { signer: signer as any, - target_address: "sov1target", + address_override: "sov1target", tx_details: txDetails, uniqueness: { nonce: 7 }, }); @@ -292,7 +292,7 @@ describe("createStandardRollup", () => { expect(client.rollup.simulate).toHaveBeenCalledWith({ sender: "abcd", call: runtimeCall, - target_address: "sov1target", + address_override: "sov1target", tx_details: txDetails, uniqueness: { nonce: 7 }, }); diff --git a/typescript/packages/web3/src/rollup/standard-rollup.ts b/typescript/packages/web3/src/rollup/standard-rollup.ts index aab9016787..7e68733bb1 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.ts @@ -63,7 +63,7 @@ export function standardTypeBuilder< runtime_call: runtimeCall, uniqueness, details, - target_address: overrides.target_address ?? null, + address_override: overrides.address_override ?? null, } as S["UnsignedTransaction"]; }, async transaction({ @@ -89,7 +89,7 @@ export type SimulateParams = Omit< SovereignClient.RollupSimulateParams, "call" | "sender" > & - SignerParams & { target_address?: string | null }; + SignerParams & { address_override?: string | null }; export class StandardRollup extends Rollup< StandardRollupSpec, From fc2289546abefb73446c1dd336a77854f78ce25a Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 5 May 2026 17:23:25 +0200 Subject: [PATCH 13/34] Address comments --- .../sov-accounts/tests/integration/main.rs | 59 +++++++++++++++++++ .../src/authentication/payload.rs | 2 - .../tests/integration/main.rs | 3 +- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs index 6b7e62babf..8d4c4bb7e3 100644 --- a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs +++ b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs @@ -698,6 +698,65 @@ fn test_v1_address_override_some_unauthorized_skipped() { }); } +/// `address_override = None` ignores any pre-existing `(X, multisig_credential_id)` +/// mapping and routes to the multisig's default address. The mapping is untouched. +#[test] +fn test_v1_address_override_none_ignores_existing_mapping() { + let MultisigEnv { + keys, + multisig, + credential_id: multisig_credential_id, + user: multisig_user, + } = make_multisig_env(); + let multisig_default_address = multisig_user.address(); + + // Alice has an authorization for the multisig credential, but the V1 tx below + // signs with `None` and so must not route through her address. + let alice = TestUser::::generate_with_default_balance(); + let alice_address = alice.address(); + + let genesis_config = + HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![multisig_user, alice]); + let mut genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + genesis.accounts.accounts.push(AccountData { + credential_id: multisig_credential_id, + address: alice_address, + }); + let mut runner: TestRunner = + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + + let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); + let mut tx = make_v1_tx(&multisig, inner_credential, None); + sign_v1(&mut tx, &keys[0]); + sign_v1(&mut tx, &keys[1]); + + runner.execute_transaction(TransactionTestCase { + input: submit_v1(tx), + assert: Box::new(move |result, state| { + assert!(result.tx_receipt.is_successful()); + let accounts = Accounts::::default(); + assert!( + accounts + .is_explicitly_authorized(&multisig_default_address, &inner_credential, state) + .unwrap(), + "None must route to multisig default" + ); + assert!( + !accounts + .is_explicitly_authorized(&alice_address, &inner_credential, state) + .unwrap(), + "None must not write under alice's address" + ); + assert!( + accounts + .is_explicitly_authorized(&alice_address, &multisig_credential_id, state) + .unwrap(), + "pre-existing (alice, multisig_credential_id) authorization must be preserved" + ); + }), + }); +} + /// `address_override` is part of the signed bytes: tampering with it after signing /// invalidates the signature. #[test] diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index 159d3ce6ae..e1200f67cf 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -34,7 +34,6 @@ pub struct SolanaOffchainUnsignedTransactionV0 /// to be signing for right now). pub chain_name: SafeString, /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. - /// Skipped from JSON when `None` so pre-change signed messages stay byte-identical. #[serde(default, skip_serializing_if = "Option::is_none")] pub address_override: Option, } @@ -84,7 +83,6 @@ pub struct SolanaOffchainUnsignedTransactionV1 /// `sov-accounts`. pub multisig_id: S::Address, /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. - /// Skipped from JSON when `None` so pre-change signed messages stay byte-identical. #[serde(default, skip_serializing_if = "Option::is_none")] pub address_override: Option, /// Message format version. Must be `1` for this struct. diff --git a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs index bd019b0bc5..2a66f11063 100644 --- a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs +++ b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs @@ -1292,8 +1292,7 @@ fn build_v1_payload( } } -/// `address_override: None` is omitted from the serialized JSON, so pre-change signed messages -/// remain byte-identical and existing signatures verify unchanged. +/// `address_override: None` is omitted from the serialized JSON. #[test] fn test_v1_payload_omits_address_override_when_none() { let multisig_address: ::Address = [0xABu8; 32].into(); From 5baef6883b4386fe84fcb2b23438295fda1aa8cb Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 5 May 2026 17:40:41 +0200 Subject: [PATCH 14/34] Keep fixing --- .../sov-accounts/tests/integration/main.rs | 6 +++++- .../packages/web3/src/rollup/solana-signable-rollup.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs index 8d4c4bb7e3..f8f2aaf5fa 100644 --- a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs +++ b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs @@ -733,7 +733,11 @@ fn test_v1_address_override_none_ignores_existing_mapping() { runner.execute_transaction(TransactionTestCase { input: submit_v1(tx), assert: Box::new(move |result, state| { - assert!(result.tx_receipt.is_successful()); + assert!( + result.tx_receipt.is_successful(), + "V1 address_override=None should succeed, got {:?}", + result.tx_receipt + ); let accounts = Accounts::::default(); assert!( accounts diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts index d49b56cada..28778743c3 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts @@ -4,8 +4,8 @@ import type { Transaction, TransactionV1, TxDetails, - UnsignedTransaction, Uniqueness, + UnsignedTransaction, } from "@sovereign-sdk/types"; import { bytesToHex, hexToBytes } from "@sovereign-sdk/utils"; import bs58 from "bs58"; From 3ed08db3352feb2d29a81fbed51c6398b5e69618 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Tue, 5 May 2026 19:54:01 +0200 Subject: [PATCH 15/34] More cleaning up --- crates/module-system/sov-capabilities/src/lib.rs | 5 ++++- .../sov-solana-offchain-auth/src/authentication/payload.rs | 6 ++++-- .../sov-solana-offchain-auth/tests/integration/main.rs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index 7306e7ad7d..861364bd65 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -349,7 +349,10 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' _state: &mut impl StateAccessor, execution_context: ExecutionContext, ) -> anyhow::Result> { - // The tx sender & sequencer are the same entity + // `address_override` is intentionally ignored: this path runs for sequencer + // self-registration before `account_owners` has any entries to authorize against, + // so the only meaningful sender is the credential's canonical address. + // The tx sender & sequencer are the same entity. Ok(Context::new( auth_data.default_address, auth_data.credentials.clone(), diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index e1200f67cf..24181bdc72 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -33,7 +33,8 @@ pub struct SolanaOffchainUnsignedTransactionV0 /// from malicious chains (if the chain name matches some other chain the use but didn't expect /// to be signing for right now). pub chain_name: SafeString, - /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. + /// Signer-declared address override. This field is part of the signed bytes. + /// See [`sov_modules_api::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default, skip_serializing_if = "Option::is_none")] pub address_override: Option, } @@ -82,7 +83,8 @@ pub struct SolanaOffchainUnsignedTransactionV1 /// This is the "multisig address" except if the credential is mapped to another address in /// `sov-accounts`. pub multisig_id: S::Address, - /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. + /// Signer-declared address override. This field is part of the signed bytes. + /// See [`sov_modules_api::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default, skip_serializing_if = "Option::is_none")] pub address_override: Option, /// Message format version. Must be `1` for this struct. diff --git a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs index 2a66f11063..c81eff1df2 100644 --- a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs +++ b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs @@ -1165,7 +1165,7 @@ async fn test_submit_ledger_signed_multisig_transaction() { /// genesis authorizes `(admin.address(), multisig_credential_id)` in `account_owners` — routes /// execution as `admin`, not as the multisig's default address. Proves that `address_override` /// flows from the signed JSON through `authenticate`, `build_auth_data`, `AuthorizationData`, -/// and into `resolve_sender`. +/// and into `resolve_context`. #[tokio::test(flavor = "multi_thread")] async fn test_submit_multisig_with_authorized_address_override() { // Build a 2-of-3 multisig (its default address is never funded). From ce7b15211ec8a69a68974bf2b1365f3059a18569 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 09:31:21 +0200 Subject: [PATCH 16/34] More cleaning up --- .../module-system/sov-modules-api/src/transaction/types/v0.rs | 3 ++- .../module-system/sov-modules-api/src/transaction/types/v1.rs | 3 ++- .../sov-modules-api/src/transaction/unsigned/v0.rs | 3 ++- .../sov-modules-api/src/transaction/unsigned/v1.rs | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs index dea7ed7ed8..49252f57a3 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs @@ -41,7 +41,8 @@ pub struct Version0, - /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. + /// Signer-declared address override. This field is part of the signed bytes. + /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default)] pub address_override: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs index e2e1c7faf5..03c0b4a4fa 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs @@ -87,7 +87,8 @@ pub struct Version1, - /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. + /// Signer-declared address override. This field is part of the signed bytes. + /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default)] pub address_override: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs index cd6d59533d..7d315fd1ad 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs @@ -29,7 +29,8 @@ pub struct UnsignedTransactionV0 { pub uniqueness: UniquenessData, /// Data related to fees and gas handling. pub details: TxDetails, - /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. + /// Signer-declared address override. This field is part of the signed bytes. + /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default)] pub address_override: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs index 1df3f23c9a..2c43362a02 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs @@ -31,7 +31,8 @@ pub struct UnsignedTransactionV1 { /// and prevent credential malleability from reusing signed bytes in a different /// multisig envelope. pub credential_address: S::Address, - /// Signer-declared address override. See [`AuthorizationData::address_override`] for routing semantics. + /// Signer-declared address override. This field is part of the signed bytes. + /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default)] pub address_override: Option, } From e95e15e0fff276560d222fe7bc6d40734d0b51ec Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 12:55:31 +0200 Subject: [PATCH 17/34] Reformat comments --- .../tests/stf_blueprint/operator/auth_eip712.rs | 7 +++---- .../sov-solana-offchain-auth/src/authentication/payload.rs | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs index 2143856c34..00bca35a69 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs @@ -276,10 +276,9 @@ fn correct_signature_is_accepted() { #[test] fn test_multisig_signature_verification() { - // Build the multisig first so we can seed genesis with its canonical address - // funded. After the accounts refactor, the sender resolver no longer auto- - // creates an `accounts` entry, so the multisig must be funded at its - // canonical address. Here we pick the canonical path. + // Build the multisig first so we can seed genesis with its canonical address funded. + // After the accounts refactor, the sender resolver no longer auto-creates an `accounts` entry, + // so the multisig must be funded at its canonical address. Here we pick the canonical path. let multisig_keys = [ TestPrivateKey::generate(), TestPrivateKey::generate(), diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index 24181bdc72..d86eaa508c 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -83,7 +83,8 @@ pub struct SolanaOffchainUnsignedTransactionV1 /// This is the "multisig address" except if the credential is mapped to another address in /// `sov-accounts`. pub multisig_id: S::Address, - /// Signer-declared address override. This field is part of the signed bytes. + /// Signer-declared address override. + /// This field is part of the signed bytes. /// See [`sov_modules_api::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default, skip_serializing_if = "Option::is_none")] pub address_override: Option, From e0fb2e9e480db8a2e8c3987f4b4f251ac57e40c6 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 14:27:16 +0200 Subject: [PATCH 18/34] Regenrate rollup_resync and start updating readme tests --- examples/demo-rollup/README.md | 2 +- examples/demo-rollup/README_CELESTIA.md | 2 +- .../tests/resync/data/mock_da.sqlite | Bin 49152 -> 28672 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/demo-rollup/README.md b/examples/demo-rollup/README.md index 0919259c92..272a17972b 100644 --- a/examples/demo-rollup/README.md +++ b/examples/demo-rollup/README.md @@ -120,7 +120,7 @@ this case have the TokenCreated Event ```sh,test-ci,bashtestmd:compare-output $ sleep 5 -$ curl -sS http://127.0.0.1:12346/ledger/txs/0xbe16e6f31255f2f5f0a1e39530f91fd7f6480d73a3fbd3d3dcf7265962417db7/events | jq +$ curl -sS http://127.0.0.1:12346/ledger/txs/0x050b4abd3bb8e3fe1ac2802c9dafc6374e6f2e4f35c0a4f2cd8533cf1d62bf80/events | jq [ { "type": "event", diff --git a/examples/demo-rollup/README_CELESTIA.md b/examples/demo-rollup/README_CELESTIA.md index ca83aaca54..74505f8238 100644 --- a/examples/demo-rollup/README_CELESTIA.md +++ b/examples/demo-rollup/README_CELESTIA.md @@ -290,7 +290,7 @@ Adding the following transaction to batch: } } }, - "chain_hash": "0x65746dac667c302c76553208e916ba9a2f5738780f789014f8a900d6c2aeb8e5", + "chain_hash": "0x48354f2a93fa684a0acde9593cf6d9a4e4c56f9fb531ce3e3570c1922e5982e9", "details": { "max_priority_fee_bips": 0, "max_fee": "100000000", diff --git a/examples/demo-rollup/tests/resync/data/mock_da.sqlite b/examples/demo-rollup/tests/resync/data/mock_da.sqlite index ab0373e36540ab9ba725c72dd31e8530ff84497d..cc915bb844eb32828861b4a948da616876a0099d 100644 GIT binary patch delta 110 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 From 06910e75d5d131df7e893f93b13c26dde2fb8b25 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 14:58:31 +0200 Subject: [PATCH 19/34] Bring back account_2 --- .../sov-accounts/tests/integration/main.rs | 42 +++++++++++-------- .../src/runtime/capabilities/authorization.rs | 4 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs index f8f2aaf5fa..5cea2f39a5 100644 --- a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs +++ b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs @@ -21,16 +21,14 @@ type RT = TestAccountsRuntime; struct TestData { account_1: TestUser, - // `account_2` is intentionally kept in the fixture for the upcoming - // `address_override` PR. Until then it has no readers — silence the lint. - #[allow(dead_code)] account_2: TestUser, non_registered_account: TestUser, } /// We set up genesis with three accounts, two of which have custom credentials -/// authorized at genesis. -fn setup() -> (TestData, TestRunner) { +/// authorized at genesis. Returns the genesis so callers can extend it +/// (e.g. push extra `account_owners` mappings) before building the runner. +fn setup_genesis() -> (TestData, GenesisConfig) { let genesis_config = HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![ TestUser::generate_with_default_balance().add_credential_id([0u8; 32].into()), TestUser::generate_with_default_balance().add_credential_id([1u8; 32].into()), @@ -43,18 +41,22 @@ fn setup() -> (TestData, TestRunner) { let genesis = GenesisConfig::from_minimal_config(genesis_config.into()); - let runner = TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); - ( TestData { account_1: user_1, account_2: user_2, non_registered_account: user_3, }, - runner, + genesis, ) } +fn setup() -> (TestData, TestRunner) { + let (data, genesis) = setup_genesis(); + let runner = TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); + (data, runner) +} + fn setup_with_disable_custom_account_mappings() -> (TestUser, TestRunner) { let mut genesis_config = HighLevelOptimisticGenesisConfig::generate(); let user = TestUser::generate_with_default_balance(); @@ -846,22 +848,28 @@ fn test_v0_address_override_none_uses_default_resolver() { /// authorized resolves context as `X`. #[test] fn test_v0_address_override_some_authorized_succeeds() { - let sender = TestUser::::generate_with_default_balance(); - let alice = TestUser::::generate_with_default_balance(); - let alice_address = alice.address(); - - let genesis_config = - HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![sender.clone(), alice]); - let mut genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + let ( + TestData { + account_2, + non_registered_account, + .. + }, + mut genesis, + ) = setup_genesis(); + let alice_address = account_2.address(); genesis.accounts.accounts.push(AccountData { - credential_id: sender.credential_id(), + credential_id: non_registered_account.credential_id(), address: alice_address, }); let mut runner: TestRunner = TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); let inner_credential = TestPrivateKey::generate().pub_key().credential_id(); - let tx = make_v0_tx(&sender, inner_credential, Some(alice_address)); + let tx = make_v0_tx( + &non_registered_account, + inner_credential, + Some(alice_address), + ); runner.execute_transaction(TransactionTestCase { input: submit_v0(tx), diff --git a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs index 786522d83f..39eebfadbb 100644 --- a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs +++ b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs @@ -105,8 +105,8 @@ pub struct AuthorizationData { /// Signer-declared override of the default execution address. /// - /// `None` ⇒ resolve to the credential's default address. - /// `Some(X)` ⇒ requires an explicit `(X, credential_id)` entry in `account_owners`; + /// `None` => resolve to the credential's default address. + /// `Some(X)` => requires an explicit `(X, credential_id)` entry in `account_owners`; /// the transaction is skipped otherwise. /// /// Set by authenticators that carry a signed `address_override` (sov-tx V0/V1, From 3544ca44eea2714a36d3439ecb3cad8126752804 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 15:24:24 +0200 Subject: [PATCH 20/34] Addressing feedback --- .../sov-rollup-apis/src/endpoints/simulate.rs | 4 +-- .../module-system/sov-capabilities/src/lib.rs | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs index 90f4a1f415..2e100b0c4e 100644 --- a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs +++ b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs @@ -305,8 +305,8 @@ impl> SovereignSimulate { params: &SimulateParameters, state: &mut StateCheckpoint, ) -> Result, SimulateError> { - let credential_id = CredentialId::from_str(¶ms.sender).map_err(|_| { - SimulateError::InvalidInput("failed to parse sender credential id".to_owned()) + let credential_id = CredentialId::from_str(¶ms.sender).map_err(|e| { + SimulateError::InvalidInput(format!("failed to parse sender credential id: {e:?}")) })?; let address_override = params .address_override diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index 861364bd65..8b31aa0f6a 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -346,17 +346,32 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' &mut self, auth_data: &AuthorizationData, sequencer: &<::Da as DaSpec>::Address, - _state: &mut impl StateAccessor, + state: &mut impl StateAccessor, execution_context: ExecutionContext, ) -> anyhow::Result> { - // `address_override` is intentionally ignored: this path runs for sequencer - // self-registration before `account_owners` has any entries to authorize against, - // so the only meaningful sender is the credential's canonical address. - // The tx sender & sequencer are the same entity. + // The tx sender & sequencer are the same entity on this path. If the signer + // declared an `address_override`, the credential must be explicitly authorized + // for it (same invariant as `resolve_context`); otherwise we fall back to the + // credential's canonical address. + let address = match auth_data.address_override { + Some(address_override) => { + anyhow::ensure!( + self.accounts.is_explicitly_authorized( + &address_override, + &auth_data.credential_id, + state, + )?, + "not authorized for address override" + ); + address_override + } + None => auth_data.default_address, + }; + Ok(Context::new( - auth_data.default_address, + address, auth_data.credentials.clone(), - auth_data.default_address, + address, *sequencer, None, execution_context, From d227aa543f9fda1c07781376846535c81170b6c2 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 16:09:58 +0200 Subject: [PATCH 21/34] Update EIP-712 tests --- .../stf_blueprint/operator/auth_eip712.rs | 100 +++++++++++------- 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs index 00bca35a69..cd06b7024d 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs @@ -8,6 +8,7 @@ use sov_modules_api::capabilities::{TransactionAuthenticator, UniquenessData}; use sov_modules_api::configurable_spec::ConfigurableSpec; use sov_modules_api::execution_mode::Native; use sov_modules_api::macros::config_value; +use sov_modules_api::prelude::UnwrapInfallible; use sov_modules_api::transaction::PubKeyAndSignature; use sov_modules_api::transaction::TxDetails; use sov_modules_api::transaction::{ @@ -226,11 +227,10 @@ pub fn create_tx>( sign_utx::(utx, signer) } -pub fn encode_message + EncodeCall>>() -> RT::Decodable { - let msg = CallMessage::SetValue { - value: 0, - gas: None, - }; +pub fn encode_message + EncodeCall>>( + value: u32, +) -> RT::Decodable { + let msg = CallMessage::SetValue { value, gas: None }; RT::to_decodable(msg) } @@ -265,7 +265,7 @@ fn execute_tx( #[test] fn correct_signature_is_accepted() { let (mut runner, admin) = setup(); - let call = encode_message::<_, RT>(); + let call = encode_message::<_, RT>(1); let tx = create_tx::<_, RT>(call, &admin); let receipt = execute_tx(&mut runner, tx); @@ -276,9 +276,9 @@ fn correct_signature_is_accepted() { #[test] fn test_multisig_signature_verification() { - // Build the multisig first so we can seed genesis with its canonical address funded. - // After the accounts refactor, the sender resolver no longer auto-creates an `accounts` entry, - // so the multisig must be funded at its canonical address. Here we pick the canonical path. + // `address_override` authorization correctness (positive + negative paths) is + // covered in sov-accounts/tests/integration/main.rs::test_v1_address_override_*. + // This test focuses on signature verification mechanics only. let multisig_keys = [ TestPrivateKey::generate(), TestPrivateKey::generate(), @@ -287,27 +287,31 @@ fn test_multisig_signature_verification() { let multisig = Multisig::new(2, multisig_keys.iter().map(|k| k.pub_key()).collect()); let multisig_credential_id = multisig.credential_id::<<::CryptoSpec as CryptoSpec>::Hasher>(); - let multisig_user = - TestUser::generate_with_default_balance().add_credential_id(multisig_credential_id); - // The multisig is the sender of every `SetValue` tx below, so ValueSetter's - // admin must be the multisig canonical address for the successful-path - // assertions to actually reach ValueSetter. - let multisig_address = multisig_user.address(); + // Alice is a regular funded user; she pays gas for the multisig's transactions. + // The multisig acts on her behalf via an `address_override` authorized in genesis. + let alice = TestUser::::generate_with_default_balance(); + let alice_address = alice.address(); + let genesis_config = HighLevelOptimisticGenesisConfig::generate() - .add_accounts_with_default_balance(2) - .add_accounts(vec![multisig_user]); + .add_accounts_with_default_balance(1) + .add_accounts(vec![alice]); let module_config = sov_value_setter::ValueSetterConfig { - admin: multisig_address, + admin: alice_address, }; - let genesis = GenesisConfig::from_minimal_config(genesis_config.clone().into(), module_config); + let mut genesis = + GenesisConfig::from_minimal_config(genesis_config.clone().into(), module_config); + genesis.accounts.accounts.push(sov_accounts::AccountData { + credential_id: multisig_credential_id, + address: alice_address, + }); let mut runner = TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()); // Generate a signature from a random private key that's not part of the multisig. We'll use this in some of the test cases. let random_private_key = TestPrivateKey::generate(); - let address_override = None; - let make_multisig_tx = |generation| { - let utx = create_utx_with_generation::(encode_message::<_, RT>(), generation); + let address_override = Some(alice_address); + let make_multisig_tx = |generation, value| { + let utx = create_utx_with_generation::(encode_message::<_, RT>(value), generation); let signatures = multisig_keys .iter() .map(|key| sign_utx_v1_in_place(&utx, &multisig, address_override, key)) @@ -341,36 +345,46 @@ fn test_multisig_signature_verification() { "Expected error to contain {reason}, got: {error}" ); }; + let assert_stored_value = |runner: &mut TestRunner, expected: u32| { + runner.query_state(|state| { + let runtime = RT::default(); + assert_eq!( + runtime.value_setter.value.get(state).unwrap_infallible(), + Some(expected), + ); + }); + }; - // A transaction with three valid signatures should succeed + // A transaction with three valid signatures should succeed and write `value = 3`. let tx_with_three_sigs = { - let (mut tx, signatures, _) = make_multisig_tx(0); + let (mut tx, signatures, _) = make_multisig_tx(0, 3); for (key, signature) in multisig_keys.iter().zip(signatures.iter()) { tx.add_signature(signature.clone(), key.pub_key()).unwrap(); } Transaction::::from(tx) }; assert_tx_success(tx_with_three_sigs, &mut runner); + assert_stored_value(&mut runner, 3); - // A transaction with only two valid signatures should succeed + // A transaction with only two valid signatures should succeed and write `value = 2`. let tx_with_two_sigs = { - let (mut tx, signatures, _) = make_multisig_tx(1); + let (mut tx, signatures, _) = make_multisig_tx(1, 2); for (key, signature) in multisig_keys.iter().zip(signatures.iter().take(2)) { tx.add_signature(signature.clone(), key.pub_key()).unwrap(); } Transaction::::from(tx) }; assert_tx_success(tx_with_two_sigs, &mut runner); + assert_stored_value(&mut runner, 2); - let (base_skipped_tx, skipped_tx_signatures, random_signature) = make_multisig_tx(2); + // From here on, every case is expected to be skipped — `value` should remain at 2. + // Each case uses a distinct sentinel for `SetValue { value }` so that a regression + // letting any case slip through points to which one leaked. // A transaction with only one valid signature should be skipped, since this is a 2/3 multisig. let tx_with_one_sig = { - let mut tx = base_skipped_tx.clone(); - for (key, signature) in multisig_keys - .iter() - .zip(skipped_tx_signatures.iter().take(1)) - { + let (mut tx, signatures, _) = make_multisig_tx(2, 99); + for (key, signature) in multisig_keys.iter().zip(signatures.iter().take(1)) { tx.add_signature(signature.clone(), key.pub_key()).unwrap(); } Transaction::::from(tx) @@ -380,15 +394,16 @@ fn test_multisig_signature_verification() { &mut runner, "Not enough valid signatures. Required: 2, Got: 1", ); + assert_stored_value(&mut runner, 2); // A transaction where one of the signatures isn't from this account should be skipped, since this changes the computed credential ID. let tx_with_random_sig = { - let mut tx = base_skipped_tx.clone(); - tx.add_signature(skipped_tx_signatures[0].clone(), multisig_keys[0].pub_key()) + let (mut tx, signatures, random_signature) = make_multisig_tx(3, 98); + tx.add_signature(signatures[0].clone(), multisig_keys[0].pub_key()) .unwrap(); tx.signatures .try_push(PubKeyAndSignature { - signature: random_signature.clone(), + signature: random_signature, pub_key: random_private_key.pub_key(), }) .unwrap(); @@ -397,27 +412,29 @@ fn test_multisig_signature_verification() { // The random key changes the credential_address so it no longer matches the signed bytes, // so signature verification fails. assert_tx_skipped(tx_with_random_sig, &mut runner, "signature error"); + assert_stored_value(&mut runner, 2); // A transaction with a duplicate signature should be skipped — the duplicate changes // the credential_address, causing signature verification failure. let tx_with_duplicate_sig = { - let mut tx = base_skipped_tx.clone(); - tx.add_signature(skipped_tx_signatures[0].clone(), multisig_keys[0].pub_key()) + let (mut tx, signatures, _) = make_multisig_tx(4, 97); + tx.add_signature(signatures[0].clone(), multisig_keys[0].pub_key()) .unwrap(); tx.signatures .try_push(PubKeyAndSignature { - signature: skipped_tx_signatures[0].clone(), + signature: signatures[0].clone(), pub_key: multisig_keys[0].pub_key(), }) .unwrap(); Transaction::::from(tx) }; assert_tx_skipped(tx_with_duplicate_sig, &mut runner, "signature error"); + assert_stored_value(&mut runner, 2); - // A transaction with a bad signature should be skipped + // A transaction with a bad signature should be skipped. let tx_with_bad_sig = { - let mut tx = base_skipped_tx; - tx.add_signature(skipped_tx_signatures[0].clone(), multisig_keys[0].pub_key()) + let (mut tx, signatures, _) = make_multisig_tx(5, 96); + tx.add_signature(signatures[0].clone(), multisig_keys[0].pub_key()) .unwrap(); tx.add_signature( multisig_keys[1].sign(&[1, 2, 3]), @@ -427,4 +444,5 @@ fn test_multisig_signature_verification() { Transaction::::from(tx) }; assert_tx_skipped(tx_with_bad_sig, &mut runner, "signature error"); + assert_stored_value(&mut runner, 2); } From a13d670f8b118e0c40d204f0f90cd492e8d9980f Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 16:42:08 +0200 Subject: [PATCH 22/34] More typescript clean ups and comments --- .../src/rollup/solana-signable-rollup.test.ts | 35 +++++++++++-- .../web3/src/rollup/solana-signable-rollup.ts | 49 ++++++++++++++++--- .../web3/src/rollup/standard-rollup.ts | 8 +++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts index e150b5528d..522ebc7237 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts @@ -933,6 +933,33 @@ describe("SolanaSignableRollup", () => { expect(parsed).not.toHaveProperty("address_override"); }); + it("includes tx.address_override in the signed JSON when params.addressOverride is omitted", async () => { + const { rollup, multisigAddress, multisigPubkeys, signers, unsignedTx } = + await setupMultisigContext(); + const txAddressOverride = new Uint8Array(32); + txAddressOverride.fill(0x24); + const txAddressOverrideBs58 = bs58.encode(txAddressOverride); + + const { mock, captured } = captureSignerInput(signers.signer1); + const signedTx = await rollup.signTransactionForMultisig( + { + ...unsignedTx, + address_override: txAddressOverrideBs58, + }, + { + signer: mock, + authenticator: "solanaSimple", + multisigAddress, + multisigPubkeys, + }, + ); + + expect(captured.bytes).toBeDefined(); + const signedJson = new TextDecoder().decode(captured.bytes!); + expect(JSON.parse(signedJson).address_override).toBe(txAddressOverrideBs58); + expect((signedTx as any).V0.address_override).toBe(txAddressOverrideBs58); + }); + it("includes address_override in the signed JSON before version when addressOverride is provided", async () => { // Field order matches Rust's `SolanaOffchainUnsignedTransactionV1`: signers sign the raw // JSON bytes, so TS and Rust must emit fields in the same order or signatures produced @@ -944,7 +971,7 @@ describe("SolanaSignableRollup", () => { addressOverride.fill(0x42); const { mock, captured } = captureSignerInput(signers.signer1); - await rollup.signTransactionForMultisig(unsignedTx, { + const signedTx = await rollup.signTransactionForMultisig(unsignedTx, { signer: mock, authenticator: "solanaSimple", multisigAddress, @@ -954,13 +981,13 @@ describe("SolanaSignableRollup", () => { expect(captured.bytes).toBeDefined(); const signedJson = new TextDecoder().decode(captured.bytes!); - expect(JSON.parse(signedJson).address_override).toBe( - bs58.encode(addressOverride), - ); + const addressOverrideBs58 = bs58.encode(addressOverride); + expect(JSON.parse(signedJson).address_override).toBe(addressOverrideBs58); const overrideIdx = signedJson.indexOf('"address_override"'); const versionIdx = signedJson.indexOf('"version"'); expect(overrideIdx).toBeGreaterThanOrEqual(0); expect(overrideIdx).toBeLessThan(versionIdx); + expect((signedTx as any).V0.address_override).toBe(addressOverrideBs58); }); it("forwards tx.address_override into the submitted JSON via submitMultisigTransaction", async () => { diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts index 28778743c3..70137e10fc 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts @@ -142,6 +142,39 @@ function compareByteArrays(left: Uint8Array, right: Uint8Array): number { return left.length - right.length; } +function resolveMultisigAddressOverride( + unsignedTx: UnsignedTransaction, + addressOverride?: Uint8Array, +): { + resolvedUnsignedTx: UnsignedTransaction; + resolvedAddressOverride?: Uint8Array; +} { + if (addressOverride !== undefined) { + return { + resolvedUnsignedTx: { + ...unsignedTx, + address_override: bs58.encode(addressOverride), + }, + resolvedAddressOverride: addressOverride, + }; + } + + if ( + unsignedTx.address_override !== null && + unsignedTx.address_override !== undefined + ) { + return { + resolvedUnsignedTx: unsignedTx, + resolvedAddressOverride: bs58.decode(unsignedTx.address_override), + }; + } + + return { + resolvedUnsignedTx: unsignedTx, + resolvedAddressOverride: undefined, + }; +} + function createSolanaPreamble( pubkeys: Uint8Array[], chainHash: Uint8Array, @@ -590,6 +623,8 @@ export class SolanaSignableRollup { multisigPubkeys: Uint8Array[], addressOverride?: Uint8Array, ): Promise> { + const { resolvedUnsignedTx, resolvedAddressOverride } = + resolveMultisigAddressOverride(unsignedTx, addressOverride); const pubkey = await signer.publicKey(); const signerPubkeyHex = bytesToHex(pubkey); const multisigPubkeyHexes = multisigPubkeys.map(bytesToHex); @@ -601,15 +636,15 @@ export class SolanaSignableRollup { } const jsonBytes = await this.createMultisigJsonBytes( - unsignedTx, + resolvedUnsignedTx, multisigAddress, - addressOverride, + resolvedAddressOverride, ); const signature = await signer.sign(jsonBytes); return this.typeBuilder.transaction({ - unsignedTx, + unsignedTx: resolvedUnsignedTx, sender: pubkey, signature, rollup: this.inner, @@ -626,6 +661,8 @@ export class SolanaSignableRollup { multisigPubkeys: Uint8Array[], addressOverride?: Uint8Array, ): Promise> { + const { resolvedUnsignedTx, resolvedAddressOverride } = + resolveMultisigAddressOverride(unsignedTx, addressOverride); const pubkey = await signer.publicKey(); const signerPubkeyHex = bytesToHex(pubkey); const multisigPubkeyHexes = multisigPubkeys.map(bytesToHex); @@ -638,15 +675,15 @@ export class SolanaSignableRollup { const signedMessageWithPreamble = await this.createSpecCompliantMultisigSignedMessage( - unsignedTx, + resolvedUnsignedTx, multisigAddress, multisigPubkeys, - addressOverride, + resolvedAddressOverride, ); const signature = await signer.sign(signedMessageWithPreamble); return this.typeBuilder.transaction({ - unsignedTx, + unsignedTx: resolvedUnsignedTx, sender: pubkey, signature, rollup: this.inner, diff --git a/typescript/packages/web3/src/rollup/standard-rollup.ts b/typescript/packages/web3/src/rollup/standard-rollup.ts index 7e68733bb1..fc1a11dc44 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.ts @@ -84,6 +84,13 @@ export function standardTypeBuilder< /** * The parameters for simulating a runtime call transaction. + * + * `address_override` is bolted on by hand here (and the corresponding cast + * in `simulate` below forces it through `RollupSimulateParams`) because the + * published `@sovereign-sdk/client` lags the OpenAPI spec by one Stainless + * publish cycle. Once the client is regenerated and the version bumped in + * `package.json`, drop both the extension and the cast — the regenerated + * `RollupSimulateParams` will carry the field natively. */ export type SimulateParams = Omit< SovereignClient.RollupSimulateParams, @@ -110,6 +117,7 @@ export class StandardRollup extends Rollup< const sender = bytesToHex(publicKey); const call = runtimeMessage as { [key: string]: unknown }; + // Cast bridges the `address_override` extension; see SimulateParams JSDoc. return this.rollup.simulate({ ...params, sender, From 7a3675ea0c15be4285984284e33c245a8d9d6f5a Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 17:44:21 +0200 Subject: [PATCH 23/34] Fixes --- examples/demo-rollup/README.md | 2 +- .../tests/resync/data/mock_da.sqlite | Bin 28672 -> 49152 bytes examples/demo-rollup/tests/wallet/mod.rs | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/demo-rollup/README.md b/examples/demo-rollup/README.md index 272a17972b..71c847c053 100644 --- a/examples/demo-rollup/README.md +++ b/examples/demo-rollup/README.md @@ -154,7 +154,7 @@ $ curl -sS http://127.0.0.1:12346/ledger/txs/0x050b4abd3bb8e3fe1ac2802c9dafc6374 "type": "moduleRef", "name": "Bank" }, - "tx_hash": "0xbe16e6f31255f2f5f0a1e39530f91fd7f6480d73a3fbd3d3dcf7265962417db7" + "tx_hash": "0x050b4abd3bb8e3fe1ac2802c9dafc6374e6f2e4f35c0a4f2cd8533cf1d62bf80" } ] ``` diff --git a/examples/demo-rollup/tests/resync/data/mock_da.sqlite b/examples/demo-rollup/tests/resync/data/mock_da.sqlite index cc915bb844eb32828861b4a948da616876a0099d..5fcb99d19e42fae1f7b2c9079b423d63d44597b7 100644 GIT binary patch literal 49152 zcmeI52UHW?*X|Pngpxup(tC#xI?@H{9i&JL9qB~@6;u#ZqzEFQpn?^o2=*=tN|Rm$ zMHB@AD@9QO^=49hdH>00a#?rX_1*hrmh1@;p5HV3OwP=(&zx~~v| zs)5)*U@*u?R1^dPF=M}A*e~Kjk9`4r5f?D=#b4IUAmylAaKvd4BTNs3_=%{7J0n72 z=ds_=7Fq^q8K7l=mH}D@Xc?eofR+JT251@he<}lEj8I-3Uhp1dSX5w8QdnS;l7B?B ze>@Na!hJ)7!a_n5aPL4jIGGu{n2B22nVPwaij)6IaZ!6aQE}YQi;KzvFaC&#%k7b4 zfbwc-f%n8FVEiM3A~5j@zQJKpn250LL4gDm**6&pXcuD>8#7UHvOj~x0j9)7!=e&` zLW1H%?d)Ad?ObhaL>-(gZH=8gM6Jy{M2%ft>@DrEa<*o6E{dY!+b|JZgUI}jTp9-i z$}1xSP7lC&C2j~NFeonmU*FLG+uOhXZ*89NIVWSv)y~q<)eP%8CN5~3Zzv`{R9ti$ zCe9z5!nkB#EZP6hMq_`7(*Fm<1z{4fdB-G(izb9c2E`{}B4d0L!xBP$fp51*M+NyN z1SKW>(G-=_p=ac^l>viryGnd)1oooS7qc}X8uvY50N2>K9QmsL>stowXMzd%TsY7% z@+v5RJ#bkiNW=%lZVieG2>Oq2A^*zkfBdIx6ecoA5&N5RX&^9^mzNiu?n`Vbf1ri@ za2D z`!9cAR1U~47`Y7Omle4Te+UnRGr}riR;C-5OK8Z-df1_)7Jyd||sADK)JO zt|HM@>h#oRMwFng!?-+Y0yWdRSAFB_HB>}p6ZG}nSC4i<>UfK*?~Tvg<`r3`PIYEP z2BRq7CG^Ug<5S3J#9&*0dvBLAy2DW6_@dumN$ zp;qs{%X$149<-tZ)|d0HQl~mIyaaW5qV3vg16y9D1c|4rcGoRuD71b} zHg+=KeVUonp{98}Ubh7k=Qas*k_d{ApsUTY#I; z0&)*D2UUbO>wGGE?XNN%RlTQ`w_iV??t`X99=yi5aEY1d5TH`6&P7m{U3fNS(sVwc ztMjU-iTwNQjrl?8pBQFYUGmzN!H?@s*>Dmz23S$u7jqKSr8?J%c_-O&>SS@*-4=v# zE;;UI=Bb+r(X$@CG=-s?x8_)-PPKOqg1W@4)s44hk;ouvI?txxkgVKAGgUxx zs}!LzPi7+6*P_l&P!|sfukA9qSFvy5P9SqrX1EVx@M>}Xp&QBLPjBjHdrZ6}HZuUB z+B+LTUF=Ygl5Bd=mz6~>Mi1r_?0XQZ1I;rfEaDlt-2!o8nG621VLRm#l-e$rj40`%iBcDfJu4T z-h=n|>RXQA{b+5Re-5;{6~0QH>de3i>VlW{G3;px1Q`YNiG1f$Lc7eHccg-&GyRiosTOu}}Q4#39a3yYm-=M=aVRSXX(c zx-VuWsPon!(hgyM$m@jAJ2hk#@CC=|IcQ|%cx{_S^qg*G|Jg_EMF0rZ-dPChJZ&*a z*K^tzUMI=k6UszK!S%Bc<+XMF4NgAsm!?G7w*qA?;zF%@niw;&FXop!d=CsZ*XNB7#+{skyqr zy4z>R^Vcm;Uds`CR$SIrkl8R*s~&4xV#V1JbK|1^3mb?0l0n{9+0N@Ml>1`hXMwmT zX7NuaP26bE-}_=dcWJKXwe-bIgcjglH>$M3tK>s-T-)LVbL7gi;#H+FPesbF{_Ybkg2Q!(Rr=4PSzTT2Mu5*Dddplne<0^Hk z`(j3dI@^x9RQ+y?iOHEKC*-~!HVxMO$|{jqQ^d^4scy|VI9vc+FA*1N_Z}|>f;xPh zZ8E?j|3}jU$2&Zm^X1;fy^HK|7TnpqH($B(>2mL#f0b9MJ;$LZsKY9vS~-?qyrM_u z4`En2Yo76UolCuRRl?9_IOmzAV(|!sXdJty)_a;5gt+IkLOp&Nr*_$)IAg6d(=8hr zCq*qwu5_rGIe@H$YQ&#Epd;x2d(Rg*uWf^(Lu)XLyZV_e$u}*PM`Wj1Hh(}ITfRRP z>EnE|m&4*=&G8opS1h{^uv#p+h3Z0~5?SiIWfy1dvU8Xy@Ax zxr7dV(^~d?IzkJu?k%hNVmDM)^mRTAu_-Z{WI1g4ETE%9dZ%aG8l3hxrl(kkHfWKy|8c?1M5jv9hQEUNERq_ z1@n5QL8fRXX6R|CKI2oySVmTcvkZpxBlPj~aL9RxG2L^zt#n9m6?g;a1t^hXq5@?Y zv9&E5a~b30fPEYu$1fM|teeeJ69~!YtlJdzVbA+-bT=MaTcCL4jr2lx_>3u+91TtG z<5B%kxJ^iF{ftScEVQcBV?w+hjY4ZGp){0Gnl7qnT~$?GG+J3hOFRv6i*Q?^Pk?6@=|T6&D^JVA!zPX@pSi$)WECXhx-hN#uJ*B zD7hdY>Nn8J=G0Jn;>u|^HC|cs+wD3f7o-$z zWgQf8TZHix;hB@7-W@6)7RvPq6^V6uS2Ww5&ng$_-aNFq{)9gF2rDHQ)i_eNsazRSbQr6KS?#Y69ir^=h zK#Q<&MM`XM!=f7t)R;T0_BfKh+-%P*K6_h36RKS~A9 zO(arsL5_mfQX^1!@D!d%$={98!;srg&q5@2K@F>qd<=cde!$3O$>}#dK#|Lwk_%Fb zI<|))?#bME3in9LaghVkc1~}dzMNq@iDo$S%}<`+>rv+@x=PdBt~HX93vv`SP2%x| z3s2!n_~0?VIrqhshJfUpfQN1TR|m&ENnR7dua}9JK4|Fdj7vv}!>cpcM8=k_pRcyL<-OGkHn*sF;mp0nidyF+F z1b08l){z@9>Jgb7rsRSg<)3pHB%Xpi%3!N9{$$uPIw61~j=k{CVw5w-BdA@*vBIMR zGKg0Wlw6QffO8mPyCCos#M^iB;W3s|Zi3SzeZKY!Ri-AN<*S5Gyj%P^Rd3sMSj4nzFB z!0;59k0o4jrmOJtXFJfmW-n_&!xOD5?NMIC*>MfX#M;wwlw6Rbpf!m-KP#TXs`;I* zy)vZ+UJ4R+sUMmHPhNAmBOnq|GRttH5>p{mM#%*U1qGbL5c@F}JcXrUwC`+&FRwfE z-hIa|9%sFfS~-5;#cZ;mn3vhHyN!c8DY+m=QPUu93p1X=JbmHu1HIWV1ADzeEstgx zpB<{Ga=E9Iap$p4{Ij&J=RQ($K}rG6VTeaDCOn17Z=1B%!S)jprAc1~$9{R!b8+>Y z|K?Ec)m3?>v;ENigOps5qo`{TKb25C1sdhSc8A`yYMo(6XxF`k=e#!=_uTneD;C~& zv|;6Igq#p17o-&69ENyy!HB0Y`hPgxHaO}1ctgSw3{>ASvzFoM{bR+vyF`~qT0H8a zoGG~=N6}Ct9$y&n6o%{X?bBb~I5OP}4nDig82=*8FYNrAl9N4l0_VTHw0$2>$ptwI zN}Kqpq{ma}mFgclz*4@1O=KxMb7d?%g9ck(4DH@pE&t`?j{Av_0!l7ODZn`l@ly%G zQy}HS`p>l`&C?csX6Jm9dLZ_~aB}GB#YPR|#S7E6{Lwm;T#%#ubCpDgr_g=lO5Zm& z@bf{YRtoAz&RI3RG_DP0w%amt+pj_Q^MtigazRP~&S40j$^bAUh#7m0Ot_d{J$B$< z*VmLxkbz)-pVi0vU|-LwX`zkakE5qzO_Fsfk1(<&ly|VI&`t0|`SiB0-2H z#23UT#5CeHViYli=s~n2ni2O9HxQQ)HHdSFa>Ox2AtD#CACZRGiP(mSL2N+;B76|; z2q%OMVk5!`p^H#QC?jMM;s`+m4}uNBf}n^0f`5n4!#}{^!e7Ck!3W@7@K)@73wPkx z;Pvne@H6le@Dg|dc9UoeEd#U+&@w>F04)Qw4A3$_%K$9{v<%QP@ZUBA3=l9lEDVUD zp+F1?0b+145QBn%7#Ik|fB+!+`vVb!0ivHD5Pf}t=;H%KZ*L%ac>!_rW*~Zc0@1?* zi01f#~D}L`O#;IyeB)-X4f{c0jbX1)_}&5Us6&Xk`UN zOG_YHSOC%79Eck?0@2J2h^D4M+^_+NCMG~MHU^@R5fBXxfoNa=M16fA>gfS-{dyqk z>H<+m2Z-9*K-AI#qNXMgH8g;zt`0;sH6WtVKvY!)A_@gW6%`;VD+5tU35be{KvYlw zqP#p1<>Y`UD+@#!86Zka15ru}h?0^(l#l?TxHu5S#DFL&3PceRAPNfuQAh}gf`UL4 z5CG!3bwK3j2O=LI5P5ll$io9fZf+oQaRHH&6NnrfKxAhJA{!eJkw_pS5I}^(fe3>E zk(CvQEG$4|W(Fb?6A+_v7UA0h7}Zz3-vYmw)X706QL5o8|n05Tn!f=oolBEyl`Sq+dLNN1!i(i~}w zT#wX1svzZ%5=bE=FOnU}iey0iM*Ki5AU-0d5aWpFh(Sa*q7Bi6xQn=sXh2jW&LU1C zjv@{taF04)Qw4A3(0KO+M`K{^yy{tv>H|ATPl z|3F;%-yc{0$KcBUKDhF~H?I8eg)9GW#+Cm)apiwET>0M>SN?awmH(Y_<$p(9`QHIo z{ATAkHVGzRdD5hMO^t`9#{UC!_og^{_OS(j;mDT;{+dJ3w?IK&vk$ii@j!GM04K8_v+IR`PKdq$T)hX7ZC zO+cfRp8^9E05Y({kNv5rqNN4>63(G>qF zxX;1x{H%rM+pcG3>B?8;5>8TbL5`wEY^OkLJjFV@Cu7}JNG^xLojlvbP_LWoWsB2( zO<7g(J_*gBUog?7*KKpkNIGE2fE#kd?*+w`+1CY;nKI$ z*3KHmw*{JeD7hdf1iL*Y7o-#| z?CBYCQX6wT#r$TKi)Txw=i~R!s_C=Q49UJTXSR;r+I9AV>?If8u()hWF33?(I>ZTj zHsUE80~e=~->Z zK3Tok@n?Pcq!r8k@UJVAGqMWi-_F3^>WCF-?3K%-pR7AV>M<0?-6cF=@`G_xhCWEX=Ice5>^bYxT;E-M68( zbN6{`AAB#WnpUUef|LSG07sl?%otBGt}ZFamKiqBI}s_#A=0xmHcR$P+FfA|c5^w+ zI=VNL^ORhWqx>@goDrU4WO3fO`<=k&v6&r4!-2sDE%zk`!8-!ZL^LptKNSy-SEA&C zlmc7;68AAfJjIZw{>w&(>+24lyb&{PR=A=FUAVMzb31fcC*K8hTKd^mN-oGz{+R&I z08cSk5%Oq}M#Sxayng{>IR0DQO-jzWQ_%&K)cx5#?b_j^lw6QfR!;z@kEiGh8nP$T z=lL5l-OK&OyUqGgp2Cd(=WiJ4=MxsaAz|tXlw6RbXs8oEFM4>2-jUBDsvH8XT^laj zNSvv|cw^<=b_$(zC|Mfh%m?b4pIS-g;xv_i=RIm$m5fNFS(+KQD)&)MGj8v`?zyR!lg zj%<7%jxj0xia7m5&S1ZqSTiLTq?FYYz@hOJ^v{S3BIak8uCJ@LRW&<+67+ZHSZDL8 zc1Sq!@BwFI`RkNikfZ$bCni-qMYXr)4b-uk`)D3gQH=O0vEVpw@ZX9MamhX=d3BYAf*83 zFvL%#3ZA0EUUuecnRTW5qIO73F?4);N*`yGtHn88>zIQ=cAw%eP;xes7r{sc^f<1@%Gx4huo}wgnZb#rG z9~*a)m9)}CIJjZ6d;8dvjNfpVm?!s}*y}AQxgbYDYZ5;%ig=2mq`uCA{Nd|^3P(0q z7A2$<7VTOP{PdYU@p0gIp?6811SJ=w6yO|&czjX7Qxt@FYwLR)YRs)AQHDb-JZPn~ z6E}>)rEfYmig!Kdk2^}q1v!eE260>D@f3MRFTUb`?b=4q8`FU36bf$f^++UM(T~yZrWc1aLxLepbQkDsz@NZ}z-Z6_ zJN@}Zjx9v{)M_{ z8iGJx5(IUFh4#RSLnV!V-B}TPYEog0&+or&AF`EjGI*SPN^+o2Uwkd<;ska5fJuK% zzPgp)uAQf1Yuc4_&s4^%e-q+mmtL$CeHcA+Tx>1sVgz+P-Ei0}sIZD@9(F)hPuM*S z_2#*7xTox7WB(~NtqiUr(N*eHKj&1GpsvHXJZSvRA&)nt}S*1>O-bD!NTC)$9lR&B7Ih(lx(3`t+3}Rm z2i2z;wL6c@F&>Z*d6V1FQpw!>cC*-t)>(VORq9kfb6Sv~uI6W#CuaO<+Q3vonW4j+ zpGj3wC>69l;z8E6sw-`-iV%Uds0$F((Rmi#%2#7PyT}NlQh4md*9*b(EW^KrCr8aL z3+kV2*}ZO+I@PVZj-ZYz(R;`<=pbBia4BX(aIRs|g~&)wxidb;*0(pfcHQ08#J?7G zeuBD+;c(YUwQ|+ceK6m0WzOe|ttaO<|5|^8HDE8gRpRo;ygv_z$5N<%<}@EcUGX$a zJ%eMviu|wAqNfYBCK6zKJE+$#}5QExG`|Ik_YPy5*7T&oX zV+)o1Uc%~`aRNu4a})G|Iby4^Uv%eyH~0-0>$x{(qL(sEceVctG`A4{@HP1O>?6aw zTj|q852rI@gkJP&mLs%l#kVs|O|x1RpDVboP2X-&2)5kFH3y>nf^OMd6%2e0&)*D2UUbO>wGGE?XNN%RlTQ`w_iV??t`X99=yi5 zaEY1d5O73w`*IP~Wfz`JnKYdb=<2-cX(InVdt-i3`X`22R+qfCW$@#=Q#PFcY$%B8 zzL=AsF4ehC%sa`JQzwhd?zSL|bIEZxGf&-2h@SQ6r6~;Eyfw!vb*jB{5Y#1Rt!}(6 zi$n%V(|I=ihGcbbDE=h>ov8wnTcrq%c`_5hz7}cx{)-y^4JccLJH4GQ)ik zgIA0558X%}e|l3t+hgJ-+gjAw2odX!|-gTCa%yp-&N7sp!3oahQtSbW;$?REr} zaUA4HeAYPtp}H?d64XURRl4cD+lo4_n2ajaTf`snD_IoS;xB&yG1{hEbw&6!VwHEQ z)e!`B;S>|wrYvqe)prbasD~b=2qa$i?~qTs&In3;H7;G zds+fPMge^y-?@~~F7xIc=^%}f%;?Pn*Zb@Rq+vv9?3!wI7(rd&Ky5(CCF?_iephuM zD+XKD#Xj-F5{EcD?ap5e9!YH8G3e)mWX zJH_aHy(M>C=K@{!cD^LWRq9mt#f$`XwjFb+`rQ^2lQU0F$bCI*8m#-3RU)ybh?$d9 z-I{Z7xBz%w8gZd^@9|6RCyu-6OU+!JpyT~4A!JW-}^OY-~F8AJ{ zCrV@2RIAey)L|7-tsKiQUeP1-hcK+1HP86F&ZS3a%P!8^W#=$a z-t&oj`hTv$#5ewI_uu#YatR&!rnT((bc7aQ-CI`k#crsq=<9rzfa;uX?heVL&ZVZY z*gNHu75cN*hXR=)E>yQKn4r#F_O`k3SK7wx*IuY5mXYm|4Z_~ycWR7orp%s~*tIjN zhNujzsZ|G#LIU(z`0M|-*qQj{;ZNXEa5mT_m<#J7w)&sI!oz%n*^_C7=>(Gz^fh!Z zRGP7saSJ1iej590w1t)dS_Wtt_&<;VFGE5<7b@M#o?j@S5b5SJtD{+NggS5IXl>2< z?z2E_@h!m}54;Rk(Wvd`(!>mi$FU$=k3jy8X~+S z9QjeMPtgDOaZG`Esf>Hb-ag044Ep%U{b||Q^O5rb$tI1Jy!r-q0(;W~cU^fSAMol& zgd5^Y{&|xWot1;7_I{~{B-D}0nI%+QO%wkrMqCpQU*N!18;pDT_v2Wo(VpxlYdMbT z6IvjM_h7Q4S~Q(yZ0QR>zTRE(69Z0DgGq<-1xi?QTv80b>a8+FZ3}qm5!3_EL25_j zc(*(6nXgdeJ$VDO?O27$o7x4?p7(eAlBRPR*RN8i`nqd9LEXQIo&U36G_){@&(*y8 ziflx&zRS7eujboUQcpjE&nV96u2QGkyDmW;lVh>8%{p=4+gt>C9FC5T-l%fTs`tdP z%h#ECII51eTkEV+r~1lBhoJ5w!1vH717jD;`MIjYc#9k3BFCQo%slOnx-RZjp0YN2 z+N;#5_O4A(_YUdmTb3E%*lFxO*e^r>N^Y#&NA$>T$w$lL)3^A`FfLkaQP(1Z{bLzNSzosJopq->+zUdvjE};`p|o{Sh@161Jf~QM}OE5QeMF zL&k|}tJJA(RW*XTtA$14!gOv<&ia|%qIp%9T$^jYU$>Ylc<}0|PVe4pza7zQQAZQh zT{7N;x1EsjNV~aWxA0I=GrQ_q)Kv-U&cQ#L5qD(5+E~A; zM&2ztw}U@LnA_L`X=*a^YoIG$dpBwg>S#jG?RJYEs71}J(EaT#3Iote-5YyBv zpOzg}8&c;$o($QQn*ueqV5}E7e*esis7-ro`fUnA`HF@3mvZ9zPfzc#YJ+hv|K8KP zi#JBKuj=Wk?r~HIE#TnDk}7ulX7b3Tu>F{>Ofz<^`F^({gIs&{ywu6r&q8g=s|-=S zZz>bi?J{J_RV2^!_8glK^$_kkn@)dIIIVxzf~PiH4+o_jcVY3yM z4Z;-;+gg96?7PJx9K3zKee%{h=YG1T%vXwQQCB3W+dM*0?7CX!A_+T~}n5UW>XkL4AWMhv%nH`>jtis-2OmoC?1k-&V~~cGAKf zECb5>SuHRtwH9?Lg1SkQ?lq>o?8gTolF9bl1>-w7E5}rXZeF!2hTW1cu?qXoYcu4LM4~>s)Hi-IBf~QZDW0<@Z|f z;<)*BwbD<@Z#0McqdQ?)24*VU)A`CuAn%W%TUb)u>@wsd-I&Zh6-t#z#%O6^ZvoaM zj?N9mQ$o4+x>^PI-Iu;M^6=>jWZL`u>y6#k!}~XU9~;eR-<(KC$ptxzsw#1GZU~+d za{8TchjUKgsH+S-^X`tbPfBfA)Za`7sRo)T6xKVIU#H}Pl!6@r{%3?cr88fp|*bwK1UywjzD0?6@f>2Y0tE z1|eX0`DLx=zPW6B=FRm6D7hd+zeFyuMsS9E?Nu(7_ks&iYcj?b4_x5^2t z##p-I>cibR@swPUP}H#_z}1M`;*Y2J?_G>#fm_H<^yFWL#Gm#CT`o%T;dj-~l~Q{- zU4${3r{sbhMNONyEf_onqj*)@aoviKCFgu+V`L8$;l4cdv3Ji!pJ&onzNJP#mZ#)` zl!6@rjwX(r^uts9`W_Y3qr%ONZz7;2=UyYkCM33U&i$-(l`7(7J^i462PGHeDC(NT zJB)ns6knOz2YnZh*C~%p++sptY(Fc6<)6L0XV+MEf1=~2gC(+*T#%!nREh24gQxg7 z3hocLnGPGh+Z5mPv*+Y}NH2VG`v-d0*u^rl#7B;BN-jt#*t^*NjLi4OQ@m3Rj%=DZ zt7}e1=cbr` z)pc*FIu_TLnw7PC`?0;>Ax1?~FDSVnrC>*Zs}tL0GoG^f+|}vjLB|KPiKw$nJg3j~ z=rHl?ZXVq06zPB!5 zr68?rG@$05C&wZs7o-&I2=G6TX7|8TJS1~@Bi&jCe>ON=ljlVp>5S2KXpu8cnsh}T zf24Q!T?+)6(d^p8gFpF<)pyOuic$k{sVwnjg7vv~v zTEy}HoA8uPD|&A{h1_ojvoRo10jjV$X+7Dp(nM32S9xJvw|A(UQF1{_!Hxh&6Z8`>`}CX#Jw>w3JDH>Yw(Mgk_&Pa^*SM9?Un=1tN8?1~z?xy5|97PpHY!^p7#Zfmuj9vRj0i50JVD#a;x6|A~%$0_I zGCJ}Zlg>@D_IZ?CkW#ScFn^Bw4tR>ghDR8sZxI_rjgD;l0@~Fq%IPrZ7JcZQLYWSO j2?z#?Pss%-1$z!dJnsK5umfhE delta 145 zcmZo@U~YK8s1O|JlUb6gkd|MRn^>Y?%pkzP#KhpBpuoVuzzW3d6BX=PSsC=I3O1(9 h;b-Fo3NZ+577TdFKRH1_aMA)6Cfr;$F1)JIr2zvxBOm|( diff --git a/examples/demo-rollup/tests/wallet/mod.rs b/examples/demo-rollup/tests/wallet/mod.rs index 7ee6def5ec..4d12ac0c79 100644 --- a/examples/demo-rollup/tests/wallet/mod.rs +++ b/examples/demo-rollup/tests/wallet/mod.rs @@ -105,7 +105,7 @@ fn test_display_unsigned_tx() { &unsigned_data ) .unwrap(), - r#"V0 { runtime_call: Bank.Mint { coins: 0.01 coins of token ID token_1zut3w9chzut3w9chzut3w9chzut3w9chzut3w9chzut3w9chzurq2akgf6, mint_to_address: sov1pv9skzctpv9skzctpv9skzctpv9skzctpv9skzctpv9skqm7ehv }, uniqueness: Generation(0), details: { max_priority_fee_bips: 0, max_fee: 100000000000, gas_limit: [1000000000, 1000000000], chain_id: 4321 } }"# + r#"V0 { runtime_call: Bank.Mint { coins: 0.01 coins of token ID token_1zut3w9chzut3w9chzut3w9chzut3w9chzut3w9chzut3w9chzurq2akgf6, mint_to_address: sov1pv9skzctpv9skzctpv9skzctpv9skzctpv9skzctpv9skqm7ehv }, uniqueness: Generation(0), details: { max_priority_fee_bips: 0, max_fee: 100000000000, gas_limit: [1000000000, 1000000000], chain_id: 4321 }, address_override: None }"# ); } @@ -143,7 +143,7 @@ fn test_display_signed_tx() { &signed_data ) .unwrap(), - format!("V0 {{ signature: 0x{signature_display}, pub_key: 0x{pubkey_display}, runtime_call: Bank.Mint {{ coins: 0.01 coins of token ID token_1zut3w9chzut3w9chzut3w9chzut3w9chzut3w9chzut3w9chzurq2akgf6, mint_to_address: sov1pv9skzctpv9skzctpv9skzctpv9skzctpv9skzctpv9skqm7ehv }}, uniqueness: Generation(0), details: {{ max_priority_fee_bips: 0, max_fee: 100000000000, gas_limit: [1000000000, 1000000000], chain_id: 4321 }} }}") + format!("V0 {{ signature: 0x{signature_display}, pub_key: 0x{pubkey_display}, runtime_call: Bank.Mint {{ coins: 0.01 coins of token ID token_1zut3w9chzut3w9chzut3w9chzut3w9chzut3w9chzut3w9chzurq2akgf6, mint_to_address: sov1pv9skzctpv9skzctpv9skzctpv9skzctpv9skzctpv9skqm7ehv }}, uniqueness: Generation(0), details: {{ max_priority_fee_bips: 0, max_fee: 100000000000, gas_limit: [1000000000, 1000000000], chain_id: 4321 }}, address_override: None }}") ); } From ee95477bf1ded6851443629027869b36f81a2c21 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Wed, 6 May 2026 19:54:36 +0200 Subject: [PATCH 24/34] More fixes --- examples/demo-rollup/README.md | 2 +- .../packages/web3/src/rollup/solana-signable-rollup.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/demo-rollup/README.md b/examples/demo-rollup/README.md index 71c847c053..939fa2e742 100644 --- a/examples/demo-rollup/README.md +++ b/examples/demo-rollup/README.md @@ -333,7 +333,7 @@ Adding the following transaction to batch: } } }, - "chain_hash": "0x65746dac667c302c76553208e916ba9a2f5738780f789014f8a900d6c2aeb8e5", + "chain_hash": "0x48354f2a93fa684a0acde9593cf6d9a4e4c56f9fb531ce3e3570c1922e5982e9", "details": { "max_priority_fee_bips": 0, "max_fee": "100000000", diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts index 522ebc7237..a943b9be55 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.test.ts @@ -956,7 +956,9 @@ describe("SolanaSignableRollup", () => { expect(captured.bytes).toBeDefined(); const signedJson = new TextDecoder().decode(captured.bytes!); - expect(JSON.parse(signedJson).address_override).toBe(txAddressOverrideBs58); + expect(JSON.parse(signedJson).address_override).toBe( + txAddressOverrideBs58, + ); expect((signedTx as any).V0.address_override).toBe(txAddressOverrideBs58); }); From 4cf139470a07a5fe32d046147183a61f9cfe0aaa Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Thu, 7 May 2026 11:37:20 +0200 Subject: [PATCH 25/34] update hyperlane image so tests are suppose to pass --- .../hyperlane/tests/integration/with_agent/helpers/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs b/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs index d09293c026..afb50f0059 100644 --- a/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs +++ b/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs @@ -215,9 +215,9 @@ impl HyperlaneBuilder { let docker_image = env::var("CUSTOM_HLP_DOCKER_IMAGE"); let has_custom_image = !matches!(docker_image, Err(env::VarError::NotPresent)); - // Current image is based on https://github.com/Sovereign-Labs/hyperlane-monorepo/tree/sovereign-lander-integration + // Current image is based on https://github.com/Sovereign-Labs/hyperlane-monorepo/tree/sovereign-rust-integration-for-multisig let docker_image = docker_image - .unwrap_or_else(|_| "ghcr.io/theodorebugnet/hyperlane-agent:multisig_upgrade".into()); + .unwrap_or_else(|_| "ghcr.io/citizen-stig/hyperlane-agent:multisig-upgrade-address-override".into()); let (name, tag) = docker_image .split_once(':') .unwrap_or((&docker_image, "latest")); From fffe0a5d745a69a23d6cc39c34eda49b80986c0f Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Thu, 7 May 2026 12:32:26 +0200 Subject: [PATCH 26/34] Lint! --- .../hyperlane/tests/integration/with_agent/helpers/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs b/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs index afb50f0059..074772158d 100644 --- a/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs +++ b/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs @@ -216,8 +216,9 @@ impl HyperlaneBuilder { let has_custom_image = !matches!(docker_image, Err(env::VarError::NotPresent)); // Current image is based on https://github.com/Sovereign-Labs/hyperlane-monorepo/tree/sovereign-rust-integration-for-multisig - let docker_image = docker_image - .unwrap_or_else(|_| "ghcr.io/citizen-stig/hyperlane-agent:multisig-upgrade-address-override".into()); + let docker_image = docker_image.unwrap_or_else(|_| { + "ghcr.io/citizen-stig/hyperlane-agent:multisig-upgrade-address-override".into() + }); let (name, tag) = docker_image .split_once(':') .unwrap_or((&docker_image, "latest")); From bb7601d91d367de374c79172d7f3933efb634630 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Thu, 7 May 2026 13:10:54 +0200 Subject: [PATCH 27/34] use lander based image --- .../hyperlane/tests/integration/with_agent/helpers/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs b/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs index 074772158d..a6c3a2e4c5 100644 --- a/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs +++ b/crates/module-system/hyperlane/tests/integration/with_agent/helpers/mod.rs @@ -215,9 +215,9 @@ impl HyperlaneBuilder { let docker_image = env::var("CUSTOM_HLP_DOCKER_IMAGE"); let has_custom_image = !matches!(docker_image, Err(env::VarError::NotPresent)); - // Current image is based on https://github.com/Sovereign-Labs/hyperlane-monorepo/tree/sovereign-rust-integration-for-multisig + // Current image is based on https://github.com/Sovereign-Labs/hyperlane-monorepo/tree/sovereign-lander-integration-for-multisig let docker_image = docker_image.unwrap_or_else(|_| { - "ghcr.io/citizen-stig/hyperlane-agent:multisig-upgrade-address-override".into() + "ghcr.io/citizen-stig/hyperlane-agent:multisig-upgrade-address-override-2".into() }); let (name, tag) = docker_image .split_once(':') From 37c05ab4d3365f85064a3515a96a64abb1142ad9 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Fri, 8 May 2026 11:45:07 +0200 Subject: [PATCH 28/34] Update crates/module-system/sov-modules-api/src/transaction/types/v0.rs Co-authored-by: Theodore Bugnet <24320578+theodorebugnet@users.noreply.github.com> --- .../module-system/sov-modules-api/src/transaction/types/v0.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs index 49252f57a3..fa175f2d55 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v0.rs @@ -41,7 +41,7 @@ pub struct Version0, - /// Signer-declared address override. This field is part of the signed bytes. + /// Signer-declared address override. /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default)] pub address_override: Option, From 84b0e9fb6b69997476be146efe02b56c1c57693c Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Fri, 8 May 2026 11:45:16 +0200 Subject: [PATCH 29/34] Update crates/module-system/sov-modules-api/src/transaction/types/v1.rs Co-authored-by: Theodore Bugnet <24320578+theodorebugnet@users.noreply.github.com> --- .../module-system/sov-modules-api/src/transaction/types/v1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs index 03c0b4a4fa..77ac5338fa 100644 --- a/crates/module-system/sov-modules-api/src/transaction/types/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/types/v1.rs @@ -87,7 +87,7 @@ pub struct Version1, - /// Signer-declared address override. This field is part of the signed bytes. + /// Signer-declared address override. /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default)] pub address_override: Option, From f005650a2b11b0522580af0b0971c21d016ea0ec Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Fri, 8 May 2026 11:45:26 +0200 Subject: [PATCH 30/34] Update crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs Co-authored-by: Theodore Bugnet <24320578+theodorebugnet@users.noreply.github.com> --- .../sov-solana-offchain-auth/src/authentication/payload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index d86eaa508c..04d2db72d3 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -33,7 +33,7 @@ pub struct SolanaOffchainUnsignedTransactionV0 /// from malicious chains (if the chain name matches some other chain the use but didn't expect /// to be signing for right now). pub chain_name: SafeString, - /// Signer-declared address override. This field is part of the signed bytes. + /// Signer-declared address override. /// See [`sov_modules_api::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default, skip_serializing_if = "Option::is_none")] pub address_override: Option, From 65158b8ba0f77e9b1b9ac79944419334787b7c06 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Fri, 8 May 2026 11:45:36 +0200 Subject: [PATCH 31/34] Update crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs Co-authored-by: Theodore Bugnet <24320578+theodorebugnet@users.noreply.github.com> --- .../sov-solana-offchain-auth/src/authentication/payload.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs index 04d2db72d3..7f902b3b50 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/payload.rs @@ -84,7 +84,6 @@ pub struct SolanaOffchainUnsignedTransactionV1 /// `sov-accounts`. pub multisig_id: S::Address, /// Signer-declared address override. - /// This field is part of the signed bytes. /// See [`sov_modules_api::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default, skip_serializing_if = "Option::is_none")] pub address_override: Option, From a813c449890fc86cb7850622f867b8ac045d4d6b Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Fri, 8 May 2026 11:45:46 +0200 Subject: [PATCH 32/34] Update crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs Co-authored-by: Theodore Bugnet <24320578+theodorebugnet@users.noreply.github.com> --- .../sov-modules-api/src/transaction/unsigned/v0.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs index 7d315fd1ad..e633f86825 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs @@ -29,7 +29,7 @@ pub struct UnsignedTransactionV0 { pub uniqueness: UniquenessData, /// Data related to fees and gas handling. pub details: TxDetails, - /// Signer-declared address override. This field is part of the signed bytes. + /// Signer-declared address override. /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default)] pub address_override: Option, From fa2c17bbedc17ca30d9d2500bc827a81bcfa2ec5 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Fri, 8 May 2026 11:53:34 +0200 Subject: [PATCH 33/34] Update crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs Co-authored-by: Theodore Bugnet <24320578+theodorebugnet@users.noreply.github.com> --- .../sov-modules-api/src/transaction/unsigned/v1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs index 2c43362a02..574fadf9b1 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v1.rs @@ -31,7 +31,7 @@ pub struct UnsignedTransactionV1 { /// and prevent credential malleability from reusing signed bytes in a different /// multisig envelope. pub credential_address: S::Address, - /// Signer-declared address override. This field is part of the signed bytes. + /// Signer-declared address override. /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. #[serde(default)] pub address_override: Option, From 425a029caa184a93621c60ef7130203c193f46cc Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Fri, 8 May 2026 13:03:01 +0200 Subject: [PATCH 34/34] Addressing feedback --- crates/full-node/sov-api-spec/openapi-v3.yaml | 2 +- .../evm/evm_account_abstraction.rs | 1 + .../sov-rollup-apis/src/endpoints/simulate.rs | 2 +- .../tests/integration/uniqueness.rs | 2 +- .../tests/stf_blueprint/mod.rs | 4 ++++ .../stf_blueprint/operator/auth_eip712.rs | 14 ++++++++----- .../stf_blueprint/optimistic/da_simulation.rs | 1 + .../stf_blueprint/optimistic/unregistered.rs | 2 ++ .../sov-accounts/tests/integration/main.rs | 18 +++++++++++------ .../tests/integration/hooks_tests.rs | 3 ++- .../sov-uniqueness/tests/integration/utils.rs | 1 + .../module-system/sov-capabilities/src/lib.rs | 5 +---- crates/module-system/sov-cli/src/lib.rs | 1 + .../sov-cli/tests/integration/transactions.rs | 1 + .../src/runtime/capabilities/authorization.rs | 3 --- .../src/transaction/unsigned/v0.rs | 10 +++++----- .../src/authentication/mod.rs | 2 -- .../tests/integration/main.rs | 4 ++++ .../sov-test-utils/src/generators/mod.rs | 1 + .../sov-test-utils/src/interface/inputs.rs | 1 + crates/utils/sov-test-utils/src/lib.rs | 1 + crates/web3/src/rust.rs | 7 +++---- .../forced_sequencer_registration/mod.rs | 2 ++ examples/demo-rollup/tests/wallet/mod.rs | 1 + .../web3/src/rollup/solana-signable-rollup.ts | 20 +++++-------------- .../web3/src/rollup/standard-rollup.ts | 8 ++------ 26 files changed, 63 insertions(+), 54 deletions(-) diff --git a/crates/full-node/sov-api-spec/openapi-v3.yaml b/crates/full-node/sov-api-spec/openapi-v3.yaml index 708e431240..6f5e111d8c 100644 --- a/crates/full-node/sov-api-spec/openapi-v3.yaml +++ b/crates/full-node/sov-api-spec/openapi-v3.yaml @@ -1221,7 +1221,7 @@ components: address_override: type: string nullable: true - description: Optional address override for execution; null or omission uses default routing + description: Optional address override for execution; null uses default routing required: - sender - call diff --git a/crates/full-node/sov-ethereum/tests/integration/evm/evm_account_abstraction.rs b/crates/full-node/sov-ethereum/tests/integration/evm/evm_account_abstraction.rs index c74d067347..fead410128 100644 --- a/crates/full-node/sov-ethereum/tests/integration/evm/evm_account_abstraction.rs +++ b/crates/full-node/sov-ethereum/tests/integration/evm/evm_account_abstraction.rs @@ -67,6 +67,7 @@ fn create_insert_credentials( max_fee, UniquenessData::Nonce(nonce), gas_limit, + None, ), ) } diff --git a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs index 2e100b0c4e..ffb28aa303 100644 --- a/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs +++ b/crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs @@ -413,7 +413,7 @@ pub struct SimulateParameters { /// Optional uniqueness data for the transaction. /// If not provided a valid uniqueness will be used. pub uniqueness: Option, - /// Optional address override for execution. If not provided, default routing is used. + /// Optional address override for execution; null uses default routing. pub address_override: Option, } diff --git a/crates/full-node/sov-sequencer/tests/integration/uniqueness.rs b/crates/full-node/sov-sequencer/tests/integration/uniqueness.rs index dcabcfc810..ac01c5cf3e 100644 --- a/crates/full-node/sov-sequencer/tests/integration/uniqueness.rs +++ b/crates/full-node/sov-sequencer/tests/integration/uniqueness.rs @@ -99,7 +99,7 @@ async fn test_mixed_nonce_and_generation_transactions() { let construct_tx = |uniqueness: UniquenessData| { let unsigned_tx = - UnsignedTransactionV0::new_with_details(msg.clone(), uniqueness, details.clone()); + UnsignedTransactionV0::new_with_details(msg.clone(), uniqueness, details.clone(), None); Transaction::::new_signed_tx( &test_user.private_key, &RT::CHAIN_HASH, diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs index bbab80f10a..99776cc8ce 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/mod.rs @@ -243,6 +243,7 @@ pub fn create_tx_bad_sig>( TEST_DEFAULT_MAX_FEE, UniquenessData::Nonce(nonce), None, + None, ); let signed_tx = Transaction::::new_signed_tx(&signer.private_key, &RT::CHAIN_HASH, utx); @@ -284,6 +285,7 @@ pub fn create_tx_bad_sender>( Amount::new(200_000), UniquenessData::Nonce(nonce), None, + None, ); let signer = TestUser::::generate(Amount::ZERO); @@ -304,6 +306,7 @@ pub fn create_tx_valid>( TEST_DEFAULT_MAX_FEE, UniquenessData::Generation(generation), None, + None, ); Transaction::::new_signed_tx(signer.private_key(), &RT::CHAIN_HASH, utx) @@ -324,6 +327,7 @@ pub fn create_tx_out_of_gas>( Amount::new(200_000), UniquenessData::Nonce(nonce), Some(<::Gas as Gas>::zero()), + None, ); Transaction::::new_signed_tx(signer.private_key(), &RT::CHAIN_HASH, utx) diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs index cd06b7024d..654636d2bb 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/operator/auth_eip712.rs @@ -134,12 +134,13 @@ fn setup() -> (TestRunner, TestUser) { } pub fn create_utx>(message: RT::Decodable) -> UnsignedTransactionV0 { - create_utx_with_generation::(message, 0) + create_utx_with_generation::(message, 0, None) } pub fn create_utx_with_generation>( message: RT::Decodable, generation: u64, + address_override: Option, ) -> UnsignedTransactionV0 { let details = TxDetails { max_priority_fee_bips: PriorityFeeBips::ZERO, @@ -151,6 +152,7 @@ pub fn create_utx_with_generation>( message, UniquenessData::Generation(generation), details, + address_override, ) } @@ -177,8 +179,6 @@ pub fn sign_utx_in_place>( /// Signs a V1 (multisig) unsigned transaction with the given private key using EIP712. /// The credential_address is computed from the provided multisig. -/// `address_override` is forwarded into the signed `UnsignedTransactionV1`; -/// pass `None` to match default routing or `Some(X)` to sign for a specific override account. pub fn sign_utx_v1_in_place>( utx: &UnsignedTransactionV0, multisig: &Multisig<::PublicKey>, @@ -311,7 +311,11 @@ fn test_multisig_signature_verification() { let random_private_key = TestPrivateKey::generate(); let address_override = Some(alice_address); let make_multisig_tx = |generation, value| { - let utx = create_utx_with_generation::(encode_message::<_, RT>(value), generation); + let utx = create_utx_with_generation::( + encode_message::<_, RT>(value), + generation, + address_override, + ); let signatures = multisig_keys .iter() .map(|key| sign_utx_v1_in_place(&utx, &multisig, address_override, key)) @@ -320,7 +324,7 @@ fn test_multisig_signature_verification() { sign_utx_v1_in_place(&utx, &multisig, address_override, &random_private_key); ( - utx.to_multisig_tx(multisig.clone(), address_override), + utx.to_multisig_tx(multisig.clone()), signatures, random_signature, ) diff --git a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs index bc982a51d1..da3fa55d4c 100644 --- a/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs +++ b/crates/module-system/module-implementations/integration-tests/tests/stf_blueprint/optimistic/da_simulation.rs @@ -87,6 +87,7 @@ pub fn simulate_da_with_bad_serialization(key: TestPrivateKey) -> Vec::generate(Amount::ZERO); @@ -290,6 +291,7 @@ mod helpers { TEST_DEFAULT_MAX_FEE, UniquenessData::Nonce(nonce), None, + None, ); Transaction::, S>::new_signed_tx( diff --git a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs index 5cea2f39a5..6c112784ca 100644 --- a/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs +++ b/crates/module-system/module-implementations/sov-accounts/tests/integration/main.rs @@ -198,8 +198,9 @@ fn test_setup_multisig_and_act() { )), sov_modules_api::capabilities::UniquenessData::Generation(0), default_test_tx_details::(), + None, ) - .to_multisig_tx(multisig.clone(), None) + .to_multisig_tx(multisig.clone()) }; let sign = |tx: &mut Version1, key: &TestPrivateKey| { @@ -521,12 +522,17 @@ fn make_v1_tx( inner_credential: sov_modules_api::CredentialId, address_override: Option<::Address>, ) -> Version1 { - UnsignedTransactionV0::::new_with_details( + let details = default_test_tx_details::(); + UnsignedTransactionV0::::new( TestAccountsRuntimeCall::Accounts(CallMessage::InsertCredentialId(inner_credential)), + details.chain_id, + details.max_priority_fee_bips, + details.max_fee, sov_modules_api::capabilities::UniquenessData::Generation(0), - default_test_tx_details::(), + details.gas_limit, + address_override, ) - .to_multisig_tx(multisig.clone(), address_override) + .to_multisig_tx(multisig.clone()) } fn make_v0_tx( @@ -534,12 +540,12 @@ fn make_v0_tx( inner_credential: sov_modules_api::CredentialId, address_override: Option<::Address>, ) -> Transaction { - let mut utx = UnsignedTransactionV0::::new_with_details( + let utx = UnsignedTransactionV0::::new_with_details( TestAccountsRuntimeCall::Accounts(CallMessage::InsertCredentialId(inner_credential)), sov_modules_api::capabilities::UniquenessData::Generation(0), default_test_tx_details::(), + address_override, ); - utx.address_override = address_override; utx.sign(sender.private_key(), &>::CHAIN_HASH) } diff --git a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/hooks_tests.rs b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/hooks_tests.rs index 0e67bdaedd..087448e768 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/hooks_tests.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/hooks_tests.rs @@ -160,8 +160,9 @@ fn send_tx_bad_generation_duplicate_with_malleated_v1_envelope() { runtime_msg, UniquenessData::Generation(0), default_test_tx_details::(), + None, ) - .to_multisig_tx(multisig, None); + .to_multisig_tx(multisig); original_tx .sign(&multisig_keys[0], &RT::CHAIN_HASH) .unwrap(); diff --git a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/utils.rs b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/utils.rs index 8f90a79570..e7aa51f127 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/utils.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/utils.rs @@ -100,6 +100,7 @@ pub(crate) fn generate_value_setter_tx( TEST_DEFAULT_MAX_FEE, UniquenessData::Generation(generation), None, + None, ); let transaction = Transaction::::new_signed_tx( diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index 8b31aa0f6a..42f5bf0c24 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -349,10 +349,7 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' state: &mut impl StateAccessor, execution_context: ExecutionContext, ) -> anyhow::Result> { - // The tx sender & sequencer are the same entity on this path. If the signer - // declared an `address_override`, the credential must be explicitly authorized - // for it (same invariant as `resolve_context`); otherwise we fall back to the - // credential's canonical address. + // The tx sender & sequencer are the same entity on this path. let address = match auth_data.address_override { Some(address_override) => { anyhow::ensure!( diff --git a/crates/module-system/sov-cli/src/lib.rs b/crates/module-system/sov-cli/src/lib.rs index 7f9b41eaae..29c6be6d2a 100644 --- a/crates/module-system/sov-cli/src/lib.rs +++ b/crates/module-system/sov-cli/src/lib.rs @@ -86,6 +86,7 @@ where self.details.max_fee, UniquenessData::Generation(generation), self.details.gas_limit, + None, ) } } diff --git a/crates/module-system/sov-cli/tests/integration/transactions.rs b/crates/module-system/sov-cli/tests/integration/transactions.rs index 8678959f39..8b465c991f 100644 --- a/crates/module-system/sov-cli/tests/integration/transactions.rs +++ b/crates/module-system/sov-cli/tests/integration/transactions.rs @@ -102,6 +102,7 @@ fn transaction_is_serialized_correctly() { max_fee, UniquenessData::Generation(initial_nonce + i as u64), gas_limit, + None, ), ); diff --git a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs index 39eebfadbb..618cd2ee60 100644 --- a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs +++ b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs @@ -108,8 +108,5 @@ pub struct AuthorizationData { /// `None` => resolve to the credential's default address. /// `Some(X)` => requires an explicit `(X, credential_id)` entry in `account_owners`; /// the transaction is skipped otherwise. - /// - /// Set by authenticators that carry a signed `address_override` (sov-tx V0/V1, - /// Solana off-chain auth). Other authenticators leave it `None`. pub address_override: Option, } diff --git a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs index e633f86825..b29a10a956 100644 --- a/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs +++ b/crates/module-system/sov-modules-api/src/transaction/unsigned/v0.rs @@ -79,6 +79,7 @@ impl UnsignedTransactionV0 { max_fee: Amount, uniqueness: UniquenessData, gas_limit: Option, + address_override: Option, ) -> Self { Self { runtime_call, @@ -89,7 +90,7 @@ impl UnsignedTransactionV0 { gas_limit, chain_id, }, - address_override: None, + address_override, } } @@ -98,12 +99,13 @@ impl UnsignedTransactionV0 { runtime_call: R::Call, uniqueness: UniquenessData, details: TxDetails, + address_override: Option, ) -> Self { Self { runtime_call, uniqueness, details, - address_override: None, + address_override, } } @@ -125,11 +127,9 @@ impl UnsignedTransactionV0 { } /// Creates a new `V1` transaction from this unsigned transaction. - /// See [`crate::capabilities::AuthorizationData::address_override`] for routing semantics. pub fn to_multisig_tx( self, multisig: Multisig<::PublicKey>, - address_override: Option, ) -> Version1 { Version1 { signatures: SafeVec::new(), @@ -141,7 +141,7 @@ impl UnsignedTransactionV0 { runtime_call: self.runtime_call, uniqueness: self.uniqueness, details: self.details, - address_override, + address_override: self.address_override, } } diff --git a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs index d860713dd7..38911f2a58 100644 --- a/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs +++ b/crates/module-system/sov-solana-offchain-auth/src/authentication/mod.rs @@ -122,8 +122,6 @@ fn verify_signatures( } /// Builds authorization data for either single-sig or multisig transactions. -/// `address_override` is forwarded from the signed payload — see -/// [`sov_modules_api::capabilities::AuthorizationData::address_override`] for routing semantics. fn build_auth_data( unpacked: &UnpackedSolanaMessage, uniqueness: UniquenessData, diff --git a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs index c81eff1df2..c44a57055e 100644 --- a/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs +++ b/crates/module-system/sov-solana-offchain-auth/tests/integration/main.rs @@ -220,6 +220,7 @@ fn create_transfer_tx_json_with_address_override( TEST_DEFAULT_MAX_FEE, UniquenessData::Generation(0), Some(TEST_DEFAULT_GAS_LIMIT.into()), + None, ); let solana_unsigned_tx = SolanaOffchainUnsignedTransactionV0:: { runtime_call: unsigned_tx.runtime_call, @@ -507,6 +508,7 @@ fn create_multisig_transfer_tx_json( TEST_DEFAULT_MAX_FEE, UniquenessData::Nonce(0), Some(TEST_DEFAULT_GAS_LIMIT.into()), + None, ); let solana_unsigned_tx = SolanaOffchainUnsignedTransactionV1:: { runtime_call: unsigned_tx.runtime_call, @@ -1280,6 +1282,7 @@ fn build_v1_payload( TEST_DEFAULT_MAX_FEE, UniquenessData::Nonce(0), Some(TEST_DEFAULT_GAS_LIMIT.into()), + None, ); SolanaOffchainUnsignedTransactionV1:: { runtime_call: unsigned_tx.runtime_call, @@ -1501,6 +1504,7 @@ fn build_v0_payload( TEST_DEFAULT_MAX_FEE, UniquenessData::Nonce(0), Some(TEST_DEFAULT_GAS_LIMIT.into()), + None, ); SolanaOffchainUnsignedTransactionV0:: { runtime_call: unsigned_tx.runtime_call, diff --git a/crates/utils/sov-test-utils/src/generators/mod.rs b/crates/utils/sov-test-utils/src/generators/mod.rs index 34f0841966..56f0ddc4e0 100644 --- a/crates/utils/sov-test-utils/src/generators/mod.rs +++ b/crates/utils/sov-test-utils/src/generators/mod.rs @@ -74,6 +74,7 @@ impl Message { self.details.max_fee, UniquenessData::Generation(self.generation), self.details.gas_limit, + None, ), ) } diff --git a/crates/utils/sov-test-utils/src/interface/inputs.rs b/crates/utils/sov-test-utils/src/interface/inputs.rs index 219e88bf89..02350574dd 100644 --- a/crates/utils/sov-test-utils/src/interface/inputs.rs +++ b/crates/utils/sov-test-utils/src/interface/inputs.rs @@ -157,6 +157,7 @@ impl, S: Spec> TransactionType { details.max_fee, UniquenessData::Nonce(nonce), details.gas_limit, + None, ), ) } diff --git a/crates/utils/sov-test-utils/src/lib.rs b/crates/utils/sov-test-utils/src/lib.rs index 16c8fe50a8..48342970d1 100644 --- a/crates/utils/sov-test-utils/src/lib.rs +++ b/crates/utils/sov-test-utils/src/lib.rs @@ -256,6 +256,7 @@ pub fn test_signed_transaction( tx_details.max_fee, uniqueness, tx_details.gas_limit, + None, ), ) } diff --git a/crates/web3/src/rust.rs b/crates/web3/src/rust.rs index 19bff661c4..20c32563cf 100644 --- a/crates/web3/src/rust.rs +++ b/crates/web3/src/rust.rs @@ -218,16 +218,15 @@ impl TransactionBui let gas_limit = self.gas_limit.unwrap_or(None); let uniqueness = self.uniqueness.unwrap_or_else(default_uniqueness); - let mut tx = UnsignedTransactionV0::new( + Ok(UnsignedTransactionV0::new( self.call, config_chain_id(), priority_fee, max_fee, uniqueness, gas_limit, - ); - tx.address_override = self.address_override; - Ok(tx) + self.address_override, + )) } /// Builds and signs a transaction in one step. diff --git a/examples/demo-rollup/tests/forced_sequencer_registration/mod.rs b/examples/demo-rollup/tests/forced_sequencer_registration/mod.rs index f55e18d839..6c99923991 100644 --- a/examples/demo-rollup/tests/forced_sequencer_registration/mod.rs +++ b/examples/demo-rollup/tests/forced_sequencer_registration/mod.rs @@ -118,6 +118,7 @@ fn build_register_sequencer_tx( max_fee, UniquenessData::Nonce(nonce), gas_limit, + None, ), ) } @@ -502,6 +503,7 @@ fn build_state_heavy_tx( max_fee, UniquenessData::Nonce(nonce), None, + None, ), ) } diff --git a/examples/demo-rollup/tests/wallet/mod.rs b/examples/demo-rollup/tests/wallet/mod.rs index 4d12ac0c79..5ccbf74cef 100644 --- a/examples/demo-rollup/tests/wallet/mod.rs +++ b/examples/demo-rollup/tests/wallet/mod.rs @@ -36,6 +36,7 @@ fn make_unsigned_tx() -> UnsignedTransactionV0, S> { TEST_DEFAULT_MAX_FEE, UniquenessData::Generation(0), Some(TEST_DEFAULT_GAS_LIMIT.into()), + None, ) } diff --git a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts index 70137e10fc..a03c046999 100644 --- a/typescript/packages/web3/src/rollup/solana-signable-rollup.ts +++ b/typescript/packages/web3/src/rollup/solana-signable-rollup.ts @@ -31,9 +31,8 @@ export type SolanaOffchainUnsignedTransaction = { export type SolanaOffchainUnsignedTransactionV0 = SolanaOffchainUnsignedTransaction & { /** - * Signer-declared address override. Omitted from the serialized JSON when not set, - * preserving byte equivalence with pre-change signed messages so existing signatures - * continue to verify. See `AuthorizationData::address_override` (Rust) for routing semantics. + * Signer-declared address override. + * See `AuthorizationData::address_override` (Rust) for routing semantics. */ address_override?: string; }; @@ -42,9 +41,8 @@ export type SolanaOffchainUnsignedTransactionV1 = SolanaOffchainUnsignedTransaction & { multisig_id: string; /** - * Signer-declared address override. Omitted from the serialized JSON when not set, - * preserving byte equivalence with pre-change signed messages so existing signatures - * continue to verify. See `AuthorizationData::address_override` (Rust) for routing semantics. + * Signer-declared address override. + * See `AuthorizationData::address_override` (Rust) for routing semantics. */ address_override?: string; version: number; @@ -99,11 +97,7 @@ export type SolanaMultisigSubmitParams = export type SolanaMultisigSignParams = SolanaMultisigSubmitParams & { signer: Signer; - /** - * Optional address override to embed in the signed V1 payload. Omitted from the JSON when - * unset to preserve byte equivalence with pre-change signed messages. Unused by the - * `"standard"` authenticator. See `AuthorizationData::address_override` (Rust) for routing semantics. - */ + /** Address override embedded in the V1 payload. Unused by the `"standard"` authenticator. */ addressOverride?: Uint8Array; }; @@ -553,10 +547,6 @@ export class SolanaSignableRollup { /** * Creates V1 JSON bytes for a multisig transaction, including multisig_id and version. * The resulting JSON is what each signer signs directly (no discriminator prefix). - * - * When `addressOverride` is omitted the JSON has no `address_override` key — this preserves - * byte equivalence with pre-change signed messages, so signatures produced by older clients - * keep verifying. */ private async createMultisigJsonBytes( unsignedTx: UnsignedTransaction, diff --git a/typescript/packages/web3/src/rollup/standard-rollup.ts b/typescript/packages/web3/src/rollup/standard-rollup.ts index fc1a11dc44..cf89606374 100644 --- a/typescript/packages/web3/src/rollup/standard-rollup.ts +++ b/typescript/packages/web3/src/rollup/standard-rollup.ts @@ -85,12 +85,8 @@ export function standardTypeBuilder< /** * The parameters for simulating a runtime call transaction. * - * `address_override` is bolted on by hand here (and the corresponding cast - * in `simulate` below forces it through `RollupSimulateParams`) because the - * published `@sovereign-sdk/client` lags the OpenAPI spec by one Stainless - * publish cycle. Once the client is regenerated and the version bumped in - * `package.json`, drop both the extension and the cast — the regenerated - * `RollupSimulateParams` will carry the field natively. + * Adds `address_override` until the regenerated `@sovereign-sdk/client` carries it natively; + * drop this extension and the cast in `simulate` once the client is republished. */ export type SimulateParams = Omit< SovereignClient.RollupSimulateParams,