-
Notifications
You must be signed in to change notification settings - Fork 175
Accounts Refactor PR 1: Adding account -> [credential_id] mapping #2770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: theodore/multisig-upgrade
Are you sure you want to change the base?
Changes from 6 commits
c1b3d6c
f22eb56
ef0701d
7ade9ef
256f9a0
ae53c6e
37efa41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,13 +23,13 @@ fn test_user_is_registered_correctly() { | |
| let route_id = register_basic_warp_route(&mut runner, &admin); | ||
|
|
||
| let payer = [1u8; 32]; | ||
| let embedded = [2u8; 32]; | ||
| let embedded = payer; | ||
|
citizen-stig marked this conversation as resolved.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be changed, the payer and embedded should explicitly be different Explains a bit more about what the module is meant to achieve: https://github.com/Sovereign-Labs/sovereign-sdk/tree/dev/crates/module-system/module-implementations/extern/hyperlane-solana-register#message-structure Namely:
|
||
| 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 | ||
| // Sanity check, ensure registration does not rely on an accounts map entry. | ||
| runner.query_state(|state| { | ||
| let account = sov_accounts::Accounts::default().get_account(credential, state); | ||
| assert!(matches!(account, sov_accounts::Response::AccountEmpty)); | ||
|
|
@@ -60,15 +60,12 @@ 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!(matches!(account, sov_accounts::Response::AccountEmpty)); | ||
| }); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_errors_if_user_already_registered() { | ||
| fn test_errors_if_embedded_pubkey_is_not_authorized_for_payer() { | ||
| let SetupParams { | ||
| mut runner, | ||
| admin, | ||
|
|
@@ -82,26 +79,11 @@ fn test_errors_if_user_already_registered() { | |
| 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()); | ||
|
|
||
| runner.execute_transaction(TransactionTestCase { | ||
| input: user.create_plain_message::<RT, Mailbox<S>>(CallMessage::Process { | ||
| metadata: HexString::new(SafeVec::new()), | ||
| message: message.clone(), | ||
| }), | ||
| assert: Box::new(move |result, _| { | ||
| assert!( | ||
| result.tx_receipt.is_successful(), | ||
| "Recipient was not registered successfully" | ||
| ); | ||
| }), | ||
| }); | ||
|
|
||
| // 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()); | ||
| let expected_error = format!( | ||
| "Embedded pubkey is not authorized for address. Address: {}, CredentialId: {}", | ||
| <S as Spec>::Address::from(payer), | ||
| CredentialId::from(embedded) | ||
| ); | ||
|
|
||
| runner.execute_transaction(TransactionTestCase { | ||
| input: user.create_plain_message::<RT, Mailbox<S>>(CallMessage::Process { | ||
|
|
@@ -110,10 +92,7 @@ fn test_errors_if_user_already_registered() { | |
| }), | ||
| 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() | ||
| ); | ||
| assert_eq!(contents.reason.to_string(), expected_error); | ||
| } | ||
| _ => panic!("Registration should have reverted: {:?}", result.tx_receipt), | ||
| }), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,77 @@ | ||
| # `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::<S::Address>()`. 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. | ||
| 1. Legacy state can contain an explicit `credential_id -> address` mapping. | ||
| This map is used for credential-only resolution and the `get_account` query. | ||
|
|
||
| 1. It is possible to add new credential to a given address using the `CallMessage::InsertCredentialId(..)` message. | ||
| 1. It is possible to authorize another credential for the caller's address using | ||
| the `CallMessage::InsertCredentialId(..)` message. This writes an | ||
| `account_owners` authorization, not a credential-indexed account entry. | ||
|
|
||
| 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. | ||
| 1. It is possible to query the `sov-accounts` module using the `get_account` | ||
| method and get the legacy/custom mapped address corresponding to the given | ||
| credential id. | ||
|
|
||
| ## Credential and Address Relations | ||
|
|
||
| The module has three credential/address relations. They answer different | ||
| questions and should not be treated as interchangeable. | ||
|
|
||
| ### Stateless canonical address | ||
|
|
||
| ```text | ||
| credential_id -> credential_id.into::<S::Address>() | ||
| ``` | ||
|
|
||
| This is the default address for a credential. It is deterministic and requires | ||
| no state write. If a credential has no explicit credential-indexed state | ||
| mapping, this canonical address is the natural fallback for credential-only | ||
| routing. | ||
|
|
||
| ### Credential-to-account map | ||
|
|
||
| ```text | ||
| accounts[credential_id] = Account { addr } | ||
| ``` | ||
|
|
||
| This legacy/custom state map records the primary address explicitly associated | ||
| with a credential. A credential has at most one primary address in this map, | ||
| while one address may be the primary address for many credentials. | ||
|
|
||
| This map is used when callers only know a `CredentialId` and need an address: | ||
| `resolve_sender_address`, `get_account`, and legacy/custom account mappings all | ||
| depend on this credential-indexed lookup. New `InsertCredentialId` calls do not | ||
| write this map. `get_account` reports entries from this explicit map; it does | ||
| not mean that every possible stateless canonical address or explicit | ||
| authorization has a stored account entry. | ||
|
|
||
| ### 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. It is not a replacement for the credential-to-account map, | ||
| because it is keyed by `(address, credential_id)` and cannot efficiently answer | ||
| "which address is this credential mapped to?" with a single point lookup. | ||
| New `InsertCredentialId` calls write this relation. | ||
|
|
||
| In short: routing from only a credential uses the explicit credential-to-account | ||
| map or the stateless canonical fallback. Callers that need to verify whether a | ||
| known address may be used with a credential should use the combined | ||
| `is_authorized_for` check: legacy/custom mapping, stateless canonical address, | ||
| or `account_owners`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,86 @@ | ||
| use sov_modules_api::{CredentialId, Spec, StateAccessor, StateReader, StateWriter}; | ||
| use sov_state::User; | ||
|
|
||
| use crate::{Account, Accounts}; | ||
| use crate::{AccountOwnerKey, Accounts}; | ||
|
|
||
| impl<S: Spec> Accounts<S> { | ||
| /// 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. | ||
| /// Authorizes `credential_id` to sign as `address`. | ||
| pub(crate) fn authorize_credential<ST: StateWriter<User>>( | ||
| &mut self, | ||
| address: &S::Address, | ||
| credential_id: &CredentialId, | ||
| state: &mut ST, | ||
| ) -> Result<(), <ST as StateWriter<User>>::Error> { | ||
| self.account_owners.set( | ||
| &AccountOwnerKey::new(*address, *credential_id), | ||
| &true, | ||
| state, | ||
| ) | ||
| } | ||
|
|
||
| /// Resolve the sender's credential to an address. | ||
| /// If `credential_id` has a legacy/custom mapping in `accounts`, return it. | ||
| /// Otherwise, return the supplied `default_address` without writing account state. | ||
| pub fn resolve_sender_address<ST: StateAccessor>( | ||
| &mut self, | ||
| default_address: &S::Address, | ||
| credential_id: &CredentialId, | ||
| state: &mut ST, | ||
| ) -> Result<S::Address, <ST as StateWriter<User>>::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)?; | ||
|
|
||
| Ok(*default_address) | ||
| } | ||
| if let Some(account) = self.accounts.get(credential_id, state)? { | ||
| return Ok(account.addr); | ||
| } | ||
| Ok(*default_address) | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
citizen-stig marked this conversation as resolved.
|
||
|
|
||
| /// Resolve the sender's public key to an address. | ||
| /// Read-only variant of [`Self::resolve_sender_address`]: returns | ||
| /// `default_address` when the credential has no legacy/custom mapping, | ||
| /// without writing. | ||
| pub fn resolve_sender_address_read_only<ST: StateReader<User>>( | ||
| &self, | ||
| default_address: &S::Address, | ||
| credential_id: &CredentialId, | ||
| state: &mut ST, | ||
| ) -> Result<S::Address, ST::Error> { | ||
| let maybe_address = self.accounts.get(credential_id, state)?.map(|a| a.addr); | ||
| match maybe_address { | ||
| Some(address) => Ok(address), | ||
| None => Ok(*default_address), | ||
| if let Some(account) = self.accounts.get(credential_id, state)? { | ||
| return Ok(account.addr); | ||
| } | ||
| Ok(*default_address) | ||
| } | ||
|
|
||
| /// Returns `true` if `credential_id` has an explicit `account_owners` | ||
| /// authorization for `address`. | ||
| pub fn is_authorized<ST: StateReader<User>>( | ||
| &self, | ||
| address: &S::Address, | ||
| credential_id: &CredentialId, | ||
| state: &mut ST, | ||
| ) -> Result<bool, ST::Error> { | ||
| Ok(self | ||
| .account_owners | ||
| .get(&AccountOwnerKey::new(*address, *credential_id), state)? | ||
| .is_some()) | ||
| } | ||
|
|
||
| /// Returns `true` if `credential_id` is authorized to act as `address` | ||
| /// under any supported relation: legacy/custom `accounts` mapping, | ||
| /// stateless canonical address, or explicit `account_owners` | ||
| /// authorization. | ||
| pub fn is_authorized_for<ST: StateReader<User>>( | ||
| &self, | ||
| address: &S::Address, | ||
| credential_id: &CredentialId, | ||
| state: &mut ST, | ||
| ) -> Result<bool, ST::Error> { | ||
| if let Some(account) = self.accounts.get(credential_id, state)? { | ||
| return Ok(account.addr == *address); | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
|
|
||
| let canonical_address: S::Address = (*credential_id).into(); | ||
| if canonical_address == *address { | ||
| return Ok(true); | ||
| } | ||
|
|
||
| self.is_authorized(address, credential_id, state) | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolve_sender_addresswas being used for the side-effect of the credential id being registered for the address, it looks like that's no longer the case here. How would the credential id be registered now?