mod auth_args;
mod builder;
mod configuration;
mod consume;
mod guardian;
mod payment;
pub use auth_args::{
MAX_APPROVAL_EXPIRATION_DELTA, TransactionRequestBuilderExt, multisig_auth_args,
proposal_auth_args, proposer_auth_args, summary_approval_expiration_block_num, summary_salt,
synced_fee_faucet_id,
};
pub use builder::{ProposalBuilder, ProposalOptions};
pub use configuration::{
build_update_procedure_threshold_transaction_request, build_update_signers_transaction_request,
};
pub(crate) use consume::ensure_notes_authenticated;
pub use consume::{
build_consume_notes_transaction_request, build_consume_notes_transaction_request_from_notes,
};
pub use guardian::build_update_guardian_transaction_request;
pub use payment::build_p2id_transaction_request;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use miden_client::ClientError;
use miden_client::transaction::{
ChainAnchor, TransactionExecutorError, TransactionRequest, TransactionSummary,
};
use miden_protocol::account::AccountId;
use miden_protocol::block::BlockNumber;
use miden_protocol::{Felt, Word};
use crate::MidenSdkClient;
use crate::error::{MultisigError, Result};
pub fn deserialize_transaction_request(bytes: &[u8]) -> Result<TransactionRequest> {
use miden_client::Deserializable;
TransactionRequest::read_from_bytes(bytes).map_err(|e| {
MultisigError::InvalidConfig(format!("failed to decode transaction request: {e}"))
})
}
pub fn chain_anchor_to_base64(anchor: &ChainAnchor) -> String {
use miden_client::Serializable;
BASE64.encode(anchor.to_bytes())
}
pub fn chain_anchor_from_base64(anchor_b64: &str) -> Result<ChainAnchor> {
use miden_client::Deserializable;
let bytes = BASE64
.decode(anchor_b64)
.map_err(|e| MultisigError::InvalidConfig(format!("invalid chain_anchor base64: {e}")))?;
ChainAnchor::read_from_bytes(&bytes)
.map_err(|e| MultisigError::InvalidConfig(format!("invalid chain_anchor: {e}")))
}
pub async fn execute_for_summary(
client: &mut MidenSdkClient,
account_id: AccountId,
request: TransactionRequest,
) -> Result<(TransactionSummary, ChainAnchor)> {
let anchor = client
.chain_anchor_for_request(&request)
.await
.map_err(|e| MultisigError::MidenClient(format!("failed to capture chain anchor: {e}")))?;
let summary = execute_for_summary_at_tip(client, account_id, request).await?;
if anchor.block_commitment() != summary.block_commitment() {
return Err(MultisigError::SummaryAnchorMismatch {
anchor_commitment: word_to_hex(&anchor.block_commitment()),
summary_block_commitment: word_to_hex(&summary.block_commitment()),
});
}
Ok((summary, anchor))
}
pub async fn execute_for_summary_at_tip(
client: &mut MidenSdkClient,
account_id: AccountId,
request: TransactionRequest,
) -> Result<TransactionSummary> {
prepare_tip_execution(client, &request).await?;
match client.execute_transaction(account_id, request).await {
Ok(_) => Err(MultisigError::UnexpectedSuccess),
Err(ClientError::TransactionExecutorError(TransactionExecutorError::Unauthorized(
summary,
))) => Ok(*summary),
Err(ClientError::TransactionExecutorError(err)) => {
Err(MultisigError::TransactionExecution(err.to_string()))
}
Err(err) => Err(MultisigError::from(err)),
}
}
pub(crate) async fn prepare_tip_execution(
client: &mut MidenSdkClient,
request: &TransactionRequest,
) -> Result<()> {
match declared_bound_block(request)? {
Some(bound_block_num) => sync_to_block(client, bound_block_num).await,
None => Ok(()),
}
}
pub(crate) async fn sync_to_block(
client: &mut MidenSdkClient,
bound_block_num: BlockNumber,
) -> Result<()> {
if sync_height(client).await? >= bound_block_num {
return Ok(());
}
client.sync_state().await.map_err(|e| {
MultisigError::miden_client_with_context(
format!("failed to sync to block {bound_block_num} the proposal binds"),
e,
)
})?;
let synced = sync_height(client).await?;
if synced < bound_block_num {
return Err(MultisigError::ChainBehindBoundBlock {
synced,
bound_block_num,
});
}
Ok(())
}
pub(crate) async fn sync_chain(client: &mut MidenSdkClient) -> Result<()> {
client.sync_state().await.map_err(|e| {
MultisigError::miden_client_with_context(
"failed to sync the Miden client before re-executing the proposal",
e,
)
})?;
Ok(())
}
pub(crate) fn is_stale_chain_error(error: &MultisigError) -> bool {
matches!(error, MultisigError::ChainBehindBoundBlock { .. })
|| error.to_string().contains("has been pruned")
}
async fn sync_height(client: &MidenSdkClient) -> Result<BlockNumber> {
client
.get_sync_height()
.await
.map_err(|e| MultisigError::miden_client_with_context("failed to read the sync height", e))
}
pub(crate) fn declared_bound_block(request: &TransactionRequest) -> Result<Option<BlockNumber>> {
let Some(bound_block_num) = request_bound_block_num(request) else {
return Ok(None);
};
if !request.block_numbers().contains(&bound_block_num) {
return Err(MultisigError::BoundBlockNotDeclared { bound_block_num });
}
Ok(Some(bound_block_num))
}
fn request_bound_block_num(request: &TransactionRequest) -> Option<BlockNumber> {
const AUTH_ARGS_NUM_ELEMENTS: usize = 12;
let auth_arg = (*request.auth_arg())?;
let preimage = request.advice_map().get(&auth_arg)?;
if preimage.len() != AUTH_ARGS_NUM_ELEMENTS {
return None;
}
u32::try_from(preimage[0].as_canonical_u64())
.ok()
.map(BlockNumber::from)
}
pub fn generate_salt() -> Word {
let mut bytes = [0u8; 32];
rand::Rng::fill_bytes(&mut rand::rng(), &mut bytes);
let mut felts = [Felt::ZERO; 4];
for (i, chunk) in bytes.chunks(8).enumerate() {
let mut arr = [0u8; 8];
arr.copy_from_slice(chunk);
felts[i] = guardian_shared::felt::felt_from_u64_reduced(u64::from_le_bytes(arr));
}
felts.into()
}
pub fn word_to_hex(word: &Word) -> String {
let bytes: Vec<u8> = word
.iter()
.flat_map(|felt| felt.as_canonical_u64().to_le_bytes())
.collect();
format!("0x{}", hex::encode(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_transaction_request_rejects_garbage_bytes() {
let err = deserialize_transaction_request(&[0xde, 0xad, 0xbe, 0xef])
.expect_err("garbage bytes must not deserialize");
assert!(
err.to_string()
.contains("failed to decode transaction request")
);
}
mod declared_bound_block {
use miden_client::transaction::TransactionRequestBuilder;
use miden_protocol::crypto::SequentialCommit;
use miden_standards::account::auth::MultisigAuthArgs;
use super::super::{TransactionRequestBuilderExt, declared_bound_block};
use super::*;
fn auth_args() -> MultisigAuthArgs {
MultisigAuthArgs::new(BlockNumber::from(11), Word::from([1u32, 2, 3, 4]))
}
#[test]
fn a_request_built_with_the_extension_declares_its_bound_block() {
let request = TransactionRequestBuilder::new()
.multisig_auth_args(&auth_args())
.build()
.expect("request builds");
assert_eq!(
declared_bound_block(&request).expect("the bound block is declared"),
Some(BlockNumber::from(11))
);
}
#[test]
fn a_request_that_binds_a_block_without_declaring_it_is_refused() {
let auth_args = auth_args();
let commitment = auth_args.to_commitment();
let request = TransactionRequestBuilder::new()
.auth_arg(commitment)
.extend_advice_map([(commitment, auth_args.to_elements())])
.build()
.expect("request builds");
match declared_bound_block(&request) {
Err(MultisigError::BoundBlockNotDeclared { bound_block_num }) => {
assert_eq!(bound_block_num, BlockNumber::from(11));
}
other => panic!("expected BoundBlockNotDeclared, got {other:?}"),
}
}
#[test]
fn a_request_without_multisig_auth_args_has_no_bound_block() {
let request = TransactionRequestBuilder::new()
.build()
.expect("request builds");
assert_eq!(
declared_bound_block(&request).expect("nothing to declare"),
None
);
}
}
#[test]
fn deserialize_transaction_request_rejects_empty_bytes() {
let err =
deserialize_transaction_request(&[]).expect_err("empty bytes must not deserialize");
assert!(
err.to_string()
.contains("failed to decode transaction request")
);
}
#[test]
fn transaction_kernel_commitment_matches_network() {
use miden_protocol::transaction::TransactionKernel;
const EXPECTED_KERNEL_COMMITMENT: &str =
"0x12b6033c1334140b2d553b871260ff87b62275a7eccd3829975ffd58f9f2a80a";
let actual = word_to_hex(&TransactionKernel.to_commitment());
assert_eq!(
actual, EXPECTED_KERNEL_COMMITMENT,
"transaction kernel commitment drifted from the network kernel; a transitive \
hashing crate (e.g. Plonky3 `p3-*`) likely changed in Cargo.lock"
);
}
}