use ootle_byte_type::ToByteType;
use tari_crypto::{
keys::{PublicKey as _, SecretKey as _},
ristretto::{RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey},
};
use tari_engine_types::{
limits::STEALTH_LIMITS,
stealth::{MerkleTree, hashlock_digest},
};
use tari_ootle_common_types::{
RistrettoSchnorrBlake2bVerifier,
crypto::create_key_pair_from_seed,
substate_type::SubstateType,
};
use tari_ootle_transaction::{Transaction, args};
use tari_template_lib::types::{
AccessRule,
Amount,
FunctionName,
Hash32,
ResourceAddress,
TemplateAddress,
bytes::Bytes,
constants::STEALTH_TARI_RESOURCE_ADDRESS,
crypto::{NoSignatureDomain, PublicKey, RistrettoPublicKeyBytes, Signature},
stealth::{BuiltinPredicate, Covenant, HashAlg, MerkleProof, SpendCondition, SpendWitness, TemplateFunction},
};
use tari_template_test_tooling::{
TemplateTest,
support::{
assert_error::assert_reject_reason,
spec::{InputAuthSpec, InputSpec, OutputAuthSpec},
stealth,
stealth::StealthSecretTransferData,
},
wallet_crypto::MaskAndValue,
};
const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR");
const TEMPLATE_PATHS: &[&str] = &["tests/templates/stealth", "tests/templates/spend_script"];
const FAUCET_TEMPLATE: &str = "StealthFaucet";
const SCRIPT_TEMPLATE: &str = "SpendScripts";
const NO_OUTPUTS: std::iter::Empty<(u64, OutputAuthSpec)> = std::iter::empty();
fn spend_script(template: TemplateAddress, function: &str, args: Vec<Bytes>) -> TemplateFunction {
TemplateFunction::new(template, FunctionName::try_from(function).unwrap(), args)
}
fn script_condition(template: TemplateAddress, function: &str, args: Vec<Bytes>) -> SpendCondition {
SpendCondition::template_function(spend_script(template, function, args))
}
fn key_path(pk: RistrettoPublicKeyBytes) -> OutputAuthSpec {
OutputAuthSpec::KeyPath(pk)
}
fn conditions(leaves: Vec<SpendCondition>) -> OutputAuthSpec {
OutputAuthSpec::Conditions(leaves)
}
fn key_and_conditions(spend_key: RistrettoPublicKeyBytes, leaves: Vec<SpendCondition>) -> OutputAuthSpec {
OutputAuthSpec::KeyAndConditions {
spend_key,
conditions: leaves,
}
}
fn out(value: u64, auth: impl Into<OutputAuthSpec>) -> (u64, OutputAuthSpec) {
(value, auth.into())
}
fn encode_arg<T: tari_bor::Encode<()>>(value: &T) -> Bytes {
Bytes::from_vec(tari_bor::encode(value).unwrap())
}
fn faucet_new_tx(test: &mut TemplateTest, mint: &StealthSecretTransferData) -> Transaction {
test.enable_auto_add_proofs_from_signers();
let faucet_template = test.get_template_address(FAUCET_TEMPLATE);
let initial_supply = mint.statement.inputs_statement.revealed_amount;
Transaction::builder_localnet()
.call_function(faucet_template, "new", args![
initial_supply,
mint.statement.clone(),
None::<RistrettoPublicKeyBytes>
])
.build_and_seal(test.secret_key())
}
fn mint_utxo(test: &mut TemplateTest, auth: impl Into<OutputAuthSpec>) -> (ResourceAddress, StealthSecretTransferData) {
let mint = stealth::generate_mint_statement(vec![out(100, auth)], 0u64, None);
let tx = faucet_new_tx(test, &mint);
test.execute_expect_success(tx, vec![]);
let resx = test
.get_previous_output_address(SubstateType::Resource)
.as_resource_address()
.unwrap();
(resx, mint)
}
fn mint_utxos(test: &mut TemplateTest, auths: Vec<OutputAuthSpec>) -> (ResourceAddress, StealthSecretTransferData) {
let outputs = auths.into_iter().map(|a| out(100, a)).collect::<Vec<_>>();
let mint = stealth::generate_mint_statement(outputs, 0u64, None);
let tx = faucet_new_tx(test, &mint);
test.execute_expect_success(tx, vec![]);
let resx = test
.get_previous_output_address(SubstateType::Resource)
.as_resource_address()
.unwrap();
(resx, mint)
}
fn spend_into(mint: &StealthSecretTransferData, output: impl Into<OutputAuthSpec>) -> StealthSecretTransferData {
stealth::generate_transfer_data([mint.input_spec_for(0, 100)], 0u64, [out(100, output)], 0u64)
}
#[test]
fn key_path_spend_authorised_by_signer_badge() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let pk = test.to_public_key_bytes();
let (resx, mint) = mint_utxo(&mut test, key_path(pk));
let transfer = spend_into(&mint, key_path(pk));
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn key_path_spend_rejected_without_signer_badge() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let (_secret, foreign) = create_key_pair_from_seed(42);
let (resx, mint) = mint_utxo(&mut test, key_path(foreign.to_byte_type()));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "was not provided or is not in scope");
}
#[test]
fn script_path_access_rule_leaf_allows_spend() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let (resx, mint) = mint_utxo(
&mut test,
conditions(vec![SpendCondition::access_rule(AccessRule::AllowAll)]),
);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn script_path_access_rule_leaf_denies_spend() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let (resx, mint) = mint_utxo(
&mut test,
conditions(vec![SpendCondition::access_rule(AccessRule::DenyAll)]),
);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Access Denied");
}
#[test]
fn multi_leaf_tree_spends_via_any_committed_leaf() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let leaves = vec![
script_condition(script_template, "always_reject", vec![]),
script_condition(script_template, "timelock", vec![encode_arg(&u64::MAX)]),
script_condition(script_template, "always_ok", vec![]),
];
let mint = stealth::generate_mint_statement(vec![out(100, conditions(leaves.clone()))], 0u64, None);
let tx = faucet_new_tx(&mut test, &mint);
test.execute_expect_success(tx, vec![]);
let resx = test
.get_previous_output_address(SubstateType::Resource)
.as_resource_address()
.unwrap();
let input = InputSpec::with_auth(
MaskAndValue {
mask: mint.output_masks[0].clone(),
value: 100,
},
InputAuthSpec::ScriptPath {
conditions: leaves.clone(),
leaf: leaves[2].clone(),
data: Bytes::default(),
},
);
let transfer =
stealth::generate_transfer_data([input], 0u64, [out(100, key_path(test.to_public_key_bytes()))], 0u64);
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn revealing_a_leaf_not_in_the_tree_is_rejected() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let committed = vec![script_condition(script_template, "always_reject", vec![])];
let mint = stealth::generate_mint_statement(vec![out(100, conditions(committed.clone()))], 0u64, None);
let tx = faucet_new_tx(&mut test, &mint);
test.execute_expect_success(tx, vec![]);
let resx = test
.get_previous_output_address(SubstateType::Resource)
.as_resource_address()
.unwrap();
let forged = vec![script_condition(script_template, "always_ok", vec![])];
let input = InputSpec::with_auth(
MaskAndValue {
mask: mint.output_masks[0].clone(),
value: 100,
},
InputAuthSpec::ScriptPath {
conditions: forged.clone(),
leaf: forged[0].clone(),
data: Bytes::default(),
},
);
let transfer =
stealth::generate_transfer_data([input], 0u64, [out(100, key_path(test.to_public_key_bytes()))], 0u64);
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "not committed in the condition_root");
}
#[test]
fn timelock_allows_spend_at_or_after_unlock_epoch() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (resx, mint) = mint_utxo(
&mut test,
script_condition(script_template, "timelock", vec![encode_arg(&0u64)]),
);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn timelock_rejects_spend_before_unlock_epoch() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (resx, mint) = mint_utxo(
&mut test,
script_condition(script_template, "timelock", vec![encode_arg(&u64::MAX)]),
);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
assert_reject_reason(&reason, "timelock");
}
#[test]
fn covenant_allows_output_that_preserves_condition() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_covenant", vec![]);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_into(&mint, covenant);
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn covenant_rejects_output_that_changes_condition() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_covenant", vec![]);
let (resx, mint) = mint_utxo(&mut test, covenant);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
#[test]
fn covenant_rejects_output_with_added_key_path() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_covenant", vec![]);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_into(&mint, key_and_conditions(test.to_public_key_bytes(), vec![covenant]));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
#[test]
fn covenant_rejects_spend_with_no_stealth_outputs() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_covenant", vec![]);
let (resx, mint) = mint_utxo(&mut test, covenant);
let transfer = stealth::generate_transfer_data([mint.input_spec_for(0, 100)], 0u64, NO_OUTPUTS, 100u64);
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
fn spend_with_covenant(
mint: &StealthSecretTransferData,
covenant: &SpendCondition,
outputs: Vec<(u64, OutputAuthSpec)>,
) -> StealthSecretTransferData {
stealth::generate_transfer_data(
[(
MaskAndValue {
mask: mint.output_masks[0].clone(),
value: 100,
},
covenant.clone(),
)],
0u64,
outputs,
0u64,
)
}
#[test]
fn covenant_balance_allows_full_conservation() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_balance", vec![]);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![out(100, covenant.clone())]);
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn covenant_balance_rejects_value_leaving_partition() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_balance", vec![]);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![out(100, key_path(test.to_public_key_bytes()))]);
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
#[test]
fn covenant_balance_allows_withdrawal_within_allowance() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_balance_with_allowance", vec![encode_arg(
&30u64,
)]);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![
out(70, covenant.clone()),
out(30, key_path(test.to_public_key_bytes())),
]);
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn covenant_balance_rejects_withdrawal_over_allowance() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_balance_with_allowance", vec![encode_arg(
&30u64,
)]);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![
out(60, covenant.clone()),
out(40, key_path(test.to_public_key_bytes())),
]);
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
#[test]
fn covenant_balance_verifies_each_partition_independently() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant_a = script_condition(script_template, "preserve_balance", vec![]);
let covenant_b = script_condition(script_template, "preserve_balance_with_allowance", vec![encode_arg(
&50u64,
)]);
let (resx, mint) = mint_utxos(&mut test, vec![
conditions(vec![covenant_a.clone()]),
conditions(vec![covenant_b.clone()]),
]);
let transfer = stealth::generate_transfer_data(
[
(
MaskAndValue {
mask: mint.output_masks[0].clone(),
value: 100,
},
covenant_a.clone(),
),
(
MaskAndValue {
mask: mint.output_masks[1].clone(),
value: 100,
},
covenant_b.clone(),
),
],
0u64,
vec![
out(100, covenant_a),
out(50, covenant_b),
out(50, key_path(test.to_public_key_bytes())),
],
0u64,
);
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn covenant_balance_rejects_understated_withdrawal() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let covenant = script_condition(script_template, "preserve_balance_with_allowance", vec![encode_arg(
&30u64,
)]);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let mut transfer = spend_with_covenant(&mint, &covenant, vec![
out(60, covenant.clone()),
out(40, key_path(test.to_public_key_bytes())),
]);
assert_eq!(transfer.statement.covenant_claims.len(), 1);
transfer.statement.covenant_claims[0].revealed_amount = Amount::from_u64(30);
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
#[test]
fn covenant_balance_allowance_vault_persists_across_spends() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let vault = script_condition(script_template, "preserve_balance_with_allowance", vec![encode_arg(
&30u64,
)]);
let recipient = key_path(test.to_public_key_bytes());
let (resx, mint) = mint_utxo(&mut test, vault.clone());
let spend1 = spend_with_covenant(&mint, &vault, vec![out(70, vault.clone()), out(30, recipient.clone())]);
let vault_70 = spend1.output_masks[0].clone();
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, spend1.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
let spend2 = stealth::generate_transfer_data(
[(
MaskAndValue {
mask: vault_70,
value: 70,
},
vault.clone(),
)],
0u64,
vec![out(40, vault.clone()), out(30, recipient.clone())],
0u64,
);
let vault_40 = spend2.output_masks[0].clone();
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, spend2.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
let over = stealth::generate_transfer_data(
[(
MaskAndValue {
mask: vault_40,
value: 40,
},
vault.clone(),
)],
0u64,
vec![out(5, vault.clone()), out(35, recipient)],
0u64,
);
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, over.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
#[test]
fn always_reject_aborts_spend() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (resx, mint) = mint_utxo(&mut test, script_condition(script_template, "always_reject", vec![]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
#[test]
fn read_only_sandbox_blocks_state_mutation() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (resx, mint) = mint_utxo(&mut test, script_condition(script_template, "try_write", vec![]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
assert_reject_reason(&reason, "read-only execution context");
}
#[test]
fn sandbox_denies_emit_event() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (resx, mint) = mint_utxo(&mut test, script_condition(script_template, "try_emit_event", vec![]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
assert_reject_reason(&reason, "forbidden inside a read-only");
}
#[test]
fn sandbox_denies_cross_template_call() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (resx, mint) = mint_utxo(
&mut test,
script_condition(script_template, "try_cross_template_call", vec![encode_arg(
&script_template,
)]),
);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
assert_reject_reason(&reason, "call_invoke");
}
#[test]
fn spend_script_exceeding_compute_budget_aborts() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (resx, mint) = mint_utxo(&mut test, script_condition(script_template, "exhaust_budget", vec![]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
}
const SPEND_SIG_DOMAIN: &[u8] = b"tari.test.spend_script signature domain";
const SPEND_SIG_MESSAGE: &[u8] = b"spend authorisation";
fn sign_spend(secret: &RistrettoSecretKey, message: &[u8]) -> Signature<NoSignatureDomain> {
let (nonce, nonce_pub) = RistrettoPublicKey::random_keypair(&mut rand::rng());
let public_key = RistrettoPublicKey::from_secret_key(secret);
let challenge = RistrettoSchnorrBlake2bVerifier::compute_challenge(
SPEND_SIG_DOMAIN,
message,
&public_key.to_byte_type(),
&nonce_pub.to_byte_type(),
);
let sig = RistrettoSchnorr::sign_raw_uniform(secret, nonce, &challenge).unwrap();
sig.to_byte_type().into()
}
#[test]
fn signature_lock_allows_valid_signature() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (secret, public) = create_key_pair_from_seed(7);
let public = PublicKey::from(public.to_byte_type());
let signature = sign_spend(&secret, SPEND_SIG_MESSAGE);
let condition = script_condition(script_template, "require_signature", vec![
encode_arg(&public),
encode_arg(&signature),
]);
let (resx, mint) = mint_utxo(&mut test, condition);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
#[test]
fn signature_lock_rejects_invalid_signature() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (secret, public) = create_key_pair_from_seed(7);
let public = PublicKey::from(public.to_byte_type());
let signature = sign_spend(&secret, b"some other message");
let condition = script_condition(script_template, "require_signature", vec![
encode_arg(&public),
encode_arg(&signature),
]);
let (resx, mint) = mint_utxo(&mut test, condition);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, "Spend script rejected the spend");
assert_reject_reason(&reason, "invalid spend signature");
}
fn assert_spend_rejected(function: &str, args: Vec<Bytes>, expected: &str) {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let (resx, mint) = mint_utxo(&mut test, script_condition(script_template, function, args));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, expected);
}
#[test]
fn spend_rejects_unknown_function() {
assert_spend_rejected("does_not_exist", vec![], "not found");
}
#[test]
fn spend_rejects_mutable_function() {
assert_spend_rejected("bad_mutable", vec![], "must not be mutable");
}
#[test]
fn spend_rejects_non_unit_function() {
assert_spend_rejected("bad_returns_value", vec![], "must return unit");
}
#[test]
fn spend_rejects_missing_context_arg() {
assert_spend_rejected("bad_no_context", vec![encode_arg(&0u64)], "must take a SpendContext");
}
#[test]
fn spend_rejects_wrong_bound_arg_count() {
assert_spend_rejected("timelock", vec![], "bound argument");
}
#[test]
fn spend_rejects_malformed_bound_arg() {
assert_spend_rejected("timelock", vec![Bytes::from_vec(vec![0x00, 0x00])], "well-formed CBOR");
}
#[test]
fn script_path_witness_increases_transaction_weight() {
let test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let large_leaf = script_condition(script_template, "always_ok", vec![Bytes::from_vec(vec![7u8; 2000])]);
let mask = RistrettoSecretKey::random(&mut rand::rng());
let script_spend = stealth::generate_transfer_data(
[(
MaskAndValue {
mask: mask.clone(),
value: 100,
},
large_leaf,
)],
0u64,
NO_OUTPUTS,
100u64,
);
let key_spend = stealth::generate_transfer_data([MaskAndValue { mask, value: 100 }], 0u64, NO_OUTPUTS, 100u64);
let script_tx = Transaction::builder_localnet()
.stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS, script_spend.statement)
.finish()
.seal(test.secret_key());
let key_tx = Transaction::builder_localnet()
.stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS, key_spend.statement)
.finish()
.seal(test.secret_key());
assert!(
script_tx.calculate_transaction_weight().as_u64() > key_tx.calculate_transaction_weight().as_u64(),
"A script-path spend should weigh more than an equivalent key-path spend",
);
}
fn builtin(predicate: BuiltinPredicate) -> SpendCondition {
SpendCondition::builtin(predicate)
}
fn covenant(covenant: Covenant) -> SpendCondition {
SpendCondition::covenant(covenant)
}
fn all(conditions: Vec<SpendCondition>) -> SpendCondition {
SpendCondition::all(conditions.into_iter().flat_map(|c| c.into_conditions().into_vec()))
}
fn submit_expect_success(test: &mut TemplateTest, resx: ResourceAddress, transfer: StealthSecretTransferData) {
test.execute_expect_success(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
}
fn submit_expect_rejected(
test: &mut TemplateTest,
resx: ResourceAddress,
transfer: StealthSecretTransferData,
expected: &str,
) {
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.stealth_transfer(resx, transfer.statement)
.finish()
.seal(test.secret_key()),
vec![],
);
assert_reject_reason(&reason, expected);
}
fn spend_leaf_with_data(
mint: &StealthSecretTransferData,
leaf: SpendCondition,
data: Bytes,
output: impl Into<OutputAuthSpec>,
) -> StealthSecretTransferData {
let input = InputSpec::with_auth(
MaskAndValue {
mask: mint.output_masks[0].clone(),
value: 100,
},
InputAuthSpec::script_path(vec![leaf.clone()], leaf, data),
);
stealth::generate_transfer_data([input], 0u64, [out(100, output)], 0u64)
}
#[test]
fn builtin_after_epoch_allows_when_reached() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let (resx, mint) = mint_utxo(&mut test, builtin(BuiltinPredicate::AfterEpoch(0)));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn builtin_after_epoch_rejects_before_unlock() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let (resx, mint) = mint_utxo(&mut test, builtin(BuiltinPredicate::AfterEpoch(u64::MAX)));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn builtin_before_epoch_allows_before_deadline() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let (resx, mint) = mint_utxo(&mut test, builtin(BuiltinPredicate::BeforeEpoch(u64::MAX)));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn builtin_before_epoch_rejects_at_or_after_deadline() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let (resx, mint) = mint_utxo(&mut test, builtin(BuiltinPredicate::BeforeEpoch(0)));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn builtin_output_preserves_condition_allows_preserving_output() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let covenant = covenant(Covenant::OutputPreservesCondition);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_into(&mint, covenant);
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn builtin_output_preserves_condition_rejects_changed_output() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let covenant = covenant(Covenant::OutputPreservesCondition);
let (resx, mint) = mint_utxo(&mut test, covenant);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn builtin_output_preserves_condition_rejects_added_key_path() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let covenant = covenant(Covenant::OutputPreservesCondition);
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_into(&mint, key_and_conditions(test.to_public_key_bytes(), vec![covenant]));
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn builtin_balance_preserved_allows_full_conservation() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let covenant = covenant(Covenant::BalancePreserved(0));
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![out(100, covenant.clone())]);
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn builtin_balance_preserved_rejects_value_leaving_partition() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let covenant = covenant(Covenant::BalancePreserved(0));
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![out(100, key_path(test.to_public_key_bytes()))]);
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn builtin_balance_preserved_rejects_added_key_path() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let covenant = covenant(Covenant::BalancePreserved(0));
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![out(
100,
key_and_conditions(test.to_public_key_bytes(), vec![covenant.clone()]),
)]);
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn builtin_balance_with_allowance_allows_within_cap() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let covenant = covenant(Covenant::BalancePreserved(30));
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![
out(70, covenant.clone()),
out(30, key_path(test.to_public_key_bytes())),
]);
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn builtin_balance_with_allowance_rejects_over_cap() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let covenant = covenant(Covenant::BalancePreserved(30));
let (resx, mint) = mint_utxo(&mut test, covenant.clone());
let transfer = spend_with_covenant(&mint, &covenant, vec![
out(60, covenant.clone()),
out(40, key_path(test.to_public_key_bytes())),
]);
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn builtin_output_to_allows_required_output() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let target = SpendCondition::access_rule(AccessRule::AllowAll);
let target_root = MerkleTree::from_conditions([&target]).unwrap().root();
let covenant = covenant(Covenant::OutputTo {
condition_root: target_root,
min_value: 0,
});
let (resx, mint) = mint_utxo(&mut test, covenant);
let transfer = spend_into(&mint, conditions(vec![target]));
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn builtin_output_to_rejects_when_target_absent() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let target = SpendCondition::access_rule(AccessRule::AllowAll);
let target_root = MerkleTree::from_conditions([&target]).unwrap().root();
let covenant = covenant(Covenant::OutputTo {
condition_root: target_root,
min_value: 0,
});
let (resx, mint) = mint_utxo(&mut test, covenant);
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn builtin_output_to_rejects_added_key_path() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let target = SpendCondition::access_rule(AccessRule::AllowAll);
let target_root = MerkleTree::from_conditions([&target]).unwrap().root();
let covenant = covenant(Covenant::OutputTo {
condition_root: target_root,
min_value: 0,
});
let (resx, mint) = mint_utxo(&mut test, covenant);
let transfer = spend_into(&mint, key_and_conditions(test.to_public_key_bytes(), vec![target]));
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn hashlock_allows_correct_preimage() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let preimage = b"open sesame".to_vec();
let leaf = builtin(BuiltinPredicate::HashLock {
hash: hashlock_digest(HashAlg::Sha256, &preimage),
alg: HashAlg::Sha256,
});
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let transfer = spend_leaf_with_data(
&mint,
leaf,
Bytes::from_vec(preimage),
key_path(test.to_public_key_bytes()),
);
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn hashlock_rejects_wrong_preimage() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let leaf = builtin(BuiltinPredicate::HashLock {
hash: hashlock_digest(HashAlg::Sha256, b"open sesame"),
alg: HashAlg::Sha256,
});
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let transfer = spend_leaf_with_data(
&mint,
leaf,
Bytes::from_vec(b"wrong".to_vec()),
key_path(test.to_public_key_bytes()),
);
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn hashlock_rejects_empty_data() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let leaf = builtin(BuiltinPredicate::HashLock {
hash: hashlock_digest(HashAlg::Sha256, b"open sesame"),
alg: HashAlg::Sha256,
});
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let transfer = spend_leaf_with_data(&mint, leaf, Bytes::default(), key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn htlc_claim_requires_both_preimage_and_signer() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let preimage = b"htlc secret".to_vec();
let leaf = all(vec![
builtin(BuiltinPredicate::HashLock {
hash: hashlock_digest(HashAlg::Sha256, &preimage),
alg: HashAlg::Sha256,
}),
SpendCondition::access_rule(AccessRule::AllowAll),
]);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let transfer = spend_leaf_with_data(
&mint,
leaf,
Bytes::from_vec(preimage),
key_path(test.to_public_key_bytes()),
);
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn template_function_reads_matching_witness_data() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let expected = vec![1u8, 2, 3, 4];
let leaf = script_condition(script_template, "require_witness_data", vec![encode_arg(&expected)]);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let transfer = spend_leaf_with_data(
&mint,
leaf,
Bytes::from_vec(expected),
key_path(test.to_public_key_bytes()),
);
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn template_function_rejects_mismatched_witness_data() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let leaf = script_condition(script_template, "require_witness_data", vec![encode_arg(&vec![
1u8, 2, 3, 4,
])]);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let transfer = spend_leaf_with_data(
&mint,
leaf,
Bytes::from_vec(vec![9u8, 9, 9]),
key_path(test.to_public_key_bytes()),
);
submit_expect_rejected(&mut test, resx, transfer, "Spend script rejected the spend");
}
#[test]
fn all_conjunction_allows_when_every_condition_holds() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let leaf = all(vec![
builtin(BuiltinPredicate::AfterEpoch(0)),
SpendCondition::access_rule(AccessRule::AllowAll),
]);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_success(&mut test, resx, transfer);
}
#[test]
fn all_conjunction_rejects_when_a_builtin_fails() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let leaf = all(vec![
builtin(BuiltinPredicate::AfterEpoch(u64::MAX)),
SpendCondition::access_rule(AccessRule::AllowAll),
]);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "Spend condition not met");
}
#[test]
fn all_conjunction_rejects_when_access_rule_fails() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let leaf = all(vec![
builtin(BuiltinPredicate::AfterEpoch(0)),
SpendCondition::access_rule(AccessRule::DenyAll),
]);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "Access Denied");
}
#[test]
fn empty_conjunction_is_rejected() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![all(vec![])]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "Empty conjunction");
}
#[test]
fn oversized_conjunction_is_rejected() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let conds =
vec![SpendCondition::access_rule(AccessRule::AllowAll); STEALTH_LIMITS.max_conditions_per_conjunction + 1];
let (resx, mint) = mint_utxo(&mut test, conditions(vec![all(conds)]));
let transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
submit_expect_rejected(&mut test, resx, transfer, "exceeding the limit");
}
#[test]
fn oversized_witness_data_is_rejected() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let leaf = builtin(BuiltinPredicate::HashLock {
hash: hashlock_digest(HashAlg::Sha256, b"x"),
alg: HashAlg::Sha256,
});
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let mut transfer = spend_leaf_with_data(
&mint,
leaf,
Bytes::from_vec(b"x".to_vec()),
key_path(test.to_public_key_bytes()),
);
if let SpendWitness::ScriptPath { leaf, proof, .. } = transfer.statement.inputs_statement.inputs[0].witness.clone()
{
let oversized = Bytes::from_vec(vec![0u8; STEALTH_LIMITS.max_witness_data_len + 1]);
transfer.statement.inputs_statement.inputs[0].witness =
SpendWitness::script_path_with_data(leaf, proof, oversized);
}
submit_expect_rejected(&mut test, resx, transfer, "exceeding the limit");
}
#[test]
fn oversized_inclusion_proof_is_rejected() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let leaf = builtin(BuiltinPredicate::AfterEpoch(0));
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf]));
let mut transfer = spend_into(&mint, key_path(test.to_public_key_bytes()));
if let SpendWitness::ScriptPath { leaf, data, .. } = transfer.statement.inputs_statement.inputs[0].witness.clone() {
let oversized = MerkleProof::new(vec![
Hash32::from_array([0u8; 32]);
STEALTH_LIMITS.max_inclusion_proof_len + 1
]);
transfer.statement.inputs_statement.inputs[0].witness =
SpendWitness::script_path_with_data(leaf, oversized, data);
}
submit_expect_rejected(&mut test, resx, transfer, "exceeding the limit");
}
#[test]
fn data_consuming_builtin_with_template_function_is_rejected() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let script_template = test.get_template_address(SCRIPT_TEMPLATE);
let leaf = all(vec![
builtin(BuiltinPredicate::HashLock {
hash: hashlock_digest(HashAlg::Sha256, b"secret"),
alg: HashAlg::Sha256,
}),
script_condition(script_template, "always_ok", vec![]),
]);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let transfer = spend_leaf_with_data(
&mint,
leaf,
Bytes::from_vec(b"secret".to_vec()),
key_path(test.to_public_key_bytes()),
);
submit_expect_rejected(&mut test, resx, transfer, "sole consumer of the witness data");
}
#[test]
fn two_data_consuming_builtins_in_one_leaf_are_rejected() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
let leaf = all(vec![
builtin(BuiltinPredicate::HashLock {
hash: hashlock_digest(HashAlg::Sha256, b"a"),
alg: HashAlg::Sha256,
}),
builtin(BuiltinPredicate::HashLock {
hash: hashlock_digest(HashAlg::Sha256, b"b"),
alg: HashAlg::Sha256,
}),
]);
let (resx, mint) = mint_utxo(&mut test, conditions(vec![leaf.clone()]));
let transfer = spend_leaf_with_data(
&mint,
leaf,
Bytes::from_vec(b"a".to_vec()),
key_path(test.to_public_key_bytes()),
);
submit_expect_rejected(&mut test, resx, transfer, "at most one may consume the witness data");
}