use core::num::{NonZeroU16, NonZeroU32};
use miden_core::deferred::PrecompileError;
use miden_processor::ExecutionError;
use miden_processor::advice::AdviceInputs;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::auth::{AuthScheme, AuthSecretKey, PublicKey};
use miden_protocol::account::{
Account,
AccountBuilder,
AccountId,
AccountProcedureRoot,
AccountType,
StorageMapKey,
};
use miden_protocol::asset::{AssetId, FungibleAsset};
use miden_protocol::crypto::SequentialCommit;
use miden_protocol::errors::MasmError;
use miden_protocol::note::{Note, NoteType};
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE,
};
use miden_protocol::transaction::{
RawOutputNote,
TransactionSummary,
TransactionSummaryUserParams,
};
use miden_protocol::{Felt, Hasher, Word};
use miden_standards::account::auth::{
Approver,
ApproverSet,
AuthMultisig,
Eip712TransactionSummary,
MultisigAuthArgs,
};
use miden_standards::account::wallets::BasicWallet;
use miden_standards::code_builder::CodeBuilder;
use miden_standards::errors::standards::{
ERR_DUPLICATE_APPROVER_PUBLIC_KEY,
ERR_EIP712_INVALID_SIGNATURE_LENGTH,
ERR_EIP712_REQUIRES_ECDSA,
ERR_MULTISIG_APPROVAL_EXPIRED,
ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS,
ERR_TOO_MANY_APPROVERS,
ERR_TX_ALREADY_EXECUTED,
};
use miden_standards::note::P2idNote;
use miden_standards::testing::account_interface::get_public_keys_from_account;
use miden_standards::tx_script::{ExpirationTransactionScript, SendNotesTransactionScript};
use miden_testing::utils::create_spawn_note;
use miden_testing::{
Auth,
MockChainBuilder,
MockTransactionBuilder,
assert_transaction_executor_error,
};
use miden_tx::TransactionExecutorError;
use miden_tx::auth::{BasicAuthenticator, SigningInputs, TransactionAuthenticator};
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
use rstest::rstest;
pub(super) trait MultisigAuthArgsExt {
fn multisig_auth_args(self, multisig_auth_args: MultisigAuthArgs) -> Self;
}
impl MultisigAuthArgsExt for MockTransactionBuilder<'_> {
fn multisig_auth_args(self, multisig_auth_args: MultisigAuthArgs) -> Self {
let commitment = multisig_auth_args.to_commitment();
self.auth_args(commitment)
.add_advice_map_entry(commitment, multisig_auth_args.to_elements())
.required_block(multisig_auth_args.bound_block_num())
}
}
pub(super) type MultisigTestSetup =
(Vec<AuthSecretKey>, Vec<AuthScheme>, Vec<PublicKey>, Vec<BasicAuthenticator>);
pub(super) fn setup_keys_and_authenticators_with_scheme(
num_approvers: usize,
threshold: usize,
auth_scheme: AuthScheme,
) -> anyhow::Result<MultisigTestSetup> {
let seed: [u8; 32] = rand::random();
let mut rng = ChaCha20Rng::from_seed(seed);
let mut secret_keys = Vec::new();
let mut auth_schemes = Vec::new();
let mut public_keys = Vec::new();
let mut authenticators = Vec::new();
for _ in 0..num_approvers {
let sec_key = match auth_scheme {
AuthScheme::EcdsaK256Keccak => AuthSecretKey::new_ecdsa_k256_keccak_with_rng(&mut rng),
AuthScheme::Falcon512Poseidon2 => {
AuthSecretKey::new_falcon512_poseidon2_with_rng(&mut rng)
},
_ => anyhow::bail!("unsupported auth scheme for this test: {auth_scheme:?}"),
};
let pub_key = sec_key.public_key();
secret_keys.push(sec_key);
auth_schemes.push(auth_scheme);
public_keys.push(pub_key);
}
for secret_key in secret_keys.iter().take(threshold) {
let authenticator = BasicAuthenticator::new(core::slice::from_ref(secret_key));
authenticators.push(authenticator);
}
Ok((secret_keys, auth_schemes, public_keys, authenticators))
}
pub(super) fn build_update_signers_config_vector(
threshold: u64,
num_of_approvers: u64,
public_keys: &[PublicKey],
auth_scheme: AuthScheme,
) -> Vec<Felt> {
let mut config_and_pubkeys_vector = Vec::new();
config_and_pubkeys_vector.extend_from_slice(&[
Felt::new_unchecked(threshold),
Felt::new_unchecked(num_of_approvers),
Felt::ZERO,
Felt::ZERO,
]);
let scheme_word = [Felt::from(auth_scheme.as_u8()), Felt::ZERO, Felt::ZERO, Felt::ZERO];
for public_key in public_keys.iter().rev() {
let key_word: Word = public_key.to_commitment().into();
config_and_pubkeys_vector.extend_from_slice(key_word.as_elements());
config_and_pubkeys_vector.extend_from_slice(&scheme_word);
}
config_and_pubkeys_vector
}
fn create_multisig_account(
threshold: u32,
approvers: &[(PublicKey, AuthScheme)],
asset_amount: u64,
proc_threshold_map: Vec<(AccountProcedureRoot, u32)>,
) -> anyhow::Result<Account> {
let approvers = approvers
.iter()
.map(|(pub_key, auth_scheme)| Approver::new(pub_key.to_commitment(), *auth_scheme))
.collect();
let approver_set = ApproverSet::new(approvers, threshold)?;
let multisig_account = AccountBuilder::new([0; 32])
.with_components(Auth::Multisig { approver_set, proc_threshold_map })
.with_component(BasicWallet)
.account_type(AccountType::Public)
.with_assets(vec![FungibleAsset::mock(asset_amount)])
.build_existing()?;
Ok(multisig_account)
}
pub(super) fn eip712_signature_witness(
secret_key: &AuthSecretKey,
public_key: &PublicKey,
tx_summary: &TransactionSummary,
) -> anyhow::Result<(Word, Vec<Felt>)> {
let AuthSecretKey::EcdsaK256Keccak(signing_key) = secret_key else {
anyhow::bail!("EIP-712 transaction-summary signatures require an ECDSA signing key");
};
let PublicKey::EcdsaK256Keccak(public_key) = public_key else {
anyhow::bail!("EIP-712 transaction-summary signatures require an ECDSA public key");
};
let signature = signing_key.sign_prehash(tx_summary.eip712_hash().into_bytes());
Ok(tx_summary.eip712_signature_advice(public_key, &signature))
}
#[derive(Clone, Copy)]
enum InvalidEip712Witness {
RawSignature,
RawAdviceKey,
WrongApprover,
WrongTransactionSummary,
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_2_of_2_with_note_creation(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_starting_balance = 10u64;
let mut multisig_account =
create_multisig_account(2, &approvers, multisig_starting_balance, vec![])?;
let output_note_asset = FungibleAsset::mock(0);
let mut mock_chain_builder =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let output_note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(),
&[output_note_asset],
NoteType::Public,
)?;
let input_note = mock_chain_builder.add_spawn_note([&output_note])?;
let mut mock_chain = mock_chain_builder.build().unwrap();
let salt = Word::from([Felt::ONE; 4]);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.authenticated_input_note(input_note.id())
.expected_output_note(RawOutputNote::Full(output_note))
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg = tx_summary.as_ref().to_commitment();
let tx_summary = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &tx_summary)
.await?;
let mock_tx_execute = mock_tx_builder
.add_signature(public_keys[0].to_commitment(), msg, sig_1)
.add_signature(public_keys[1].to_commitment(), msg, sig_2)
.build()?
.execute()
.await?;
multisig_account.apply_patch(mock_tx_execute.account_patch())?;
mock_chain.add_pending_executed_transaction(&mock_tx_execute)?;
mock_chain.prove_next_block()?;
assert_eq!(
multisig_account
.vault()
.get_balance(AssetId::new_fungible(AccountId::try_from(
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
)?))?
.as_u64(),
multisig_starting_balance - output_note_asset.unwrap_fungible().amount().as_u64()
);
Ok(())
}
#[rstest]
#[case::raw_signer_at_index_0(Some(0), &[1])]
#[case::raw_signer_at_index_1(Some(1), &[0])]
#[case::both_signers_use_eip712(None, &[0, 1])]
#[tokio::test]
async fn test_multisig_2_of_2_with_eip712_signatures(
#[case] raw_signer_index: Option<usize>,
#[case] eip712_signer_indices: &[usize],
) -> anyhow::Result<()> {
let (secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(2, 2, AuthScheme::EcdsaK256Keccak)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(public_key, auth_scheme)| (public_key.clone(), *auth_scheme))
.collect::<Vec<_>>();
let starting_balance = 10u64;
let mut multisig_account = create_multisig_account(2, &approvers, starting_balance, vec![])?;
let output_note_asset = FungibleAsset::mock(1);
let mut mock_chain_builder =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let output_note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(),
&[output_note_asset],
NoteType::Public,
)?;
let input_note = mock_chain_builder.add_spawn_note([&output_note])?;
let mut mock_chain = mock_chain_builder.build().unwrap();
let salt = Word::from([Felt::ONE; 4]);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.authenticated_input_note(input_note.id())
.expected_output_note(RawOutputNote::Full(output_note))
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let tx_summary_hash = tx_summary.as_ref().to_commitment();
let signing_inputs = SigningInputs::TransactionSummary(tx_summary.clone());
let mut signed_tx_builder = mock_tx_builder;
if let Some(signer_index) = raw_signer_index {
let raw_signature = authenticators[signer_index]
.get_signature(public_keys[signer_index].to_commitment(), &signing_inputs)
.await?;
signed_tx_builder = signed_tx_builder.add_signature(
public_keys[signer_index].to_commitment(),
tx_summary_hash,
raw_signature,
);
}
for &signer_index in eip712_signer_indices {
let (signature_key, witness) = eip712_signature_witness(
&secret_keys[signer_index],
&public_keys[signer_index],
tx_summary.as_ref(),
)?;
signed_tx_builder = signed_tx_builder.add_advice_map_entry(signature_key, witness);
}
let executed_transaction = signed_tx_builder.build()?.execute().await?;
multisig_account.apply_patch(executed_transaction.account_patch())?;
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;
assert_eq!(
multisig_account
.vault()
.get_balance(AssetId::new_fungible(AccountId::try_from(
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
)?))?
.as_u64(),
starting_balance - output_note_asset.unwrap_fungible().amount().as_u64()
);
Ok(())
}
#[rstest]
#[case::raw_signature_under_eip712_key(InvalidEip712Witness::RawSignature)]
#[case::eip712_signature_under_raw_key(InvalidEip712Witness::RawAdviceKey)]
#[case::signature_bound_to_another_approver(InvalidEip712Witness::WrongApprover)]
#[case::signature_bound_to_another_summary(InvalidEip712Witness::WrongTransactionSummary)]
#[tokio::test]
async fn test_multisig_rejects_invalid_eip712_witness(
#[case] invalid_witness: InvalidEip712Witness,
) -> anyhow::Result<()> {
let (secret_keys, auth_schemes, public_keys, _) =
setup_keys_and_authenticators_with_scheme(2, 0, AuthScheme::EcdsaK256Keccak)?;
let approvers = public_keys
.iter()
.zip(auth_schemes)
.map(|(public_key, scheme)| (public_key.clone(), scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(1, &approvers, 10, vec![])?;
let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])?.build()?;
let salt = Word::from([Felt::new_unchecked(9); 4]);
let mock_tx_builder = mock_chain.build_transaction(multisig_account.id()).multisig_auth_args(
MultisigAuthArgs::new(mock_chain.latest_block_header().block_num(), salt),
);
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let tx_summary_hash = tx_summary.as_ref().to_commitment();
let raw_key = Hasher::merge(&[public_keys[0].to_commitment().into(), tx_summary_hash]);
let raw_witness = secret_keys[0].sign(tx_summary_hash).to_encoded_signature(tx_summary_hash);
let (eip712_key, eip712_witness) =
eip712_signature_witness(&secret_keys[0], &public_keys[0], tx_summary.as_ref())?;
let (key, witness) = match invalid_witness {
InvalidEip712Witness::RawSignature => (eip712_key, raw_witness),
InvalidEip712Witness::RawAdviceKey => (raw_key, eip712_witness),
InvalidEip712Witness::WrongApprover => {
let (_, signer_b_witness) =
eip712_signature_witness(&secret_keys[1], &public_keys[1], tx_summary.as_ref())?;
(eip712_key, signer_b_witness)
},
InvalidEip712Witness::WrongTransactionSummary => {
let other_summary = TransactionSummary::new(
tx_summary.account_delta().clone(),
tx_summary.input_notes().clone(),
tx_summary.output_notes().clone(),
tx_summary.block_number(),
tx_summary.block_commitment(),
tx_summary.expiration_delta(),
TransactionSummaryUserParams::new(
[Felt::new_unchecked(42); TransactionSummaryUserParams::NUM_ELEMENTS],
),
);
let (_, other_summary_witness) =
eip712_signature_witness(&secret_keys[0], &public_keys[0], &other_summary)?;
(eip712_key, other_summary_witness)
},
};
let result = mock_tx_builder.add_advice_map_entry(key, witness).build()?.execute().await;
match invalid_witness {
InvalidEip712Witness::WrongApprover => assert_transaction_executor_error!(
result,
MasmError::from_static_str("invalid public key commitment")
),
InvalidEip712Witness::RawSignature
| InvalidEip712Witness::RawAdviceKey
| InvalidEip712Witness::WrongTransactionSummary => {
assert_transaction_executor_error!(
result,
matches ExecutionError::DeferredError {
err: PrecompileError::Precompile { name: "uint256", source },
..
} if matches!(source.as_ref(), PrecompileError::AssertionFailed)
)
},
}
Ok(())
}
#[tokio::test]
async fn test_multisig_eip712_replay_protection() -> anyhow::Result<()> {
let (secret_keys, auth_schemes, public_keys, _) =
setup_keys_and_authenticators_with_scheme(1, 0, AuthScheme::EcdsaK256Keccak)?;
let approvers = vec![(public_keys[0].clone(), auth_schemes[0])];
let multisig_account = create_multisig_account(1, &approvers, 10, vec![])?;
let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])?.build()?;
let salt = Word::from([Felt::new_unchecked(11); 4]);
let reference_block = mock_chain.latest_block_header().block_num();
let auth_args = MultisigAuthArgs::new(reference_block, salt);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.multisig_auth_args(auth_args);
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let (signature_key, witness) =
eip712_signature_witness(&secret_keys[0], &public_keys[0], tx_summary.as_ref())?;
let executed_transaction = mock_tx_builder
.add_advice_map_entry(signature_key, witness.clone())
.build()?
.execute()
.await?;
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;
let replay = mock_chain
.build_transaction(multisig_account.id())
.reference_block(reference_block)
.multisig_auth_args(auth_args)
.add_advice_map_entry(signature_key, witness)
.build()?
.execute()
.await;
assert_transaction_executor_error!(replay, ERR_TX_ALREADY_EXECUTED);
Ok(())
}
#[tokio::test]
async fn test_multisig_does_not_double_count_raw_and_eip712_signatures() -> anyhow::Result<()> {
let (secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(2, 1, AuthScheme::EcdsaK256Keccak)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(public_key, auth_scheme)| (public_key.clone(), *auth_scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let output_note_asset = FungibleAsset::mock(1);
let mut mock_chain_builder =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let output_note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(),
&[output_note_asset],
NoteType::Public,
)?;
let input_note = mock_chain_builder.add_spawn_note([&output_note])?;
let mock_chain = mock_chain_builder.build().unwrap();
let salt = Word::from([Felt::ONE; 4]);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.authenticated_input_note(input_note.id())
.expected_output_note(RawOutputNote::Full(output_note))
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let tx_summary_hash = tx_summary.as_ref().to_commitment();
let signing_inputs = SigningInputs::TransactionSummary(tx_summary.clone());
let raw_signature = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &signing_inputs)
.await?;
let (eip712_key, eip712_witness) =
eip712_signature_witness(&secret_keys[0], &public_keys[0], tx_summary.as_ref())?;
let result = mock_tx_builder
.add_signature(public_keys[0].to_commitment(), tx_summary_hash, raw_signature)
.add_advice_map_entry(eip712_key, eip712_witness)
.build()?
.execute()
.await;
assert!(matches!(result, Err(TransactionExecutorError::Unauthorized(_))));
Ok(())
}
#[tokio::test]
async fn test_multisig_rejects_eip712_for_falcon_approver() -> anyhow::Result<()> {
let (_, auth_schemes, public_keys, _) =
setup_keys_and_authenticators_with_scheme(1, 0, AuthScheme::Falcon512Poseidon2)?;
let approvers = vec![(public_keys[0].clone(), auth_schemes[0])];
let multisig_account = create_multisig_account(1, &approvers, 10, vec![])?;
let output_note_asset = FungibleAsset::mock(1);
let mut mock_chain_builder =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let output_note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(),
&[output_note_asset],
NoteType::Public,
)?;
let input_note = mock_chain_builder.add_spawn_note([&output_note])?;
let mock_chain = mock_chain_builder.build().unwrap();
let salt = Word::from([Felt::ONE; 4]);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.authenticated_input_note(input_note.id())
.expected_output_note(RawOutputNote::Full(output_note))
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let eip712_key = tx_summary.eip712_signature_key(public_keys[0].to_commitment());
let result = mock_tx_builder
.add_advice_map_entry(eip712_key, vec![Felt::ZERO; 32])
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_EIP712_REQUIRES_ECDSA);
Ok(())
}
#[rstest]
#[case::short(31)]
#[case::long(33)]
#[tokio::test]
async fn test_multisig_rejects_invalid_eip712_signature_length(
#[case] witness_length: usize,
) -> anyhow::Result<()> {
let (secret_keys, auth_schemes, public_keys, _) =
setup_keys_and_authenticators_with_scheme(1, 1, AuthScheme::EcdsaK256Keccak)?;
let approvers = vec![(public_keys[0].clone(), auth_schemes[0])];
let multisig_account = create_multisig_account(1, &approvers, 10, vec![])?;
let output_note_asset = FungibleAsset::mock(1);
let mut mock_chain_builder =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let output_note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(),
&[output_note_asset],
NoteType::Public,
)?;
let input_note = mock_chain_builder.add_spawn_note([&output_note])?;
let mock_chain = mock_chain_builder.build().unwrap();
let salt = Word::from([Felt::ONE; 4]);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.authenticated_input_note(input_note.id())
.expected_output_note(RawOutputNote::Full(output_note))
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let (signature_key, mut witness) =
eip712_signature_witness(&secret_keys[0], &public_keys[0], tx_summary.as_ref())?;
witness.resize(witness_length, Felt::ZERO);
let result = mock_tx_builder
.add_advice_map_entry(signature_key, witness)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_EIP712_INVALID_SIGNATURE_LENGTH);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_2_of_4_all_signer_combinations(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(4, 4, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let signer_combinations = [
(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3), ];
for (i, (signer1_idx, signer2_idx)) in signer_combinations.iter().enumerate() {
let salt = Word::from([Felt::new_unchecked(10 + i as u64); 4]);
let mock_tx_builder =
mock_chain.build_transaction(multisig_account.id()).multisig_auth_args(
MultisigAuthArgs::new(mock_chain.latest_block_header().block_num(), salt),
);
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg = tx_summary.as_ref().to_commitment();
let tx_summary = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[*signer1_idx]
.get_signature(public_keys[*signer1_idx].to_commitment(), &tx_summary)
.await?;
let sig_2 = authenticators[*signer2_idx]
.get_signature(public_keys[*signer2_idx].to_commitment(), &tx_summary)
.await?;
let mock_tx_execute = mock_tx_builder
.add_signature(public_keys[*signer1_idx].to_commitment(), msg, sig_1)
.add_signature(public_keys[*signer2_idx].to_commitment(), msg, sig_2)
.build()?;
let executed_tx = mock_tx_execute.execute().await.unwrap_or_else(|_| {
panic!("Transaction should succeed with signers {signer1_idx} and {signer2_idx}")
});
mock_chain.add_pending_executed_transaction(&executed_tx)?;
mock_chain.prove_next_block()?;
}
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_replay_protection(#[case] auth_scheme: AuthScheme) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 20, vec![])?;
let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let salt = Word::from([Felt::new_unchecked(3); 4]);
let ref_block = mock_chain.latest_block_header().block_num();
let mock_tx_builder = mock_chain.build_transaction(multisig_account.id()).multisig_auth_args(
MultisigAuthArgs::new(mock_chain.latest_block_header().block_num(), salt),
);
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg = tx_summary.as_ref().to_commitment();
let tx_summary = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &tx_summary)
.await?;
let mock_tx_execute = mock_tx_builder
.add_signature(public_keys[0].to_commitment(), msg, sig_1.clone())
.add_signature(public_keys[1].to_commitment(), msg, sig_2.clone())
.build()?
.execute()
.await?;
mock_chain.add_pending_executed_transaction(&mock_tx_execute)?;
mock_chain.prove_next_block()?;
let mock_tx_replay = mock_chain
.build_transaction(multisig_account.id())
.reference_block(ref_block)
.add_signature(public_keys[0].to_commitment(), msg, sig_1)
.add_signature(public_keys[1].to_commitment(), msg, sig_2)
.multisig_auth_args(MultisigAuthArgs::new(ref_block, salt))
.build()?;
let result = mock_tx_replay.execute().await;
assert_transaction_executor_error!(result, ERR_TX_ALREADY_EXECUTED);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_stale_signatures_fail_after_expiration(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 20, vec![])?;
let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let salt = Word::from([Felt::new_unchecked(5); 4]);
let expiration_delta = NonZeroU16::new(3).unwrap();
let expiration_script = ExpirationTransactionScript::new(expiration_delta);
let ref_block = mock_chain.latest_block_header().block_num();
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.tx_script(expiration_script.into())
.tx_script_args(expiration_script.tx_script_args())
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
assert_eq!(tx_summary.expiration_delta(), expiration_delta.get());
let original_block_commitment = tx_summary.block_commitment();
let msg = tx_summary.to_commitment();
let tx_summary = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &tx_summary)
.await?;
mock_tx_builder
.add_signature(public_keys[0].to_commitment(), msg, sig_1.clone())
.add_signature(public_keys[1].to_commitment(), msg, sig_2.clone())
.build()?
.execute()
.await?;
mock_chain.prove_until_block(ref_block + 5)?;
let stale_summary = mock_chain
.build_transaction(multisig_account.id())
.tx_script(expiration_script.into())
.tx_script_args(expiration_script.tx_script_args())
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
))
.add_signature(public_keys[0].to_commitment(), msg, sig_1)
.add_signature(public_keys[1].to_commitment(), msg, sig_2)
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
assert_ne!(stale_summary.block_commitment(), original_block_commitment);
assert_ne!(stale_summary.to_commitment(), msg);
Ok(())
}
#[tokio::test]
async fn test_multisig_binds_block_older_than_reference_block() -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, AuthScheme::Falcon512Poseidon2)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 20, vec![])?;
let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])?.build()?;
let salt = Word::from([Felt::new_unchecked(7); 4]);
let signed_block = mock_chain.latest_block_header().block_num();
let auth_args = MultisigAuthArgs::new(signed_block, salt);
let tx_summary = mock_chain
.build_transaction(multisig_account.id())
.multisig_auth_args(auth_args)
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
assert_eq!(tx_summary.block_number(), signed_block);
let msg = tx_summary.to_commitment();
let signing_inputs = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &signing_inputs)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &signing_inputs)
.await?;
mock_chain.prove_until_block(signed_block + 5)?;
let executed_transaction = mock_chain
.build_transaction(multisig_account.id())
.multisig_auth_args(auth_args)
.add_signature(public_keys[0].to_commitment(), msg, sig_1)
.add_signature(public_keys[1].to_commitment(), msg, sig_2)
.build()?
.execute()
.await?;
assert_eq!(executed_transaction.block_header().block_num(), signed_block + 5);
Ok(())
}
#[tokio::test]
async fn test_multisig_tx_summary_is_stable_while_the_chain_advances() -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, AuthScheme::Falcon512Poseidon2)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 20, vec![])?;
let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])?.build()?;
let salt = Word::from([Felt::from(17u32); 4]);
let expiration_delta = NonZeroU16::new(10).unwrap();
let expiration_script = ExpirationTransactionScript::new(expiration_delta);
let signed_block = mock_chain.latest_block_header().block_num();
let auth_args = MultisigAuthArgs::new(signed_block, salt);
let tx_summary = mock_chain
.build_transaction(multisig_account.id())
.tx_script(expiration_script.into())
.tx_script_args(expiration_script.tx_script_args())
.multisig_auth_args(auth_args)
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let tx_summary_commitment = tx_summary.to_commitment();
let signing_inputs = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &signing_inputs)
.await?;
mock_chain.prove_until_block(signed_block + 2)?;
let later_summary = mock_chain
.build_transaction(multisig_account.id())
.tx_script(expiration_script.into())
.tx_script_args(expiration_script.tx_script_args())
.multisig_auth_args(auth_args)
.add_signature(public_keys[0].to_commitment(), tx_summary_commitment, sig_1)
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
assert_eq!(later_summary.to_commitment(), tx_summary_commitment);
Ok(())
}
#[rstest]
#[case::first_block_of_the_window(1, None)]
#[case::last_block_the_tx_can_be_built_from(2, None)]
#[case::approval_expired(3, Some(ERR_MULTISIG_APPROVAL_EXPIRED))]
#[tokio::test]
async fn test_multisig_approval_expires_relative_to_bound_block(
#[case] blocks_advanced: u32,
#[case] expected_error: Option<MasmError>,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, AuthScheme::Falcon512Poseidon2)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 20, vec![])?;
let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])?.build()?;
let salt = Word::from([Felt::from(11u32); 4]);
let approval_expiration_delta = NonZeroU32::new(3).unwrap();
let signed_block = mock_chain.latest_block_header().block_num();
let auth_args = MultisigAuthArgs::new(signed_block, salt)
.with_approval_expiration_delta(approval_expiration_delta)?;
let expiration_block = signed_block + approval_expiration_delta.get();
let tx_summary = mock_chain
.build_transaction(multisig_account.id())
.multisig_auth_args(auth_args)
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
assert_eq!(tx_summary.expiration_delta(), 0);
assert_eq!(tx_summary.user_params().as_elements()[0], Felt::from(expiration_block));
let tx_summary_commitment = tx_summary.to_commitment();
let signing_inputs = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &signing_inputs)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &signing_inputs)
.await?;
mock_chain.prove_until_block(signed_block + blocks_advanced)?;
let result = mock_chain
.build_transaction(multisig_account.id())
.multisig_auth_args(auth_args)
.add_signature(public_keys[0].to_commitment(), tx_summary_commitment, sig_1)
.add_signature(public_keys[1].to_commitment(), tx_summary_commitment, sig_2)
.build()?
.execute()
.await;
match expected_error {
None => assert_eq!(result?.expiration_block_num(), expiration_block),
Some(expected_error) => assert_transaction_executor_error!(result, expected_error),
}
Ok(())
}
#[tokio::test]
async fn test_multisig_freshness_delta_is_relative_to_the_reference_block() -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, AuthScheme::Falcon512Poseidon2)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 20, vec![])?;
let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])?.build()?;
let salt = Word::from([Felt::from(23u32); 4]);
let freshness_delta = NonZeroU16::new(3).unwrap();
let expiration_script = ExpirationTransactionScript::new(freshness_delta);
let signed_block = mock_chain.latest_block_header().block_num();
let approval_expiration_delta = NonZeroU32::new(50).unwrap();
let auth_args = MultisigAuthArgs::new(signed_block, salt)
.with_approval_expiration_delta(approval_expiration_delta)?;
let tx_summary = mock_chain
.build_transaction(multisig_account.id())
.tx_script(expiration_script.into())
.tx_script_args(expiration_script.tx_script_args())
.multisig_auth_args(auth_args)
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
assert_eq!(tx_summary.expiration_delta(), freshness_delta.get());
assert_eq!(
tx_summary.user_params().as_elements()[0],
Felt::from(signed_block.as_u32() + approval_expiration_delta.get())
);
let tx_summary_commitment = tx_summary.to_commitment();
let signing_inputs = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &signing_inputs)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &signing_inputs)
.await?;
let blocks_advanced = 10;
mock_chain.prove_until_block(signed_block + blocks_advanced)?;
let executed_transaction = mock_chain
.build_transaction(multisig_account.id())
.tx_script(expiration_script.into())
.tx_script_args(expiration_script.tx_script_args())
.multisig_auth_args(auth_args)
.add_signature(public_keys[0].to_commitment(), tx_summary_commitment, sig_1)
.add_signature(public_keys[1].to_commitment(), tx_summary_commitment, sig_2)
.build()?
.execute()
.await?;
assert_eq!(
executed_transaction.expiration_block_num(),
signed_block + blocks_advanced + u32::from(freshness_delta.get())
);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_update_signers(#[case] auth_scheme: AuthScheme) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let mut mock_chain_builder =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let output_note_asset = FungibleAsset::mock(0);
let output_note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(),
&[output_note_asset],
NoteType::Public,
)?;
let mut mock_chain = mock_chain_builder.clone().build().unwrap();
let salt = Word::from([Felt::new_unchecked(3); 4]);
let (_new_secret_keys, _new_auth_schemes, new_public_keys, _new_authenticators) =
setup_keys_and_authenticators_with_scheme(4, 4, auth_scheme)?;
let threshold = 3u64;
let num_of_approvers = 4u64;
let config_and_pubkeys_vector = build_update_signers_config_vector(
threshold,
num_of_approvers,
&new_public_keys,
auth_scheme,
);
let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector);
let tx_script_code = "
@transaction_script
pub proc main
call.::miden::standards::components::auth::multisig::update_signers_and_threshold
end
";
let tx_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(tx_script_code)?;
let advice_inputs =
AdviceInputs::default().with_map([(multisig_config_hash, config_and_pubkeys_vector)]);
let tx_script_args: Word = multisig_config_hash;
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.tx_script(tx_script)
.tx_script_args(tx_script_args)
.extend_advice_inputs(advice_inputs)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg = tx_summary.as_ref().to_commitment();
let tx_summary = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &tx_summary)
.await?;
let update_approvers_tx = mock_tx_builder
.add_signature(public_keys[0].to_commitment(), msg, sig_1)
.add_signature(public_keys[1].to_commitment(), msg, sig_2)
.build()?
.execute()
.await?;
assert_eq!(
update_approvers_tx.account_patch().final_nonce(),
Some(multisig_account.nonce() + Felt::ONE)
);
mock_chain.add_pending_executed_transaction(&update_approvers_tx)?;
mock_chain.prove_next_block()?;
let mut updated_multisig_account = multisig_account.clone();
updated_multisig_account.apply_patch(update_approvers_tx.account_patch())?;
for (i, expected_key) in new_public_keys.iter().enumerate() {
let storage_key = StorageMapKey::from_index(i as u32);
let storage_item = updated_multisig_account
.storage()
.get_map_item(AuthMultisig::approver_public_keys_slot(), storage_key)
.unwrap();
let expected_word: Word = expected_key.to_commitment().into();
assert_eq!(storage_item, expected_word, "Public key {} doesn't match expected value", i);
}
let threshold_config_storage = updated_multisig_account
.storage()
.get_item(AuthMultisig::threshold_config_slot())
.unwrap();
assert_eq!(
threshold_config_storage[0],
Felt::new_unchecked(threshold),
"Threshold was not updated correctly"
);
assert_eq!(
threshold_config_storage[1],
Felt::new_unchecked(num_of_approvers),
"Num approvers was not updated correctly"
);
let extracted_pub_keys = get_public_keys_from_account(&updated_multisig_account);
assert_eq!(
extracted_pub_keys.len(),
4,
"get_public_keys_from_account should return 4 public keys after update"
);
for (i, expected_key) in new_public_keys.iter().enumerate() {
let expected_word: Word = expected_key.to_commitment().into();
let found_key = extracted_pub_keys.iter().find(|&key| *key == expected_word);
assert!(
found_key.is_some(),
"Public key {} not found in extracted keys: expected {:?}, got {:?}",
i,
expected_word,
extracted_pub_keys
);
}
let mut new_authenticators = Vec::new();
for secret_key in _new_secret_keys.iter().take(3) {
let authenticator = BasicAuthenticator::new(core::slice::from_ref(secret_key));
new_authenticators.push(authenticator);
}
let output_note_new: Note = P2idNote::builder()
.sender(updated_multisig_account.id())
.target(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap())
.asset(output_note_asset)
.note_type(NoteType::Public)
.generate_serial_number(&mut RandomCoin::new(Word::empty()))
.build()?
.into();
let input_note_new = create_spawn_note([&output_note_new])?;
let salt_new = Word::from([Felt::new_unchecked(4); 4]);
let mut new_mock_chain_builder =
MockChainBuilder::with_accounts([updated_multisig_account.clone()]).unwrap();
new_mock_chain_builder.add_output_note(RawOutputNote::Full(input_note_new.clone()));
let new_mock_chain = new_mock_chain_builder.build().unwrap();
let mock_tx_builder_new = new_mock_chain
.build_transaction(updated_multisig_account.id())
.authenticated_input_note(input_note_new.id())
.multisig_auth_args(MultisigAuthArgs::new(
new_mock_chain.latest_block_header().block_num(),
salt_new,
));
let tx_summary_new = mock_tx_builder_new
.clone()
.expected_output_note(RawOutputNote::Full(output_note.clone()))
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg_new = tx_summary_new.as_ref().to_commitment();
let tx_summary_new = SigningInputs::TransactionSummary(tx_summary_new);
let sig_1_new = new_authenticators[0]
.get_signature(new_public_keys[0].to_commitment(), &tx_summary_new)
.await?;
let sig_2_new = new_authenticators[1]
.get_signature(new_public_keys[1].to_commitment(), &tx_summary_new)
.await?;
let sig_3_new = new_authenticators[2]
.get_signature(new_public_keys[2].to_commitment(), &tx_summary_new)
.await?;
let mock_tx_execute_new = mock_tx_builder_new
.expected_output_note(RawOutputNote::Full(output_note_new))
.add_signature(new_public_keys[0].to_commitment(), msg_new, sig_1_new)
.add_signature(new_public_keys[1].to_commitment(), msg_new, sig_2_new)
.add_signature(new_public_keys[2].to_commitment(), msg_new, sig_3_new)
.build()?
.execute()
.await?;
assert_eq!(
mock_tx_execute_new.account_patch().final_nonce(),
Some(updated_multisig_account.nonce() + Felt::ONE)
);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_update_signers_remove_owner(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(5, 5, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(4, &approvers, 10, vec![])?;
let mock_chain_builder = MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let mut mock_chain = mock_chain_builder.build().unwrap();
let new_public_keys = &public_keys[0..2];
let threshold = 1u64;
let num_of_approvers = 2u64;
let config_and_pubkeys_vector = build_update_signers_config_vector(
threshold,
num_of_approvers,
new_public_keys,
auth_scheme,
);
let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector);
let tx_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script("@transaction_script\npub proc main\n call.::miden::standards::components::auth::multisig::update_signers_and_threshold\nend")?;
let advice_inputs =
AdviceInputs::default().with_map([(multisig_config_hash, config_and_pubkeys_vector)]);
let salt = Word::from([Felt::new_unchecked(3); 4]);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.tx_script(tx_script)
.tx_script_args(multisig_config_hash)
.extend_advice_inputs(advice_inputs)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg = tx_summary.as_ref().to_commitment();
let tx_summary = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &tx_summary)
.await?;
let sig_3 = authenticators[2]
.get_signature(public_keys[2].to_commitment(), &tx_summary)
.await?;
let sig_4 = authenticators[3]
.get_signature(public_keys[3].to_commitment(), &tx_summary)
.await?;
let update_approvers_tx = mock_tx_builder
.add_signature(public_keys[0].to_commitment(), msg, sig_1)
.add_signature(public_keys[1].to_commitment(), msg, sig_2)
.add_signature(public_keys[2].to_commitment(), msg, sig_3)
.add_signature(public_keys[3].to_commitment(), msg, sig_4)
.build()?
.execute()
.await?;
assert_eq!(
update_approvers_tx.account_patch().final_nonce(),
Some(multisig_account.nonce() + Felt::ONE)
);
mock_chain.add_pending_executed_transaction(&update_approvers_tx)?;
mock_chain.prove_next_block()?;
let mut updated_multisig_account = multisig_account.clone();
updated_multisig_account.apply_patch(update_approvers_tx.account_patch())?;
for (i, expected_key) in new_public_keys.iter().enumerate() {
let storage_key = StorageMapKey::from_index(i as u32);
let storage_item = updated_multisig_account
.storage()
.get_map_item(AuthMultisig::approver_public_keys_slot(), storage_key)
.unwrap();
let expected_word: Word = expected_key.to_commitment().into();
assert_eq!(storage_item, expected_word, "Public key {} doesn't match", i);
}
let threshold_config = updated_multisig_account
.storage()
.get_item(AuthMultisig::threshold_config_slot())
.unwrap();
assert_eq!(threshold_config[0], Felt::new_unchecked(threshold), "Threshold not updated");
assert_eq!(
threshold_config[1],
Felt::new_unchecked(num_of_approvers),
"Num approvers not updated"
);
let extracted_pub_keys = get_public_keys_from_account(&updated_multisig_account);
assert_eq!(extracted_pub_keys.len(), 2, "Should have 2 public keys after update");
for expected_key in new_public_keys.iter() {
let expected_word: Word = expected_key.to_commitment().into();
assert!(
extracted_pub_keys.contains(&expected_word),
"Public key not found in extracted keys"
);
}
for removed_idx in 2..5 {
let removed_owner_key = StorageMapKey::from_index(removed_idx as u32);
let removed_owner_slot = updated_multisig_account
.storage()
.get_map_item(AuthMultisig::approver_public_keys_slot(), removed_owner_key)
.unwrap();
assert_eq!(
removed_owner_slot,
Word::empty(),
"Removed owner's slot at index {} should be empty",
removed_idx
);
}
let mut non_empty_count = 0;
for i in 0..5 {
let storage_key = StorageMapKey::from_index(i as u32);
let storage_item = updated_multisig_account
.storage()
.get_map_item(AuthMultisig::approver_public_keys_slot(), storage_key)
.unwrap();
if storage_item != Word::empty() {
non_empty_count += 1;
assert!(i < 2, "Found non-empty key at index {} which should be removed", i);
let expected_word: Word = new_public_keys.get(i).unwrap().to_commitment().into();
assert_eq!(storage_item, expected_word, "Key at index {} doesn't match", i);
}
}
assert_eq!(
non_empty_count, 2,
"Should have exactly 2 non-empty keys after removing 3 owners"
);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_update_signers_rejects_unreachable_proc_thresholds(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, _authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(
2,
&approvers,
10,
vec![
(BasicWallet::receive_asset_root(), 3),
(AuthMultisig::set_procedure_threshold_root(), 3),
],
)?;
let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let new_public_keys = &public_keys[0..2];
let threshold = 2u64;
let num_of_approvers = 2u64;
let config_and_pubkeys_vector = build_update_signers_config_vector(
threshold,
num_of_approvers,
new_public_keys,
auth_scheme,
);
let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector);
let tx_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script("@transaction_script\npub proc main\n call.::miden::standards::components::auth::multisig::update_signers_and_threshold\nend")?;
let advice_inputs =
AdviceInputs::default().with_map([(multisig_config_hash, config_and_pubkeys_vector)]);
let salt = Word::from([Felt::new_unchecked(8); 4]);
let result = mock_chain
.build_transaction(multisig_account.id())
.tx_script(tx_script)
.tx_script_args(multisig_config_hash)
.extend_advice_inputs(advice_inputs)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
))
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS);
Ok(())
}
#[tokio::test]
async fn test_multisig_update_signers_rejects_duplicate_public_keys() -> anyhow::Result<()> {
let auth_scheme = AuthScheme::EcdsaK256Keccak;
let (_secret_keys, auth_schemes, public_keys, _authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let duplicate_public_keys =
vec![public_keys[0].clone(), public_keys[0].clone(), public_keys[1].clone()];
let threshold = 2u64;
let num_of_approvers = 3u64;
let config_and_pubkeys_vector = build_update_signers_config_vector(
threshold,
num_of_approvers,
&duplicate_public_keys,
auth_scheme,
);
let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector);
let tx_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(
"
@transaction_script
pub proc main
call.::miden::standards::components::auth::multisig::update_signers_and_threshold
end
",
)?;
let advice_inputs =
AdviceInputs::default().with_map([(multisig_config_hash, config_and_pubkeys_vector)]);
let salt = Word::from([Felt::new_unchecked(9); 4]);
let result = mock_chain
.build_transaction(multisig_account.id())
.tx_script(tx_script)
.tx_script_args(multisig_config_hash)
.extend_advice_inputs(advice_inputs)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
))
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_DUPLICATE_APPROVER_PUBLIC_KEY);
Ok(())
}
#[tokio::test]
async fn test_multisig_update_signers_rejects_too_many_approvers() -> anyhow::Result<()> {
let auth_scheme = AuthScheme::EcdsaK256Keccak;
let num_of_approvers = u64::from(ApproverSet::MAX_APPROVERS) + 1;
let (_secret_keys, auth_schemes, public_keys, _authenticators) =
setup_keys_and_authenticators_with_scheme(num_of_approvers as usize, 1, auth_scheme)?;
let approvers = public_keys
.iter()
.take(2)
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let config_and_pubkeys_vector =
build_update_signers_config_vector(2, num_of_approvers, &public_keys, auth_scheme);
let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector);
let tx_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(
"
@transaction_script
pub proc main
call.::miden::standards::components::auth::multisig::update_signers_and_threshold
end
",
)?;
let advice_inputs =
AdviceInputs::default().with_map([(multisig_config_hash, config_and_pubkeys_vector)]);
let salt = Word::from([Felt::new_unchecked(9); 4]);
let result = mock_chain
.build_transaction(multisig_account.id())
.tx_script(tx_script)
.tx_script_args(multisig_config_hash)
.extend_advice_inputs(advice_inputs)
.auth_args(salt)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_TOO_MANY_APPROVERS);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_new_approvers_cannot_sign_before_update(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, _authenticators) =
setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let salt = Word::from([Felt::new_unchecked(5); 4]);
let (_new_secret_keys, _new_auth_schemes, new_public_keys, new_authenticators) =
setup_keys_and_authenticators_with_scheme(4, 4, auth_scheme)?;
let threshold = 3u64;
let num_of_approvers = 4u64;
let config_and_pubkeys_vector = build_update_signers_config_vector(
threshold,
num_of_approvers,
&new_public_keys,
auth_scheme,
);
let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector);
let tx_script_code = "
@transaction_script
pub proc main
call.::miden::standards::components::auth::multisig::update_signers_and_threshold
end
";
let tx_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(tx_script_code)?;
let advice_inputs =
AdviceInputs::default().with_map([(multisig_config_hash, config_and_pubkeys_vector)]);
let tx_script_args: Word = multisig_config_hash;
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.tx_script(tx_script)
.tx_script_args(tx_script_args)
.extend_advice_inputs(advice_inputs)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg = tx_summary.as_ref().to_commitment();
let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary.clone());
let new_sig_1 = new_authenticators[0]
.get_signature(new_public_keys[0].to_commitment(), &tx_summary_signing)
.await?;
let new_sig_2 = new_authenticators[1]
.get_signature(new_public_keys[1].to_commitment(), &tx_summary_signing)
.await?;
let mock_tx_with_new_sigs = mock_tx_builder
.add_signature(new_public_keys[0].to_commitment(), msg, new_sig_1)
.add_signature(new_public_keys[1].to_commitment(), msg, new_sig_2)
.build()?;
let result = mock_tx_with_new_sigs.execute().await;
assert!(
result.is_err(),
"Transaction should fail when signed by unauthorized new approvers"
);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_proc_threshold_overrides(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?;
let proc_threshold_map = vec![(BasicWallet::receive_asset_root(), 1)];
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_starting_balance = 10u64;
let mut multisig_account =
create_multisig_account(2, &approvers, multisig_starting_balance, proc_threshold_map)?;
let mut mock_chain_builder =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
multisig_account.id(),
&[FungibleAsset::mock(1)],
NoteType::Public,
)?;
let mut mock_chain = mock_chain_builder.build()?;
let salt = Word::from([Felt::ONE; 4]);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.authenticated_input_note(note.id())
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg = tx_summary.as_ref().to_commitment();
let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary.clone());
let sig = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary_signing)
.await?;
let tx_result = mock_tx_builder
.add_signature(public_keys[0].to_commitment(), msg, sig)
.build()?
.execute()
.await;
assert!(tx_result.is_ok(), "Note consumption with 1 signature should succeed");
multisig_account.apply_patch(tx_result.as_ref().unwrap().account_patch())?;
mock_chain.add_pending_executed_transaction(&tx_result.unwrap())?;
mock_chain.prove_next_block()?;
let salt2 = Word::from([Felt::new_unchecked(2); 4]);
let asset = FungibleAsset::mock(5);
let output_note: Note = P2idNote::builder()
.sender(multisig_account.id())
.target(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap())
.asset(asset)
.note_type(NoteType::Public)
.generate_serial_number(&mut RandomCoin::new(Word::from([Felt::new_unchecked(42); 4])))
.build()?
.into();
let send_note_transaction_script = SendNotesTransactionScript::new(
&multisig_account.code_interface(),
&[output_note.clone().into()],
)?;
let mock_tx_builder2 = mock_chain
.build_transaction(multisig_account.id())
.expected_output_note(RawOutputNote::Full(output_note))
.send_notes_script(&send_note_transaction_script)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt2,
));
let tx_summary2 = mock_tx_builder2
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg2 = tx_summary2.as_ref().to_commitment();
let tx_summary2_signing = SigningInputs::TransactionSummary(tx_summary2.clone());
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary2_signing)
.await?;
let result = mock_tx_builder2
.clone()
.add_signature(public_keys[0].to_commitment(), msg2, sig_1)
.build()?
.execute()
.await;
result.unwrap_err().unwrap_unauthorized_err();
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary2_signing)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &tx_summary2_signing)
.await?;
let result = mock_tx_builder2
.add_signature(public_keys[0].to_commitment(), msg2, sig_1)
.add_signature(public_keys[1].to_commitment(), msg2, sig_2)
.build()?
.execute()
.await;
assert!(result.is_ok(), "Transaction should succeed with 2 signatures for note sending");
multisig_account.apply_patch(result.as_ref().unwrap().account_patch())?;
mock_chain.add_pending_executed_transaction(&result.unwrap())?;
mock_chain.prove_next_block()?;
assert_eq!(multisig_account.vault().get_balance(asset.id())?.as_u64(), 6);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_set_procedure_threshold(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let mut multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let mut mock_chain_builder =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
let one_sig_note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
multisig_account.id(),
&[FungibleAsset::mock(1)],
NoteType::Public,
)?;
let clear_check_note = mock_chain_builder.add_p2id_note(
multisig_account.id(),
multisig_account.id(),
&[FungibleAsset::mock(1)],
NoteType::Public,
)?;
let mut mock_chain = mock_chain_builder.build().unwrap();
let proc_root = BasicWallet::receive_asset_root().as_word();
let set_script_code = format!(
r#"
@transaction_script
pub proc main
push.{proc_root}
push.1
call.::miden::standards::components::auth::multisig::set_procedure_threshold
dropw
drop
end
"#
);
let set_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(set_script_code)?;
let set_salt = Word::from([Felt::new_unchecked(50); 4]);
let set_builder = mock_chain
.build_transaction(multisig_account.id())
.tx_script(set_script)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
set_salt,
));
let set_summary = set_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let set_msg = set_summary.as_ref().to_commitment();
let set_summary = SigningInputs::TransactionSummary(set_summary);
let set_sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &set_summary)
.await?;
let set_sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &set_summary)
.await?;
let set_tx = set_builder
.add_signature(public_keys[0].to_commitment(), set_msg, set_sig_1)
.add_signature(public_keys[1].to_commitment(), set_msg, set_sig_2)
.build()?
.execute()
.await?;
multisig_account.apply_patch(set_tx.account_patch())?;
mock_chain.add_pending_executed_transaction(&set_tx)?;
mock_chain.prove_next_block()?;
let one_sig_salt = Word::from([Felt::new_unchecked(51); 4]);
let one_sig_builder = mock_chain
.build_transaction(multisig_account.id())
.authenticated_input_note(one_sig_note.id())
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
one_sig_salt,
));
let one_sig_summary = one_sig_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let one_sig_msg = one_sig_summary.as_ref().to_commitment();
let one_sig_summary = SigningInputs::TransactionSummary(one_sig_summary);
let one_sig = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &one_sig_summary)
.await?;
let one_sig_tx = one_sig_builder
.add_signature(public_keys[0].to_commitment(), one_sig_msg, one_sig)
.build()?
.execute()
.await
.expect("override=1 should allow receive_asset with one signature");
multisig_account.apply_patch(one_sig_tx.account_patch())?;
mock_chain.add_pending_executed_transaction(&one_sig_tx)?;
mock_chain.prove_next_block()?;
let clear_script_code = format!(
r#"
@transaction_script
pub proc main
push.{proc_root}
push.0
call.::miden::standards::components::auth::multisig::set_procedure_threshold
dropw
drop
end
"#
);
let clear_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(clear_script_code)?;
let clear_salt = Word::from([Felt::new_unchecked(52); 4]);
let clear_builder = mock_chain
.build_transaction(multisig_account.id())
.tx_script(clear_script)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
clear_salt,
));
let clear_summary = clear_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let clear_msg = clear_summary.as_ref().to_commitment();
let clear_summary = SigningInputs::TransactionSummary(clear_summary);
let clear_sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &clear_summary)
.await?;
let clear_sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &clear_summary)
.await?;
let clear_tx = clear_builder
.add_signature(public_keys[0].to_commitment(), clear_msg, clear_sig_1)
.add_signature(public_keys[1].to_commitment(), clear_msg, clear_sig_2)
.build()?
.execute()
.await?;
multisig_account.apply_patch(clear_tx.account_patch())?;
mock_chain.add_pending_executed_transaction(&clear_tx)?;
mock_chain.prove_next_block()?;
let clear_check_salt = Word::from([Felt::new_unchecked(53); 4]);
let clear_check_builder = mock_chain
.build_transaction(multisig_account.id())
.authenticated_input_note(clear_check_note.id())
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
clear_check_salt,
));
let clear_check_summary = clear_check_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let clear_check_msg = clear_check_summary.as_ref().to_commitment();
let clear_check_summary = SigningInputs::TransactionSummary(clear_check_summary);
let clear_check_sig = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &clear_check_summary)
.await?;
let clear_check_result = clear_check_builder
.add_signature(public_keys[0].to_commitment(), clear_check_msg, clear_check_sig)
.build()?
.execute()
.await;
assert!(
matches!(clear_check_result, Err(TransactionExecutorError::Unauthorized(_))),
"override cleared via threshold=0 should restore default threshold requirements"
);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_set_procedure_threshold_rejects_exceeding_approvers(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, _authenticators) =
setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let proc_root = BasicWallet::receive_asset_root().as_word();
let script_code = format!(
r#"
@transaction_script
pub proc main
push.{proc_root}
push.3
call.::miden::standards::components::auth::multisig::set_procedure_threshold
end
"#
);
let script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(script_code)?;
let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let salt = Word::from([Felt::new_unchecked(54); 4]);
let mock_tx_init = mock_chain
.build_transaction(multisig_account.id())
.tx_script(script.clone())
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
))
.build()?;
let result = mock_tx_init.execute().await;
assert_transaction_executor_error!(result, ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS);
Ok(())
}
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_set_procedure_threshold_uses_current_num_approvers(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
let (_secret_keys, auth_schemes, public_keys, _authenticators) =
setup_keys_and_authenticators_with_scheme(3, 2, auth_scheme)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let proc_root = BasicWallet::receive_asset_root().as_word();
let (_new_sec, _new_schemes, new_public_keys, _new_auth) =
setup_keys_and_authenticators_with_scheme(1, 1, auth_scheme)?;
let new_threshold = 1u64;
let new_num_of_approvers = 1u64;
let config_and_pubkeys_vector = build_update_signers_config_vector(
new_threshold,
new_num_of_approvers,
&new_public_keys,
auth_scheme,
);
let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector);
let advice_inputs =
AdviceInputs::default().with_map([(multisig_config_hash, config_and_pubkeys_vector)]);
let script_code = format!(
r#"
@transaction_script
pub proc main
call.::miden::standards::components::auth::multisig::update_signers_and_threshold
push.{proc_root}
push.2
call.::miden::standards::components::auth::multisig::set_procedure_threshold
dropw
drop
end
"#
);
let script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(script_code)?;
let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let salt = Word::from([Felt::from_u8(55); 4]);
let mock_tx = mock_chain
.build_transaction(multisig_account.id())
.tx_script(script)
.tx_script_args(multisig_config_hash)
.extend_advice_inputs(advice_inputs)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
))
.build()?;
let result = mock_tx.execute().await;
assert_transaction_executor_error!(result, ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS);
Ok(())
}
async fn execute_multisig_getter_call<F>(build_script: F) -> anyhow::Result<()>
where
F: FnOnce(&[PublicKey]) -> String,
{
let (_secret_keys, auth_schemes, public_keys, authenticators) =
setup_keys_and_authenticators_with_scheme(4, 2, AuthScheme::EcdsaK256Keccak)?;
let approvers = public_keys
.iter()
.zip(auth_schemes.iter())
.map(|(pk, scheme)| (pk.clone(), *scheme))
.collect::<Vec<_>>();
let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?;
let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()])
.unwrap()
.build()
.unwrap();
let script_code = build_script(&public_keys);
let tx_script = CodeBuilder::default()
.with_dynamically_linked_package(AuthMultisig::code())?
.compile_tx_script(&script_code)?;
let salt = Word::from([Felt::from_u8(77); 4]);
let mock_tx_builder = mock_chain
.build_transaction(multisig_account.id())
.tx_script(tx_script)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
));
let tx_summary = mock_tx_builder
.clone()
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let msg = tx_summary.as_ref().to_commitment();
let tx_summary = SigningInputs::TransactionSummary(tx_summary);
let sig_1 = authenticators[0]
.get_signature(public_keys[0].to_commitment(), &tx_summary)
.await?;
let sig_2 = authenticators[1]
.get_signature(public_keys[1].to_commitment(), &tx_summary)
.await?;
mock_tx_builder
.add_signature(public_keys[0].to_commitment(), msg, sig_1)
.add_signature(public_keys[1].to_commitment(), msg, sig_2)
.build()?
.execute()
.await?;
Ok(())
}
#[tokio::test]
async fn test_get_threshold_and_num_approvers_call_abi() -> anyhow::Result<()> {
execute_multisig_getter_call(|_| {
"
@transaction_script
pub proc main
call.::miden::standards::components::auth::multisig::get_threshold_and_num_approvers
# => [default_threshold, num_approvers, pad(14)]
push.2 eq assert
# => [num_approvers, pad(14)]
push.4 eq assert
# => [pad(14)]
exec.::miden::core::sys::truncate_stack
end
"
.to_string()
})
.await
}
#[tokio::test]
async fn test_get_signer_at_call_abi() -> anyhow::Result<()> {
let expected_scheme_id = AuthScheme::EcdsaK256Keccak.as_u8();
execute_multisig_getter_call(move |_| {
format!(
r#"
@transaction_script
pub proc main
# query the signer at index 0
push.0
call.::miden::standards::components::auth::multisig::get_signer_at
# => [PUB_KEY, scheme_id, pad(11)]
movup.4 eq.{expected_scheme_id} assert.err="expected scheme ID {expected_scheme_id}"
# => [PUB_KEY, pad(11)]
dropw
# => [pad(12)]
exec.::miden::core::sys::truncate_stack
end
"#
)
})
.await
}
#[tokio::test]
async fn test_is_signer_true_call_abi() -> anyhow::Result<()> {
execute_multisig_getter_call(|public_keys| {
let pub_key = public_keys[0].to_commitment();
format!(
r#"
@transaction_script
pub proc main
push.{pub_key}
call.::miden::standards::components::auth::multisig::is_signer
# => [is_signer, pad(15)]
assert
# => [pad(15)]
exec.::miden::core::sys::truncate_stack
end
"#
)
})
.await
}
#[tokio::test]
async fn test_is_signer_false_call_abi() -> anyhow::Result<()> {
execute_multisig_getter_call(|_| {
let non_signer = Word::from([Felt::from_u8(0xff); 4]);
format!(
r#"
@transaction_script
pub proc main
push.{non_signer}
call.::miden::standards::components::auth::multisig::is_signer
# => [is_signer, pad(15)]
assertz
# => [pad(15)]
exec.::miden::core::sys::truncate_stack
end
"#
)
})
.await
}