use std::collections::HashSet;
use std::sync::OnceLock;
use get_size2::GetSize;
use itertools::Itertools;
use neptune_mutator_set::addition_record::AdditionRecord;
use neptune_mutator_set::mutator_set_accumulator::MutatorSetAccumulator;
use neptune_mutator_set::removal_record::removal_record_list::RemovalRecordListUnpackError;
use neptune_mutator_set::removal_record::RemovalRecord;
use neptune_primitives::mast_hash::HasDiscriminant;
use neptune_primitives::mast_hash::MastHash;
use neptune_primitives::timestamp::Timestamp;
use num_traits::Zero;
use serde::Deserialize;
use serde::Serialize;
use strum::EnumCount;
use strum::VariantArray;
use tasm_lib::structure::tasm_object::TasmObject;
use tasm_lib::twenty_first::math::b_field_element::BFieldElement;
use tasm_lib::twenty_first::math::bfield_codec::BFieldCodec;
use tasm_lib::twenty_first::tip5::digest::Digest;
use super::announcement::Announcement;
use crate::transaction::transparent_input::TransparentInput;
use crate::type_scripts::native_currency_amount::NativeCurrencyAmount;
pub(crate) const LUSTRATION_FLAG: BFieldElement = BFieldElement::new(51022176260u64);
#[readonly::make]
#[derive(Debug, Clone, Serialize, Deserialize, GetSize, BFieldCodec, TasmObject)]
pub struct TransactionKernel {
pub inputs: Vec<RemovalRecord>,
pub outputs: Vec<AdditionRecord>,
pub announcements: Vec<Announcement>,
pub fee: NativeCurrencyAmount,
pub coinbase: Option<NativeCurrencyAmount>,
pub timestamp: Timestamp,
pub mutator_set_hash: Digest,
pub merge_bit: bool,
#[serde(skip)]
#[bfield_codec(ignore)]
#[tasm_object(ignore)]
#[get_size(ignore)]
mast_sequences: OnceLock<Vec<Vec<BFieldElement>>>,
}
impl std::fmt::Display for TransactionKernel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"
kernel hash: {mast_hash}
inputs: {inputs}
outputs: {outputs}
announcements: {announcements}
coinbase: {coinbase}
timestamp: {timestamp}
mutator_set_hash: {ms_hash}
merge_bit: {merge_bit}
",
mast_hash = self.mast_hash().to_hex(),
inputs = self.inputs.len(),
outputs = self.outputs.len(),
announcements = self.announcements.len(),
coinbase = self
.coinbase
.unwrap_or_else(|| NativeCurrencyAmount::coins(0)),
timestamp = self.timestamp,
ms_hash = self.mutator_set_hash.to_hex(),
merge_bit = self.merge_bit,
)
}
}
impl PartialEq for TransactionKernel {
fn eq(&self, o: &Self) -> bool {
self.inputs == o.inputs
&& self.outputs == o.outputs
&& self.announcements == o.announcements
&& self.fee == o.fee
&& self.coinbase == o.coinbase
&& self.timestamp == o.timestamp
&& self.mutator_set_hash == o.mutator_set_hash
&& self.merge_bit == o.merge_bit
}
}
impl Eq for TransactionKernel {}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TransactionConfirmabilityError {
InvalidRemovalRecord(usize),
DuplicateInputs,
AlreadySpentInput(usize),
RemovalRecordUnpackFailure,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TransactionLustrationError {
InvalidAoclRangeForIndexSet,
MissingLustrationAnnouncement,
}
impl From<RemovalRecordListUnpackError> for TransactionConfirmabilityError {
fn from(_: RemovalRecordListUnpackError) -> Self {
Self::RemovalRecordUnpackFailure
}
}
impl TransactionKernel {
pub fn is_confirmable_relative_to(
&self,
mutator_set_accumulator: &MutatorSetAccumulator,
) -> Result<(), TransactionConfirmabilityError> {
let inputs = &self.inputs;
let maybe_invalid_removal_record = inputs
.iter()
.enumerate()
.find(|(_, rr)| !rr.validate(mutator_set_accumulator));
if let Some((index, _invalid_removal_record)) = maybe_invalid_removal_record {
return Err(TransactionConfirmabilityError::InvalidRemovalRecord(index));
}
let has_unique_inputs =
inputs.iter().unique_by(|rr| rr.absolute_indices).count() == inputs.len();
if !has_unique_inputs {
return Err(TransactionConfirmabilityError::DuplicateInputs);
}
let already_spent_removal_record = inputs
.iter()
.enumerate()
.find(|(_, rr)| !mutator_set_accumulator.can_remove(rr));
if let Some((index, _already_spent_removal_record)) = already_spent_removal_record {
return Err(TransactionConfirmabilityError::AlreadySpentInput(index));
}
Ok(())
}
pub fn have_merge_relationship(output: &Self, input: &Self) -> bool {
if !output.merge_bit {
return false;
}
if output.inputs.len() < input.inputs.len() {
return false;
}
if output.outputs.len() < input.outputs.len() {
return false;
}
if output.announcements.len() < input.announcements.len() {
return false;
}
if output.timestamp < input.timestamp {
return false;
}
if output.inputs.len() == input.inputs.len()
&& output.outputs.len() == input.outputs.len()
&& output.announcements.len() == input.announcements.len()
{
return false;
}
let new_txs_outputs: HashSet<_> = output.outputs.clone().into_iter().collect();
for old_tx_output in &input.outputs {
if !new_txs_outputs.contains(old_tx_output) {
return false;
}
}
let new_txs_inputs: HashSet<_> = output.inputs.iter().map(|x| x.absolute_indices).collect();
for old_tx_input in &input.inputs {
if !new_txs_inputs.contains(&old_tx_input.absolute_indices) {
return false;
}
}
let new_txs_announcements: HashSet<_> = output.announcements.clone().into_iter().collect();
for old_tx_announcement in &input.announcements {
if !new_txs_announcements.contains(old_tx_announcement) {
return false;
}
}
true
}
pub fn verified_lustration_amount(
&self,
max_lustrating_aocl_leaf_index: u64,
fix_lustration_double_counting: bool,
) -> Result<NativeCurrencyAmount, TransactionLustrationError> {
let mut required_lustrations = vec![];
for input in &self.inputs {
let Ok((input_index_lower_end, _)) = input.absolute_indices.aocl_range() else {
return Err(TransactionLustrationError::InvalidAoclRangeForIndexSet);
};
let must_lustrate = input_index_lower_end <= max_lustrating_aocl_leaf_index;
if must_lustrate {
required_lustrations.push(input.absolute_indices);
}
}
let mut required_lustrations: HashSet<_> = required_lustrations.into_iter().collect();
let all_lustrations = self
.announcements
.iter()
.filter(|ann| {
ann.message
.first()
.is_some_and(|elem0| *elem0 == LUSTRATION_FLAG)
})
.collect_vec();
let mut acc_amount = NativeCurrencyAmount::zero();
for lustration in all_lustrations {
let Ok(lustration) = TransparentInput::decode(&lustration.message[1..]) else {
continue;
};
let implied_index_set = lustration.absolute_index_set();
let was_present = required_lustrations.remove(&implied_index_set);
if was_present {
let is_before_barrier =
lustration.aocl_leaf_index <= max_lustrating_aocl_leaf_index;
if is_before_barrier || !fix_lustration_double_counting {
acc_amount += lustration.utxo.get_native_currency_amount();
}
}
}
if !required_lustrations.is_empty() {
return Err(TransactionLustrationError::MissingLustrationAnnouncement);
}
Ok(acc_amount)
}
}
#[derive(VariantArray, Debug, Clone, EnumCount, Copy, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum TransactionKernelField {
Inputs,
Outputs,
Announcements,
Fee,
Coinbase,
Timestamp,
MutatorSetHash,
MergeBit,
}
impl HasDiscriminant for TransactionKernelField {
fn discriminant(&self) -> usize {
*self as usize
}
}
impl MastHash for TransactionKernel {
type FieldEnum = TransactionKernelField;
fn mast_sequences(&self) -> Vec<Vec<BFieldElement>> {
self.mast_sequences
.get_or_init(|| {
let input_utxos_sequence = self.inputs.encode();
let output_utxos_sequence = self.outputs.encode();
let announcements_sequence = self.announcements.encode();
let fee_sequence = self.fee.encode();
let coinbase_sequence = self.coinbase.encode();
let timestamp_sequence = self.timestamp.encode();
let mutator_set_hash_sequence = self.mutator_set_hash.encode();
let merge_bit_sequence = self.merge_bit.encode();
vec![
input_utxos_sequence,
output_utxos_sequence,
announcements_sequence,
fee_sequence,
coinbase_sequence,
timestamp_sequence,
mutator_set_hash_sequence,
merge_bit_sequence,
]
})
.clone() }
}
#[cfg(any(test, feature = "arbitrary-impls"))]
pub mod neptune_arbitrary {
use arbitrary::Arbitrary;
use itertools::Itertools;
use proptest::prelude::Strategy;
use super::*;
impl TransactionKernel {
pub(crate) fn arbitrary_with_fee<'a>(
u: &mut ::arbitrary::Unstructured<'a>,
fee: NativeCurrencyAmount,
) -> ::arbitrary::Result<Self> {
let num_inputs = u.int_in_range(0..=4)?;
let num_outputs = u.int_in_range(0..=4)?;
let num_announcements = u.int_in_range(0..=2)?;
let num_aocl_leafs = u.int_in_range(0u64..=(1u64 << 63))?;
let seed = u.bytes(32)?;
let rng = proptest::test_runner::TestRng::from_seed(
proptest::test_runner::RngAlgorithm::ChaCha,
&seed.try_into().unwrap_or([0u8; 32]), );
let config = proptest::test_runner::Config::default();
let mut runner = proptest::test_runner::TestRunner::new_with_rng(config, rng);
let inputs = RemovalRecord::arbitrary_synchronized_set(num_aocl_leafs, num_inputs)
.new_tree(&mut runner)
.unwrap()
.current();
let outputs: Vec<AdditionRecord> = (0..num_outputs)
.map(|_| u.arbitrary().unwrap())
.collect_vec();
let announcements: Vec<Announcement> = (0..num_announcements)
.map(|_| u.arbitrary().unwrap())
.collect_vec();
let coinbase: Option<NativeCurrencyAmount> = u.arbitrary()?;
let timestamp: Timestamp = u.arbitrary()?;
let mutator_set_hash: Digest = u.arbitrary()?;
let merge_bit: bool = u.arbitrary()?;
let transaction_kernel = TransactionKernelProxy {
inputs,
outputs,
announcements,
fee,
coinbase,
timestamp,
mutator_set_hash,
merge_bit,
}
.into_kernel();
Ok(transaction_kernel)
}
}
impl<'a> Arbitrary<'a> for TransactionKernel {
fn arbitrary(u: &mut ::arbitrary::Unstructured<'a>) -> ::arbitrary::Result<Self> {
let fee: NativeCurrencyAmount = u.arbitrary()?;
Self::arbitrary_with_fee(u, fee)
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(any(test, feature = "arbitrary-impls"), derive(arbitrary::Arbitrary))]
pub struct TransactionKernelProxy {
pub inputs: Vec<RemovalRecord>,
pub outputs: Vec<AdditionRecord>,
pub announcements: Vec<Announcement>,
pub fee: NativeCurrencyAmount,
pub coinbase: Option<NativeCurrencyAmount>,
pub timestamp: Timestamp,
pub mutator_set_hash: Digest,
pub merge_bit: bool,
}
impl From<TransactionKernel> for TransactionKernelProxy {
fn from(k: TransactionKernel) -> Self {
Self {
inputs: k.inputs,
outputs: k.outputs,
announcements: k.announcements,
fee: k.fee,
coinbase: k.coinbase,
timestamp: k.timestamp,
mutator_set_hash: k.mutator_set_hash,
merge_bit: k.merge_bit,
}
}
}
impl TransactionKernelProxy {
pub fn into_kernel(self) -> TransactionKernel {
TransactionKernel {
inputs: self.inputs,
outputs: self.outputs,
announcements: self.announcements,
fee: self.fee,
coinbase: self.coinbase,
timestamp: self.timestamp,
mutator_set_hash: self.mutator_set_hash,
merge_bit: self.merge_bit,
mast_sequences: Default::default(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct TransactionKernelModifier {
pub inputs: Option<Vec<RemovalRecord>>,
pub outputs: Option<Vec<AdditionRecord>>,
pub announcements: Option<Vec<Announcement>>,
pub fee: Option<NativeCurrencyAmount>,
pub coinbase: Option<Option<NativeCurrencyAmount>>,
pub timestamp: Option<Timestamp>,
pub mutator_set_hash: Option<Digest>,
pub merge_bit: Option<bool>,
}
impl TransactionKernelModifier {
pub fn inputs(mut self, inputs: Vec<RemovalRecord>) -> Self {
self.inputs = Some(inputs);
self
}
pub fn outputs(mut self, outputs: Vec<AdditionRecord>) -> Self {
self.outputs = Some(outputs);
self
}
pub fn announcements(mut self, announcements: Vec<Announcement>) -> Self {
self.announcements = Some(announcements);
self
}
pub fn fee(mut self, fee: NativeCurrencyAmount) -> Self {
self.fee = Some(fee);
self
}
pub fn coinbase(mut self, coinbase: Option<NativeCurrencyAmount>) -> Self {
self.coinbase = Some(coinbase);
self
}
pub fn timestamp(mut self, timestamp: Timestamp) -> Self {
self.timestamp = Some(timestamp);
self
}
pub fn mutator_set_hash(mut self, mutator_set_hash: Digest) -> Self {
self.mutator_set_hash = Some(mutator_set_hash);
self
}
pub fn merge_bit(mut self, merge_bit: bool) -> Self {
self.merge_bit = Some(merge_bit);
self
}
pub fn modify(self, k: TransactionKernel) -> TransactionKernel {
TransactionKernel {
inputs: self.inputs.unwrap_or(k.inputs),
outputs: self.outputs.unwrap_or(k.outputs),
announcements: self.announcements.unwrap_or(k.announcements),
fee: self.fee.unwrap_or(k.fee),
coinbase: self.coinbase.unwrap_or(k.coinbase),
timestamp: self.timestamp.unwrap_or(k.timestamp),
mutator_set_hash: self.mutator_set_hash.unwrap_or(k.mutator_set_hash),
merge_bit: self.merge_bit.unwrap_or(k.merge_bit),
mast_sequences: Default::default(),
}
}
pub fn clone_modify(self, k: &TransactionKernel) -> TransactionKernel {
self.modify(k.clone())
}
}
#[cfg(any(test, feature = "test-helpers"))]
mod test_support {
use arbitrary::Unstructured;
use proptest::prelude::BoxedStrategy;
use proptest::prelude::Strategy;
use proptest_arbitrary_interop::arb;
use super::*;
impl rand::distr::Distribution<TransactionKernel> for rand::distr::StandardUniform {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> TransactionKernel {
TransactionKernel {
inputs: (0..10).map(|_| rng.random()).collect_vec(),
outputs: (0..10).map(|_| rng.random()).collect_vec(),
announcements: (0..10).map(|_| rng.random()).collect_vec(),
fee: rng.random::<NativeCurrencyAmount>().abs(),
coinbase: if rng.random_bool(0.5) {
Some(rng.random())
} else {
None
},
timestamp: rng.random(),
mutator_set_hash: rng.random(),
merge_bit: rng.random(),
mast_sequences: OnceLock::new(),
}
}
}
impl TransactionKernel {
pub fn strategy_with_fee(fee: NativeCurrencyAmount) -> BoxedStrategy<Self> {
const MAX_BYTES: usize = 262144;
proptest::collection::vec(arb::<u8>(), 0..=MAX_BYTES)
.prop_filter_map("could not construct from bytes", move |bytes| {
let mut u = Unstructured::new(&bytes);
Self::arbitrary_with_fee(&mut u, fee).ok()
})
.boxed()
}
pub fn lowest_aocl_leaf_index(&self) -> Option<u64> {
self.inputs
.iter()
.map(|input| {
let (min_leaf, _) = input.absolute_indices.aocl_range().unwrap();
min_leaf
})
.min()
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod tests {
use itertools::Itertools;
use proptest::prelude::Strategy;
use proptest::strategy::ValueTree;
use proptest::test_runner::TestRunner;
use proptest_arbitrary_interop::arb;
use test_strategy::proptest;
use super::*;
use crate::block::mutator_set_update::MutatorSetUpdate;
use crate::transaction::PrimitiveWitness;
use crate::transaction::Transaction;
use crate::transaction::TransactionProof;
#[test]
pub fn arbitrary_tx_kernel_is_deterministic() {
use proptest::prelude::Strategy;
use proptest::strategy::ValueTree;
use proptest::test_runner::TestRunner;
use proptest_arbitrary_interop::arb;
let mut test_runner = TestRunner::deterministic();
let a = arb::<TransactionKernel>()
.new_tree(&mut test_runner)
.unwrap()
.current();
test_runner = TestRunner::deterministic();
let b = arb::<TransactionKernel>()
.new_tree(&mut test_runner)
.unwrap()
.current();
assert_eq!(a.outputs, b.outputs);
assert_eq!(a.fee, b.fee);
assert_eq!(a.coinbase, b.coinbase);
assert_eq!(a.mutator_set_hash, b.mutator_set_hash);
assert_eq!(a.merge_bit, b.merge_bit);
assert_eq!(a.announcements, b.announcements);
assert_eq!(a.timestamp, b.timestamp);
assert_eq!(a.inputs, b.inputs);
assert_eq!(a, b);
}
#[test]
fn can_identify_double_spends() {
let mut test_runner = TestRunner::deterministic();
let pw = PrimitiveWitness::arbitrary_with_size_numbers(Some(2), 2, 2)
.new_tree(&mut test_runner)
.unwrap()
.current();
let mut msa = pw.mutator_set_accumulator.clone();
let tx = Transaction {
kernel: pw.kernel.clone(),
proof: TransactionProof::Witness(pw),
};
assert_eq!(Ok(()), tx.kernel.is_confirmable_relative_to(&msa));
let repeated_input = [tx.kernel.inputs.clone(), vec![tx.kernel.inputs[0].clone()]].concat();
let repeated_input = TransactionKernelModifier::default()
.inputs(repeated_input)
.modify(tx.kernel.clone());
assert!(matches!(
repeated_input.is_confirmable_relative_to(&msa),
Err(TransactionConfirmabilityError::DuplicateInputs),
));
let mut removal_records = tx.kernel.inputs.clone();
let ms_update = MutatorSetUpdate::new(removal_records.clone(), tx.kernel.outputs.clone());
ms_update
.apply_to_accumulator_and_records(
&mut msa,
&mut removal_records.iter_mut().collect_vec(),
&mut [],
)
.unwrap();
let new_tx = TransactionKernelModifier::default()
.inputs(removal_records)
.mutator_set_hash(msa.hash())
.modify(tx.kernel.clone());
assert!(
matches!(
new_tx.is_confirmable_relative_to(&msa),
Err(TransactionConfirmabilityError::AlreadySpentInput(_))
),
"{:?}",
repeated_input.is_confirmable_relative_to(&msa)
);
}
#[proptest]
fn decode_announcement(#[strategy(arb::<Announcement>())] announcement: Announcement) {
let encoded = announcement.encode();
let decoded = *Announcement::decode(&encoded).unwrap();
assert_eq!(announcement, decoded);
}
#[proptest]
fn decode_announcements(#[strategy([arb(), arb()])] announcements: [Announcement; 2]) {
let announcements = announcements.to_vec();
let encoded = announcements.encode();
let decoded = *Vec::<Announcement>::decode(&encoded).unwrap();
assert_eq!(announcements, decoded);
}
#[proptest]
fn test_decode_transaction_kernel(
#[strategy(crate::transaction::test_helpers::txkernel::default(false))]
kernel: TransactionKernel,
) {
let encoded = kernel.encode();
let decoded = *TransactionKernel::decode(&encoded).unwrap();
assert_eq!(kernel, decoded);
}
proptest::proptest! {
#[test]
fn test_decode_transaction_kernel_small(
absolute_indices in neptune_mutator_set::strategies::absindset(),
canonical_commitment in arb::<Digest>(),
mutator_set_hash in arb::<Digest>(),
) {
let removal_record = RemovalRecord {
absolute_indices,
target_chunks: Default::default(),
};
let kernel = TransactionKernelProxy {
inputs: vec![removal_record],
outputs: vec![AdditionRecord {
canonical_commitment
}],
announcements: Default::default(),
fee: NativeCurrencyAmount::one_nau(),
coinbase: None,
timestamp: Default::default(),
mutator_set_hash,
merge_bit: true,
}
.into_kernel();
let encoded = kernel.encode();
println!(
"encoded: {}",
encoded.iter().map(|x| x.to_string()).join(", ")
);
let decoded = *TransactionKernel::decode(&encoded).unwrap();
assert_eq!(kernel, decoded);
}
}
mod lustrations {
use tasm_lib::twenty_first::bfe;
use super::*;
use crate::transaction::utxo::Utxo;
#[proptest(cases = 5)]
fn no_lustration_required_on_new_aocl_leafs(
#[strategy(PrimitiveWitness::arbitrary_with_size_numbers(Some(2), 2, 2))]
primitive_witness: PrimitiveWitness,
) {
let kernel = &primitive_witness.kernel;
assert_eq!(
Ok(NativeCurrencyAmount::zero()),
kernel.verified_lustration_amount(
kernel.lowest_aocl_leaf_index().unwrap() - 1,
false
)
);
}
#[proptest(cases = 5)]
fn returns_error_on_missing_lustration(
#[strategy(PrimitiveWitness::arbitrary_with_size_numbers(Some(2), 2, 2))]
primitive_witness: PrimitiveWitness,
) {
let kernel = &primitive_witness.kernel;
let min_aocl_leaf_index = kernel.lowest_aocl_leaf_index().unwrap();
assert_eq!(
Err(TransactionLustrationError::MissingLustrationAnnouncement),
kernel.verified_lustration_amount(min_aocl_leaf_index, false)
);
assert_eq!(
Err(TransactionLustrationError::MissingLustrationAnnouncement),
kernel.verified_lustration_amount(min_aocl_leaf_index + 1, false)
);
}
#[proptest(cases = 5)]
fn tx_without_inputs_requires_no_lustration(
#[strategy(PrimitiveWitness::arbitrary_with_size_numbers(Some(0), 2, 2))]
primitive_witness: PrimitiveWitness,
) {
let kernel = &primitive_witness.kernel;
assert_eq!(
Ok(NativeCurrencyAmount::zero()),
kernel.verified_lustration_amount(u64::MAX, false)
);
}
fn one_input_kernel(
test_runner: &mut TestRunner,
include_lustration: bool,
) -> TransactionKernel {
use crate::transaction::lock_script::LockScriptAndWitness;
let lock_script_and_witness =
LockScriptAndWitness::genaddr_like_hash_lock_from_seed(Digest::default());
let input_utxo = Utxo::new_native_currency(
lock_script_and_witness.program.hash(),
NativeCurrencyAmount::coins(12),
);
let fee = NativeCurrencyAmount::zero();
let coinbase = None;
let primitive_witness = PrimitiveWitness::arbitrary_primitive_witness_with(
std::slice::from_ref(&input_utxo),
std::slice::from_ref(&lock_script_and_witness),
&[],
&[],
fee,
coinbase,
)
.new_tree(test_runner)
.unwrap()
.current();
let mut kernel = primitive_witness.kernel.clone();
if include_lustration {
let msmp = &primitive_witness.input_membership_proofs[0];
let input = TransparentInput {
utxo: input_utxo.clone(),
aocl_leaf_index: msmp.aocl_leaf_index,
sender_randomness: msmp.sender_randomness,
receiver_preimage: msmp.receiver_preimage,
};
kernel
.announcements
.push(Announcement::lustration_announcement(&input));
}
kernel
}
#[test]
fn lustration_check_one_input() {
let mut test_runner = TestRunner::deterministic();
let no_lustration = one_input_kernel(&mut test_runner, false);
assert_eq!(
Err(TransactionLustrationError::MissingLustrationAnnouncement),
no_lustration.verified_lustration_amount(u64::MAX, false)
);
let mut with_lustration = one_input_kernel(&mut test_runner, true);
assert_eq!(
1,
with_lustration.announcements.len(),
"Lustrating kernel must contain exactly one announcement"
);
assert_eq!(
Ok(NativeCurrencyAmount::coins(12)),
with_lustration.verified_lustration_amount(u64::MAX, false)
);
with_lustration.announcements[0].message[15] = bfe!(u64::MAX);
assert_eq!(
Err(TransactionLustrationError::MissingLustrationAnnouncement),
with_lustration.verified_lustration_amount(u64::MAX, false)
);
}
}
}