diff --git a/crates/module-system/module-implementations/extern/hyperlane-solana-register/README.md b/crates/module-system/module-implementations/extern/hyperlane-solana-register/README.md index 6ddd90a2c2..f1d2f7551a 100644 --- a/crates/module-system/module-implementations/extern/hyperlane-solana-register/README.md +++ b/crates/module-system/module-implementations/extern/hyperlane-solana-register/README.md @@ -62,7 +62,6 @@ The `SolanaRegistration` module is a Sovereign SDK module that implements the - **Selective Message Handling**: Only processes messages from the configured Solana domain and trusted program ID - **Account Linking**: Associates Solana embedded wallets with rollup addresses via the `sov-accounts` module -- **Duplicate Prevention**: Rejects attempts to register an embedded wallet that's already linked to a different address - **Admin Controls**: Allows configuration updates via admin-only calls - **Fallback to Warp**: Non-Solana messages are forwarded to the underlying `Warp` module @@ -132,7 +131,6 @@ pub enum Event { The module defines several error types: -- `AlreadyRegistered`: The embedded public key is already linked to a different address - `InvalidBodyLength`: The message body doesn't contain exactly 64 bytes - `ExtractPubKey`: Failed to parse public keys from the message body - `AdminNotFound`: Admin address not configured @@ -146,9 +144,8 @@ See `src/lib.rs:196-213` for the `handle` implementation: 2. Verify the sender matches the trusted Solana program ID 3. Extract the two 32-byte public keys from the message body 4. Convert the payer public key to a rollup address -5. Use `sov-accounts` to resolve or create the address-credential mapping -6. Reject if the embedded wallet is already linked to a different address -7. Emit a `UserRegistered` event +5. Authorize the embedded credential to act as the payer's address by recording `(payer, embedded)` in `sov-accounts` +6. Emit a `UserRegistered` event --- @@ -372,4 +369,3 @@ cargo test ``` Program tests are located in `solana/program-tests/src/tests.rs`. - diff --git a/crates/module-system/module-implementations/extern/hyperlane-solana-register/src/lib.rs b/crates/module-system/module-implementations/extern/hyperlane-solana-register/src/lib.rs index d5c20fa675..a37a6b8445 100644 --- a/crates/module-system/module-implementations/extern/hyperlane-solana-register/src/lib.rs +++ b/crates/module-system/module-implementations/extern/hyperlane-solana-register/src/lib.rs @@ -53,11 +53,6 @@ where pub enum SolanaRegistrationError { #[error("Core module error: {0}")] CoreModuleError(#[from] CoreModuleError), - #[error("Embedded pubkey already registered to different address. Attempted: {attempted_address}, Registered: {registered_address}")] - AlreadyRegistered { - attempted_address: String, - registered_address: String, - }, #[error("Invalid body length. Expected {expected}, found {found}")] InvalidBodyLength { expected: usize, found: usize }, #[error("Failed to extract public key from body")] @@ -297,26 +292,19 @@ where let (user_pubkey, embedded_pubkey) = self.unpack_body(body.as_ref())?; let credential_id = CredentialId::from(embedded_pubkey); let address = S::Address::try_from(&user_pubkey).map_err(CoreModuleError::from)?; - let resolved_address = self - .accounts - .resolve_sender_address(&address, &credential_id, state) + + self.accounts + .authorize_credential(&address, &credential_id, state) .map_err(CoreModuleError::state_write)?; - if address != resolved_address { - Err(SolanaRegistrationError::AlreadyRegistered { - attempted_address: address.to_string(), - registered_address: resolved_address.to_string(), - }) - } else { - self.emit_event( - state, - Event::UserRegistered { - address, - credential_id, - }, - ); - Ok(()) - } + self.emit_event( + state, + Event::UserRegistered { + address, + credential_id, + }, + ); + Ok(()) } pub fn admin( diff --git a/crates/module-system/module-implementations/extern/hyperlane-solana-register/tests/integration/registration.rs b/crates/module-system/module-implementations/extern/hyperlane-solana-register/tests/integration/registration.rs index 189340d28a..d155be963f 100644 --- a/crates/module-system/module-implementations/extern/hyperlane-solana-register/tests/integration/registration.rs +++ b/crates/module-system/module-implementations/extern/hyperlane-solana-register/tests/integration/registration.rs @@ -4,6 +4,7 @@ use sov_hyperlane_integration::{CallMessage, Ism, Recipient}; use sov_hyperlane_register_module::{ CallMessage as RegistrationCallMessage, SolanaDeployment, SolanaRegistration, }; +use sov_modules_api::prelude::UnwrapInfallible; use sov_modules_api::{Base58Address, CredentialId, HexString, SafeVec, Spec}; use sov_test_utils::{AsUser, TransactionTestCase}; @@ -23,16 +24,20 @@ fn test_user_is_registered_correctly() { let route_id = register_basic_warp_route(&mut runner, &admin); let payer = [1u8; 32]; + let payer_addr = ::Address::from(payer); let embedded = [2u8; 32]; + let credential = CredentialId::from(embedded); let body = [payer, embedded].concat(); let valid_message = make_valid_message(0, route_id, HexString::new(body)); let message = HexString::new(SafeVec::try_from(valid_message.encode().0).unwrap()); - let credential = CredentialId::from(embedded); - // Sanity check, ensure account definently doesnt already exist runner.query_state(|state| { - let account = sov_accounts::Accounts::default().get_account(credential, state); - assert!(matches!(account, sov_accounts::Response::AccountEmpty)); + assert!( + !sov_accounts::Accounts::::default() + .is_explicitly_authorized(&payer_addr, &credential, state) + .unwrap_infallible(), + "Embedded credential should not yet be authorized for the payer address" + ); }); runner.execute_transaction(TransactionTestCase { @@ -41,17 +46,18 @@ fn test_user_is_registered_correctly() { message, }), assert: Box::new(move |result, _| { + let receipt = &result.tx_receipt; assert!( result.tx_receipt.is_successful(), - "Recipient was not registered successfully" + "Recipient was not registered successfully: {receipt:?}" ); assert_eq!( result.events.last().unwrap(), &TestRuntimeEvent::SolanaRegister( sov_hyperlane_register_module::Event::UserRegistered { - address: ::Address::from(payer), - credential_id: CredentialId::from(embedded), + address: payer_addr, + credential_id: credential, } ) ); @@ -59,16 +65,17 @@ fn test_user_is_registered_correctly() { }); runner.query_state(|state| { - let account = sov_accounts::Accounts::default().get_account(credential, state); - assert!(matches!( - account, - sov_accounts::Response::AccountExists { .. } - )); + assert!( + sov_accounts::Accounts::::default() + .is_explicitly_authorized(&payer_addr, &credential, state) + .unwrap_infallible(), + "Embedded credential should be authorized for the payer address after registration" + ); }); } #[test] -fn test_errors_if_user_already_registered() { +fn test_two_payers_registering_same_embedded_both_succeed() { let SetupParams { mut runner, admin, @@ -77,47 +84,64 @@ fn test_errors_if_user_already_registered() { } = setup(); let route_id = register_basic_warp_route(&mut runner, &admin); - let payer = [1u8; 32]; + let payer_a = [1u8; 32]; + let payer_a_addr = ::Address::from(payer_a); + let payer_b = [3u8; 32]; + let payer_b_addr = ::Address::from(payer_b); let embedded = [2u8; 32]; - let body = [payer, embedded].concat(); - let valid_message = make_valid_message(0, route_id, HexString::new(body)); - let message = HexString::new(SafeVec::try_from(valid_message.encode().0).unwrap()); + let credential = CredentialId::from(embedded); + // First registration: (payer_a, embedded). + let body_a = [payer_a, embedded].concat(); + let msg_a = make_valid_message(0, route_id, HexString::new(body_a)); + let envelope_a = HexString::new(SafeVec::try_from(msg_a.encode().0).unwrap()); runner.execute_transaction(TransactionTestCase { input: user.create_plain_message::>(CallMessage::Process { metadata: HexString::new(SafeVec::new()), - message: message.clone(), + message: envelope_a, }), assert: Box::new(move |result, _| { + let receipt = &result.tx_receipt; assert!( result.tx_receipt.is_successful(), - "Recipient was not registered successfully" + "First registration should succeed: {receipt:?}" ); }), }); - // payer is different so will try to register to different address - let payer = [3u8; 32]; - let embedded = [2u8; 32]; - let body = [payer, embedded].concat(); - let valid_message = make_valid_message(1, route_id, HexString::new(body)); - let message = HexString::new(SafeVec::try_from(valid_message.encode().0).unwrap()); - + // Same embedded credential, different payer — many-to-many authorization is allowed. + let body_b = [payer_b, embedded].concat(); + let msg_b = make_valid_message(1, route_id, HexString::new(body_b)); + let envelope_b = HexString::new(SafeVec::try_from(msg_b.encode().0).unwrap()); runner.execute_transaction(TransactionTestCase { input: user.create_plain_message::>(CallMessage::Process { metadata: HexString::new(SafeVec::new()), - message, + message: envelope_b, }), - assert: Box::new(move |result, _| match result.tx_receipt { - sov_rollup_interface::stf::TxEffect::Reverted(contents) => { - assert_eq!( - contents.reason.to_string(), - "Embedded pubkey already registered to different address. Attempted: CktRuQ2mttgRGkXJtyksdKHjUdc2C4TgDzyB98oEzy8, Registered: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi".to_string() - ); - } - _ => panic!("Registration should have reverted: {:?}", result.tx_receipt), + assert: Box::new(move |result, _| { + let receipt = &result.tx_receipt; + assert!( + result.tx_receipt.is_successful(), + "Second registration with same embedded but different payer should also succeed: {receipt:?}" + ); }), }); + + runner.query_state(|state| { + let accounts = sov_accounts::Accounts::::default(); + assert!( + accounts + .is_explicitly_authorized(&payer_a_addr, &credential, state) + .unwrap_infallible(), + "First (payer_a, embedded) authorization should be recorded" + ); + assert!( + accounts + .is_explicitly_authorized(&payer_b_addr, &credential, state) + .unwrap_infallible(), + "Second (payer_b, embedded) authorization should also be recorded" + ); + }); } #[test] 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 452cfd7257..e8c7a369ed 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 @@ -1,4 +1,3 @@ -use sov_accounts::{Accounts, CallMessage as AccountsCallMessage}; use sov_address::{EthereumAddress, EvmCryptoSpec}; use sov_eip712_auth::{ Eip712Authenticator, Eip712AuthenticatorInput, Eip712AuthenticatorTrait, SchemaProvider, @@ -22,7 +21,6 @@ use sov_rollup_interface::da::RelevantBlobs; use sov_rollup_interface::stf::{TxEffect, TxReceiptContents}; use sov_test_utils::runtime::genesis::optimistic::HighLevelOptimisticGenesisConfig; use sov_test_utils::runtime::{TestRunner, ValueSetter}; -use sov_test_utils::TransactionTestCase; use sov_test_utils::{generate_runtime, EncodeCall, TestStorage, TestUser, TEST_DEFAULT_MAX_FEE}; use sov_value_setter::CallMessage; @@ -274,28 +272,34 @@ fn correct_signature_is_accepted() { #[test] fn test_multisig_signature_verification() { - use sov_test_utils::AsUser; - let (mut runner, admin) = setup(); - - // First, create and register a multisig + // 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. let multisig_keys = [ TestPrivateKey::generate(), TestPrivateKey::generate(), TestPrivateKey::generate(), ]; - - // Create the multisig and register it 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>(); - runner.execute_transaction(TransactionTestCase { - input: admin.create_plain_message::>( - AccountsCallMessage::InsertCredentialId(multisig_credential_id), - ), - assert: Box::new(move |result, _state| { - assert!(result.tx_receipt.is_successful()); - }), - }); + 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(); + let genesis_config = HighLevelOptimisticGenesisConfig::generate() + .add_accounts_with_default_balance(2) + .add_accounts(vec![multisig_user]); + let module_config = sov_value_setter::ValueSetterConfig { + admin: multisig_address, + }; + let genesis = GenesisConfig::from_minimal_config(genesis_config.clone().into(), module_config); + 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(); diff --git a/crates/module-system/module-implementations/sov-accounts/README.md b/crates/module-system/module-implementations/sov-accounts/README.md index 08b73c9c37..f3cf64be8a 100644 --- a/crates/module-system/module-implementations/sov-accounts/README.md +++ b/crates/module-system/module-implementations/sov-accounts/README.md @@ -1,15 +1,87 @@ # `sov-accounts` module -The `sov-accounts` module is responsible for managing accounts on the rollup. +The `sov-accounts` module resolves transaction credentials to rollup +addresses and records which credentials may act for which addresses. +### The `sov-accounts` module offers the following functionality -### The `sov-accounts` module offers the following functionality: +1. A credential has a deterministic canonical address, computed as `credential_id.into::()`. + This relation is stateless: using the canonical address does not require an account entry to be written. -1. When a sender sends their first message, the `sov-accounts` module will create a new address by deriving it from the sender's credential. - The module will then add a mapping between the credential id and the address to its state. For all subsequent messages that include the sender's credential, - the module will retrieve the sender's address from the mapping and pass it along with the original message to an intended module. +2. It is possible to authorize another credential for the caller's address using + the `CallMessage::InsertCredentialId(..)` message. + This writes an `account_owners` authorization. -1. It is possible to add new credential to a given address using the `CallMessage::InsertCredentialId(..)` message. +## Credential and Address Relations -1. It is possible to query the `sov-accounts` module using the `get_account` method and get the account corresponding to the given credential id. +### Stateless canonical address +```text +credential_id -> credential_id.into::() +``` + +This is the default address for a credential. It is deterministic and requires no state write. +If a credential has no explicit authorization, this canonical address is the natural fallback. + +### Account-credential authorization map + +```text +account_owners[(address, credential_id)] = true +``` + +This state map records authorization. +A present entry means the credential is authorized to sign transactions that execute as the given address. +The key is the exact `(address, credential_id)` pair, so this relation does not provide a credential-only lookup by itself. + +This relation answers "may this credential act as this address?" once the target address is known. +New `InsertCredentialId` calls write this relation. + +Callers that need to verify whether a known address may be used with a credential should use +`is_authorized_for`, which checks the stateless canonical address and `account_owners`. + +## Upgrade procedure for chains with legacy `accounts` entries + +The module used to have a separate `accounts` mapping with different semantics, now deprecated and unused. +Chains whose genesis was before the `accounts` deprecation need to have a migration run at the upgrade height +(including whenever resyncing from genesis). + +The migration ships as a CLI binary in `examples/demo-rollup`: + +```sh +# Inspect what would change without committing. +cargo run --features migration-script \ + --bin legacy-accounts-migrate -- \ + --rollup-config-path /path/to/rollup_config.toml \ + --dry-run + +# Commit the migration in-place at the current head version. +cargo run --features migration-script \ + --bin legacy-accounts-migrate -- \ + --rollup-config-path /path/to/rollup_config.toml +``` + +The binary requires a stopped node (the storage manager opens the DB exclusively). +The reported `pre_state_root` and `post_state_root` are written in JSON; verify the +post-root matches what the rollup loads on restart. + +### Behavior change: canonical-address authorization is no longer suppressed + +A pre-migration `accounts[C] = A` row was authoritative: `is_authorized_for(X, C)` returned strictly +`A == X` and bypassed the canonical-address fallback. The refactored `is_authorized_for` returns +`true` whenever `X == credential_id.into::()` or `account_owners[(X, C)] = true`, with +no suppression. + +The migration converts each `accounts[C] = A` row into `account_owners[(A, C)] = true`. For any +legacy row where `A != canonical(credential_id)`, the credential gains authorization to act as +`canonical(credential_id)` after the migration, in addition to keeping authorization for `A`. The +canonical fallback is computed, not stored, so it cannot be revoked through `account_owners`. + +Operators should treat each migrated entry as also implicitly authorizing +`credential_id.into::()`. If that address holds assets or permissions whose security +relied on the legacy exclusive semantic, retire the credential before deploying the new binary. + +The migration requires NOMT prefix iteration; JMT-backed deployments are not supported. + +For non-demo rollups, copy `examples/demo-rollup/src/migrations/legacy_accounts.rs` +and swap in your own runtime/spec types — the migration logic itself lives in +`sov_accounts::migrations` and is reusable. diff --git a/crates/module-system/module-implementations/sov-accounts/src/call.rs b/crates/module-system/module-implementations/sov-accounts/src/call.rs index 6b23a9ece5..7a731cd5b5 100644 --- a/crates/module-system/module-implementations/sov-accounts/src/call.rs +++ b/crates/module-system/module-implementations/sov-accounts/src/call.rs @@ -1,20 +1,20 @@ -use anyhow::bail; -use anyhow::{anyhow, Result}; +use anyhow::{bail, Context as _}; use schemars::JsonSchema; use sov_modules_api::macros::{serialize, UniversalWallet}; use sov_modules_api::{Context, CredentialId, Spec, StateReader, TxState}; use sov_state::namespaces::User; -use crate::{Account, Accounts}; +use crate::{AccountOwnerKey, Accounts}; /// Represents the available call messages for interacting with the sov-accounts module. #[derive(Debug, PartialEq, Eq, Clone, JsonSchema, UniversalWallet)] #[serialize(Borsh, Serde)] #[serde(rename_all = "snake_case")] pub enum CallMessage { - /// Inserts a new credential id for the corresponding Account. + /// Authorizes `credential_id` as a signer for the caller's address. + /// Fails if the credential is already authorized for the caller's address. InsertCredentialId( - /// The new credential id. + /// The credential id being authorized. CredentialId, ), } @@ -25,35 +25,31 @@ impl Accounts { new_credential_id: CredentialId, context: &Context, state: &mut impl TxState, - ) -> Result<()> { + ) -> anyhow::Result<()> { if !self.enable_custom_account_mappings.get(state)?.expect( "`enable_custom_account_mappings` should not be None; it must be set at genesis.", ) { bail!("Custom account mappings are disabled"); } - self.exit_if_credential_exists(&new_credential_id, state)?; - - // Insert the new credential id -> account mapping - let account = Account { - addr: *context.sender(), - }; - self.accounts.set(&new_credential_id, &account, state)?; + self.exit_if_credential_exists(&new_credential_id, context.sender(), state)?; + self.authorize_credential(context.sender(), &new_credential_id, state)?; Ok(()) } fn exit_if_credential_exists( &self, new_credential_id: &CredentialId, + address: &S::Address, state: &mut impl StateReader, - ) -> Result<()> { + ) -> anyhow::Result<()> { anyhow::ensure!( - self.accounts - .get(new_credential_id, state) - .map_err(|err| anyhow!("Error raised while getting account: {err:?}"))? + self.account_owners + .get(&AccountOwnerKey::new(*address, *new_credential_id), state) + .context("Failed to read account owner")? .is_none(), - "New CredentialId already exists" + "CredentialId already authorized for this address" ); Ok(()) } diff --git a/crates/module-system/module-implementations/sov-accounts/src/capabilities.rs b/crates/module-system/module-implementations/sov-accounts/src/capabilities.rs index 8d84fc04d4..7128610e4b 100644 --- a/crates/module-system/module-implementations/sov-accounts/src/capabilities.rs +++ b/crates/module-system/module-implementations/sov-accounts/src/capabilities.rs @@ -1,45 +1,54 @@ -use sov_modules_api::{CredentialId, Spec, StateAccessor, StateReader, StateWriter}; +use sov_modules_api::{CredentialId, Spec, StateReader, StateWriter}; use sov_state::User; -use crate::{Account, Accounts}; +use crate::{AccountOwnerKey, Accounts}; impl Accounts { - /// Resolve the sender's public key to an address. - /// If the sender is not registered, but a fallback address if provided, immediately registers - /// the credential to the fallback and then returns it. - pub fn resolve_sender_address( + /// Authorizes `credential_id` to sign as `address`. + pub fn authorize_credential>( &mut self, - default_address: &S::Address, + address: &S::Address, credential_id: &CredentialId, state: &mut ST, - ) -> Result>::Error> { - let maybe_address = self.accounts.get(credential_id, state)?.map(|a| a.addr); - - match maybe_address { - Some(address) => Ok(address), - None => { - // 1. Add the credential -> account mapping - let new_account = Account { - addr: *default_address, - }; - self.accounts.set(credential_id, &new_account, state)?; + ) -> Result<(), >::Error> { + self.account_owners.set( + &AccountOwnerKey::new(*address, *credential_id), + &true, + state, + ) + } - Ok(*default_address) - } - } + /// Returns `true` only if `(address, credential_id)` has an explicit entry + /// in `account_owners`. For the full authorization check including the + /// canonical fallback, use [`Self::is_authorized_for`]. + pub fn is_explicitly_authorized>( + &self, + address: &S::Address, + credential_id: &CredentialId, + state: &mut ST, + ) -> Result { + Ok(self + .account_owners + .get(&AccountOwnerKey::new(*address, *credential_id), state)? + .is_some()) } - /// Resolve the sender's public key to an address. - pub fn resolve_sender_address_read_only>( + /// Returns `true` if `credential_id` is authorized to act as `address`. + /// + /// Returns `true` when `address` is the canonical address of + /// `credential_id` (i.e. `credential_id.into() == address`) or when an + /// explicit `account_owners` authorization exists. + pub fn is_authorized_for>( &self, - default_address: &S::Address, + address: &S::Address, credential_id: &CredentialId, state: &mut ST, - ) -> Result { - let maybe_address = self.accounts.get(credential_id, state)?.map(|a| a.addr); - match maybe_address { - Some(address) => Ok(address), - None => Ok(*default_address), + ) -> Result { + let canonical_address: S::Address = (*credential_id).into(); + if canonical_address == *address { + return Ok(true); } + + self.is_explicitly_authorized(address, credential_id, state) } } diff --git a/crates/module-system/module-implementations/sov-accounts/src/fuzz.rs b/crates/module-system/module-implementations/sov-accounts/src/fuzz.rs index b00b8422e9..2abc5e43a3 100644 --- a/crates/module-system/module-implementations/sov-accounts/src/fuzz.rs +++ b/crates/module-system/module-implementations/sov-accounts/src/fuzz.rs @@ -51,7 +51,7 @@ where ::BlockHeader: Default, ::PublicKey: Arbitrary<'a>, { - /// Creates an arbitrary set of accounts and stores it under `state`. + /// Creates arbitrary genesis credential/address authorizations under `state`. pub fn arbitrary_workset( u: &mut Unstructured<'a>, state: &mut StateCheckpoint, diff --git a/crates/module-system/module-implementations/sov-accounts/src/genesis.rs b/crates/module-system/module-implementations/sov-accounts/src/genesis.rs index 5bf6eefaec..e4f4d6e409 100644 --- a/crates/module-system/module-implementations/sov-accounts/src/genesis.rs +++ b/crates/module-system/module-implementations/sov-accounts/src/genesis.rs @@ -4,17 +4,17 @@ use serde_with::{serde_as, DisplayFromStr}; use sov_modules_api::prelude::*; use sov_modules_api::{CredentialId, GenesisState}; -use crate::{Account, Accounts}; +use crate::{AccountOwnerKey, Accounts}; -/// Account data for the genesis. +/// Credential/address authorization data for genesis. #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct AccountData
{ - /// Credential ID of the account. + /// Credential ID to authorize. #[serde_as(as = "DisplayFromStr")] pub credential_id: CredentialId, - /// Address of the account. + /// Address the credential may act as. pub address: Address, } @@ -23,9 +23,9 @@ pub struct AccountData
{ #[serde(deny_unknown_fields)] #[schemars(bound = "S: ::sov_modules_api::Spec", rename = "AccountConfig")] pub struct AccountConfig { - /// Accounts to initialize the rollup. + /// Credential/address authorizations to initialize. pub accounts: Vec>, - /// Enable custom `CredentailId` => `Account` mapping. + /// Enable configured credential authorizations and `InsertCredentialId`. #[serde(default = "default_true")] pub enable_custom_account_mappings: bool, } @@ -51,13 +51,15 @@ impl Accounts { } for acc in &config.accounts { - if self.accounts.get(&acc.credential_id, state)?.is_some() { - bail!("Account already exists") + let key = AccountOwnerKey::new(acc.address, acc.credential_id); + if self.account_owners.get(&key, state)?.is_some() { + bail!( + "Authorization already exists for address {} and credential {}", + acc.address, + acc.credential_id + ) } - - let new_account = Account { addr: acc.address }; - - self.accounts.set(&acc.credential_id, &new_account, state)?; + self.authorize_credential(&acc.address, &acc.credential_id, state)?; } Ok(()) diff --git a/crates/module-system/module-implementations/sov-accounts/src/lib.rs b/crates/module-system/module-implementations/sov-accounts/src/lib.rs index 6bfe8bbda0..5a9c452091 100644 --- a/crates/module-system/module-implementations/sov-accounts/src/lib.rs +++ b/crates/module-system/module-implementations/sov-accounts/src/lib.rs @@ -1,4 +1,7 @@ #![deny(missing_docs)] +// Tombstone field `_accounts` makes the `ModuleInfo`-derived +// `_prefix__accounts` accessor double-underscored. +#![allow(non_snake_case)] #![doc = include_str!("../README.md")] mod call; mod capabilities; @@ -7,6 +10,8 @@ mod fuzz; mod genesis; pub use genesis::*; #[cfg(feature = "native")] +pub mod migrations; +#[cfg(feature = "native")] mod query; #[cfg(feature = "native")] pub use query::*; @@ -18,7 +23,7 @@ use sov_modules_api::{ StateMap, StateValue, TxState, }; -/// An account on the rollup. +/// Stored address for a legacy/custom credential-indexed account mapping. #[derive( borsh::BorshDeserialize, borsh::BorshSerialize, @@ -30,11 +35,54 @@ use sov_modules_api::{ Clone, )] pub struct Account { - /// The address of the account. + /// The mapped address. pub addr: S::Address, } -/// A module responsible for managing accounts on the rollup. +/// Composite key for [`Accounts::account_owners`]. A present entry +/// `(address, credential_id)` means `credential_id` is authorized to sign +/// transactions that execute as `address`. +#[derive( + borsh::BorshDeserialize, borsh::BorshSerialize, Debug, Clone, Copy, PartialEq, Eq, Hash, +)] +pub(crate) struct AccountOwnerKey { + address: S::Address, + credential_id: CredentialId, +} + +impl AccountOwnerKey { + pub(crate) fn new(address: S::Address, credential_id: CredentialId) -> Self { + Self { + address, + credential_id, + } + } +} + +// `Display` / `FromStr` exist only to satisfy `StateMap`'s trait bound; on-chain +// keys are Borsh-serialized. +impl std::fmt::Display for AccountOwnerKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.address, self.credential_id) + } +} + +impl std::str::FromStr for AccountOwnerKey { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + let (addr_str, cred_str) = s + .rsplit_once('/') + .ok_or_else(|| anyhow::anyhow!("invalid AccountOwnerKey: missing '/' separator"))?; + Ok(Self { + address: ::from_str(addr_str) + .map_err(|e| anyhow::Error::from_boxed(e.into()))?, + credential_id: cred_str.parse()?, + }) + } +} + +/// A module responsible for resolving credentials to addresses and recording +/// credential authorizations. #[derive(Clone, ModuleInfo, ModuleRestApi)] #[cfg_attr(feature = "arbitrary", derive(Debug))] pub struct Accounts { @@ -42,13 +90,29 @@ pub struct Accounts { #[id] pub id: ModuleId, - /// Mapping from a credential to its corresponding account. + /// Tombstone for the legacy `credential_id -> address` routing index. + /// + /// **Do not read or write outside [`crate::migrations`].** The field is + /// retained only to preserve the `#[state]` field discriminant ordering + /// derived by the `ModuleInfo` macro (this is the first state field, so + /// removing it would shift the discriminants of every following field + /// and corrupt their on-disk data). Existing entries are migrated to + /// [`Self::account_owners`] by + /// [`crate::migrations::apply_legacy_account_migration`] and the source + /// rows are deleted. The leading underscore signals to readers that this + /// field is intentionally unused. #[state] - pub(crate) accounts: StateMap>, + pub(crate) _accounts: StateMap>, - /// If this field is false, `CallMessage::InsertCredentialId` messages will be rejected. + /// If this field is false, configured genesis authorizations and + /// `CallMessage::InsertCredentialId` messages will be rejected. #[state] enable_custom_account_mappings: StateValue, + + /// Authorization set: a present entry means `credential_id` may sign as + /// `address`. + #[state] + pub(crate) account_owners: StateMap, bool>, } impl Module for Accounts { diff --git a/crates/module-system/module-implementations/sov-accounts/src/migrations.rs b/crates/module-system/module-implementations/sov-accounts/src/migrations.rs new file mode 100644 index 0000000000..a7fb97900c --- /dev/null +++ b/crates/module-system/module-implementations/sov-accounts/src/migrations.rs @@ -0,0 +1,224 @@ +//! One-time data migration from the legacy +//! [`Accounts::accounts`](crate::Accounts::accounts) credential→account index +//! to the [`Accounts::account_owners`](crate::Accounts::account_owners) +//! authorization set. +//! +//! Offline only: requires backend prefix iteration via +//! [`NativeStorage::maybe_iter_user_values_with_prefix`], which is supported by +//! NOMT but not by JMT. Run before deploying a binary that has dropped the +//! layer-1 `accounts` reads. +//! +//! Two-phase API: +//! 1. [`collect_legacy_account_entries`] reads all entries from `storage` +//! while it is still borrowable. +//! 2. [`apply_legacy_account_migration`] writes the entries to +//! `account_owners` and deletes them from `accounts` via a +//! [`sov_modules_api::StateCheckpoint`] (which consumes storage on +//! construction, so collection has to happen first). + +use anyhow::Context; +use sov_modules_api::{CredentialId, Spec, StateWriter}; +use sov_state::namespaces::User; +use sov_state::NativeStorage; + +use crate::{Account, AccountOwnerKey, Accounts}; + +/// Summary of a single legacy-accounts migration run. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct MigrationReport { + /// Number of `accounts` entries copied to `account_owners` and deleted + /// from the source map. + pub entries_migrated: u64, +} + +/// Reads every `(credential_id, Account)` pair currently stored in the +/// legacy [`Accounts::accounts`](crate::Accounts::accounts) map. +/// +/// # Errors +/// +/// - The backend does not support prefix iteration (e.g. JMT). NOMT does. +/// - A stored value cannot be borsh-decoded as [`Account`]. +#[allow(deprecated)] +pub fn collect_legacy_account_entries( + accounts: &Accounts, + storage: &Storage, +) -> anyhow::Result)>> +where + S: Spec, + Storage: NativeStorage, +{ + let raw_entries: Vec<(CredentialId, Vec)> = accounts + ._accounts + .iter_raw(storage) + .context("failed to start prefix iteration over legacy accounts map")? + .ok_or_else(|| { + anyhow::anyhow!( + "storage backend does not support prefix iteration; \ + legacy-accounts migration requires NOMT" + ) + })? + .collect::>>()?; + + raw_entries + .into_iter() + .map(|(credential_id, value_bytes)| { + let account: Account = borsh::from_slice(&value_bytes) + .with_context(|| format!("failed to decode legacy Account for {credential_id}"))?; + Ok((credential_id, account)) + }) + .collect() +} + +/// Writes every entry from `entries` to +/// [`Accounts::account_owners`](crate::Accounts::account_owners) as +/// `(addr, credential_id) -> true` and deletes the corresponding +/// [`Accounts::accounts`](crate::Accounts::accounts) row. +/// +/// Idempotent: passing an empty slice (or running a second time after the +/// first run cleared the source) returns `entries_migrated: 0` without error. +/// +/// # Errors +/// +/// - The `writer` returns an error from `set`/`delete`. +#[allow(deprecated)] +pub fn apply_legacy_account_migration( + accounts: &mut Accounts, + entries: &[(CredentialId, Account)], + writer: &mut Writer, +) -> anyhow::Result +where + S: Spec, + Writer: StateWriter, +{ + for (credential_id, account) in entries { + let owner_key = AccountOwnerKey::new(account.addr, *credential_id); + accounts.account_owners.set(&owner_key, &true, writer)?; + accounts._accounts.delete(credential_id, writer)?; + } + + Ok(MigrationReport { + entries_migrated: entries.len() as u64, + }) +} + +#[cfg(test)] +mod tests { + use sov_modules_api::Spec; + use sov_test_utils::runtime::genesis::optimistic::HighLevelOptimisticGenesisConfig; + use sov_test_utils::runtime::TestRunner; + use sov_test_utils::{generate_optimistic_runtime, TestSpec}; + + use super::*; + use crate::Accounts; + + type S = TestSpec; + generate_optimistic_runtime!(MigrationTestRuntime <=); + type RT = MigrationTestRuntime; + + fn setup_runner() -> TestRunner { + let genesis_config = HighLevelOptimisticGenesisConfig::generate(); + let genesis = GenesisConfig::from_minimal_config(genesis_config.into()); + TestRunner::new_with_genesis(genesis.into_genesis_params(), RT::default()) + } + + fn cred(byte: u8) -> CredentialId { + [byte; 32].into() + } + + fn addr(byte: u8) -> ::Address { + let mut bytes = [0u8; 28]; + bytes[0] = byte; + ::Address::from(bytes) + } + + /// Migrating a populated `accounts` map writes `account_owners` entries + /// for every pair and clears the source rows. + #[test] + #[allow(deprecated)] + fn apply_migration_moves_entries_to_owners() { + let mut runner = setup_runner(); + let cred_1 = cred(1); + let cred_2 = cred(2); + let addr_1 = addr(0xAA); + let addr_2 = addr(0xBB); + + runner.__apply_to_state(|state| { + let mut accounts = Accounts::::default(); + + accounts + ._accounts + .set(&cred_1, &Account { addr: addr_1 }, state) + .unwrap(); + accounts + ._accounts + .set(&cred_2, &Account { addr: addr_2 }, state) + .unwrap(); + + assert_eq!( + accounts._accounts.get(&cred_1, state).unwrap(), + Some(Account { addr: addr_1 }) + ); + assert_eq!( + accounts._accounts.get(&cred_2, state).unwrap(), + Some(Account { addr: addr_2 }) + ); + + let entries = vec![ + (cred_1, Account { addr: addr_1 }), + (cred_2, Account { addr: addr_2 }), + ]; + let report = apply_legacy_account_migration(&mut accounts, &entries, state).unwrap(); + assert_eq!(report.entries_migrated, 2); + + assert!(accounts._accounts.get(&cred_1, state).unwrap().is_none()); + assert!(accounts._accounts.get(&cred_2, state).unwrap().is_none()); + assert!(accounts + .is_explicitly_authorized(&addr_1, &cred_1, state) + .unwrap()); + assert!(accounts + .is_explicitly_authorized(&addr_2, &cred_2, state) + .unwrap()); + }); + } + + /// Empty input is a valid no-op. + #[test] + fn apply_migration_empty_input() { + let mut runner = setup_runner(); + runner.__apply_to_state(|state| { + let mut accounts = Accounts::::default(); + let report = apply_legacy_account_migration(&mut accounts, &[], state).unwrap(); + assert_eq!(report.entries_migrated, 0); + }); + } + + /// Re-applying the same entries after they've already been migrated + /// re-writes the same `account_owners` entries (still `true`) and + /// no-op-deletes the (already empty) source rows; the post-state is + /// indistinguishable from a single application. + #[test] + #[allow(deprecated)] + fn apply_migration_is_safe_to_repeat() { + let mut runner = setup_runner(); + let cred_1 = cred(7); + let addr_1 = addr(0xCC); + let entries = vec![(cred_1, Account { addr: addr_1 })]; + + runner.__apply_to_state(|state| { + let mut accounts = Accounts::::default(); + + accounts + ._accounts + .set(&cred_1, &Account { addr: addr_1 }, state) + .unwrap(); + + apply_legacy_account_migration(&mut accounts, &entries, state).unwrap(); + apply_legacy_account_migration(&mut accounts, &entries, state).unwrap(); + + assert!(accounts._accounts.get(&cred_1, state).unwrap().is_none()); + assert!(accounts + .is_explicitly_authorized(&addr_1, &cred_1, state) + .unwrap()); + }); + } +} diff --git a/crates/module-system/module-implementations/sov-accounts/src/query.rs b/crates/module-system/module-implementations/sov-accounts/src/query.rs index 15ae9c039d..80e8807900 100644 --- a/crates/module-system/module-implementations/sov-accounts/src/query.rs +++ b/crates/module-system/module-implementations/sov-accounts/src/query.rs @@ -1,35 +1,101 @@ -//! Defines queries exposed by the accounts module, along with the relevant types -use sov_modules_api::prelude::UnwrapInfallible; +//! Read-only REST endpoints for inspecting `sov-accounts` state. + +use std::str::FromStr; + +use axum::routing::get; +use sov_modules_api::prelude::{axum, UnwrapInfallible}; +use sov_modules_api::rest::utils::{errors, ApiResult, Path}; +use sov_modules_api::rest::{ApiState, HasCustomRestApi}; use sov_modules_api::{ApiStateAccessor, CredentialId, Spec}; -use crate::{Account, Accounts}; +use crate::Accounts; -/// This is the response returned from the accounts_getAccount endpoint. +/// Response of `GET /authorizations/{address}/{credential_id}`. #[derive(Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone)] -#[serde( - bound = "Addr: serde::Serialize + serde::de::DeserializeOwned", - rename_all = "snake_case" -)] -pub enum Response { - /// The account corresponding to the given credential id exists. - AccountExists { - /// The address of the account, - addr: Addr, - }, - /// The account corresponding to the credential id does not exist. - AccountEmpty, +pub struct AuthorizationResponse { + /// `true` iff `credential_id` is authorized to act as `address`, + /// including the canonical-address fallback. + pub authorized: bool, } impl Accounts { - /// Get the account corresponding to the given credential id. - pub fn get_account( - &self, - credential_id: CredentialId, - state: &mut ApiStateAccessor, - ) -> Response { - match self.accounts.get(&credential_id, state).unwrap_infallible() { - Some(Account { addr }) => Response::AccountExists { addr }, - None => Response::AccountEmpty, - } + async fn route_is_authorized( + state: ApiState, + mut accessor: ApiStateAccessor, + Path((address_str, credential_id_str)): Path<(String, String)>, + ) -> ApiResult { + let address = ::from_str(&address_str).map_err(|_| { + errors::bad_request_400( + &format!("invalid address `{address_str}`"), + "address parse failed", + ) + })?; + let credential_id = CredentialId::from_str(&credential_id_str).map_err(|e| { + errors::bad_request_400( + &format!("invalid credential_id `{credential_id_str}`"), + e.to_string(), + ) + })?; + + let authorized = state + .is_authorized_for(&address, &credential_id, &mut accessor) + .unwrap_infallible(); + Ok(AuthorizationResponse { authorized }.into()) + } +} + +impl HasCustomRestApi for Accounts { + type Spec = S; + + fn custom_rest_api(&self, state: ApiState) -> axum::Router<()> { + axum::Router::new() + .route( + "/authorizations/{address}/{credential_id}", + get(Self::route_is_authorized), + ) + .with_state(state.with(self.clone())) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use sov_modules_api::capabilities::mocks::MockKernel; + use sov_modules_api::rest::utils::Path; + use sov_modules_api::{ConcurrentStateCheckpoint, StateCheckpoint}; + use sov_test_utils::storage::SimpleStorageManager; + use sov_test_utils::TestSpec; + + use super::*; + + type S = TestSpec; + + #[test] + fn route_includes_canonical_fallback() { + let kernel = Arc::new(MockKernel::::default()); + let storage = SimpleStorageManager::new().create_storage(); + let checkpoint = Arc::new(ConcurrentStateCheckpoint::from_state_checkpoint( + StateCheckpoint::::new(storage, kernel.as_ref(), None), + )); + let (_sender, receiver) = sov_modules_api::prelude::tokio::sync::watch::channel(checkpoint); + + let accounts = Accounts::::default(); + let state = ApiState::build(Arc::new(()), receiver, kernel, None).with(accounts); + let accessor = state.default_api_state_accessor(); + + let credential_id = CredentialId::from([7u8; 32]); + let address = ::Address::from(credential_id); + + let response = sov_modules_api::prelude::tokio::runtime::Runtime::new() + .unwrap() + .block_on(Accounts::::route_is_authorized( + state, + accessor, + Path((address.to_string(), credential_id.to_string())), + )) + .unwrap(); + + assert!(response.0.authorized); } } diff --git a/crates/module-system/module-implementations/sov-accounts/src/tests.rs b/crates/module-system/module-implementations/sov-accounts/src/tests.rs index 8db2d82b8f..a7c975cab4 100644 --- a/crates/module-system/module-implementations/sov-accounts/src/tests.rs +++ b/crates/module-system/module-implementations/sov-accounts/src/tests.rs @@ -1,56 +1,8 @@ use sov_modules_api::prelude::*; use sov_modules_api::sov_universal_wallet::schema::Schema; -use crate::query::Response; use crate::CallMessage; -type S = sov_test_utils::TestSpec; - -#[test] -fn test_response_serialization() { - let addr: Vec = (1..=28).collect(); - let mut addr_array = [0u8; 28]; - addr_array.copy_from_slice(&addr); - let response = Response::AccountExists::<::Address> { - addr: ::Address::from(addr_array), - }; - - let json = serde_json::to_string(&response).unwrap(); - assert_eq!( - json, - r#"{"account_exists":{"addr":"sov1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5z5tpwxqergd3crhxalf"}}"# - ); -} - -#[test] -fn test_response_deserialization() { - let json = - r#"{"account_exists":{"addr":"sov1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5z5tpwxqergd3crhxalf"}}"#; - let response: Response<::Address> = serde_json::from_str(json).unwrap(); - - let expected_addr: Vec = (1..=28).collect(); - let mut addr_array = [0u8; 28]; - addr_array.copy_from_slice(&expected_addr); - let expected_response = Response::AccountExists::<::Address> { - addr: ::Address::from(addr_array), - }; - - assert_eq!(response, expected_response); -} - -#[test] -fn test_response_deserialization_on_wrong_hrp() { - let json = r#"{"account_exists":{"addr":"hax1qypqx68ju0l"}}"#; - let response: Result::Address>, serde_json::Error> = - serde_json::from_str(json); - match response { - Ok(response) => panic!("Expected error, got {response:?}"), - Err(err) => { - assert_eq!(err.to_string(), "Wrong HRP: hax at line 1 column 44"); - } - } -} - #[test] fn test_display_accounts_call() { #[derive(Debug, Clone, PartialEq, borsh::BorshSerialize, UniversalWallet)] 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 b4b0d42138..de534a4773 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, Response}; +use sov_accounts::{Accounts, CallMessage}; use sov_modules_api::transaction::{UnsignedTransactionV0, Version1}; use sov_modules_api::{ CryptoSpec, PrivateKey, PublicKey, RawTx, Runtime, SkippedTxContents, Spec, TxEffect, @@ -21,11 +21,15 @@ 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. + #[allow(dead_code)] account_2: TestUser, non_registered_account: TestUser, } -/// We setup genesis with three accounts, two of which are registered at genesis. +/// We set up genesis with three accounts, two of which have custom credentials +/// authorized at genesis. fn setup() -> (TestData, TestRunner) { let genesis_config = HighLevelOptimisticGenesisConfig::generate().add_accounts(vec![ TestUser::generate_with_default_balance().add_credential_id([0u8; 32].into()), @@ -73,16 +77,13 @@ fn test_config_account() { runner, ) = setup(); - // The account is registered at genesis. + // The account is registered at genesis: its credential is authorized for + // the user's address via `account_owners`. runner.query_visible_state(|state| { let accounts = Accounts::::default(); - let response = accounts.get_account(user.credential_id(), state); - assert_eq!( - response, - Response::AccountExists { - addr: user.address() - } - ); + assert!(accounts + .is_explicitly_authorized(&user.address(), &user.credential_id(), state) + .unwrap()); }); } @@ -107,65 +108,62 @@ fn test_update_account() { let accounts = Accounts::::default(); - // New account with the new public key and an old address is created. - assert_eq!( - accounts.get_account(new_credential, state), - Response::AccountExists { - addr: user.address() - } - ); - // Account corresponding to the old credential still exists. - assert_eq!( - accounts.get_account(user.credential_id(), state), - Response::AccountExists { - addr: user.address() - } - ); - + // The new credential is authorized for the user's address. + assert!(accounts + .is_explicitly_authorized(&user.address(), &new_credential, state) + .unwrap()); assert_ne!(new_credential, user.credential_id()); }), }); } +/// A credential already authorized for an address cannot be inserted twice. #[test] -fn test_update_account_fails() { +fn test_insert_existing_credential_fails() { let ( TestData { - account_1, - account_2, + non_registered_account: sender, .. }, mut runner, ) = setup(); + let new_credential = TestPrivateKey::generate().pub_key().credential_id(); + runner.execute_transaction(TransactionTestCase { - input: account_1.create_plain_message::>(CallMessage::InsertCredentialId( - account_2.credential_id(), + input: sender.create_plain_message::>(CallMessage::InsertCredentialId( + new_credential, )), assert: Box::new(move |result, _state| { - if let TxEffect::Reverted(contents) = result.tx_receipt { + assert!(result.tx_receipt.is_successful()); + }), + }); + + runner.execute_transaction(TransactionTestCase { + input: sender.create_plain_message::>(CallMessage::InsertCredentialId( + new_credential, + )), + assert: Box::new(move |result, _state| match result.tx_receipt { + TxEffect::Reverted(contents) => { assert_eq!( contents.reason.to_string(), - "New CredentialId already exists" + "CredentialId already authorized for this address" ); } + _ => panic!("Expected reverted transaction for existing credential"), }), }); } /// Tests the multisig functionality of the Accounts module. +/// +/// Seeds genesis with a `TestUser` whose custom `credential_id` matches the +/// multisig, so the multisig's canonical address is funded before any tx is +/// submitted. This keeps the focus on signature-level invariants. #[test] fn test_setup_multisig_and_act() { use sov_modules_api::Multisig; - let ( - TestData { - non_registered_account: user, - .. - }, - mut runner, - ) = setup(); - // First, create and register a multisig let multisig_keys = [ TestPrivateKey::generate(), TestPrivateKey::generate(), @@ -174,33 +172,15 @@ fn test_setup_multisig_and_act() { 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>(); - runner.execute_transaction(TransactionTestCase { - input: user.create_plain_message::>(CallMessage::InsertCredentialId( - multisig_credential_id, - )), - assert: Box::new(move |result, state| { - assert!(result.tx_receipt.is_successful()); - let accounts = Accounts::::default(); + // Build a funded `TestUser` whose address is the multisig's canonical address. + let multisig_user = + TestUser::generate_with_default_balance().add_credential_id(multisig_credential_id); - // New account with the new public key and an old address is created. - assert_eq!( - accounts.get_account(multisig_credential_id, state), - Response::AccountExists { - addr: user.address() - } - ); - // Account corresponding to the old credential still exists. - assert_eq!( - accounts.get_account(user.credential_id(), state), - Response::AccountExists { - addr: user.address() - } - ); - - assert_ne!(multisig_credential_id, user.credential_id()); - }), - }); + 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()); // Define utilities for... // - Generating a valid multisig (version 1) transaction @@ -383,13 +363,24 @@ fn test_register_new_account() { mut runner, ) = setup(); - // The account is empty at the start because it is not registered at genesis. assert_eq!(non_registered_account.custom_credential_id, None); runner.query_visible_state(|state| { let accounts = Accounts::::default(); - let response = accounts.get_account(non_registered_account.credential_id(), state); - assert_eq!(response, Response::AccountEmpty); + assert!(!accounts + .is_explicitly_authorized( + &non_registered_account.address(), + &non_registered_account.credential_id(), + state + ) + .unwrap()); + assert!(accounts + .is_authorized_for( + &non_registered_account.address(), + &non_registered_account.credential_id(), + state + ) + .unwrap()); }); let new_credential = TestPrivateKey::generate().pub_key().credential_id(); @@ -403,79 +394,37 @@ fn test_register_new_account() { let accounts = Accounts::::default(); - // New account with the new public key and an old address is created. - assert_eq!( - accounts.get_account(new_credential, state), - Response::AccountExists { - addr: non_registered_account.address() - } - ); - - // The default credential of the account exists - assert_eq!( - accounts.get_account(non_registered_account.credential_id(), state), - Response::AccountExists { - addr: non_registered_account.address() - } - ); - - assert_ne!(new_credential, non_registered_account.credential_id()); - }), - }); -} - -#[test] -fn test_resolve_sender_address_with_default_address_non_registered() { - let ( - TestData { - non_registered_account, - .. - }, - runner, - ) = setup(); + assert!(accounts + .is_explicitly_authorized(&non_registered_account.address(), &new_credential, state) + .unwrap()); + assert!(accounts + .is_authorized_for(&non_registered_account.address(), &new_credential, state) + .unwrap()); - runner.query_visible_state(|state| { - let mut accounts = Accounts::::default(); - assert_eq!( - accounts - .resolve_sender_address( + assert!(!accounts + .is_explicitly_authorized( &non_registered_account.address(), &non_registered_account.credential_id(), state ) - .unwrap(), - non_registered_account.address() - ); - }); -} - -#[test] -fn test_resolve_sender_address_registered() { - let ( - TestData { - account_1, - account_2, - .. - }, - runner, - ) = setup(); + .unwrap()); + assert!(accounts + .is_authorized_for( + &non_registered_account.address(), + &non_registered_account.credential_id(), + state + ) + .unwrap()); - runner.query_visible_state(|state| { - let mut accounts = Accounts::::default(); - - // Ensure correct (registered) address is used even if another fallback is provided - assert_eq!( - accounts - .resolve_sender_address(&account_2.address(), &account_1.credential_id(), state) - .unwrap(), - account_1.address() - ); + assert_ne!(new_credential, non_registered_account.credential_id()); + }), }); } -/// Tests what happens if one tries to resolve an address when there is more than one credential available. +/// After `InsertCredentialId` from a user, each inserted credential is +/// authorized under that user's address. #[test] -fn test_resolve_address_if_more_than_one_credential() { +fn test_authorize_multiple_credentials_for_same_address() { let ( TestData { non_registered_account, @@ -484,62 +433,34 @@ fn test_resolve_address_if_more_than_one_credential() { mut runner, ) = setup(); - let pub_key_1 = TestPrivateKey::generate().pub_key(); - let credential_1 = pub_key_1.credential_id(); - let default_address_1 = credential_1.into(); - - let pub_key_2 = TestPrivateKey::generate().pub_key(); - let credential_2 = pub_key_2.credential_id(); - let default_address_2 = credential_2.into(); + let credential_1 = TestPrivateKey::generate().pub_key().credential_id(); + let credential_2 = TestPrivateKey::generate().pub_key().credential_id(); runner.execute( non_registered_account .create_plain_message::>(CallMessage::InsertCredentialId(credential_1)), ); - runner.execute( non_registered_account .create_plain_message::>(CallMessage::InsertCredentialId(credential_2)), ); runner.query_visible_state(|state| { - let mut accounts = Accounts::::default(); - - assert_eq!( - accounts - .resolve_sender_address(&default_address_1, &credential_1, state) - .unwrap(), - non_registered_account.address() - ); - - assert_eq!( - accounts - .resolve_sender_address(&default_address_2, &credential_2, state) - .unwrap(), - non_registered_account.address() - ); - }); -} - -/// This test should verify that when a new credential is specified with an existing account's -/// address as fallback, that the credential is appended to that address. However -/// query_visible_state doesn't mutate the state so it simply verifies that the fallback address is -/// returned correctly -#[test] -fn test_resolve_with_different_default_address() { - let (TestData { account_1, .. }, runner) = setup(); - - let random_credential = TestPrivateKey::generate().pub_key().credential_id(); - - runner.query_visible_state(|state| { - let mut accounts = Accounts::::default(); - - assert_eq!( - accounts - .resolve_sender_address(&account_1.address(), &random_credential, state) - .unwrap(), - account_1.address() - ); + let accounts = Accounts::::default(); + let addr = non_registered_account.address(); + + assert!(accounts + .is_explicitly_authorized(&addr, &credential_1, state) + .unwrap()); + assert!(accounts + .is_explicitly_authorized(&addr, &credential_2, state) + .unwrap()); + assert!(accounts + .is_authorized_for(&addr, &credential_1, state) + .unwrap()); + assert!(accounts + .is_authorized_for(&addr, &credential_2, state) + .unwrap()); }); } diff --git a/crates/module-system/module-schemas/genesis-schemas/sov-accounts.json b/crates/module-system/module-schemas/genesis-schemas/sov-accounts.json index 5b4c04b15a..fec517182f 100644 --- a/crates/module-system/module-schemas/genesis-schemas/sov-accounts.json +++ b/crates/module-system/module-schemas/genesis-schemas/sov-accounts.json @@ -8,14 +8,14 @@ ], "properties": { "accounts": { - "description": "Accounts to initialize the rollup.", + "description": "Credential/address authorizations to initialize.", "type": "array", "items": { "$ref": "#/definitions/AccountData_for_Address" } }, "enable_custom_account_mappings": { - "description": "Enable custom `CredentailId` => `Account` mapping.", + "description": "Enable configured credential authorizations and `InsertCredentialId`.", "default": true, "type": "boolean" } @@ -23,7 +23,7 @@ "additionalProperties": false, "definitions": { "AccountData_for_Address": { - "description": "Account data for the genesis.", + "description": "Credential/address authorization data for genesis.", "type": "object", "required": [ "address", @@ -31,7 +31,7 @@ ], "properties": { "address": { - "description": "Address of the account.", + "description": "Address the credential may act as.", "allOf": [ { "$ref": "#/definitions/Address" @@ -39,7 +39,7 @@ ] }, "credential_id": { - "description": "Credential ID of the account.", + "description": "Credential ID to authorize.", "type": "string" } }, diff --git a/crates/module-system/module-schemas/schemas/sov-accounts.json b/crates/module-system/module-schemas/schemas/sov-accounts.json index f2405b3954..629b469441 100644 --- a/crates/module-system/module-schemas/schemas/sov-accounts.json +++ b/crates/module-system/module-schemas/schemas/sov-accounts.json @@ -4,7 +4,7 @@ "description": "Represents the available call messages for interacting with the sov-accounts module.", "oneOf": [ { - "description": "Inserts a new credential id for the corresponding Account.", + "description": "Authorizes `credential_id` as a signer for the caller's address. Fails if the credential is already authorized for the caller's address.", "type": "object", "required": [ "insert_credential_id" diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index b462f840d3..c0e1274958 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -311,19 +311,13 @@ 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> { - // This should be resolved by the sequencer registry during blob selection - let sender = self.accounts.resolve_sender_address( - &auth_data.default_address, - &auth_data.credential_id, - state, - )?; Ok(Context::new( - sender, + auth_data.default_address, auth_data.credentials.clone(), sequencer_rollup_address, *sequencer, @@ -337,19 +331,14 @@ 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> { - let sender = self.accounts.resolve_sender_address( - &auth_data.default_address, - &auth_data.credential_id, - state, - )?; // The tx sender & sequencer are the same entity Ok(Context::new( - sender, + auth_data.default_address, auth_data.credentials.clone(), - sender, + auth_data.default_address, *sequencer, None, execution_context, diff --git a/crates/utils/sov-test-utils/src/runtime/macros.rs b/crates/utils/sov-test-utils/src/runtime/macros.rs index 68e7761e2a..7fb2818e1f 100644 --- a/crates/utils/sov-test-utils/src/runtime/macros.rs +++ b/crates/utils/sov-test-utils/src/runtime/macros.rs @@ -172,10 +172,10 @@ macro_rules! generate_runtime_without_capabilities { fn resolve_address>( &self, default_address: &S::Address, - credential_id: &::sov_modules_api::CredentialId, - state: &mut ST, + _credential_id: &::sov_modules_api::CredentialId, + _state: &mut ST, ) -> ::std::result::Result{ - self.accounts.resolve_sender_address_read_only(default_address, credential_id, state) + ::std::result::Result::Ok(*default_address) } fn genesis_config(_input: &Self::GenesisInput) -> ::sov_modules_api::prelude::anyhow::Result { diff --git a/examples/demo-rollup/Cargo.toml b/examples/demo-rollup/Cargo.toml index 3ca0126090..c2fb60e774 100644 --- a/examples/demo-rollup/Cargo.toml +++ b/examples/demo-rollup/Cargo.toml @@ -22,6 +22,7 @@ sov-stf-runner = { workspace = true } sov-metrics = { workspace = true, features = ["native"] } # Sovereign crates +sov-accounts = { workspace = true, features = ["native"], optional = true } sov-bank = { workspace = true, features = ["native"] } sov-blob-storage = { workspace = true, features = ["native"], optional = true } sov-chain-state = { workspace = true, features = ["native"], optional = true } @@ -144,6 +145,7 @@ default = [] # Used for using different encoding between host and guest bincode = ["risc0/bincode", "sov-risc0-adapter/bincode"] migration-script = [ + "dep:sov-accounts", "dep:sov-blob-storage", "dep:rockbound", "dep:bincode", @@ -171,6 +173,11 @@ name = "mockda-to-celestia-migrate" path = "src/migrations/mockda_to_celestia.rs" required-features = ["migration-script"] +[[bin]] +name = "legacy-accounts-migrate" +path = "src/migrations/legacy_accounts.rs" +required-features = ["migration-script"] + [[bin]] name = "sov-hive-genesis-adapter" path = "src/hive/genesis_adapter.rs" diff --git a/examples/demo-rollup/src/migrations/common.rs b/examples/demo-rollup/src/migrations/common.rs new file mode 100644 index 0000000000..88a90889cb --- /dev/null +++ b/examples/demo-rollup/src/migrations/common.rs @@ -0,0 +1,66 @@ +//! Shared helpers for offline migration binaries. +//! +//! Each binary in `examples/demo-rollup/src/migrations` includes this file via +//! `#[path = "common.rs"] mod common;` since binaries don't share a crate +//! root with the package library. + +use anyhow::{bail, Context}; +use rockbound::SchemaBatch; +use sov_db::ledger_db::LedgerDb; +use sov_db::schema::tables::SlotByNumber; +use sov_rollup_interface::common::SlotNumber; +use sov_state::NativeStorage; + +pub fn assert_storage_latest_version_matches_ledger_head( + storage: &S, + ledger_db: &LedgerDb, + phase: &str, +) -> anyhow::Result { + let (head_slot_number, _head_slot) = ledger_db + .get_head_slot()? + .ok_or_else(|| anyhow::anyhow!("ledger has no head slot; cannot migrate an empty DB"))?; + let storage_latest_version = storage.latest_version(); + if storage_latest_version != head_slot_number { + bail!( + "{phase} invariant failed: storage.latest_version ({}) != ledger head slot ({})", + storage_latest_version, + head_slot_number + ); + } + Ok(head_slot_number) +} + +pub fn assert_ledger_head_state_root_matches_storage_root( + storage: &S, + ledger_db: &LedgerDb, + phase: &str, +) -> anyhow::Result<()> { + let (head_slot_number, head_slot) = ledger_db + .get_head_slot()? + .ok_or_else(|| anyhow::anyhow!("ledger has no head slot; cannot migrate an empty DB"))?; + let storage_root = storage + .get_root_hash(head_slot_number) + .context("failed to read storage root at ledger head slot")?; + if head_slot.state_root.as_ref() != storage_root.as_ref() { + bail!( + "{phase} invariant failed: ledger head state_root does not match storage root at slot {}", + head_slot_number + ); + } + Ok(()) +} + +pub fn make_ledger_root_patch( + ledger_db: &LedgerDb, + new_state_root: &[u8], +) -> anyhow::Result { + let (head_slot_number, mut head_slot) = ledger_db + .get_head_slot()? + .ok_or_else(|| anyhow::anyhow!("ledger has no head slot; cannot patch state root"))?; + + head_slot.state_root = new_state_root.to_vec().into(); + + let mut batch = SchemaBatch::new(); + batch.put::(&head_slot_number, &head_slot)?; + Ok(batch) +} diff --git a/examples/demo-rollup/src/migrations/legacy_accounts.rs b/examples/demo-rollup/src/migrations/legacy_accounts.rs new file mode 100644 index 0000000000..e51de6677d --- /dev/null +++ b/examples/demo-rollup/src/migrations/legacy_accounts.rs @@ -0,0 +1,214 @@ +//! Offline migration tool that copies every legacy +//! `sov_accounts::Accounts::accounts` entry into `Accounts::account_owners` +//! and deletes the source row. +//! +//! Run this against a stopped MockDA demo-rollup DB before deploying a binary +//! that has dropped the layer-1 `accounts` reads. The tool is structurally +//! identical for the Celestia rollup; swap `MockDemoRollup` for +//! `CelestiaDemoRollup` (and `MockDaSpec` for `CelestiaSpec`). + +use std::path::PathBuf; + +use anyhow::{bail, Context}; +use clap::Parser; +use demo_stf::runtime::Runtime; +use serde::Serialize; +use sov_db::ledger_db::LedgerDb; +use sov_db::storage_manager::NomtStorageManager; +use sov_full_node_configs::runner::from_toml_path; +use sov_mock_da::storable::StorableMockDaService; +use sov_mock_da::MockDaSpec; +use sov_modules_api::capabilities::HasKernel; +use sov_modules_api::execution_mode::Native; +use sov_modules_api::{ModuleInfo, Spec, StateCheckpoint, StateWriter}; +use sov_modules_rollup_blueprint::RollupBlueprint; +use sov_state::{Kernel, NativeStorage, Prefix, SlotKey, StateUpdate}; +use sov_stf_runner::RollupConfig; + +use sov_demo_rollup::MockDemoRollup; + +#[path = "common.rs"] +mod common; +use common::{ + assert_ledger_head_state_root_matches_storage_root, + assert_storage_latest_version_matches_ledger_head, make_ledger_root_patch, +}; + +#[derive(Parser, Debug)] +#[command( + author, + version, + about = "Offline migration tool for sov-accounts: copies legacy `accounts` map entries to \ + `account_owners` and deletes the source rows." +)] +struct Args { + /// Path to the rollup config file used by the running node. + #[arg(long)] + rollup_config_path: PathBuf, + + /// Override `storage.path` from the rollup config. + #[arg(long)] + db_path: Option, + + /// Compute the post-migration state root but do not commit changes. + #[arg(long, default_value_t = false)] + dry_run: bool, + + /// Optional path to write the JSON migration report. + #[arg(long)] + report_out: Option, +} + +type RollupSpec = as RollupBlueprint>::Spec; +type Hasher = <::CryptoSpec as sov_modules_api::CryptoSpec>::Hasher; +type DemoStorage = ::Storage; +type DemoStorageManager = NomtStorageManager; + +#[derive(Serialize)] +struct MigrationReport { + dry_run: bool, + db_path: String, + pre_state_root: String, + post_state_root: String, + head_rollup_slot: u64, + entries_migrated: u64, +} + +fn main() { + if let Err(err) = run() { + eprintln!("legacy-accounts migration failed: {err:#}"); + std::process::exit(1); + } +} + +fn run() -> anyhow::Result<()> { + let args = Args::parse(); + + let storage_config = load_storage_config(&args)?; + let db_path = storage_config.path.clone(); + + let mut storage_manager = DemoStorageManager::new(storage_config, false) + .with_context(|| format!("failed to open storage manager at {}", db_path.display()))?; + let (storage, ledger_reader) = storage_manager + .create_state_for_migration() + .context("failed to create migration storage view")?; + let ledger_db = + LedgerDb::with_reader(ledger_reader).context("failed to initialize ledger db")?; + + let head_slot_number = + assert_storage_latest_version_matches_ledger_head(&storage, &ledger_db, "pre-migration")?; + assert_ledger_head_state_root_matches_storage_root(&storage, &ledger_db, "pre-migration")?; + + let pre_state_root = storage + .get_root_hash(head_slot_number) + .context("failed to read pre-migration state root")?; + + let mut runtime = Runtime::::default(); + let entries = + sov_accounts::migrations::collect_legacy_account_entries(&runtime.accounts, &storage) + .context("failed to collect legacy accounts entries")?; + + // The historical-state DB rejects User-namespace writes that don't also touch + // Kernel (sov-db/src/historical_state.rs). Round-trip + // `chain_state.true_slot_number` to satisfy the invariant — NOMT roots are + // pure functions of (key, value) pairs, so re-writing the same bytes is + // state-root-neutral. + let true_slot_key = SlotKey::singleton(&Prefix::new( + runtime.chain_state.discriminant(), + sov_chain_state::ChainState::::TRUE_SLOT_NUMBER_ITEM_DISCRIMINANT, + )); + let true_slot_value = storage + .get_unbound::(true_slot_key.clone()) + .ok_or_else(|| { + anyhow::anyhow!( + "kernel `chain_state.true_slot_number` is unset; \ + cannot perform no-op kernel write" + ) + })?; + + let mut checkpoint = StateCheckpoint::new(storage, &runtime.kernel(), None); + let report = sov_accounts::migrations::apply_legacy_account_migration( + &mut runtime.accounts, + &entries, + &mut checkpoint, + ) + .context("failed to apply legacy-accounts migration to checkpoint")?; + StateWriter::::set(&mut checkpoint, &true_slot_key, true_slot_value) + .context("failed to round-trip kernel value to satisfy historical-state invariant")?; + + let (next_state_root, mut state_update, accessory_delta, _witness, storage_after) = + checkpoint.materialize_update(pre_state_root); + state_update.add_accessory_items(accessory_delta.freeze()); + let change_set = storage_after.materialize_changes_at_version(state_update, head_slot_number); + + let post_state_root = if args.dry_run { + next_state_root + } else { + let ledger_change_set = make_ledger_root_patch(&ledger_db, next_state_root.as_ref())?; + storage_manager + .commit_migration_change_set_at_head(head_slot_number, change_set, ledger_change_set) + .context("failed to commit migration changeset at head version")?; + + let (post_storage, post_ledger_reader) = storage_manager + .create_state_for_migration() + .context("failed to create post-migration storage view")?; + let post_ledger_db = LedgerDb::with_reader(post_ledger_reader) + .context("failed to initialize post-migration ledger db view")?; + let post_head_slot_number = assert_storage_latest_version_matches_ledger_head( + &post_storage, + &post_ledger_db, + "post-migration", + )?; + if post_head_slot_number != head_slot_number { + bail!( + "post-migration head slot changed unexpectedly: expected {}, found {}", + head_slot_number, + post_head_slot_number + ); + } + assert_ledger_head_state_root_matches_storage_root( + &post_storage, + &post_ledger_db, + "post-migration", + )?; + post_storage + .get_root_hash(post_head_slot_number) + .context("failed to read post-migration state root")? + }; + + let migration_report = MigrationReport { + dry_run: args.dry_run, + db_path: db_path.display().to_string(), + pre_state_root: pre_state_root.to_string(), + post_state_root: post_state_root.to_string(), + head_rollup_slot: head_slot_number.get(), + entries_migrated: report.entries_migrated, + }; + + let json = serde_json::to_string_pretty(&migration_report) + .context("failed to serialize migration report JSON")?; + + if let Some(path) = args.report_out { + std::fs::write(&path, &json) + .with_context(|| format!("failed to write migration report to {}", path.display()))?; + } + + println!("{json}"); + Ok(()) +} + +fn load_storage_config(args: &Args) -> anyhow::Result { + let rollup_config: RollupConfig<::Address, StorableMockDaService> = + from_toml_path(&args.rollup_config_path).with_context(|| { + format!( + "failed to read rollup config from {}", + args.rollup_config_path.display() + ) + })?; + + let mut storage = rollup_config.storage; + if let Some(db_path_override) = &args.db_path { + storage.path = db_path_override.clone(); + } + Ok(storage) +} diff --git a/examples/demo-rollup/src/migrations/mockda_to_celestia.rs b/examples/demo-rollup/src/migrations/mockda_to_celestia.rs index f696c90540..85683dbcdd 100644 --- a/examples/demo-rollup/src/migrations/mockda_to_celestia.rs +++ b/examples/demo-rollup/src/migrations/mockda_to_celestia.rs @@ -14,7 +14,7 @@ use sov_celestia_adapter::types::TmHash; use sov_celestia_adapter::verifier::address::CelestiaAddress; use sov_db::config::RollupDbConfig; use sov_db::ledger_db::LedgerDb; -use sov_db::schema::tables::{BatchByNumber, SlotByNumber}; +use sov_db::schema::tables::BatchByNumber; use sov_db::schema::types::{BatchNumber, DbBytes, StoredBatch}; use sov_db::storage_manager::NomtStorageManager; use sov_full_node_configs::runner::from_toml_path; @@ -35,6 +35,13 @@ use sov_stf_runner::RollupConfig; use sov_demo_rollup::{CelestiaDemoRollup, MockDemoRollup}; +#[path = "common.rs"] +mod common; +use common::{ + assert_ledger_head_state_root_matches_storage_root, + assert_storage_latest_version_matches_ledger_head, make_ledger_root_patch, +}; + #[derive(Parser, Debug)] #[command( author, @@ -1220,21 +1227,6 @@ fn select_source_sequencer( Ok(only.clone()) } -fn make_ledger_root_patch( - ledger_db: &LedgerDb, - new_state_root: &[u8], -) -> anyhow::Result { - let (head_slot_number, mut head_slot) = ledger_db - .get_head_slot()? - .ok_or_else(|| anyhow::anyhow!("ledger has no head slot; cannot patch state root"))?; - - head_slot.state_root = new_state_root.to_vec().into(); - - let mut batch = SchemaBatch::new(); - batch.put::(&head_slot_number, &head_slot)?; - Ok(batch) -} - fn make_batch_receipt_patch( ledger_db: &LedgerDb, sequencer_plan: &ResolvedSequencerPlan, @@ -1355,44 +1347,3 @@ fn migrate_stored_batch_receipt( ); Ok(BatchReceiptMigrationOutcome::NeedsRewrite(batch)) } - -fn assert_storage_latest_version_matches_ledger_head( - storage: &S, - ledger_db: &LedgerDb, - phase: &str, -) -> anyhow::Result { - let (head_slot_number, _head_slot) = ledger_db - .get_head_slot()? - .ok_or_else(|| anyhow::anyhow!("ledger has no head slot; cannot migrate an empty DB"))?; - let storage_latest_version = storage.latest_version(); - if storage_latest_version != head_slot_number { - bail!( - "{phase} invariant failed: storage.latest_version ({}) != ledger head slot ({})", - storage_latest_version, - head_slot_number - ); - } - - Ok(head_slot_number) -} - -fn assert_ledger_head_state_root_matches_storage_root( - storage: &S, - ledger_db: &LedgerDb, - phase: &str, -) -> anyhow::Result<()> { - let (head_slot_number, head_slot) = ledger_db - .get_head_slot()? - .ok_or_else(|| anyhow::anyhow!("ledger has no head slot; cannot migrate an empty DB"))?; - let storage_root = storage - .get_root_hash(head_slot_number) - .context("failed to read storage root at ledger head slot")?; - if head_slot.state_root.as_ref() != storage_root.as_ref() { - bail!( - "{phase} invariant failed: ledger head state_root does not match storage root at slot {}", - head_slot_number - ); - } - - Ok(()) -} diff --git a/examples/demo-rollup/stf/src/runtime.rs b/examples/demo-rollup/stf/src/runtime.rs index 6352567300..b5c0937599 100644 --- a/examples/demo-rollup/stf/src/runtime.rs +++ b/examples/demo-rollup/stf/src/runtime.rs @@ -122,12 +122,10 @@ where fn resolve_address>( &self, default_address: &S::Address, - credential_id: &sov_modules_api::CredentialId, - state: &mut ST, + _credential_id: &sov_modules_api::CredentialId, + _state: &mut ST, ) -> Result { - self.0 - .accounts - .resolve_sender_address_read_only(default_address, credential_id, state) + Ok(*default_address) } #[cfg(feature = "native")]