use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{AccountId, AccountType, StorageMapKey};
use miden_protocol::block::account_tree::AccountIdKey;
use miden_protocol::note::Note;
use miden_protocol::transaction::RawOutputNote;
use miden_protocol::{Felt, Word};
use miden_standards::account::policies::AllowlistStorage;
use miden_standards::errors::standards::{
ERR_ALLOWLIST_CONFIG_TARGET_ACCOUNT_MISMATCH,
ERR_ALLOWLIST_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS,
ERR_ALLOWLIST_CONFIG_UNKNOWN_SELECTOR,
};
use miden_standards::note::{
AllowlistConfig,
AllowlistConfigNote,
NetworkAccountTarget,
NoteExecutionHint,
};
use miden_standards::testing::note::NoteBuilder;
use miden_testing::{Auth, MockChain, assert_transaction_executor_error};
use super::{
add_faucet_with_owner_allowlist_transfer,
add_rbac_faucet_with_allowlist,
dummy_owner,
};
use crate::consume_note;
use crate::scripts::rbac::{build_grant_role_note, role, test_account_id};
fn allowlist_config_note(
sender: AccountId,
faucet_id: AccountId,
config: AllowlistConfig,
rng_seed: u32,
) -> anyhow::Result<Note> {
let mut rng = RandomCoin::new([Felt::from(rng_seed); 4].into());
let note = AllowlistConfigNote::builder()
.sender(sender)
.target(faucet_id)
.config(config)
.generate_serial_number(&mut rng)
.build()?
.into();
Ok(note)
}
fn malformed_allowlist_config_note(
sender: AccountId,
target: AccountId,
storage: Vec<Felt>,
rng_seed: u32,
) -> anyhow::Result<Note> {
let mut rng = RandomCoin::new([Felt::from(rng_seed); 4].into());
let note = NoteBuilder::new(sender, &mut rng)
.script(AllowlistConfigNote::script())
.note_storage(storage)?
.attachment(NetworkAccountTarget::new(target, NoteExecutionHint::Always)?)
.build()?;
Ok(note)
}
fn is_allowed(
mock_chain: &MockChain,
faucet_id: AccountId,
target: AccountId,
) -> anyhow::Result<bool> {
let faucet = mock_chain.committed_account(faucet_id)?;
let key = StorageMapKey::new(AccountIdKey::from(target).as_word());
let word = faucet.storage().get_map_item(AllowlistStorage::allowed_accounts_slot(), key)?;
Ok(word != Word::default())
}
#[tokio::test]
async fn allow_then_disallow_dispatch() -> anyhow::Result<()> {
let owner_id = dummy_owner();
let mut builder = MockChain::builder();
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let faucet = add_faucet_with_owner_allowlist_transfer(&mut builder, owner_id)?;
let allow = allowlist_config_note(
owner_id,
faucet.id(),
AllowlistConfig::AllowAccount { account: target_account.id() },
1,
)?;
let disallow = allowlist_config_note(
owner_id,
faucet.id(),
AllowlistConfig::DisallowAccount { account: target_account.id() },
2,
)?;
for note in [&allow, &disallow] {
builder.add_output_note(RawOutputNote::Full(note.clone()));
}
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
assert!(!is_allowed(&mock_chain, faucet.id(), target_account.id())?);
consume_note(&mut mock_chain, faucet.id(), &allow).await?;
assert!(is_allowed(&mock_chain, faucet.id(), target_account.id())?);
consume_note(&mut mock_chain, faucet.id(), &disallow).await?;
assert!(!is_allowed(&mock_chain, faucet.id(), target_account.id())?);
Ok(())
}
#[tokio::test]
async fn rbac_allowlister_can_allow() -> anyhow::Result<()> {
let admin = test_account_id(80);
let allowlister = test_account_id(81);
let mut builder = MockChain::builder();
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let faucet = add_rbac_faucet_with_allowlist(&mut builder, admin, [])?;
let grant = build_grant_role_note(admin, &role("ALLOWLISTER"), allowlister)?;
let allow = allowlist_config_note(
allowlister,
faucet.id(),
AllowlistConfig::AllowAccount { account: target_account.id() },
3,
)?;
for note in [&grant, &allow] {
builder.add_output_note(RawOutputNote::Full(note.clone()));
}
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
consume_note(&mut mock_chain, faucet.id(), &grant).await?;
consume_note(&mut mock_chain, faucet.id(), &allow).await?;
assert!(is_allowed(&mock_chain, faucet.id(), target_account.id())?);
Ok(())
}
#[tokio::test]
async fn unknown_selector_fails() -> anyhow::Result<()> {
let owner_id = dummy_owner();
let mut builder = MockChain::builder();
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let faucet = add_faucet_with_owner_allowlist_transfer(&mut builder, owner_id)?;
let note = malformed_allowlist_config_note(
owner_id,
faucet.id(),
vec![
Felt::from(99u32),
target_account.id().suffix(),
target_account.id().prefix().as_felt(),
],
6,
)?;
let mock_chain = builder.build()?;
let result = mock_chain
.build_transaction(faucet.id())
.unauthenticated_input_note(note)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_ALLOWLIST_CONFIG_UNKNOWN_SELECTOR);
Ok(())
}
#[tokio::test]
async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
let owner_id = dummy_owner();
let mut builder = MockChain::builder();
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let faucet = add_faucet_with_owner_allowlist_transfer(&mut builder, owner_id)?;
let note = malformed_allowlist_config_note(
owner_id,
faucet.id(),
vec![Felt::from(0u32), target_account.id().suffix()],
7,
)?;
let mock_chain = builder.build()?;
let result = mock_chain
.build_transaction(faucet.id())
.unauthenticated_input_note(note)
.build()?
.execute()
.await;
assert_transaction_executor_error!(
result,
ERR_ALLOWLIST_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
);
Ok(())
}
#[tokio::test]
async fn decoy_faucet_cannot_consume_note_of_another_faucet() -> anyhow::Result<()> {
let owner_id = dummy_owner();
let mut builder = MockChain::builder();
let target_account = builder.add_existing_wallet(Auth::IncrNonce)?;
let decoy = add_faucet_with_owner_allowlist_transfer(&mut builder, owner_id)?;
let target = AccountId::builder().account_type(AccountType::Public).build_with_seed([9; 32]);
let note = allowlist_config_note(
owner_id,
target,
AllowlistConfig::AllowAccount { account: target_account.id() },
9,
)?;
let mock_chain = builder.build()?;
let result = mock_chain
.build_transaction(decoy.id())
.unauthenticated_input_note(note)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_ALLOWLIST_CONFIG_TARGET_ACCOUNT_MISMATCH);
Ok(())
}