use std::num::NonZeroU32;
use miden_client::transaction::{TransactionRequestBuilder, TransactionSummary};
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::crypto::SequentialCommit;
use miden_standards::account::auth::{FeeConversionInfo, MultisigAuthArgs};
use crate::MidenSdkClient;
use crate::error::{MultisigError, Result};
const APPROVAL_EXPIRATION_USER_PARAM_INDEX: usize = 0;
const SALT_USER_PARAM_OFFSET: usize = 2;
pub const MAX_APPROVAL_EXPIRATION_DELTA: u32 = 65_535;
pub fn multisig_auth_args(
fee_faucet_id: AccountId,
bound_block_num: BlockNumber,
salt: Word,
approval_expiration_delta: Option<NonZeroU32>,
) -> Result<MultisigAuthArgs> {
let auth_args = MultisigAuthArgs::new(bound_block_num, salt)
.with_conversion_info(FeeConversionInfo::one_to_one(fee_faucet_id));
let Some(delta) = approval_expiration_delta else {
return Ok(auth_args);
};
if delta.get() > MAX_APPROVAL_EXPIRATION_DELTA {
return Err(MultisigError::InvalidConfig(format!(
"approval expiration delta {delta} exceeds the {MAX_APPROVAL_EXPIRATION_DELTA} blocks a \
transaction can stay valid for; the auth procedure would clamp it and the approval \
would outlive the transaction"
)));
}
auth_args
.with_approval_expiration_delta(delta)
.map_err(|e| {
MultisigError::InvalidConfig(format!("invalid approval expiration delta: {e}"))
})
}
async fn fee_faucet_id_at(client: &MidenSdkClient, header: &BlockHeader) -> Result<AccountId> {
let protocol_config = client
.get_protocol_config(header.protocol_config_commitment())
.await
.map_err(|e| {
MultisigError::miden_client_with_context(
format!(
"no protocol configuration is stored for block {}; sync the client first",
header.block_num()
),
e,
)
})?;
Ok(protocol_config.fee_asset_id().faucet_id())
}
pub async fn synced_fee_faucet_id(client: &MidenSdkClient) -> Result<AccountId> {
let header = client.get_latest_block_header().await.map_err(|e| {
MultisigError::miden_client_with_context("failed to read the latest block header", e)
})?;
fee_faucet_id_at(client, &header).await
}
pub async fn proposer_auth_args(
client: &MidenSdkClient,
salt: Word,
approval_expiration_delta: Option<NonZeroU32>,
) -> Result<MultisigAuthArgs> {
let header = client.get_latest_block_header().await.map_err(|e| {
MultisigError::miden_client_with_context(
"failed to read the latest block header for the auth args",
e,
)
})?;
multisig_auth_args(
fee_faucet_id_at(client, &header).await?,
header.block_num(),
salt,
approval_expiration_delta,
)
}
pub async fn proposal_auth_args(
client: &MidenSdkClient,
summary: &TransactionSummary,
) -> Result<MultisigAuthArgs> {
let bound_block_num = summary.block_number();
multisig_auth_args(
synced_fee_faucet_id(client).await?,
bound_block_num,
summary_salt(summary),
approval_expiration_delta_of(summary, bound_block_num)?,
)
}
pub trait TransactionRequestBuilderExt {
fn multisig_auth_args(self, auth_args: &MultisigAuthArgs) -> Self;
}
impl TransactionRequestBuilderExt for TransactionRequestBuilder {
fn multisig_auth_args(self, auth_args: &MultisigAuthArgs) -> Self {
let commitment = auth_args.to_commitment();
self.auth_arg(commitment)
.extend_advice_map([(commitment, auth_args.to_elements())])
.block_numbers([auth_args.bound_block_num()])
}
}
pub fn summary_salt(summary: &TransactionSummary) -> Word {
let user_params = summary.user_params();
let elements = user_params.as_elements();
Word::new([
elements[SALT_USER_PARAM_OFFSET],
elements[SALT_USER_PARAM_OFFSET + 1],
elements[SALT_USER_PARAM_OFFSET + 2],
elements[SALT_USER_PARAM_OFFSET + 3],
])
}
pub fn summary_approval_expiration_block_num(summary: &TransactionSummary) -> Option<BlockNumber> {
let user_params = summary.user_params();
let value = user_params.as_elements()[APPROVAL_EXPIRATION_USER_PARAM_INDEX].as_canonical_u64();
(value != 0).then(|| BlockNumber::from(value as u32))
}
fn approval_expiration_delta_of(
summary: &TransactionSummary,
bound_block_num: BlockNumber,
) -> Result<Option<NonZeroU32>> {
let Some(expiration) = summary_approval_expiration_block_num(summary) else {
return Ok(None);
};
expiration
.as_u32()
.checked_sub(bound_block_num.as_u32())
.and_then(NonZeroU32::new)
.map(Some)
.ok_or_else(|| {
MultisigError::InvalidConfig(format!(
"approval expires at block {expiration}, at or before the block {bound_block_num} \
its summary binds"
))
})
}
#[cfg(test)]
mod tests {
use super::*;
fn fee_faucet_id() -> AccountId {
AccountId::from_hex("0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b").expect("valid faucet id")
}
#[test]
fn accepts_an_approval_expiration_up_to_the_kernel_maximum() {
let delta = NonZeroU32::new(MAX_APPROVAL_EXPIRATION_DELTA).expect("non-zero");
let auth_args = multisig_auth_args(
fee_faucet_id(),
BlockNumber::from(10),
Word::default(),
Some(delta),
)
.expect("the maximum delta is accepted");
assert_eq!(
auth_args.approval_expiration_block_num(),
Some(BlockNumber::from(10 + MAX_APPROVAL_EXPIRATION_DELTA))
);
}
#[test]
fn rejects_an_approval_expiration_the_auth_procedure_would_clamp() {
let delta = NonZeroU32::new(MAX_APPROVAL_EXPIRATION_DELTA + 1).expect("non-zero");
let error = multisig_auth_args(
fee_faucet_id(),
BlockNumber::from(10),
Word::default(),
Some(delta),
)
.expect_err("a delta past the kernel maximum is refused");
assert!(
error.to_string().contains("65535"),
"the error names the limit: {error}"
);
}
}