Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ The `SolanaRegistration<S>` 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
Comment thread
citizen-stig marked this conversation as resolved.
- **Admin Controls**: Allows configuration updates via admin-only calls
- **Fallback to Warp**: Non-Solana messages are forwarded to the underlying `Warp` module

Expand Down Expand Up @@ -132,7 +131,6 @@ pub enum Event<S: Spec> {

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
Expand All @@ -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

---

Expand Down Expand Up @@ -372,4 +369,3 @@ cargo test
```

Program tests are located in `solana/program-tests/src/tests.rs`.

Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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(
Expand Down
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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 = <S as Spec>::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::<S>::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 {
Expand All @@ -41,34 +46,36 @@ 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: <S as Spec>::Address::from(payer),
credential_id: CredentialId::from(embedded),
address: payer_addr,
credential_id: credential,
}
)
);
}),
});

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::<S>::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,
Expand All @@ -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 = <S as Spec>::Address::from(payer_a);
let payer_b = [3u8; 32];
let payer_b_addr = <S as Spec>::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::<RT, Mailbox<S>>(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::<RT, Mailbox<S>>(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::<S>::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]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use sov_accounts::{Accounts, CallMessage as AccountsCallMessage};
use sov_address::{EthereumAddress, EvmCryptoSpec};
use sov_eip712_auth::{
Eip712Authenticator, Eip712AuthenticatorInput, Eip712AuthenticatorTrait, SchemaProvider,
Expand All @@ -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;

Expand Down Expand Up @@ -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::<<<S as Spec>::CryptoSpec as CryptoSpec>::Hasher>();
runner.execute_transaction(TransactionTestCase {
input: admin.create_plain_message::<RT, Accounts<S>>(
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();
Expand Down
Loading
Loading