extern crate alloc;
use alloc::vec::Vec;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::Felt;
use miden_protocol::account::{Account, AccountId, AccountType};
use miden_protocol::note::Note;
use miden_standards::errors::standards::{
ERR_NOTE_ACTIVE_ACCOUNT_IS_NOT_NETWORK_TARGET_ACCOUNT,
ERR_RBAC_CONFIG_NOTE_IS_NOT_PUBLIC,
ERR_RBAC_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS,
ERR_RBAC_CONFIG_UNKNOWN_VARIANT,
ERR_SENDER_NOT_ROLE_ADMIN,
};
use miden_standards::note::config::{RbacConfig, RbacConfigNote};
use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint};
use miden_standards::testing::note::NoteBuilder;
use miden_testing::{MockChain, assert_transaction_executor_error};
use super::{create_rbac_chain, get_role_admin, is_role_member, role, test_account_id};
use crate::into_private_note;
fn rbac_config_note(
sender: AccountId,
account: AccountId,
config: RbacConfig,
rng: &mut RandomCoin,
) -> anyhow::Result<Note> {
let note = RbacConfigNote::builder()
.sender(sender)
.target(account)
.config(config)
.generate_serial_number(rng)
.build()?
.into();
Ok(note)
}
fn malformed_rbac_config_note(
sender: AccountId,
target: AccountId,
storage: Vec<Felt>,
rng: &mut RandomCoin,
) -> anyhow::Result<Note> {
let note = NoteBuilder::new(sender, rng)
.script(RbacConfigNote::script())
.note_storage(storage)?
.attachment(NetworkAccountTarget::new(target, NoteExecutionHint::Always)?)
.build()?;
Ok(note)
}
async fn execute_note_and_apply(
mock_chain: &MockChain,
account: &Account,
note: Note,
) -> anyhow::Result<Account> {
let executed = mock_chain
.build_transaction(account.clone())
.unauthenticated_input_note(note)
.build()?
.execute()
.await?;
let mut updated = account.clone();
updated.apply_patch(executed.account_patch())?;
Ok(updated)
}
#[tokio::test]
async fn grant_then_revoke_dispatch() -> anyhow::Result<()> {
let admin = test_account_id(41);
let member = test_account_id(42);
let minter = role("MINTER");
let (account, mock_chain) = create_rbac_chain(admin)?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let grant = rbac_config_note(
admin,
account.id(),
RbacConfig::GrantRole { role: minter.clone(), account: member },
&mut rng,
)?;
let granted = execute_note_and_apply(&mock_chain, &account, grant).await?;
assert!(is_role_member(&granted, &minter, member)?);
let revoke = rbac_config_note(
admin,
granted.id(),
RbacConfig::RevokeRole { role: minter.clone(), account: member },
&mut rng,
)?;
let revoked = execute_note_and_apply(&mock_chain, &granted, revoke).await?;
assert!(!is_role_member(&revoked, &minter, member)?);
Ok(())
}
#[tokio::test]
async fn set_role_admin_dispatch() -> anyhow::Result<()> {
let admin = test_account_id(41);
let minter = role("MINTER");
let mint_admin = role("MINT_ADMIN");
let (account, mock_chain) = create_rbac_chain(admin)?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let note = rbac_config_note(
admin,
account.id(),
RbacConfig::SetRoleAdmin {
role: minter.clone(),
admin_role: Some(mint_admin.clone()),
},
&mut rng,
)?;
let updated = execute_note_and_apply(&mock_chain, &account, note).await?;
let admin_role_symbol = get_role_admin(&updated, &minter)?;
assert_eq!(admin_role_symbol, mint_admin.as_element());
Ok(())
}
#[tokio::test]
async fn renounce_dispatch() -> anyhow::Result<()> {
let admin = test_account_id(41);
let member = test_account_id(42);
let minter = role("MINTER");
let (account, mock_chain) = create_rbac_chain(admin)?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let grant = rbac_config_note(
admin,
account.id(),
RbacConfig::GrantRole { role: minter.clone(), account: member },
&mut rng,
)?;
let granted = execute_note_and_apply(&mock_chain, &account, grant).await?;
assert!(is_role_member(&granted, &minter, member)?);
let renounce = rbac_config_note(
member,
granted.id(),
RbacConfig::RenounceRole { role: minter.clone() },
&mut rng,
)?;
let renounced = execute_note_and_apply(&mock_chain, &granted, renounce).await?;
assert!(!is_role_member(&renounced, &minter, member)?);
Ok(())
}
#[tokio::test]
async fn unknown_variant_fails() -> anyhow::Result<()> {
let admin = test_account_id(41);
let (account, mock_chain) = create_rbac_chain(admin)?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let note = malformed_rbac_config_note(admin, account.id(), vec![Felt::from(99u32)], &mut rng)?;
let result = mock_chain
.build_transaction(account.clone())
.unauthenticated_input_note(note)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_RBAC_CONFIG_UNKNOWN_VARIANT);
Ok(())
}
#[tokio::test]
async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
let admin = test_account_id(41);
let (account, mock_chain) = create_rbac_chain(admin)?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let note = malformed_rbac_config_note(admin, account.id(), vec![Felt::from(0u32)], &mut rng)?;
let result = mock_chain
.build_transaction(account.clone())
.unauthenticated_input_note(note)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_RBAC_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS);
Ok(())
}
#[tokio::test]
async fn unauthorized_sender_grant_fails() -> anyhow::Result<()> {
let admin = test_account_id(41);
let outsider = test_account_id(45);
let member = test_account_id(46);
let (account, mock_chain) = create_rbac_chain(admin)?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let note = rbac_config_note(
outsider,
account.id(),
RbacConfig::GrantRole { role: role("MINTER"), account: member },
&mut rng,
)?;
let result = mock_chain
.build_transaction(account.clone())
.unauthenticated_input_note(note)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_SENDER_NOT_ROLE_ADMIN);
Ok(())
}
#[tokio::test]
async fn decoy_account_cannot_consume_note_of_another_account() -> anyhow::Result<()> {
let admin = test_account_id(41);
let member = test_account_id(42);
let (decoy, mock_chain) = create_rbac_chain(admin)?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let target = AccountId::builder().account_type(AccountType::Public).build_with_seed([9; 32]);
let note = rbac_config_note(
admin,
target,
RbacConfig::GrantRole { role: role("MINTER"), account: member },
&mut rng,
)?;
let result = mock_chain
.build_transaction(decoy.clone())
.unauthenticated_input_note(note)
.build()?
.execute()
.await;
assert_transaction_executor_error!(
result,
ERR_NOTE_ACTIVE_ACCOUNT_IS_NOT_NETWORK_TARGET_ACCOUNT
);
Ok(())
}
#[tokio::test]
async fn private_note_cannot_dispatch_the_action() -> anyhow::Result<()> {
let admin = test_account_id(41);
let member = test_account_id(42);
let (account, mock_chain) = create_rbac_chain(admin)?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let note = rbac_config_note(
admin,
account.id(),
RbacConfig::GrantRole { role: role("MINTER"), account: member },
&mut rng,
)?;
let result = mock_chain
.build_transaction(account.clone())
.unauthenticated_input_note(into_private_note(note))
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_RBAC_CONFIG_NOTE_IS_NOT_PUBLIC);
Ok(())
}