use std::sync::Arc;
use guardian_client::GetDeltaProposalsResponse;
use guardian_client::testing::mocks::{MockGuardianService, start_mock_server};
use miden_protocol::Word;
use miden_protocol::note::NoteType;
use miden_protocol::transaction::RawOutputNote;
use super::test_support::{
chain_with_notes, multisig_account, offline_client_parts_with_keystore,
offline_client_with_node_parts, p2id_note_for, pending_proto_delta, registered_state,
};
use crate::account::MultisigAccount;
use crate::execution::build_final_transaction_request;
use crate::keystore::{GuardianKeyStore, KeyManager};
use crate::payload::ProposalPayload;
use crate::proposal::{SerializedNote, TransactionType};
use crate::transaction::{chain_anchor_to_base64, execute_for_summary, word_to_hex};
#[tokio::test]
async fn cosigner_at_a_later_sync_height_verifies_a_pending_proposal_at_its_tip() {
let keystore = Arc::new(GuardianKeyStore::generate());
let signer_commitment = keystore.commitment();
let guardian_commitment = Word::from([9u32, 9, 9, 9]);
let account = multisig_account(signer_commitment, guardian_commitment, 47);
let note = p2id_note_for(&account, 7, NoteType::Private);
let api = chain_with_notes(vec![RawOutputNote::Full(note.clone())]);
let dir1 = tempfile::tempdir().unwrap();
let (mut proposer, _store1) =
offline_client_parts_with_keystore(dir1.path(), api.clone(), None, keystore.clone()).await;
proposer.set_node_rpc_client(api.clone());
proposer
.add_or_update_account(&account, true)
.await
.unwrap();
proposer.account = Some(MultisigAccount::new(account.clone()));
proposer.miden_client.sync_state().await.unwrap();
let node_rpc = proposer.node_rpc_client();
crate::transaction::ensure_notes_authenticated(
&mut proposer.miden_client,
&node_rpc,
std::slice::from_ref(¬e),
)
.await
.unwrap();
let salt = Word::from([5u32, 6, 7, 8]);
let tx_type =
TransactionType::consume_notes_v2(vec![note.id()], vec![SerializedNote::from_note(¬e)]);
let auth_args = proposer.multisig_auth_args(salt, None, None).await.unwrap();
let tx_request = build_final_transaction_request(
&proposer.miden_client,
&tx_type,
&account,
&auth_args,
Vec::new(),
None,
Some(&[]),
proposer.key_manager.scheme(),
)
.await
.unwrap();
let (tx_summary, chain_anchor) =
execute_for_summary(&mut proposer.miden_client, account.id(), tx_request)
.await
.unwrap();
let proposal_id = word_to_hex(&tx_summary.to_commitment());
let anchor_height = chain_anchor.block_num();
let delta_payload = ProposalPayload::new(&tx_summary)
.with_note_consumption_metadata_v2(
vec![note.id().to_hex()],
vec![SerializedNote::from_note(¬e).into_inner()],
word_to_hex(&salt),
)
.with_required_signatures(1)
.with_chain_anchor(chain_anchor_to_base64(&chain_anchor))
.to_json()
.to_string();
let service = MockGuardianService::default();
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
handle.set_persistent_get_state(registered_state(&account));
handle.set_persistent_get_delta_proposals(GetDeltaProposalsResponse {
success: true,
message: String::new(),
proposals: vec![pending_proto_delta(
&account,
1,
delta_payload,
&word_to_hex(&signer_commitment),
)],
});
api.advance_blocks(5);
let dir2 = tempfile::tempdir().unwrap();
let (mut cosigner, _store2) = offline_client_with_node_parts(dir2.path(), api.clone()).await;
cosigner
.set_guardian_endpoint(&endpoint, false)
.await
.unwrap();
cosigner
.add_or_update_account(&account, true)
.await
.unwrap();
cosigner.account = Some(MultisigAccount::new(account.clone()));
cosigner.miden_client.sync_state().await.unwrap();
let cosigner_height = cosigner.miden_client.get_sync_height().await.unwrap();
assert!(
cosigner_height > anchor_height,
"the cosigner must sync past the anchor ({anchor_height}) to exercise the bug; \
synced at {cosigner_height}"
);
let proposals = cosigner
.list_proposals()
.await
.expect("the tip re-execution reproduces the signed summary at any later sync height");
assert_eq!(proposals.len(), 1, "proposals: {proposals:?}");
assert!(
proposals[0].id.eq_ignore_ascii_case(&proposal_id),
"listed {} but the proposer signed {proposal_id}",
proposals[0].id
);
assert!(
proposals[0].is_verified(),
"the listing must have reproduced the signed summary at the tip: {:?}",
proposals[0].verification
);
let auth_args = cosigner.multisig_auth_args(salt, None, None).await.unwrap();
let tip_request = build_final_transaction_request(
&cosigner.miden_client,
&tx_type,
&account,
&auth_args,
Vec::new(),
None,
Some(&[]),
cosigner.key_manager.scheme(),
)
.await
.unwrap();
let (tip_summary, tip_anchor) =
execute_for_summary(&mut cosigner.miden_client, account.id(), tip_request)
.await
.unwrap();
assert!(tip_anchor.block_num() > anchor_height);
assert_ne!(
word_to_hex(&tip_summary.to_commitment()),
proposal_id,
"a request bound to a later block must not reproduce the proposer's summary"
);
}
#[tokio::test]
async fn fresh_cosigner_verifies_a_consume_proposal_whose_proposer_held_the_notes_with_proofs() {
let keystore = Arc::new(GuardianKeyStore::generate());
let signer_commitment = keystore.commitment();
let guardian_commitment = Word::from([9u32, 9, 9, 9]);
let account = multisig_account(signer_commitment, guardian_commitment, 48);
let notes = vec![
p2id_note_for(&account, 11, NoteType::Public),
p2id_note_for(&account, 12, NoteType::Public),
];
let api = chain_with_notes(notes.iter().cloned().map(RawOutputNote::Full).collect());
let dir1 = tempfile::tempdir().unwrap();
let (mut proposer, _store1) =
offline_client_parts_with_keystore(dir1.path(), api.clone(), None, keystore.clone()).await;
proposer.set_node_rpc_client(api.clone());
proposer
.add_or_update_account(&account, true)
.await
.unwrap();
proposer.account = Some(MultisigAccount::new(account.clone()));
proposer.miden_client.sync_state().await.unwrap();
proposer
.miden_client
.import_notes(
¬es
.iter()
.map(|n| miden_client::note::NoteFile::NoteId(n.id()))
.collect::<Vec<_>>(),
)
.await
.unwrap();
for note in ¬es {
let record = proposer
.miden_client
.get_input_note(note.id())
.await
.unwrap()
.expect("the proposer tracks the public note");
assert!(
record.is_authenticated(),
"the proposer's note must carry its inclusion proof"
);
}
let salt = Word::from([5u32, 6, 7, 8]);
let tx_type = TransactionType::consume_notes_v2(
notes.iter().map(miden_protocol::note::Note::id).collect(),
notes.iter().map(SerializedNote::from_note).collect(),
);
let auth_args = proposer.multisig_auth_args(salt, None, None).await.unwrap();
let tx_request = build_final_transaction_request(
&proposer.miden_client,
&tx_type,
&account,
&auth_args,
Vec::new(),
None,
Some(&[]),
proposer.key_manager.scheme(),
)
.await
.unwrap();
let (tx_summary, chain_anchor) =
execute_for_summary(&mut proposer.miden_client, account.id(), tx_request)
.await
.unwrap();
let proposal_id = word_to_hex(&tx_summary.to_commitment());
let delta_payload = ProposalPayload::new(&tx_summary)
.with_note_consumption_metadata_v2(
notes.iter().map(|n| n.id().to_hex()).collect(),
notes
.iter()
.map(|n| SerializedNote::from_note(n).into_inner())
.collect(),
word_to_hex(&salt),
)
.with_required_signatures(1)
.with_chain_anchor(chain_anchor_to_base64(&chain_anchor))
.to_json()
.to_string();
let service = MockGuardianService::default();
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
handle.set_persistent_get_state(registered_state(&account));
handle.set_persistent_get_delta_proposals(GetDeltaProposalsResponse {
success: true,
message: String::new(),
proposals: vec![pending_proto_delta(
&account,
1,
delta_payload,
&word_to_hex(&signer_commitment),
)],
});
api.advance_blocks(3);
let dir2 = tempfile::tempdir().unwrap();
let (mut cosigner, _store2) = offline_client_with_node_parts(dir2.path(), api.clone()).await;
cosigner
.set_guardian_endpoint(&endpoint, false)
.await
.unwrap();
cosigner
.add_or_update_account(&account, true)
.await
.unwrap();
cosigner.account = Some(MultisigAccount::new(account.clone()));
cosigner.miden_client.sync_state().await.unwrap();
for note in ¬es {
let record = cosigner
.miden_client
.get_input_note(note.id())
.await
.unwrap();
assert!(
record.is_none_or(|r| !r.is_authenticated()),
"precondition: the cosigner must not already hold the note authenticated"
);
}
let proposals = cosigner
.list_proposals()
.await
.expect("the rebuild authenticates the notes first, so the summary reproduces");
assert_eq!(proposals.len(), 1, "proposals: {proposals:?}");
assert!(proposals[0].id.eq_ignore_ascii_case(&proposal_id));
assert!(
proposals[0].is_verified(),
"the listing must have reproduced the signed summary with authenticated notes: {:?}",
proposals[0].verification
);
for note in ¬es {
let record = cosigner
.miden_client
.get_input_note(note.id())
.await
.unwrap()
.expect("verification imported the note");
assert!(
record.is_authenticated(),
"verification must leave the note authenticated, ready for execution"
);
}
}
#[tokio::test]
async fn ensure_notes_authenticated_imports_committed_notes_and_refuses_uncommitted_ones() {
let keystore = Arc::new(GuardianKeyStore::generate());
let account = multisig_account(keystore.commitment(), Word::from([9u32, 9, 9, 9]), 49);
let committed = p2id_note_for(&account, 21, NoteType::Private);
let uncommitted = p2id_note_for(&account, 22, NoteType::Private);
let api = chain_with_notes(vec![RawOutputNote::Full(committed.clone())]);
let dir = tempfile::tempdir().unwrap();
let (mut client, _store) =
offline_client_parts_with_keystore(dir.path(), api.clone(), None, keystore).await;
client.set_node_rpc_client(api.clone());
client.add_or_update_account(&account, true).await.unwrap();
client.miden_client.sync_state().await.unwrap();
assert!(
client
.miden_client
.get_input_note(committed.id())
.await
.unwrap()
.is_none(),
"precondition: a private note is not discovered by syncing"
);
let node_rpc = client.node_rpc_client();
crate::transaction::ensure_notes_authenticated(
&mut client.miden_client,
&node_rpc,
std::slice::from_ref(&committed),
)
.await
.expect("a committed note is authenticated from the node's proof");
let record = client
.miden_client
.get_input_note(committed.id())
.await
.unwrap()
.expect("the note was imported");
assert!(
record.is_authenticated(),
"imported with its inclusion proof"
);
let calls_before = api.get_notes_by_id_call_count();
crate::transaction::ensure_notes_authenticated(
&mut client.miden_client,
&node_rpc,
std::slice::from_ref(&committed),
)
.await
.unwrap();
assert_eq!(api.get_notes_by_id_call_count(), calls_before);
let err = crate::transaction::ensure_notes_authenticated(
&mut client.miden_client,
&node_rpc,
std::slice::from_ref(&uncommitted),
)
.await
.expect_err("a note the chain does not know cannot be authenticated");
match err {
crate::error::MultisigError::ConsumeNoteNotAuthenticated { note_id, reason } => {
assert_eq!(note_id, uncommitted.id());
assert!(reason.contains("not committed"), "reason: {reason}");
}
other => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn listing_reports_an_unverifiable_proposal_instead_of_failing_the_whole_listing() {
use crate::transaction::build_update_signers_transaction_request;
let keystore = Arc::new(GuardianKeyStore::generate());
let signer_commitment = keystore.commitment();
let guardian_commitment = Word::from([9u32, 9, 9, 9]);
let account = multisig_account(signer_commitment, guardian_commitment, 50);
let api = chain_with_notes(Vec::new());
let dir = tempfile::tempdir().unwrap();
let (mut client, _store) =
offline_client_parts_with_keystore(dir.path(), api.clone(), None, keystore.clone()).await;
client.set_node_rpc_client(api.clone());
client.add_or_update_account(&account, true).await.unwrap();
client.account = Some(MultisigAccount::new(account.clone()));
client.miden_client.sync_state().await.unwrap();
let new_cosigner = Word::from([7u32, 7, 7, 7]);
let signers = vec![signer_commitment, new_cosigner];
let signers_hex: Vec<String> = signers.iter().map(word_to_hex).collect();
let salt = Word::from([5u32, 6, 7, 8]);
let auth_args = client.multisig_auth_args(salt, None, None).await.unwrap();
let (tx_request, _) = build_update_signers_transaction_request(
1,
&signers,
&auth_args,
std::iter::empty(),
client.key_manager.scheme(),
)
.unwrap();
let (tx_summary, chain_anchor) =
execute_for_summary(&mut client.miden_client, account.id(), tx_request)
.await
.unwrap();
let good_id = word_to_hex(&tx_summary.to_commitment());
let anchor_b64 = chain_anchor_to_base64(&chain_anchor);
let payload = |salt_hex: String| {
ProposalPayload::new(&tx_summary)
.with_add_signer_metadata(1, signers_hex.clone(), salt_hex)
.with_required_signatures(1)
.with_chain_anchor(anchor_b64.clone())
.to_json()
.to_string()
};
let good = pending_proto_delta(
&account,
1,
payload(word_to_hex(&salt)),
&word_to_hex(&signer_commitment),
);
let bad = pending_proto_delta(
&account,
2,
payload(word_to_hex(&Word::from([1u32, 1, 1, 1]))),
&word_to_hex(&signer_commitment),
);
let service = MockGuardianService::default();
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
handle.set_persistent_get_state(registered_state(&account));
handle.set_persistent_get_delta_proposals(GetDeltaProposalsResponse {
success: true,
message: String::new(),
proposals: vec![bad, good],
});
client
.set_guardian_endpoint(&endpoint, false)
.await
.unwrap();
let proposals = client
.list_proposals()
.await
.expect("an unverifiable proposal is reported, not fatal");
assert_eq!(proposals.len(), 2, "proposals: {proposals:?}");
let (unverifiable, verified): (Vec<_>, Vec<_>) =
proposals.iter().partition(|p| !p.is_verified());
assert_eq!(verified.len(), 1, "proposals: {proposals:?}");
assert!(verified[0].id.eq_ignore_ascii_case(&good_id));
assert_eq!(unverifiable.len(), 1);
assert_eq!(unverifiable[0].nonce, 2);
match &unverifiable[0].verification {
crate::proposal::ProposalVerification::Failed { retryable, message } => {
assert!(
!retryable,
"a tampered proposal is not worth retrying: {message}"
);
assert!(
message.contains("metadata salt does not match the salt bound into its tx_summary"),
"message: {message}"
);
}
other => panic!("expected a failed verification, got {other:?}"),
}
assert!(!unverifiable[0].is_actionable());
}
#[tokio::test]
async fn sign_proposal_returns_a_verified_actionable_proposal_after_the_final_signature() {
use crate::transaction::build_update_signers_transaction_request;
use guardian_client::{GetDeltaProposalResponse, SignDeltaProposalResponse};
let keystore = Arc::new(GuardianKeyStore::generate());
let signer_commitment = keystore.commitment();
let signer_hex = word_to_hex(&signer_commitment);
let account = multisig_account(signer_commitment, Word::from([9u32, 9, 9, 9]), 51);
let api = chain_with_notes(Vec::new());
let dir = tempfile::tempdir().unwrap();
let (mut client, _store) =
offline_client_parts_with_keystore(dir.path(), api.clone(), None, keystore.clone()).await;
client.set_node_rpc_client(api.clone());
client.add_or_update_account(&account, true).await.unwrap();
client.account = Some(MultisigAccount::new(account.clone()));
client.miden_client.sync_state().await.unwrap();
let new_cosigner = Word::from([7u32, 7, 7, 7]);
let signers = vec![signer_commitment, new_cosigner];
let signers_hex: Vec<String> = signers.iter().map(word_to_hex).collect();
let salt = Word::from([5u32, 6, 7, 8]);
let auth_args = client.multisig_auth_args(salt, None, None).await.unwrap();
let (tx_request, _) = build_update_signers_transaction_request(
1,
&signers,
&auth_args,
std::iter::empty(),
client.key_manager.scheme(),
)
.unwrap();
let (tx_summary, chain_anchor) =
execute_for_summary(&mut client.miden_client, account.id(), tx_request)
.await
.unwrap();
let proposal_id = word_to_hex(&tx_summary.to_commitment());
let payload = ProposalPayload::new(&tx_summary)
.with_add_signer_metadata(1, signers_hex.clone(), word_to_hex(&salt))
.with_required_signatures(1)
.with_chain_anchor(chain_anchor_to_base64(&chain_anchor));
let unsigned = pending_proto_delta(&account, 1, payload.to_json().to_string(), &signer_hex);
let mut signed_payload = payload;
signed_payload
.signatures
.push(guardian_shared::DeltaSignature {
signer_id: signer_hex.clone(),
signature: guardian_shared::ProposalSignature::Falcon {
signature: client.key_manager.sign_word_hex(tx_summary.to_commitment()),
},
});
let signed = pending_proto_delta(
&account,
1,
signed_payload.to_json().to_string(),
&signer_hex,
);
let service =
MockGuardianService::default().with_sign_delta_proposal(Ok(SignDeltaProposalResponse {
success: true,
message: String::new(),
delta: Some(signed),
}));
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
handle.set_persistent_get_state(registered_state(&account));
handle.set_persistent_get_delta_proposal(GetDeltaProposalResponse {
success: true,
message: String::new(),
proposal: Some(unsigned),
});
client
.set_guardian_endpoint(&endpoint, false)
.await
.unwrap();
let updated = client.sign_proposal(&proposal_id).await.unwrap();
assert!(
updated.is_verified(),
"the signed response must be re-verified, got {:?}",
updated.verification
);
assert!(
updated.status.is_ready(),
"1-of-1 threshold met: {:?}",
updated.status
);
assert!(updated.is_actionable());
}
#[tokio::test]
async fn custom_proposal_keeps_the_anchor_the_producer_bound_when_the_chain_moves_on() {
use guardian_client::PushDeltaProposalResponse;
use miden_protocol::utils::serde::Serializable;
use crate::procedures::ProcedureName;
let keystore = Arc::new(GuardianKeyStore::generate());
let signer_commitment = keystore.commitment();
let guardian_commitment = Word::from([9u32, 9, 9, 9]);
let account = multisig_account(signer_commitment, guardian_commitment, 48);
let api = chain_with_notes(Vec::new());
let dir = tempfile::tempdir().unwrap();
let (mut proposer, _store) =
offline_client_parts_with_keystore(dir.path(), api.clone(), None, keystore.clone()).await;
proposer.set_node_rpc_client(api.clone());
proposer
.add_or_update_account(&account, true)
.await
.unwrap();
proposer.account = Some(MultisigAccount::new(account.clone()));
proposer.miden_client.sync_state().await.unwrap();
let bound_height = proposer.miden_client.get_sync_height().await.unwrap();
let auth_args = proposer
.multisig_auth_args(Word::from([5u32, 6, 7, 8]), None, None)
.await
.unwrap();
let tx_type = TransactionType::UpdateProcedureThreshold {
procedure: ProcedureName::SendAsset,
new_threshold: 1,
};
let tx_request = build_final_transaction_request(
&proposer.miden_client,
&tx_type,
&account,
&auth_args,
Vec::new(),
None,
None,
proposer.key_manager.scheme(),
)
.await
.unwrap();
let request_bytes = tx_request.to_bytes();
let (expected_summary, _) =
execute_for_summary(&mut proposer.miden_client, account.id(), tx_request)
.await
.unwrap();
let expected_id = word_to_hex(&expected_summary.to_commitment());
let service =
MockGuardianService::default().with_push_delta_proposal(Ok(PushDeltaProposalResponse {
success: true,
message: String::new(),
commitment: expected_id.clone(),
delta: None,
}));
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
handle.set_persistent_get_state(registered_state(&account));
proposer
.set_guardian_endpoint(&endpoint, false)
.await
.unwrap();
api.advance_blocks(3);
let proposal = proposer
.propose_custom_transaction(&request_bytes, "b2agg")
.await
.expect("the proposal anchors at the height the request binds, not at the node's tip");
assert!(proposal.id.eq_ignore_ascii_case(&expected_id));
assert_eq!(
proposal.metadata.chain_anchor().unwrap().block_num(),
bound_height,
"the anchor must name the block the producer bound the request to"
);
}
fn fee_charging_chain_holding(
account: &mut miden_protocol::account::Account,
) -> Arc<miden_client::testing::mock::MockRpcApi> {
use miden_protocol::asset::FungibleAsset;
use miden_standards::account::policies::MintPolicy;
let mut builder = miden_client::testing::MockChain::builder();
let fee_faucet = builder
.add_existing_network_faucet(
"FEE",
1_000_000_000,
account.id(),
Some(1_000_000),
MintPolicy::allow_all(),
[],
)
.expect("fee faucet is added");
assert!(
fee_faucet.id().asset_callback_flag().is_enabled(),
"the fee faucet must enable asset callbacks, as devnet's does"
);
account
.vault_mut()
.add_asset(
FungibleAsset::new(fee_faucet.id(), 1_000_000)
.expect("fee asset")
.into(),
)
.expect("fee asset is added");
account
.set_nonce(miden_protocol::Felt::ONE)
.expect("nonce is set");
builder
.add_account(account.clone())
.expect("account is added");
let chain = builder
.fee_faucet_id(fee_faucet.id())
.verification_base_fee(500)
.build()
.expect("mock chain builds");
let api = Arc::new(miden_client::testing::mock::MockRpcApi::new(chain));
api.advance_blocks(4);
api
}
#[tokio::test]
async fn proposal_verifies_at_the_tip_after_the_node_prunes_its_bound_block_state() {
use crate::transaction::build_update_signers_transaction_request;
let keystore = Arc::new(GuardianKeyStore::generate());
let signer_commitment = keystore.commitment();
let mut account = multisig_account(signer_commitment, Word::from([9u32, 9, 9, 9]), 53);
let api = fee_charging_chain_holding(&mut account);
let dir = tempfile::tempdir().unwrap();
let (mut client, _store) =
offline_client_parts_with_keystore(dir.path(), api.clone(), None, keystore.clone()).await;
client.set_node_rpc_client(api.clone());
client.add_or_update_account(&account, true).await.unwrap();
client.account = Some(MultisigAccount::new(account.clone()));
client.miden_client.sync_state().await.unwrap();
let signers = vec![signer_commitment, Word::from([7u32, 7, 7, 7])];
let signers_hex: Vec<String> = signers.iter().map(word_to_hex).collect();
let salt = Word::from([5u32, 6, 7, 8]);
let auth_args = client.multisig_auth_args(salt, None, None).await.unwrap();
let (tx_request, _) = build_update_signers_transaction_request(
1,
&signers,
&auth_args,
std::iter::empty(),
client.key_manager.scheme(),
)
.unwrap();
let (tx_summary, chain_anchor) =
execute_for_summary(&mut client.miden_client, account.id(), tx_request.clone())
.await
.unwrap();
let proposal_id = word_to_hex(&tx_summary.to_commitment());
let bound_block = chain_anchor.block_num();
assert_eq!(tx_summary.block_number(), bound_block);
let payload = ProposalPayload::new(&tx_summary)
.with_add_signer_metadata(1, signers_hex, word_to_hex(&salt))
.with_required_signatures(1)
.with_chain_anchor(chain_anchor_to_base64(&chain_anchor))
.to_json()
.to_string();
let service = MockGuardianService::default();
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
handle.set_persistent_get_state(registered_state(&account));
handle.set_persistent_get_delta_proposals(GetDeltaProposalsResponse {
success: true,
message: String::new(),
proposals: vec![pending_proto_delta(
&account,
2,
payload,
&word_to_hex(&signer_commitment),
)],
});
client
.set_guardian_endpoint(&endpoint, false)
.await
.unwrap();
api.advance_blocks(5);
api.prune_account_state_at(bound_block);
assert_eq!(
client.miden_client.get_sync_height().await.unwrap(),
bound_block
);
let anchored_error = match client
.miden_client
.execute_transaction_at(account.id(), tx_request, chain_anchor)
.await
{
Ok(_) => panic!("anchored re-execution must fail once the bound block is pruned"),
Err(error) => format!("{error:?}"),
};
assert!(
anchored_error.contains(&format!("no mock chain snapshot at block {bound_block}")),
"the anchored re-execution must fail on the pruned foreign load: {anchored_error}"
);
let proposals = client
.list_proposals()
.await
.expect("the listing succeeds after the prune");
assert_eq!(proposals.len(), 1, "proposals: {proposals:?}");
assert!(proposals[0].id.eq_ignore_ascii_case(&proposal_id));
assert!(
proposals[0].is_verified(),
"the tip re-execution must reproduce the signed summary: {:?}",
proposals[0].verification
);
assert!(
client.miden_client.get_sync_height().await.unwrap() > bound_block,
"the listing must have synced the store past the pruned block"
);
}
#[tokio::test]
async fn cosigner_below_the_bound_block_syncs_before_verifying() {
use crate::transaction::build_update_signers_transaction_request;
let keystore = Arc::new(GuardianKeyStore::generate());
let signer_commitment = keystore.commitment();
let account = multisig_account(signer_commitment, Word::from([9u32, 9, 9, 9]), 54);
let api = chain_with_notes(Vec::new());
let cosigner_dir = tempfile::tempdir().unwrap();
let (mut cosigner, _cosigner_store) =
offline_client_with_node_parts(cosigner_dir.path(), api.clone()).await;
cosigner
.add_or_update_account(&account, true)
.await
.unwrap();
cosigner.account = Some(MultisigAccount::new(account.clone()));
cosigner.miden_client.sync_state().await.unwrap();
let cosigner_height = cosigner.miden_client.get_sync_height().await.unwrap();
api.advance_blocks(3);
let proposer_dir = tempfile::tempdir().unwrap();
let (mut proposer, _proposer_store) = offline_client_parts_with_keystore(
proposer_dir.path(),
api.clone(),
None,
keystore.clone(),
)
.await;
proposer.set_node_rpc_client(api.clone());
proposer
.add_or_update_account(&account, true)
.await
.unwrap();
proposer.account = Some(MultisigAccount::new(account.clone()));
proposer.miden_client.sync_state().await.unwrap();
let signers = vec![signer_commitment, Word::from([7u32, 7, 7, 7])];
let signers_hex: Vec<String> = signers.iter().map(word_to_hex).collect();
let salt = Word::from([5u32, 6, 7, 8]);
let auth_args = proposer.multisig_auth_args(salt, None, None).await.unwrap();
let (tx_request, _) = build_update_signers_transaction_request(
1,
&signers,
&auth_args,
std::iter::empty(),
proposer.key_manager.scheme(),
)
.unwrap();
let (tx_summary, chain_anchor) =
execute_for_summary(&mut proposer.miden_client, account.id(), tx_request)
.await
.unwrap();
let bound_block = chain_anchor.block_num();
assert!(
bound_block > cosigner_height,
"the proposal must bind a block the cosigner has not synced to ({bound_block} vs \
{cosigner_height})"
);
let payload = ProposalPayload::new(&tx_summary)
.with_add_signer_metadata(1, signers_hex, word_to_hex(&salt))
.with_required_signatures(1)
.with_chain_anchor(chain_anchor_to_base64(&chain_anchor))
.to_json()
.to_string();
let service = MockGuardianService::default();
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
handle.set_persistent_get_state(registered_state(&account));
handle.set_persistent_get_delta_proposals(GetDeltaProposalsResponse {
success: true,
message: String::new(),
proposals: vec![pending_proto_delta(
&account,
1,
payload,
&word_to_hex(&signer_commitment),
)],
});
cosigner
.set_guardian_endpoint(&endpoint, false)
.await
.unwrap();
let proposals = cosigner
.list_proposals()
.await
.expect("the cosigner syncs to the bound block and verifies");
assert_eq!(proposals.len(), 1, "proposals: {proposals:?}");
assert!(
proposals[0].is_verified(),
"the cosigner must verify after syncing: {:?}",
proposals[0].verification
);
assert!(cosigner.miden_client.get_sync_height().await.unwrap() >= bound_block);
}
#[tokio::test]
async fn cosigner_that_never_synced_verifies_after_syncing() {
use crate::transaction::build_update_signers_transaction_request;
let keystore = Arc::new(GuardianKeyStore::generate());
let signer_commitment = keystore.commitment();
let account = multisig_account(signer_commitment, Word::from([9u32, 9, 9, 9]), 55);
let api = chain_with_notes(Vec::new());
let proposer_dir = tempfile::tempdir().unwrap();
let (mut proposer, _proposer_store) = offline_client_parts_with_keystore(
proposer_dir.path(),
api.clone(),
None,
keystore.clone(),
)
.await;
proposer.set_node_rpc_client(api.clone());
proposer
.add_or_update_account(&account, true)
.await
.unwrap();
proposer.account = Some(MultisigAccount::new(account.clone()));
proposer.miden_client.sync_state().await.unwrap();
let signers = vec![signer_commitment, Word::from([7u32, 7, 7, 7])];
let signers_hex: Vec<String> = signers.iter().map(word_to_hex).collect();
let salt = Word::from([5u32, 6, 7, 8]);
let auth_args = proposer.multisig_auth_args(salt, None, None).await.unwrap();
let (tx_request, _) = build_update_signers_transaction_request(
1,
&signers,
&auth_args,
std::iter::empty(),
proposer.key_manager.scheme(),
)
.unwrap();
let (tx_summary, chain_anchor) =
execute_for_summary(&mut proposer.miden_client, account.id(), tx_request)
.await
.unwrap();
let payload = ProposalPayload::new(&tx_summary)
.with_add_signer_metadata(1, signers_hex, word_to_hex(&salt))
.with_required_signatures(1)
.with_chain_anchor(chain_anchor_to_base64(&chain_anchor))
.to_json()
.to_string();
let service = MockGuardianService::default();
let handle = service.handle();
let endpoint = start_mock_server(service).await.unwrap();
handle.set_persistent_get_state(registered_state(&account));
handle.set_persistent_get_delta_proposals(GetDeltaProposalsResponse {
success: true,
message: String::new(),
proposals: vec![pending_proto_delta(
&account,
1,
payload,
&word_to_hex(&signer_commitment),
)],
});
let cosigner_dir = tempfile::tempdir().unwrap();
let (mut cosigner, _cosigner_store) =
offline_client_with_node_parts(cosigner_dir.path(), api.clone()).await;
cosigner
.add_or_update_account(&account, true)
.await
.unwrap();
cosigner.account = Some(MultisigAccount::new(account.clone()));
cosigner
.set_guardian_endpoint(&endpoint, false)
.await
.unwrap();
let proposals = cosigner
.list_proposals()
.await
.expect("the listing succeeds");
assert_eq!(proposals.len(), 1, "proposals: {proposals:?}");
assert!(
proposals[0].is_verified(),
"a cosigner that never synced must sync and verify: {:?}",
proposals[0].verification
);
}