use core::slice;
use std::collections::BTreeSet;
use miden_protocol::account::{Account, AccountBuilder, AccountType};
use miden_protocol::note::{Note, NoteScriptRoot};
use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER;
use miden_protocol::transaction::{RawOutputNote, TransactionScript, TransactionScriptRoot};
use miden_protocol::{Felt, Word};
use miden_standards::account::auth::AuthNetworkAccount;
use miden_standards::account::wallets::BasicWallet;
use miden_standards::code_builder::CodeBuilder;
use miden_standards::errors::standards::{
ERR_NOTE_SCRIPT_ALLOWLIST_NOTE_NOT_ALLOWED,
ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED,
};
use miden_standards::testing::note::NoteBuilder;
use miden_testing::{MockChain, assert_transaction_executor_error};
use rstest::rstest;
fn placeholder_script_root() -> Word {
NoteScriptRoot::from_array([1, 0, 0, 0]).into()
}
fn build_allowlist_account(allowed_script_roots: Vec<Word>) -> anyhow::Result<Account> {
build_account_with_allowlists(allowed_script_roots, Vec::new())
}
fn build_account_with_allowlists(
allowed_note_script_roots: Vec<Word>,
allowed_tx_script_roots: Vec<TransactionScriptRoot>,
) -> anyhow::Result<Account> {
let auth_component = AuthNetworkAccount::with_allowed_notes(
allowed_note_script_roots.into_iter().map(NoteScriptRoot::from_raw).collect(),
)?
.with_allowed_tx_scripts(allowed_tx_script_roots.into_iter().collect::<BTreeSet<_>>());
Ok(AccountBuilder::new([0; 32])
.with_auth_component(auth_component)
.with_component(BasicWallet)
.account_type(AccountType::Public)
.build_existing()?)
}
fn build_input_note() -> anyhow::Result<Note> {
Ok(NoteBuilder::new(ACCOUNT_ID_SENDER.try_into()?, &mut rand::rng()).build()?)
}
fn expiration_tx_script(delta: u16) -> TransactionScript {
let code = format!(
"
use miden::protocol::tx
begin
push.{delta}
exec.tx::update_expiration_block_delta
end
"
);
CodeBuilder::default()
.compile_tx_script(code)
.expect("expiration tx script should compile")
}
fn expiration_from_args_tx_script() -> TransactionScript {
let code = "
use miden::protocol::tx
begin
exec.tx::update_expiration_block_delta
drop drop drop
end
";
CodeBuilder::default()
.compile_tx_script(code)
.expect("expiration-from-args tx script should compile")
}
#[tokio::test]
async fn test_auth_network_account_rejects_tx_script() -> anyhow::Result<()> {
let account = build_allowlist_account(vec![placeholder_script_root()])?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
let mock_chain = builder.build()?;
let tx_script = CodeBuilder::default().compile_tx_script("begin nop end")?;
let result = mock_chain
.build_tx_context(account.id(), &[], &[])?
.tx_script(tx_script)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED);
Ok(())
}
#[tokio::test]
async fn test_auth_network_account_accepts_allowlisted_tx_script() -> anyhow::Result<()> {
const DELTA: u16 = 10;
let tx_script = expiration_tx_script(DELTA);
let mut builder = MockChain::builder();
let note = build_input_note()?;
builder.add_output_note(RawOutputNote::Full(note.clone()));
let account =
build_account_with_allowlists(vec![note.script().root().into()], vec![tx_script.root()])?;
builder.add_account(account.clone())?;
let mock_chain = builder.build()?;
let executed = mock_chain
.build_tx_context(account.id(), &[], slice::from_ref(¬e))?
.tx_script(tx_script)
.build()?
.execute()
.await?;
let reference_block = executed.block_header().block_num();
assert_eq!(
executed.expiration_block_num(),
reference_block + u32::from(DELTA),
"the allowlisted expiration script should have set the expiration block number",
);
Ok(())
}
#[tokio::test]
async fn test_auth_network_account_allows_no_tx_script_with_non_empty_allowlist()
-> anyhow::Result<()> {
let mut builder = MockChain::builder();
let note = build_input_note()?;
builder.add_output_note(RawOutputNote::Full(note.clone()));
let account = build_account_with_allowlists(
vec![note.script().root().into()],
vec![expiration_tx_script(10).root()],
)?;
builder.add_account(account.clone())?;
let mock_chain = builder.build()?;
mock_chain
.build_tx_context(account.id(), &[], slice::from_ref(¬e))?
.build()?
.execute()
.await?;
Ok(())
}
#[tokio::test]
async fn test_auth_network_account_rejects_non_allowlisted_tx_script() -> anyhow::Result<()> {
let allowed_script = expiration_tx_script(10);
let account = build_account_with_allowlists(
vec![placeholder_script_root()],
vec![allowed_script.root()],
)?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
let mock_chain = builder.build()?;
let other_script = CodeBuilder::default().compile_tx_script("begin nop end")?;
assert_ne!(
other_script.root(),
allowed_script.root(),
"the other script must differ from the allowlisted one",
);
let result = mock_chain
.build_tx_context(account.id(), &[], &[])?
.tx_script(other_script)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED);
Ok(())
}
#[rstest]
#[case(10)]
#[case(30)]
#[tokio::test]
async fn test_auth_network_account_accepts_any_of_multiple_allowlisted_roots(
#[case] delta: u16,
) -> anyhow::Result<()> {
let script_10 = expiration_tx_script(10);
let script_30 = expiration_tx_script(30);
let tx_script = expiration_tx_script(delta);
let mut builder = MockChain::builder();
let note = build_input_note()?;
builder.add_output_note(RawOutputNote::Full(note.clone()));
let account = build_account_with_allowlists(
vec![note.script().root().into()],
vec![script_10.root(), script_30.root()],
)?;
builder.add_account(account.clone())?;
let mock_chain = builder.build()?;
let executed = mock_chain
.build_tx_context(account.id(), &[], slice::from_ref(¬e))?
.tx_script(tx_script)
.build()?
.execute()
.await?;
assert_eq!(
executed.expiration_block_num(),
executed.block_header().block_num() + u32::from(delta),
"running one of several allowlisted scripts should set the expiration to reference + delta",
);
Ok(())
}
#[rstest]
#[case(10)]
#[case(30)]
#[case(u16::MAX)]
#[tokio::test]
async fn test_auth_network_account_accepts_allowlisted_tx_script_with_caller_args(
#[case] delta: u16,
) -> anyhow::Result<()> {
let tx_script = expiration_from_args_tx_script();
let mut builder = MockChain::builder();
let note = build_input_note()?;
builder.add_output_note(RawOutputNote::Full(note.clone()));
let account =
build_account_with_allowlists(vec![note.script().root().into()], vec![tx_script.root()])?;
builder.add_account(account.clone())?;
let mock_chain = builder.build()?;
let tx_script_args = Word::new([Felt::from(delta), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
let executed = mock_chain
.build_tx_context(account.id(), &[], slice::from_ref(¬e))?
.tx_script(tx_script)
.tx_script_args(tx_script_args)
.build()?
.execute()
.await?;
assert_eq!(
executed.expiration_block_num(),
executed.block_header().block_num() + u32::from(delta),
"the caller-supplied expiration delta should be applied",
);
Ok(())
}
#[tokio::test]
async fn test_auth_network_account_rejects_when_any_note_disallowed() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let note_allowed = build_input_note()?;
let account = build_allowlist_account(vec![note_allowed.script().root().into()])?;
builder.add_account(account.clone())?;
let note_disallowed = NoteBuilder::new(ACCOUNT_ID_SENDER.try_into()?, &mut rand::rng())
.code(
"\
@note_script
pub proc main
push.1 drop
end
",
)
.build()?;
assert_ne!(
note_disallowed.script().root(),
note_allowed.script().root(),
"disallowed note must have a different script root than the allowed one",
);
builder.add_output_note(RawOutputNote::Full(note_allowed.clone()));
builder.add_output_note(RawOutputNote::Full(note_disallowed.clone()));
let mock_chain = builder.build()?;
let input_notes = [note_allowed, note_disallowed];
let result = mock_chain
.build_tx_context(account.id(), &[], &input_notes)?
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_NOTE_SCRIPT_ALLOWLIST_NOTE_NOT_ALLOWED);
Ok(())
}
#[tokio::test]
async fn test_auth_network_account_accepts_allowed_note() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let note = build_input_note()?;
let account = build_allowlist_account(vec![note.script().root().into()])?;
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(note.clone()));
let mock_chain = builder.build()?;
mock_chain
.build_tx_context(account.id(), &[], slice::from_ref(¬e))?
.build()?
.execute()
.await?;
Ok(())
}