diff --git a/shared/src/ledger/storage_api/collections/lazy_map.rs b/shared/src/ledger/storage_api/collections/lazy_map.rs index a47ff0734a..3801848b8f 100644 --- a/shared/src/ledger/storage_api/collections/lazy_map.rs +++ b/shared/src/ledger/storage_api/collections/lazy_map.rs @@ -3,12 +3,15 @@ use std::marker::PhantomData; use borsh::{BorshDeserialize, BorshSerialize}; +use derivative::Derivative; +use thiserror::Error; use super::super::Result; use super::{LazyCollection, ReadError}; -use crate::ledger::storage_api::validation::Data; +use crate::ledger::storage_api::validation::{self, Data}; use crate::ledger::storage_api::{self, ResultExt, StorageRead, StorageWrite}; -use crate::types::storage::{self, KeySeg}; +use crate::ledger::vp_env::VpEnv; +use crate::types::storage::{self, DbKeySeg, KeySeg}; /// Subkey corresponding to the data elements of the LazyMap pub const DATA_SUBKEY: &str = "data"; @@ -52,12 +55,21 @@ pub enum SubKeyWithData { /// the methods that have `StorageWrite` access. /// TODO: In a nested collection, `V` may be an action inside the nested /// collection. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum Action { /// Insert or update a value `V` at key `K` in a [`LazyMap`]. Insert(K, V), /// Remove a value `V` at key `K` from a [`LazyMap`]. Remove(K, V), + /// Update a value `V` at key `K` in a [`LazyMap`]. + Update { + /// key at which the value is updated + key: K, + /// value before the update + pre: V, + /// value after the update + post: V, + }, } /// TODO: In a nested collection, `V` may be an action inside the nested @@ -70,6 +82,46 @@ pub enum Nested { Remove(K, V), } +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum ValidationError { + #[error("Storage error in reading key {0}")] + StorageError(storage::Key), + // #[error("Incorrect difference in LazyVec's length")] + // InvalidLenDiff, + // #[error("An empty LazyVec must be deleted from storage")] + // EmptyVecShouldBeDeleted, + // #[error("Push at a wrong index. Got {got}, expected {expected}.")] + // UnexpectedPushIndex { got: Index, expected: Index }, + // #[error("Pop at a wrong index. Got {got}, expected {expected}.")] + // UnexpectedPopIndex { got: Index, expected: Index }, + // #[error( + // "Update (combination of pop and push) at a wrong index. Got {got}, + // \ expected {expected}." + // )] + // UnexpectedUpdateIndex { got: Index, expected: Index }, + // #[error("An index has overflown its representation: {0}")] + // IndexOverflow(>::Error), + // #[error("Unexpected underflow in `{0} - {0}`")] + // UnexpectedUnderflow(Index, Index), + #[error("Invalid storage key {0}")] + InvalidSubKey(storage::Key), +} + +/// [`LazyMap`] validation result +pub type ValidationResult = std::result::Result; + +/// [`LazyMap`] validation builder from storage changes. The changes can be +/// accumulated with `LazyMap::validate()` and then turned into a list +/// of valid actions on the map with `ValidationBuilder::build()`. +#[derive(Debug, Derivative)] +// https://mcarton.github.io/rust-derivative/latest/Default.html#custom-bound +#[derivative(Default(bound = ""))] +pub struct ValidationBuilder { + /// The accumulator of found changes under the vector + pub changes: Vec>, +} + impl LazyCollection for LazyMap where K: storage::KeySeg, @@ -247,6 +299,96 @@ where ) -> Result<()> { storage.write(storage_key, val) } + + /// Check if the given storage key is a valid LazyMap sub-key and if so + /// return which one + pub fn is_valid_sub_key( + &self, + key: &storage::Key, + ) -> storage_api::Result>> { + let suffix = match key.split_prefix(&self.key) { + None => { + // not matching prefix, irrelevant + return Ok(None); + } + Some(None) => { + // no suffix, invalid + return Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(); + } + Some(Some(suffix)) => suffix, + }; + + // Match the suffix against expected sub-keys + match &suffix.segments[..] { + [DbKeySeg::StringSeg(sub_a), DbKeySeg::StringSeg(sub_b)] + if sub_a == DATA_SUBKEY => + { + if let Ok(key_in_kv) = storage::KeySeg::parse(sub_b.clone()) { + Ok(Some(SubKey::Data(key_in_kv))) + } else { + Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result() + } + } + _ => Err(ValidationError::InvalidSubKey(key.clone())) + .into_storage_result(), + } + } + + /// Accumulate storage changes inside a [`ValidationBuilder`]. This is + /// typically done by the validity predicate while looping through the + /// changed keys. If the resulting `builder` is not `None`, one must + /// call `fn build()` on it to get the validation result. + /// This function will return `Ok(true)` if the storage key is a valid + /// sub-key of this collection, `Ok(false)` if the storage key doesn't match + /// the prefix of this collection, or fail with + /// [`ValidationError::InvalidSubKey`] if the prefix matches this + /// collection, but the key itself is not recognized. + pub fn accumulate( + &self, + env: &ENV, + builder: &mut Option>, + key_changed: &storage::Key, + ) -> storage_api::Result + where + ENV: for<'a> VpEnv<'a>, + { + if let Some(sub) = self.is_valid_sub_key(key_changed)? { + let SubKey::Data(key) = sub; + let data = validation::read_data(env, key_changed)?; + let change = data.map(|data| SubKeyWithData::Data(key, data)); + if let Some(change) = change { + let builder = + builder.get_or_insert(ValidationBuilder::default()); + builder.changes.push(change); + } + return Ok(true); + } + Ok(false) + } +} + +impl ValidationBuilder +where + K: storage::KeySeg + Ord + Clone, +{ + /// Build a list of actions from storage changes. + pub fn build(self) -> Vec> { + self.changes + .into_iter() + .map(|change| { + let SubKeyWithData::Data(key, data) = change; + match data { + Data::Add { post } => Action::Insert(key, post), + Data::Update { pre, post } => { + Action::Update { key, pre, post } + } + Data::Delete { pre } => Action::Remove(key, pre), + } + }) + .collect() + } } #[cfg(test)] diff --git a/tests/proptest-regressions/storage_api/collections/lazy_map.txt b/tests/proptest-regressions/storage_api/collections/lazy_map.txt new file mode 100644 index 0000000000..2de7510923 --- /dev/null +++ b/tests/proptest-regressions/storage_api/collections/lazy_map.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 59b8eaaf5d8e03e58b346ef229a2487f68fea488197420f150682f7275ce2b83 # shrinks to (initial_state, transitions) = (AbstractLazyMapState { valid_transitions: [], committed_transitions: [] }, [Insert(11178241982156558453, TestVal { x: 9618691367534591266, y: true }), CommitTx, Update(11178241982156558453, TestVal { x: 2635377083098935189, y: false }), Update(11178241982156558453, TestVal { x: 11485387163946255361, y: false }), Insert(4380901092919801530, TestVal { x: 17235291421018840542, y: false }), Update(11178241982156558453, TestVal { x: 1936190700145956620, y: false }), Update(11178241982156558453, TestVal { x: 6934621224353358508, y: false }), Update(11178241982156558453, TestVal { x: 16175036327810390362, y: true }), Remove(5606457884982633480), Insert(7124206407862523505, TestVal { x: 5513772825695605555, y: true }), CommitTxAndBlock, CommitTx, Insert(13347045100814804679, TestVal { x: 5157295776286367034, y: false }), Update(7124206407862523505, TestVal { x: 1989909525753197955, y: false }), Update(4380901092919801530, TestVal { x: 13085578877588425331, y: false }), Update(7124206407862523505, TestVal { x: 1620781139263176467, y: true }), Insert(5806457332157050619, TestVal { x: 14632354209749334932, y: true }), Remove(1613213961397167063), Update(7124206407862523505, TestVal { x: 3848976302483310370, y: true }), Update(4380901092919801530, TestVal { x: 15281186775251770467, y: false }), Remove(5303306623647571548), Insert(5905425607805327902, TestVal { x: 1274794101048822414, y: false }), Insert(2305446651611241243, TestVal { x: 7872403441503057017, y: true }), Insert(2843165193114615911, TestVal { x: 13698490566286768452, y: false }), Insert(3364298091459048760, TestVal { x: 8891279000465212397, y: true }), CommitTx, Insert(17278527568142155478, TestVal { x: 8166151895050476136, y: false }), Remove(9206713523174765253), Remove(1148985045479283759), Insert(13346103305566843535, TestVal { x: 13148026974798633058, y: true }), Remove(17185699086139524651), CommitTx, Update(7124206407862523505, TestVal { x: 3047872255943216792, y: false }), CommitTxAndBlock, CommitTxAndBlock, Remove(4672009405538026945), Update(5905425607805327902, TestVal { x: 6635343936644805461, y: false }), Insert(14100441716981493843, TestVal { x: 8068697312326956479, y: true }), Insert(8370580326875672309, TestVal { x: 18416630552728813406, y: false }), Update(2305446651611241243, TestVal { x: 3777718192999015176, y: false }), Remove(1532142753559370584), Remove(10097030807802775125), Insert(10080356901530935857, TestVal { x: 17171047520093964037, y: false }), Update(3364298091459048760, TestVal { x: 702372485798608773, y: true }), Insert(5504969092734638033, TestVal { x: 314752460808087203, y: true }), Remove(5486040497128339175), Insert(7884678026881625058, TestVal { x: 4313610278903495077, y: true }), CommitTx, Insert(11228024342874184864, TestVal { x: 428512502841968552, y: false }), Insert(4684666745142518471, TestVal { x: 13122515680485564107, y: true }), Remove(14243063045921130600), Remove(4530767959521683042), Insert(10236349778753659715, TestVal { x: 3138294567956031715, y: true }), Update(2305446651611241243, TestVal { x: 8133236604817109805, y: false }), Update(2843165193114615911, TestVal { x: 12001998927296899868, y: false }), CommitTxAndBlock, CommitTx, CommitTxAndBlock]) diff --git a/tests/src/storage_api/collections/lazy_map.rs b/tests/src/storage_api/collections/lazy_map.rs new file mode 100644 index 0000000000..c7a309ab4d --- /dev/null +++ b/tests/src/storage_api/collections/lazy_map.rs @@ -0,0 +1,608 @@ +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::convert::TryInto; + + use borsh::{BorshDeserialize, BorshSerialize}; + use namada::types::address::{self, Address}; + use namada::types::storage; + use namada_tx_prelude::storage::KeySeg; + use namada_tx_prelude::storage_api::collections::{ + lazy_map, LazyCollection, LazyMap, + }; + use proptest::prelude::*; + use proptest::prop_state_machine; + use proptest::state_machine::{AbstractStateMachine, StateMachineTest}; + use proptest::test_runner::Config; + use test_log::test; + + use crate::tx::tx_host_env; + use crate::vp::vp_host_env; + + prop_state_machine! { + #![proptest_config(Config { + // Instead of the default 256, we only run 5 because otherwise it + // takes too long and it's preferable to crank up the number of + // transitions instead, to allow each case to run for more epochs as + // some issues only manifest once the model progresses further. + // Additionally, more cases will be explored every time this test is + // executed in the CI. + cases: 5, + .. Config::default() + })] + #[test] + fn lazy_map_api_state_machine_test(sequential 1..100 => ConcreteLazyMapState); + } + + /// Type of key used in the map + type TestKey = u64; + + /// Some borsh-serializable type with arbitrary fields to be used inside + /// LazyMap state machine test + #[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + )] + struct TestVal { + x: u64, + y: bool, + } + + /// A `StateMachineTest` implemented on this struct manipulates it with + /// `Transition`s, which are also being accumulated into + /// `current_transitions`. It then: + /// + /// - checks its state against an in-memory `std::collections::HashMap` + /// - runs validation and checks that the `LazyMap::Action`s reported from + /// validation match with transitions that were applied + /// + /// Additionally, one of the transitions is to commit a block and/or + /// transaction, during which the currently accumulated state changes are + /// persisted, or promoted from transaction write log to block's write log. + #[derive(Debug)] + struct ConcreteLazyMapState { + /// Address is used to prefix the storage key of the `lazy_map` in + /// order to simulate a transaction and a validity predicate + /// check from changes on the `lazy_map` + address: Address, + /// In the test, we apply the same transitions on the `lazy_map` as on + /// `eager_map` to check that `lazy_map`'s state is consistent with + /// `eager_map`. + eager_map: BTreeMap, + /// Handle to a lazy map + lazy_map: LazyMap, + /// Valid LazyMap changes in the current transaction + current_transitions: Vec, + } + + #[derive(Clone, Debug, Default)] + struct AbstractLazyMapState { + /// Valid LazyMap changes in the current transaction + valid_transitions: Vec, + /// Valid LazyMap changes committed to storage + committed_transitions: Vec, + } + + /// Possible transitions that can modify a [`LazyMap`]. + /// This roughly corresponds to the methods that have `StorageWrite` + /// access and is very similar to [`Action`] + #[derive(Clone, Debug)] + enum Transition { + /// Commit all valid transitions in the current transaction + CommitTx, + /// Commit all valid transitions in the current transaction and also + /// commit the current block + CommitTxAndBlock, + /// Insert a key-val into a [`LazyMap`] + Insert(TestKey, TestVal), + /// Remove a key-val from a [`LazyMap`] + Remove(TestKey), + /// Update a value at key from pre to post state in a + /// [`LazyMap`] + Update(TestKey, TestVal), + } + + impl AbstractStateMachine for AbstractLazyMapState { + type State = Self; + type Transition = Transition; + + fn init_state() -> BoxedStrategy { + Just(Self::default()).boxed() + } + + // Apply a random transition to the state + fn transitions(state: &Self::State) -> BoxedStrategy { + let length = state.len(); + if length == 0 { + prop_oneof![ + 1 => Just(Transition::CommitTx), + 1 => Just(Transition::CommitTxAndBlock), + 3 => (arb_map_key(), arb_map_val()).prop_map(|(key, val)| Transition::Insert(key, val)) + ] + .boxed() + } else { + let keys = state.find_existing_keys(); + let arb_existing_map_key = + || proptest::sample::select(keys.clone()); + prop_oneof![ + 1 => Just(Transition::CommitTx), + 1 => Just(Transition::CommitTxAndBlock), + 3 => (arb_existing_map_key(), arb_map_val()).prop_map(|(key, val)| + Transition::Update(key, val) + ), + 3 => arb_existing_map_key().prop_map(Transition::Remove), + 5 => (arb_map_key().prop_filter("insert on non-existing keys only", move |key| !keys.contains(&key)), arb_map_val()).prop_map(|(key, val)| Transition::Insert(key, val)) + ] + .boxed() + } + } + + fn apply_abstract( + mut state: Self::State, + transition: &Self::Transition, + ) -> Self::State { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + let valid_actions_to_commit = + std::mem::take(&mut state.valid_transitions); + state + .committed_transitions + .extend(valid_actions_to_commit.into_iter()); + } + _ => state.valid_transitions.push(transition.clone()), + } + state + } + + fn preconditions( + state: &Self::State, + transition: &Self::Transition, + ) -> bool { + let length = state.len(); + // Ensure that the remove or update transitions are not applied + // to an empty state + if length == 0 + && matches!( + transition, + Transition::Remove(_) | Transition::Update(_, _) + ) + { + return false; + } + match transition { + Transition::Update(key, _) | Transition::Remove(key) => { + let keys = state.find_existing_keys(); + // Ensure that the update/remove key is an existing one + keys.contains(key) + } + Transition::Insert(key, _) => { + let keys = state.find_existing_keys(); + // Ensure that the insert key is not an existing one + !keys.contains(key) + } + _ => true, + } + } + } + + impl StateMachineTest for ConcreteLazyMapState { + type Abstract = AbstractLazyMapState; + type ConcreteState = Self; + + fn init_test( + _initial_state: ::State, + ) -> Self::ConcreteState { + // Init transaction env in which we'll be applying the transitions + tx_host_env::init(); + + // The lazy_map's path must be prefixed by the address to be able + // to trigger a validity predicate on it + let address = address::testing::established_address_1(); + tx_host_env::with(|env| env.spawn_accounts([&address])); + let lazy_map_prefix: storage::Key = address.to_db_key().into(); + + Self { + address, + eager_map: BTreeMap::new(), + lazy_map: LazyMap::open( + lazy_map_prefix.push(&"arbitrary".to_string()).unwrap(), + ), + current_transitions: vec![], + } + } + + fn apply_concrete( + mut state: Self::ConcreteState, + transition: ::Transition, + ) -> Self::ConcreteState { + // Apply transitions in transaction env + let ctx = tx_host_env::ctx(); + + // Persist the transitions in the current tx, or clear previous ones + // if we're committing a tx + match &transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + state.current_transitions = vec![]; + } + _ => { + state.current_transitions.push(transition.clone()); + } + } + + // Transition application on lazy map and post-conditions: + match &transition { + Transition::CommitTx => { + // commit the tx without committing the block + tx_host_env::with(|env| env.write_log.commit_tx()); + } + Transition::CommitTxAndBlock => { + // commit the tx and the block + tx_host_env::commit_tx_and_block(); + } + Transition::Insert(key, value) => { + state.lazy_map.insert(ctx, *key, value.clone()).unwrap(); + + // Post-conditions: + let stored_value = + state.lazy_map.get(ctx, key).unwrap().unwrap(); + assert_eq!( + &stored_value, value, + "the new item must be added to the back" + ); + + state.assert_validation_accepted(); + } + Transition::Remove(key) => { + let removed = + state.lazy_map.remove(ctx, key).unwrap().unwrap(); + + // Post-conditions: + assert_eq!( + &removed, + state.eager_map.get(key).unwrap(), + "removed element matches the value in eager map \ + before it's updated" + ); + + state.assert_validation_accepted(); + } + Transition::Update(key, value) => { + let old_val = + state.lazy_map.get(ctx, key).unwrap().unwrap(); + + state.lazy_map.insert(ctx, *key, value.clone()).unwrap(); + + // Post-conditions: + let new_val = + state.lazy_map.get(ctx, key).unwrap().unwrap(); + assert_eq!( + &old_val, + state.eager_map.get(key).unwrap(), + "old value must match the value at the same key in \ + the eager map before it's updated" + ); + assert_eq!( + &new_val, value, + "new value must match that which was passed into the \ + Transition::Update" + ); + + state.assert_validation_accepted(); + } + } + + // Apply transition in the eager map for comparison + apply_transition_on_eager_map(&mut state.eager_map, &transition); + + // Global post-conditions: + + // All items in eager map must be present in lazy map + for (key, expected_item) in state.eager_map.iter() { + let got = + state.lazy_map.get(ctx, key).unwrap().expect( + "The expected item must be present in lazy map", + ); + assert_eq!(expected_item, &got, "at key {key}"); + } + + // All items in lazy map must be present in eager map + for key_val in state.lazy_map.iter(ctx).unwrap() { + let (key, expected_val) = key_val.unwrap(); + let got = state + .eager_map + .get(&key) + .expect("The expected item must be present in eager map"); + assert_eq!(&expected_val, got, "at key {key}"); + } + + state + } + } + + impl AbstractLazyMapState { + /// Find the length of the map from the applied transitions + fn len(&self) -> u64 { + (map_len_diff_from_transitions(self.committed_transitions.iter()) + + map_len_diff_from_transitions(self.valid_transitions.iter())) + .try_into() + .expect( + "It shouldn't be possible to underflow length from all \ + transactions applied in abstract state", + ) + } + + /// Build an eager map from the committed and current transitions + fn eager_map(&self) -> BTreeMap { + let mut eager_map = BTreeMap::new(); + for transition in &self.committed_transitions { + apply_transition_on_eager_map(&mut eager_map, transition); + } + for transition in &self.valid_transitions { + apply_transition_on_eager_map(&mut eager_map, transition); + } + eager_map + } + + /// Find the keys currently present in the map + fn find_existing_keys(&self) -> Vec { + self.eager_map().keys().cloned().collect() + } + } + + /// Find the difference in length of the map from the applied transitions + fn map_len_diff_from_transitions<'a>( + transitions: impl Iterator, + ) -> i64 { + let mut insert_count: i64 = 0; + let mut remove_count: i64 = 0; + + for trans in transitions { + match trans { + Transition::CommitTx + | Transition::CommitTxAndBlock + | Transition::Update(_, _) => {} + Transition::Insert(_, _) => insert_count += 1, + Transition::Remove(_) => remove_count += 1, + } + } + insert_count - remove_count + } + + impl ConcreteLazyMapState { + fn assert_validation_accepted(&self) { + // Init the VP env from tx env in which we applied the map + // transitions + let tx_env = tx_host_env::take(); + vp_host_env::init_from_tx(self.address.clone(), tx_env, |_| {}); + + // Simulate a validity predicate run using the lazy map's validation + // helpers + let changed_keys = + vp_host_env::with(|env| env.all_touched_storage_keys()); + + let mut validation_builder = None; + + // Push followed by pop is a no-op, in which case we'd still see the + // changed keys for these actions, but they wouldn't affect the + // validation result and they never get persisted, but we'd still + // them as changed key here. To guard against this case, + // we check that `map_len_from_transitions` is not empty. + let map_len_diff = + map_len_diff_from_transitions(self.current_transitions.iter()); + + // To help debug validation issues... + dbg!( + &self.current_transitions, + &changed_keys + .iter() + .map(storage::Key::to_string) + .collect::>() + ); + + for key in &changed_keys { + let is_sub_key = self + .lazy_map + .accumulate( + vp_host_env::ctx(), + &mut validation_builder, + key, + ) + .unwrap(); + + assert!( + is_sub_key, + "We're only modifying the lazy_map's keys here. Key: \ + \"{key}\", map length diff {map_len_diff}" + ); + } + if !changed_keys.is_empty() && map_len_diff != 0 { + assert!( + validation_builder.is_some(), + "If some keys were changed, the builder must get filled in" + ); + let actions = validation_builder.unwrap().build(); + let mut actions_to_check = actions.clone(); + + // Check that every transition has a corresponding action from + // validation. We drop the found actions to check that all + // actions are matched too. + let current_transitions = + normalize_transitions(&self.current_transitions); + for transition in ¤t_transitions { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => { + } + Transition::Insert(expected_key, expected_val) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let lazy_map::Action::Insert(key, val) = + action + { + if expected_key == key + && expected_val == val + { + actions_to_check.remove(ix); + break; + } + } + } + } + Transition::Remove(expected_key) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let lazy_map::Action::Remove(key, _val) = + action + { + if expected_key == key { + actions_to_check.remove(ix); + break; + } + } + } + } + Transition::Update(expected_key, value) => { + for (ix, action) in + actions_to_check.iter().enumerate() + { + if let lazy_map::Action::Update { + key, + pre: _, + post, + } = action + { + if expected_key == key && post == value { + actions_to_check.remove(ix); + break; + } + } + } + } + } + } + + assert!( + actions_to_check.is_empty(), + "All the actions reported from validation {actions:#?} \ + should have been matched with SM transitions \ + {current_transitions:#?}, but these actions didn't \ + match: {actions_to_check:#?}", + ) + } + + // Put the tx_env back before checking the result + tx_host_env::set_from_vp_env(vp_host_env::take()); + } + } + + /// Generate an arbitrary `TestKey` + fn arb_map_key() -> impl Strategy { + any::() + } + + /// Generate an arbitrary `TestVal` + fn arb_map_val() -> impl Strategy { + (any::(), any::()).prop_map(|(x, y)| TestVal { x, y }) + } + + /// Apply `Transition` on an eager `Map`. + fn apply_transition_on_eager_map( + map: &mut BTreeMap, + transition: &Transition, + ) { + match transition { + Transition::CommitTx | Transition::CommitTxAndBlock => {} + Transition::Insert(key, value) => { + map.insert(*key, value.clone()); + } + Transition::Remove(key) => { + let _popped = map.remove(key); + } + Transition::Update(key, value) => { + let entry = map.get_mut(key).unwrap(); + *entry = value.clone(); + } + } + } + + /// Normalize transitions: + /// - remove(key) + insert(key, val) -> update(key, val) + /// - insert(key, val) + update(key, new_val) -> insert(key, new_val) + /// - update(key, val) + update(key, new_val) -> update(key, new_val) + /// + /// Note that the normalizable transitions pairs do not have to be directly + /// next to each other, but their order does matter. + fn normalize_transitions(transitions: &[Transition]) -> Vec { + let mut collapsed = vec![]; + 'outer: for transition in transitions { + match transition { + Transition::CommitTx + | Transition::CommitTxAndBlock + | Transition::Remove(_) => collapsed.push(transition.clone()), + Transition::Insert(key, val) => { + for (ix, collapsed_transition) in + collapsed.iter().enumerate() + { + if let Transition::Remove(remove_key) = + collapsed_transition + { + if key == remove_key { + // remove(key) + insert(key, val) -> update(key, + // val) + + // Replace the Remove with an Update instead of + // inserting the Insert + *collapsed.get_mut(ix).unwrap() = + Transition::Update(*key, val.clone()); + continue 'outer; + } + } + } + collapsed.push(transition.clone()); + } + Transition::Update(key, value) => { + for (ix, collapsed_transition) in + collapsed.iter().enumerate() + { + if let Transition::Insert(insert_key, _) = + collapsed_transition + { + if key == insert_key { + // insert(key, val) + update(key, new_val) -> + // insert(key, new_val) + + // Replace the insert with the new update's + // value instead of inserting it + *collapsed.get_mut(ix).unwrap() = + Transition::Insert(*key, value.clone()); + continue 'outer; + } + } else if let Transition::Update(update_key, _) = + collapsed_transition + { + if key == update_key { + // update(key, val) + update(key, new_val) -> + // update(key, new_val) + + // Replace the insert with the new update's + // value instead of inserting it + *collapsed.get_mut(ix).unwrap() = + Transition::Update(*key, value.clone()); + continue 'outer; + } + } + } + collapsed.push(transition.clone()); + } + } + } + collapsed + } +} diff --git a/tests/src/storage_api/collections/mod.rs b/tests/src/storage_api/collections/mod.rs index d874b88e22..fc7c5832ce 100644 --- a/tests/src/storage_api/collections/mod.rs +++ b/tests/src/storage_api/collections/mod.rs @@ -1 +1,2 @@ +mod lazy_map; mod lazy_vec;