use alloc::string::{String, ToString};
use alloc::vec::Vec;
use miden_processor::ExecutionError;
use miden_processor::operation::OperationError;
use miden_protocol::account::Account;
use miden_protocol::errors::MasmError;
use miden_protocol::errors::tx_kernel::{
ERR_TX_COMPUTE_FEE_EXCLUDE_NOTE_INDEX_OUT_OF_BOUNDS,
ERR_TX_COMPUTE_FEE_EXCLUDE_NOTES_COMMITMENT_MISMATCH,
ERR_TX_COMPUTE_FEE_EXCLUDE_NOTES_COUNT_EXCEEDS_MAX,
ERR_TX_COMPUTE_FEE_EXCLUDE_NOTES_UNSORTED,
ERR_TX_COMPUTE_FEE_EXTRA_CYCLES_NOT_U32,
};
use miden_protocol::testing::tx::TransactionFee;
use miden_protocol::{Felt, Hasher, MAX_OUTPUT_NOTES_PER_TX, Word};
use rstest::rstest;
use crate::kernel_tests::tx::ExecutionOutputExt;
use crate::{Auth, MockChain, assert_execution_error};
const VERIFICATION_BASE_FEE: u32 = 500;
const NUM_EXTRA_CYCLES: u32 = 1 << 28;
const EXPECTED_VERIFICATION_CYCLES: u32 = 29;
fn mock_chain_with_fee() -> anyhow::Result<(MockChain, Account)> {
let mut builder = MockChain::builder().verification_base_fee(VERIFICATION_BASE_FEE);
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let mock_chain = builder.build()?;
Ok((mock_chain, account))
}
pub fn build_exclude_notes_commitment(indices: &[u32]) -> (Word, Vec<Felt>) {
let elements: Vec<Felt> = indices.iter().copied().map(Felt::from).collect();
let commitment = Hasher::hash_elements(&elements);
(commitment, elements)
}
fn compute_fee_code(
exclude_notes_commitment: &str,
num_extra_cycles: u64,
num_output_notes: u32,
) -> String {
let create_output_notes =
"exec.util::create_default_note drop\n".repeat(num_output_notes as usize);
format!(
"
use miden::tx_kernel_core::prologue
use miden::protocol::tx
use miden::core::sys
use mock::util
begin
exec.prologue::prepare_transaction
{create_output_notes}
push.{exclude_notes_commitment}
push.{num_extra_cycles}
exec.tx::compute_fee
# => [fee_amount]
exec.sys::truncate_stack
end
"
)
}
#[tokio::test]
async fn compute_fee_adds_extra_cycles() -> anyhow::Result<()> {
let (mock_chain, account) = mock_chain_with_fee()?;
let tx_context = mock_chain.build_tx_context(account, &[], &[])?.build()?;
let code = compute_fee_code("0.0.0.0", u64::from(NUM_EXTRA_CYCLES), 0);
let verification_base_fee =
tx_context.tx_inputs().block_header().fee_parameters().verification_base_fee();
let exec_output = tx_context.execute_code(&code).await?;
let expected_fee = verification_base_fee * EXPECTED_VERIFICATION_CYCLES;
assert_eq!(exec_output.get_stack_element(0), Felt::from(expected_fee));
Ok(())
}
#[tokio::test]
async fn compute_fee_derives_fee_from_concrete_clk() -> anyhow::Result<()> {
let (mock_chain, account) = mock_chain_with_fee()?;
let mock_tx = mock_chain.build_transaction(account).build()?;
let code = "
use miden::tx_kernel_core::prologue
use miden::protocol::tx
use miden::core::sys
begin
exec.prologue::prepare_transaction
clk padw push.0
# => [num_extra_cycles = 0, EXCLUDE_NOTES_COMMITMENT = EMPTY_WORD, captured_clk]
exec.tx::compute_fee
# => [fee_amount, captured_clk]
exec.sys::truncate_stack
end
";
let exec_output = mock_tx.execute_code(code).await?;
let actual_fee = exec_output.get_stack_element(0).as_canonical_u64();
let captured_clk = u32::try_from(exec_output.get_stack_element(1).as_canonical_u64())?;
let expected_fee = TransactionFee::new(captured_clk.next_power_of_two())
.compute_fee(mock_tx.tx_inputs().block_header().fee_parameters());
assert_eq!(actual_fee, expected_fee.as_u64());
Ok(())
}
#[tokio::test]
async fn compute_fee_fails_on_non_u32_extra_cycles() -> anyhow::Result<()> {
let (mock_chain, account) = mock_chain_with_fee()?;
let tx_context = mock_chain.build_tx_context(account, &[], &[])?.build()?;
let code = compute_fee_code("0.0.0.0", u64::from(u32::MAX) + 1, 0);
let exec_output = tx_context.execute_code(&code).await;
assert_execution_error!(
exec_output,
matches ExecutionError::OperationError {
err: OperationError::U32AssertionFailed { err_code, .. },
..
} if err_code == ERR_TX_COMPUTE_FEE_EXTRA_CYCLES_NOT_U32.code()
);
Ok(())
}
#[tokio::test]
async fn compute_fee_fails_on_exclude_notes_commitment_mismatch() -> anyhow::Result<()> {
let (mock_chain, account) = mock_chain_with_fee()?;
let (commitment, _) = build_exclude_notes_commitment(&[1, 2, 3]);
let (_, mismatching_elements) = build_exclude_notes_commitment(&[1, 2, 4]);
let tx_context = mock_chain
.build_tx_context(account, &[], &[])?
.extend_advice_map(vec![(commitment, mismatching_elements)])
.build()?;
let code = compute_fee_code(&commitment.to_string(), 0, 0);
let exec_output = tx_context.execute_code(&code).await;
assert_execution_error!(exec_output, ERR_TX_COMPUTE_FEE_EXCLUDE_NOTES_COMMITMENT_MISMATCH);
Ok(())
}
#[rstest]
#[case::exceeds_max_output_notes(
(0..=MAX_OUTPUT_NOTES_PER_TX as u32).collect(),
ERR_TX_COMPUTE_FEE_EXCLUDE_NOTES_COUNT_EXCEEDS_MAX
)]
#[case::out_of_bounds(vec![6], ERR_TX_COMPUTE_FEE_EXCLUDE_NOTE_INDEX_OUT_OF_BOUNDS)]
#[case::unsorted(vec![3, 1], ERR_TX_COMPUTE_FEE_EXCLUDE_NOTES_UNSORTED)]
#[case::duplicate(vec![2, 2, 3], ERR_TX_COMPUTE_FEE_EXCLUDE_NOTES_UNSORTED)]
#[tokio::test]
async fn compute_fee_fails_on_invalid_exclude_notes(
#[case] exclude_indices: Vec<u32>,
#[case] expected_error: MasmError,
) -> anyhow::Result<()> {
let num_output_notes = 6;
let (mock_chain, account) = mock_chain_with_fee()?;
let (commitment, elements) = build_exclude_notes_commitment(&exclude_indices);
let tx_context = mock_chain
.build_tx_context(account, &[], &[])?
.extend_advice_map(vec![(commitment, elements)])
.build()?;
let code = compute_fee_code(&commitment.to_string(), 0, num_output_notes);
let exec_output = tx_context.execute_code(&code).await;
assert_execution_error!(exec_output, expected_error);
Ok(())
}
#[rstest]
#[case::none(vec![])]
#[case::single(vec![2])]
#[case::multiple(vec![1, 3, 5])]
#[tokio::test]
async fn compute_fee_accepts_sorted_in_bounds_exclude_notes(
#[case] exclude_indices: Vec<u32>,
) -> anyhow::Result<()> {
let (mock_chain, account) = mock_chain_with_fee()?;
let (commitment, elements) = build_exclude_notes_commitment(&exclude_indices);
let tx_context = mock_chain
.build_tx_context(account, &[], &[])?
.extend_advice_map(vec![(commitment, elements)])
.build()?;
let code = compute_fee_code(&commitment.to_string(), u64::from(NUM_EXTRA_CYCLES), 6);
let verification_base_fee =
tx_context.tx_inputs().block_header().fee_parameters().verification_base_fee();
let exec_output = tx_context.execute_code(&code).await?;
let expected_fee = verification_base_fee * EXPECTED_VERIFICATION_CYCLES;
assert_eq!(exec_output.get_stack_element(0), Felt::from(expected_fee));
Ok(())
}