use std::collections::HashMap;
use get_size2::GetSize;
use itertools::Itertools;
use num_traits::Zero;
use serde::Deserialize;
use serde::Serialize;
use tasm_lib::prelude::Digest;
use tasm_lib::prelude::TasmObject;
use tasm_lib::prelude::Tip5;
use tasm_lib::twenty_first::math::b_field_element::BFieldElement;
use tasm_lib::twenty_first::math::bfield_codec::BFieldCodec;
use tasm_lib::twenty_first::util_types::mmr::mmr_accumulator::MmrAccumulator;
use tasm_lib::twenty_first::util_types::mmr::mmr_membership_proof::MmrMembershipProof;
use tasm_lib::twenty_first::util_types::mmr::mmr_trait::LeafMutation;
use tasm_lib::twenty_first::util_types::mmr::mmr_trait::Mmr;
use super::active_window::ActiveWindow;
use super::addition_record::AdditionRecord;
use super::ms_membership_proof::MsMembershipProof;
use super::removal_record::absolute_index_set::AbsoluteIndexSet;
use super::removal_record::chunk::Chunk;
use super::removal_record::chunk_dictionary::ChunkDictionary;
use super::removal_record::RemovalRecord;
use super::shared::BATCH_SIZE;
use super::shared::CHUNK_SIZE;
use super::shared::WINDOW_SIZE;
use crate::aocl_to_swbfi_leaf_counts;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, GetSize, BFieldCodec, TasmObject)]
#[cfg_attr(any(test, feature = "arbitrary-impls"), derive(arbitrary::Arbitrary))]
pub struct MutatorSetAccumulator {
pub aocl: MmrAccumulator,
pub swbf_inactive: MmrAccumulator,
pub swbf_active: ActiveWindow,
}
impl Default for MutatorSetAccumulator {
fn default() -> Self {
Self {
aocl: MmrAccumulator::new_from_leafs(vec![]),
swbf_inactive: MmrAccumulator::new_from_leafs(vec![]),
swbf_active: Default::default(),
}
}
}
impl MutatorSetAccumulator {
pub fn new(
aocl: &[Digest],
aocl_leaf_count: u64,
swbf_inactive: &[Digest],
swbf_active: &ActiveWindow,
) -> Self {
let swbf_inactive_leaf_count = aocl_to_swbfi_leaf_counts(aocl_leaf_count);
Self {
aocl: MmrAccumulator::init(aocl.to_vec(), aocl_leaf_count),
swbf_inactive: MmrAccumulator::init(swbf_inactive.to_vec(), swbf_inactive_leaf_count),
swbf_active: swbf_active.clone(),
}
}
pub fn add_helper(&mut self, addition_record: &AdditionRecord) -> Option<(u64, Chunk)> {
let item_index = self.aocl.num_leafs();
self.aocl
.append(addition_record.canonical_commitment.to_owned());
if !Self::window_slides(item_index) {
return None;
}
let new_chunk: Chunk = self.swbf_active.slid_chunk();
let chunk_digest: Digest = Tip5::hash(&new_chunk);
let new_chunk_index = self.swbf_inactive.num_leafs();
self.swbf_inactive.append(chunk_digest);
self.swbf_active.slide_window();
Some((new_chunk_index, new_chunk))
}
pub fn get_batch_index(&self) -> u64 {
aocl_to_swbfi_leaf_counts(self.aocl.num_leafs())
}
pub fn active_window_chunk_interval(&self) -> (u64, u64) {
let batch_index = self.get_batch_index();
(
batch_index,
batch_index + u64::from(WINDOW_SIZE / CHUNK_SIZE),
)
}
pub fn remove_helper(&mut self, removal_record: &RemovalRecord) -> HashMap<u64, Chunk> {
let batch_index = self.get_batch_index();
let active_window_start = u128::from(batch_index) * u128::from(CHUNK_SIZE);
let mut new_target_chunks: ChunkDictionary = removal_record.target_chunks.clone();
let chunkindices_to_indices_dict: HashMap<u64, Vec<u128>> =
removal_record.get_chunkidx_to_indices_dict();
for (chunk_index, absolute_indices) in chunkindices_to_indices_dict {
if chunk_index >= batch_index {
for index in absolute_indices {
let relative_index = u32::try_from(index - active_window_start)
.expect("Relative index for active window must be valid u32");
self.swbf_active.insert(relative_index);
}
continue;
}
let new_target_chunks_clone = new_target_chunks.clone();
let relevant_chunk = new_target_chunks
.get_mut(&chunk_index)
.unwrap_or_else(|| {
panic!(
"Can't get chunk index {chunk_index} from removal record dictionary! dictionary: {:?}\nAOCL size: {}\nbatch index: {}\nRemoval record: {:?}",
new_target_chunks_clone,
self.aocl.num_leafs(),
batch_index,
removal_record
)
});
for index in absolute_indices {
let relative_index = (index % u128::from(CHUNK_SIZE)) as u32;
relevant_chunk.1.insert(relative_index);
}
}
let mutation_data = new_target_chunks.chunk_indices_and_membership_proofs_and_leafs();
self.swbf_inactive.batch_mutate_leaf_and_update_mps(
&mut [],
&[],
mutation_data
.iter()
.map(|(i, p, l)| LeafMutation::new(*i, *l, p.clone()))
.collect_vec(),
);
new_target_chunks
.into_iter()
.map(|(chunk_index, (_mp, chunk))| (chunk_index, chunk))
.collect()
}
pub fn can_remove(&self, removal_record: &RemovalRecord) -> bool {
let mut have_absent_index = false;
if !removal_record.validate(self) {
return false;
}
let swbfi_num_leafs = self.get_batch_index();
let active_window_start = u128::from(swbfi_num_leafs) * u128::from(CHUNK_SIZE);
for inserted_index in removal_record.absolute_indices.to_vec() {
if inserted_index < active_window_start {
let inserted_index_chunkidx = (inserted_index / u128::from(CHUNK_SIZE)) as u64;
let (_mmr_mp, chunk) = removal_record
.target_chunks
.get(&inserted_index_chunkidx)
.expect("Presence of required MMR MPs should have already been established.");
let relative_index = (inserted_index % u128::from(CHUNK_SIZE)) as u32;
if !chunk.contains(relative_index) {
have_absent_index = true;
break;
}
} else {
let relative_index = (inserted_index - active_window_start) as u32;
if !self.swbf_active.contains(relative_index) {
have_absent_index = true;
break;
}
}
}
have_absent_index
}
}
impl MutatorSetAccumulator {
pub fn prove(
&self,
item: Digest,
sender_randomness: Digest,
receiver_preimage: Digest,
) -> MsMembershipProof {
let item_commitment = Tip5::hash_pair(item, sender_randomness);
let aocl_leaf_index = self.aocl.num_leafs();
let auth_path_aocl = self.aocl.to_accumulator().append(item_commitment);
let target_chunks: ChunkDictionary = ChunkDictionary::default();
MsMembershipProof {
sender_randomness: sender_randomness.to_owned(),
receiver_preimage: receiver_preimage.to_owned(),
auth_path_aocl,
target_chunks,
aocl_leaf_index,
}
}
pub fn verify(&self, item: Digest, membership_proof: &MsMembershipProof) -> bool {
let aocl_leaf_count = self.aocl.num_leafs();
if aocl_leaf_count <= membership_proof.aocl_leaf_index {
return false;
}
let leaf = Tip5::hash_pair(
Tip5::hash_pair(item, membership_proof.sender_randomness),
Tip5::hash_pair(
membership_proof.receiver_preimage,
Digest::new([BFieldElement::zero(); Digest::LEN]),
),
);
let is_aocl_member = membership_proof.auth_path_aocl.verify(
membership_proof.aocl_leaf_index,
leaf,
&self.aocl.peaks(),
aocl_leaf_count,
);
if !is_aocl_member {
return false;
}
let mut has_absent_index = false;
let mut entries_in_dictionary = true;
let mut all_auth_paths_are_valid = true;
let current_batch_index: u64 = self.get_batch_index();
let window_start = u128::from(current_batch_index) * u128::from(CHUNK_SIZE);
let all_indices = AbsoluteIndexSet::compute(
item,
membership_proof.sender_randomness,
membership_proof.receiver_preimage,
membership_proof.aocl_leaf_index,
);
let Ok((indices_in_inactive_swbf, indices_in_active_swbf)) =
all_indices.split_by_activity(self)
else {
return false;
};
for (chunk_index, indices) in indices_in_inactive_swbf {
if !membership_proof.target_chunks.contains_key(&chunk_index) {
entries_in_dictionary = false;
break;
}
let (swbf_inactive_mp, swbf_inactive_chunk): &(MmrMembershipProof, Chunk) =
membership_proof.target_chunks.get(&chunk_index).unwrap();
let valid_auth_path = swbf_inactive_mp.verify(
chunk_index,
Tip5::hash(swbf_inactive_chunk),
&self.swbf_inactive.peaks(),
self.swbf_inactive.num_leafs(),
);
all_auth_paths_are_valid = all_auth_paths_are_valid && valid_auth_path;
'inner_inactive: for index in indices {
let index_within_chunk = index % u128::from(CHUNK_SIZE);
if !swbf_inactive_chunk.contains(index_within_chunk as u32) {
has_absent_index = true;
break 'inner_inactive;
}
}
}
for index in indices_in_active_swbf {
let relative_index = index - window_start;
if !self.swbf_active.contains(relative_index as u32) {
has_absent_index = true;
break;
}
}
is_aocl_member && entries_in_dictionary && all_auth_paths_are_valid && has_absent_index
}
pub fn drop(&self, item: Digest, membership_proof: &MsMembershipProof) -> RemovalRecord {
let indices: AbsoluteIndexSet = AbsoluteIndexSet::compute(
item,
membership_proof.sender_randomness,
membership_proof.receiver_preimage,
membership_proof.aocl_leaf_index,
);
RemovalRecord {
absolute_indices: indices,
target_chunks: membership_proof.target_chunks.clone(),
}
}
pub fn add(&mut self, addition_record: &AdditionRecord) {
self.add_helper(addition_record);
}
pub fn remove(&mut self, removal_record: &RemovalRecord) {
self.remove_helper(removal_record);
}
pub fn hash(&self) -> Digest {
let aocl_mmr_bagged = self.aocl.bag_peaks();
let inactive_swbf_bagged = self.swbf_inactive.bag_peaks();
let active_swbf_bagged = Tip5::hash(&self.swbf_active);
let default = Digest::default();
Tip5::hash_pair(
Tip5::hash_pair(aocl_mmr_bagged, inactive_swbf_bagged),
Tip5::hash_pair(active_swbf_bagged, default),
)
}
pub fn batch_remove(
&mut self,
mut removal_records: Vec<RemovalRecord>,
preserved_membership_proofs: &mut [&mut MsMembershipProof],
) -> HashMap<u64, Chunk> {
{
let batch_index = self.get_batch_index();
let active_window_start = u128::from(batch_index) * u128::from(CHUNK_SIZE);
let all_removal_records_indices: Vec<u128> = removal_records
.iter()
.map(|x| x.absolute_indices.to_vec())
.concat();
let mut chunkidx_to_chunk_difference_dict: HashMap<u64, Chunk> = HashMap::new();
for index in all_removal_records_indices {
if index >= active_window_start {
let relative_index = (index - active_window_start) as u32;
self.swbf_active.insert(relative_index);
} else {
chunkidx_to_chunk_difference_dict
.entry((index / u128::from(CHUNK_SIZE)) as u64)
.or_insert_with(Chunk::empty_chunk)
.insert((index % u128::from(CHUNK_SIZE)) as u32);
}
}
let mut mutation_data_preimage: HashMap<u64, (&mut Chunk, MmrMembershipProof)> =
HashMap::new();
for removal_record in &mut removal_records {
for (chunk_index, (mmr_mp, chunk)) in removal_record.target_chunks.iter_mut() {
let chunk_hash = Tip5::hash(chunk);
let prev_val =
mutation_data_preimage.insert(*chunk_index, (chunk, mmr_mp.to_owned()));
if let Some((chnk, mm)) = prev_val {
assert!(mm == *mmr_mp && chunk_hash == Tip5::hash(chnk))
}
}
}
for (chunk_index, (chunk, _)) in &mut mutation_data_preimage {
**chunk = chunk
.clone()
.combine(chunkidx_to_chunk_difference_dict[chunk_index].clone())
.clone();
}
for mp in preserved_membership_proofs.iter_mut() {
for (chunk_index, (_, chunk)) in mp.target_chunks.iter_mut() {
if mutation_data_preimage.contains_key(chunk_index) {
mutation_data_preimage[chunk_index].0.clone_into(chunk);
}
}
}
let swbf_inactive_mutation_data = mutation_data_preimage
.into_iter()
.map(|(k, v)| (k, Tip5::hash(v.0), v.1))
.collect_vec();
let preserved_mmr_leaf_indices = preserved_membership_proofs
.iter()
.flat_map(|msmp| msmp.target_chunks.iter().map(|(i, _)| *i).collect_vec())
.collect_vec();
let mut preserved_mmr_membership_proofs: Vec<&mut MmrMembershipProof> =
preserved_membership_proofs
.iter_mut()
.flat_map(|x| {
x.target_chunks
.iter_mut()
.map(|y| &mut y.1 .0)
.collect::<Vec<_>>()
})
.collect();
self.swbf_inactive.batch_mutate_leaf_and_update_mps(
&mut preserved_mmr_membership_proofs,
&preserved_mmr_leaf_indices,
swbf_inactive_mutation_data
.iter()
.map(|(i, l, p)| LeafMutation::new(*i, *l, p.clone()))
.collect_vec(),
);
chunkidx_to_chunk_difference_dict
}
}
pub fn window_slides(added_index: u64) -> bool {
added_index != 0 && added_index.is_multiple_of(u64::from(BATCH_SIZE))
}
pub fn window_slides_back(removed_index: u64) -> bool {
Self::window_slides(removed_index)
}
}
#[cfg(any(test, feature = "arbitrary-impls"))]
impl MutatorSetAccumulator {
pub(crate) fn is_consistent(&self) -> bool {
let expected_num_leafs_bfmmr = aocl_to_swbfi_leaf_counts(self.aocl.num_leafs());
let correct_bfmmr_num_leafs = expected_num_leafs_bfmmr == self.swbf_inactive.num_leafs();
let consistent_aocl =
usize::try_from(self.aocl.num_leafs().count_ones()).unwrap() == self.aocl.peaks().len();
let consistent_swbf = usize::try_from(self.swbf_inactive.num_leafs().count_ones()).unwrap()
== self.swbf_inactive.peaks().len();
correct_bfmmr_num_leafs && consistent_aocl && consistent_swbf
}
}
#[cfg(any(test, feature = "test-helpers"))]
impl rand::distr::Distribution<MutatorSetAccumulator> for rand::distr::StandardUniform {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> MutatorSetAccumulator {
let random_mmr_accumulator = |seed| {
use rand::rngs::StdRng;
use rand::Rng;
use rand::RngCore;
use rand::SeedableRng;
let mut inner_rng = StdRng::from_seed(seed);
let leaf_count = inner_rng.next_u64();
let num_peaks = leaf_count.count_ones();
MmrAccumulator::init(
(0..num_peaks).map(|_| inner_rng.random()).collect_vec(),
leaf_count,
)
};
MutatorSetAccumulator {
aocl: random_mmr_accumulator(rng.random()),
swbf_inactive: random_mmr_accumulator(rng.random()),
swbf_active: ActiveWindow {
sbf: (0..10).map(|_| rng.random()).collect_vec(),
},
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use itertools::izip;
use itertools::Itertools;
use macro_rules_attr::apply;
use proptest::prelude::*;
use proptest::prop_assert_eq;
use rand::Rng;
use test_strategy::proptest;
use super::*;
use crate::commit;
use crate::shared::BATCH_SIZE;
use crate::shared::CHUNK_SIZE;
use crate::shared::NUM_TRIALS;
use crate::shared::WINDOW_SIZE;
use crate::test_shared::*;
use crate::test_utils::shared_tokio_runtime;
mod can_remove {
use proptest::arbitrary::Arbitrary;
use proptest::collection::vec;
use proptest::test_runner::TestRunner;
use proptest_arbitrary_interop::arb;
use rand::rng;
use super::*;
use crate::msa_and_records::MsaAndRecords;
#[proptest]
fn missing_chunk_dictionary_entry_small(
#[strategy((1u64)..=(u64::from(u8::MAX)))] _num_leafs_aocl: u64,
#[strategy(vec((arb::<Digest>(), arb::<Digest>(), arb::<Digest>()), 1usize))]
_removables: Vec<(Digest, Digest, Digest)>,
#[strategy(MsaAndRecords::arbitrary_with((#_removables, #_num_leafs_aocl)))]
msa_and_records: MsaAndRecords,
) {
let mut removal_record = msa_and_records.unpacked_removal_records()[0].clone();
prop_assume!(!removal_record.target_chunks.dictionary.is_empty());
assert!(msa_and_records
.mutator_set_accumulator
.can_remove(&removal_record));
removal_record.target_chunks.dictionary.pop();
assert!(!msa_and_records
.mutator_set_accumulator
.can_remove(&removal_record));
}
#[proptest]
fn missing_chunk_dictionary_entry(
#[strategy(0usize..30)] _num_removals: usize,
#[strategy((#_num_removals as u64)..=(u64::from(u8::MAX)))] _num_leafs_aocl: u64,
#[strategy(vec((arb::<Digest>(), arb::<Digest>(), arb::<Digest>()), #_num_removals))]
_removables: Vec<(Digest, Digest, Digest)>,
#[strategy(MsaAndRecords::arbitrary_with((#_removables, #_num_leafs_aocl)))]
msa_and_records: MsaAndRecords,
) {
let removal_records = msa_and_records.unpacked_removal_records();
for rr in &removal_records {
assert!(msa_and_records.mutator_set_accumulator.can_remove(rr));
}
for rr in removal_records {
for i in 0..rr.target_chunks.dictionary.len() {
let mut bad = rr.clone();
bad.target_chunks.dictionary.remove(i);
assert!(!msa_and_records.mutator_set_accumulator.can_remove(&bad));
}
}
}
#[test]
fn can_remove_false_when_already_removed_small_aocl() {
let mut test_runner = TestRunner::deterministic();
let mut rng = rng();
for num_leafs in 1..=(3 * BATCH_SIZE as usize) {
let msa_and_records = MsaAndRecords::arbitrary_with((
vec![(rng.random(), rng.random(), Digest::default()); num_leafs],
num_leafs as u64,
))
.new_tree(&mut test_runner)
.unwrap()
.current();
let removal_records = msa_and_records.unpacked_removal_records();
for rr in &removal_records {
assert!(msa_and_records.mutator_set_accumulator.can_remove(rr));
}
for rr in removal_records {
let mut mutated_msa = msa_and_records.mutator_set_accumulator.clone();
mutated_msa.remove(&rr);
assert!(
!mutated_msa.can_remove(&rr),
"Can remove must return false after RR has been applied"
);
}
}
}
}
#[test]
fn active_window_chunk_interval_unit_test() {
let mut accumulator: MutatorSetAccumulator = MutatorSetAccumulator::default();
let (start_empty, end_empty) = accumulator.active_window_chunk_interval();
assert_eq!(0, start_empty);
assert_eq!(u64::from(WINDOW_SIZE / CHUNK_SIZE), end_empty);
for _ in 0..BATCH_SIZE + 1 {
let (item, sender_randomness, receiver_preimage) = mock_item_and_randomnesses();
let addition_record = commit(item, sender_randomness, receiver_preimage.hash());
let (start, end) = accumulator.active_window_chunk_interval();
assert_eq!(0, start);
assert_eq!(u64::from(WINDOW_SIZE / CHUNK_SIZE), end);
accumulator.add(&addition_record);
}
let (start_final, end_final) = accumulator.active_window_chunk_interval();
assert_eq!(1, start_final);
assert_eq!(u64::from(WINDOW_SIZE / CHUNK_SIZE) + 1, end_final);
}
#[proptest(cases = 10)]
fn batch_index_and_active_window_chunk_interval_agree(
#[strategy(1u64..10u64 * u64::from(BATCH_SIZE))] num_insertions: u64,
) {
let mut accumulator: MutatorSetAccumulator = MutatorSetAccumulator::default();
for _ in 0..num_insertions {
let (start, end) = accumulator.active_window_chunk_interval();
let batch_interval = accumulator.get_batch_index();
prop_assert_eq!(batch_interval, start);
prop_assert_eq!(batch_interval + u64::from(WINDOW_SIZE / CHUNK_SIZE), end);
let (item, sender_randomness, receiver_preimage) = mock_item_and_randomnesses();
let addition_record = commit(item, sender_randomness, receiver_preimage.hash());
accumulator.add(&addition_record);
}
}
#[apply(shared_tokio_runtime)]
async fn mutator_set_batch_remove_accumulator_test() {
let mut accumulator: MutatorSetAccumulator = MutatorSetAccumulator::default();
let mut membership_proofs: Vec<MsMembershipProof> = vec![];
let mut items: Vec<Digest> = vec![];
let num_additions = 400;
for _ in 0..num_additions {
let (item, sender_randomness, receiver_preimage) = mock_item_and_randomnesses();
let addition_record = commit(item, sender_randomness, receiver_preimage.hash());
let membership_proof = accumulator.prove(item, sender_randomness, receiver_preimage);
MsMembershipProof::batch_update_from_addition(
&mut membership_proofs.iter_mut().collect::<Vec<_>>(),
&items,
&accumulator,
&addition_record,
)
.expect("MS membership update must work");
accumulator.add(&addition_record);
membership_proofs.push(membership_proof);
items.push(item);
}
let mut rng = rand::rng();
let mut skipped_removes: Vec<bool> = vec![];
let mut removal_records: Vec<RemovalRecord> = vec![];
for (mp, &item) in membership_proofs.iter().zip_eq(items.iter()) {
let skipped = rng.random_range(0.0..1.0) < 0.5;
skipped_removes.push(skipped);
if !skipped {
removal_records.push(accumulator.drop(item, mp));
}
}
for (mp, &item) in membership_proofs.iter().zip_eq(items.iter()) {
assert!(accumulator.verify(item, mp));
}
let mut accumulator_batch_remove = accumulator.clone();
let updated_chunks = accumulator_batch_remove.batch_remove(
removal_records.clone(),
&mut membership_proofs.iter_mut().collect::<Vec<_>>(),
);
for (chk_idx, chunk) in &updated_chunks {
for msmp in &membership_proofs {
for (chidx, (_, chnk)) in &msmp.target_chunks.dictionary {
if *chidx == *chk_idx {
assert_eq!(chunk, chnk);
}
}
}
}
let mut accumulator_individual_remove = accumulator;
while let Some(rr) = removal_records.pop() {
RemovalRecord::batch_update_from_remove(
&mut removal_records.iter_mut().collect_vec(),
&rr,
);
accumulator_individual_remove.remove(&rr);
}
assert_eq!(
accumulator_individual_remove, accumulator_batch_remove,
"Mutator sets must agree regardless of removal method"
);
for (mp, &item, skipped) in izip!(
membership_proofs.iter(),
items.iter(),
skipped_removes.into_iter()
) {
assert_eq!(skipped, accumulator_individual_remove.verify(item, mp));
}
}
#[apply(shared_tokio_runtime)]
async fn mutator_set_accumulator_pbt() {
let mut accumulator: MutatorSetAccumulator = MutatorSetAccumulator::default();
let mut rms_after = empty_rusty_mutator_set().await;
let archival_after_remove = rms_after.ams_mut();
let mut rms_before = empty_rusty_mutator_set().await;
let archival_before_remove = rms_before.ams_mut();
let number_of_interactions = 100;
let mut rng = rand::rng();
for start_fill in [false, true] {
let mut membership_proofs_batch: Vec<MsMembershipProof> = vec![];
let mut membership_proofs_sequential: Vec<MsMembershipProof> = vec![];
let mut items: Vec<Digest> = vec![];
let mut rands: Vec<(Digest, Digest)> = vec![];
let mut last_ms_commitment: Option<Digest> = None;
for i in 0..number_of_interactions {
let new_commitment = accumulator.hash();
assert_eq!(
new_commitment,
archival_after_remove.hash().await,
"Commitment to archival/accumulator MS must agree"
);
match last_ms_commitment {
None => (),
Some(commitment) => assert_ne!(
commitment, new_commitment,
"MS commitment must change upon insertion/deletion"
),
};
last_ms_commitment = Some(new_commitment);
if rng.random_range(0u8..2) == 0 || start_fill && i < number_of_interactions / 2 {
let (item, sender_randomness, receiver_preimage) = mock_item_and_randomnesses();
let addition_record: AdditionRecord =
commit(item, sender_randomness, receiver_preimage.hash());
let membership_proof_acc =
accumulator.prove(item, sender_randomness, receiver_preimage);
let previous_mps = membership_proofs_batch.clone();
let indices_of_updated_mps = MsMembershipProof::batch_update_from_addition(
&mut membership_proofs_batch.iter_mut().collect::<Vec<_>>(),
&items,
&accumulator,
&addition_record,
)
.expect("Batch mutation must return OK");
for (mp, &own_item) in membership_proofs_sequential.iter_mut().zip(items.iter())
{
let update_res_seq =
mp.update_from_addition(own_item, &accumulator, &addition_record);
assert!(update_res_seq.is_ok());
}
accumulator.add(&addition_record);
archival_after_remove.add(&addition_record).await;
archival_before_remove.add(&addition_record).await;
println!("{}: Inserted", i);
for j in 0..items.len() {
if indices_of_updated_mps.contains(&j) {
assert_ne!(
previous_mps[j], membership_proofs_batch[j],
"membership proof marked as updated but still identical"
);
assert!(
!accumulator.verify(items[j], &previous_mps[j]),
"Verify must fail for old proof, j = {}. AOCL data index was: {}.\n\nOld mp:\n {:?}.\n\nNew mp is\n {:?}",
j,
previous_mps[j].aocl_leaf_index,
previous_mps[j],
membership_proofs_batch[j]
);
} else {
assert_eq!(
previous_mps[j], membership_proofs_batch[j],
"membership proof underwent update but not marked as such"
);
assert!(
accumulator.verify(items[j], &previous_mps[j]),
"Verify must succeed for old proof, j = {}. AOCL data index was: {}.\n\nOld mp:\n {:?}.\n\nNew mp is\n {:?}",
j,
previous_mps[j].aocl_leaf_index,
previous_mps[j],
membership_proofs_batch[j]
);
}
}
membership_proofs_batch.push(membership_proof_acc.clone());
membership_proofs_sequential.push(membership_proof_acc);
items.push(item);
rands.push((sender_randomness, receiver_preimage));
} else {
if membership_proofs_batch.is_empty() {
last_ms_commitment = None;
continue;
}
let item_index = rng.random_range(0..membership_proofs_batch.len());
let removal_item = items.remove(item_index);
let removal_mp = membership_proofs_batch.remove(item_index);
let _removal_mp_seq = membership_proofs_sequential.remove(item_index);
let _removal_rand = rands.remove(item_index);
let removal_record: RemovalRecord = accumulator.drop(removal_item, &removal_mp);
assert!(removal_record.validate(&accumulator));
let original_membership_proofs_batch = membership_proofs_batch.clone();
let batch_update_ret = MsMembershipProof::batch_update_from_remove(
&mut membership_proofs_batch.iter_mut().collect::<Vec<_>>(),
&removal_record,
);
assert!(batch_update_ret.is_ok());
let original_membership_proofs_sequential =
membership_proofs_sequential.clone();
let mut update_by_remove_return_values: Vec<bool> = vec![];
for mp in &mut membership_proofs_sequential {
let update_res_seq = mp.update_from_remove(&removal_record);
update_by_remove_return_values.push(update_res_seq);
}
assert!(accumulator.verify(removal_item, &removal_mp));
let removal_record_copy = removal_record.clone();
accumulator.remove(&removal_record);
archival_after_remove.remove(&removal_record).await;
for removed_index in removal_record.absolute_indices.to_vec() {
assert!(
archival_after_remove
.bloom_filter_contains(removed_index)
.await
);
}
archival_before_remove.remove(&removal_record_copy).await;
assert!(!accumulator.verify(removal_item, &removal_mp));
for (j, updated, original_mp, &item) in izip!(
0..,
update_by_remove_return_values,
original_membership_proofs_sequential.iter(),
items.iter()
) {
if updated {
assert!(
!accumulator.verify(item, original_mp),
"j = {}, \n\nOriginal mp:\n{:#?}\n\nNew mp:\n{:#?}",
j,
original_mp,
membership_proofs_sequential[j]
);
} else {
assert!(
accumulator.verify(item, original_mp),
"j = {}, \n\nOriginal mp:\n{:#?}\n\nNew mp:\n{:#?}",
j,
original_mp,
membership_proofs_sequential[j]
);
}
}
let updated_indices: Vec<usize> = batch_update_ret.unwrap();
for (j, (original_mp, &item)) in original_membership_proofs_batch
.iter()
.zip(items.iter())
.enumerate()
{
let item_was_updated = updated_indices.contains(&j);
let item_verifies = accumulator.verify(item, original_mp);
let item_verifies_iff_not_updated = item_verifies != item_was_updated;
assert!(item_verifies_iff_not_updated);
}
println!("{}: Removed", i);
}
for (mp_batch, mp_seq, &item, &(sender_randomness, receiver_preimage)) in izip!(
membership_proofs_batch.iter(),
membership_proofs_sequential.iter(),
items.iter(),
rands.iter()
) {
assert!(accumulator.verify(item, mp_batch));
let arch_mp = archival_after_remove
.restore_membership_proof(
item,
sender_randomness,
receiver_preimage,
mp_batch.aocl_leaf_index,
)
.await
.unwrap();
let arch_mp_alt = archival_after_remove
.restore_membership_proof_privacy_preserving(arch_mp.compute_indices(item))
.await
.unwrap()
.extract_ms_membership_proof(
mp_batch.aocl_leaf_index,
sender_randomness,
receiver_preimage,
)
.unwrap();
assert_eq!(arch_mp, arch_mp_alt);
assert_eq!(arch_mp, mp_batch.to_owned());
assert_eq!(mp_batch, mp_seq);
}
}
}
}
proptest::proptest! {
#![proptest_config(ProptestConfig {
cases: 100, .. ProptestConfig::default()
})]
#[test]
fn test_mutator_set_accumulator_decode(
msa in proptest_arbitrary_interop::arb::<MutatorSetAccumulator>()
) {
let encoded = msa.encode();
let decoded: MutatorSetAccumulator = *MutatorSetAccumulator::decode(&encoded).unwrap();
prop_assert_eq!(msa, decoded);
}
}
#[ignore]
#[test]
fn profile() {
let mut rng = rand::rng();
println!(
"profiling Mutator Set (w, b, s, k) = ({}, {}, {}, {}) ...",
WINDOW_SIZE, BATCH_SIZE, CHUNK_SIZE, NUM_TRIALS
);
let mut msa = MutatorSetAccumulator::default();
let mut items_and_membership_proofs: Vec<(Digest, MsMembershipProof)> = vec![];
let target_set_size = 100;
let num_iterations = 10000;
for i in 0..num_iterations {
if i % 100 == 0 {
println!("{}/{}", i, num_iterations);
}
let operation = if items_and_membership_proofs.len()
> (1.25 * f64::from(target_set_size)) as usize
{
rng.random_range(0..10) >= 3
} else if items_and_membership_proofs.len()
< (0.8 * f64::from(target_set_size)) as usize
{
rng.random_range(0..10) < 3
} else {
rng.random_range(0..10) < 5
};
if operation && !items_and_membership_proofs.is_empty() {
let index = rng.random_range(0..items_and_membership_proofs.len());
let (item, membership_proof) = items_and_membership_proofs.swap_remove(index);
let removal_record = msa.drop(item, &membership_proof);
for (_it, mp) in &mut items_and_membership_proofs {
mp.update_from_remove(&removal_record);
}
msa.remove(&removal_record);
} else {
let item = rng.random::<Digest>();
let sender_randomness = rng.random::<Digest>();
let receiver_preimage = rng.random::<Digest>();
let addition_record = commit(item, sender_randomness, receiver_preimage);
for (it, mp) in &mut items_and_membership_proofs {
mp.update_from_addition(*it, &msa, &addition_record)
.unwrap();
}
let membership_proof = msa.prove(item, sender_randomness, receiver_preimage);
msa.add(&addition_record);
items_and_membership_proofs.push((item, membership_proof));
}
}
println!("{} operations resulted in a set containing {} elements; mutator set accumulator size: {} bytes", num_iterations, items_and_membership_proofs.len(), msa.get_size());
}
}