diff --git a/packages/storage-plus/src/indexed_snapshot.rs b/packages/storage-plus/src/indexed_snapshot.rs index 74188518f..13ef30420 100644 --- a/packages/storage-plus/src/indexed_snapshot.rs +++ b/packages/storage-plus/src/indexed_snapshot.rs @@ -7,16 +7,11 @@ use serde::Serialize; use crate::keys::{EmptyPrefix, Prefixer, PrimaryKey}; use crate::prefix::{Bound, Prefix}; -use crate::snapshot::SnapshotMap; +use crate::snapshot_map::SnapshotMap; use crate::{IndexList, Path, Strategy}; /// IndexedSnapshotMap works like a SnapshotMap but has a secondary index -pub struct IndexedSnapshotMap<'a, K, T, I> -where - K: PrimaryKey<'a>, - T: Serialize + DeserializeOwned + Clone, - I: IndexList, -{ +pub struct IndexedSnapshotMap<'a, K, T, I> { pk_namespace: &'a [u8], primary: SnapshotMap<'a, K, T>, /// This is meant to be read directly to get the proper types, like: @@ -24,12 +19,7 @@ where pub idx: I, } -impl<'a, K, T, I> IndexedSnapshotMap<'a, K, T, I> -where - K: PrimaryKey<'a>, - T: Serialize + DeserializeOwned + Clone, - I: IndexList, -{ +impl<'a, K, T, I> IndexedSnapshotMap<'a, K, T, I> { pub fn new( pk_namespace: &'a str, checkpoints: &'a str, @@ -43,7 +33,14 @@ where idx: indexes, } } +} +impl<'a, K, T, I> IndexedSnapshotMap<'a, K, T, I> +where + T: Serialize + DeserializeOwned + Clone, + K: PrimaryKey<'a> + Prefixer<'a>, + I: IndexList, +{ pub fn add_checkpoint(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { self.primary.add_checkpoint(store, height) } @@ -51,14 +48,7 @@ where pub fn remove_checkpoint(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { self.primary.remove_checkpoint(store, height) } -} -impl<'a, K, T, I> IndexedSnapshotMap<'a, K, T, I> -where - T: Serialize + DeserializeOwned + Clone, - K: PrimaryKey<'a> + Prefixer<'a>, - I: IndexList, -{ pub fn may_load_at_height( &self, store: &dyn Storage, diff --git a/packages/storage-plus/src/keys.rs b/packages/storage-plus/src/keys.rs index 1a6ec1534..9bb5a8742 100644 --- a/packages/storage-plus/src/keys.rs +++ b/packages/storage-plus/src/keys.rs @@ -19,6 +19,16 @@ pub trait PrimaryKey<'a>: Clone { } } +// Empty / no primary key +impl<'a> PrimaryKey<'a> for () { + type Prefix = (); + type SubPrefix = (); + + fn key(&self) -> Vec<&[u8]> { + vec![] + } +} + impl<'a> PrimaryKey<'a> for &'a [u8] { type Prefix = (); type SubPrefix = (); diff --git a/packages/storage-plus/src/lib.rs b/packages/storage-plus/src/lib.rs index bd8030585..d0519e8e5 100644 --- a/packages/storage-plus/src/lib.rs +++ b/packages/storage-plus/src/lib.rs @@ -10,6 +10,8 @@ mod map; mod path; mod prefix; mod snapshot; +mod snapshot_item; +mod snapshot_map; pub use endian::Endian; #[cfg(feature = "iterator")] @@ -27,5 +29,7 @@ pub use map::Map; pub use path::Path; #[cfg(feature = "iterator")] pub use prefix::{range_with_prefix, Bound, Prefix}; +pub use snapshot::Strategy; +pub use snapshot_item::SnapshotItem; #[cfg(feature = "iterator")] -pub use snapshot::{SnapshotMap, Strategy}; +pub use snapshot_map::SnapshotMap; diff --git a/packages/storage-plus/src/snapshot.rs b/packages/storage-plus/src/snapshot.rs index a4e6fe29a..061d6fb67 100644 --- a/packages/storage-plus/src/snapshot.rs +++ b/packages/storage-plus/src/snapshot.rs @@ -1,56 +1,27 @@ -#![cfg(feature = "iterator")] - +use crate::{Bound, Map, Prefixer, PrimaryKey, U64Key}; +use cosmwasm_std::{Order, StdError, StdResult, Storage}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use cosmwasm_std::{Order, StdError, StdResult, Storage}; - -use crate::keys::{EmptyPrefix, PrimaryKey, U64Key}; -use crate::map::Map; -use crate::path::Path; -use crate::prefix::Prefix; -use crate::{Bound, Prefixer}; -use std::fmt::Debug; - -/// Map that maintains a snapshots of one or more checkpoints. -/// We can query historical data as well as current state. -/// What data is snapshotted depends on the Strategy. -pub struct SnapshotMap<'a, K, T> { - primary: Map<'a, K, T>, - - // maps height to number of checkpoints (only used for selected) +#[derive(Debug, Clone)] +pub(crate) struct Snapshot<'a, K, T> { checkpoints: Map<'a, U64Key, u32>, // this stores all changes (key, height). Must differentiate between no data written, // and explicit None (just inserted) - changelog: Map<'a, (K, U64Key), ChangeSet>, + pub changelog: Map<'a, (K, U64Key), ChangeSet>, // How aggressive we are about checkpointing all data strategy: Strategy, } -#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] -pub enum Strategy { - EveryBlock, - Never, - /// Only writes for linked blocks - does a few more reads to save some writes. - /// Probably uses more gas, but less total disk usage. - /// - /// Note that you need a trusted source (eg. own contract) to set/remove checkpoints. - /// Useful when the checkpoint setting happens in the same contract as the snapshotting. - Selected, -} - -impl<'a, K, T> SnapshotMap<'a, K, T> { - /// Usage: SnapshotMap::new(snapshot_names!("foobar"), Strategy::EveryBlock) +impl<'a, K, T> Snapshot<'a, K, T> { pub const fn new( - pk: &'a str, checkpoints: &'a str, changelog: &'a str, strategy: Strategy, - ) -> Self { - SnapshotMap { - primary: Map::new(pk), + ) -> Snapshot<'a, K, T> { + Snapshot { checkpoints: Map::new(checkpoints), changelog: Map::new(changelog), strategy, @@ -79,25 +50,13 @@ impl<'a, K, T> SnapshotMap<'a, K, T> { } } -impl<'a, K, T> SnapshotMap<'a, K, T> +impl<'a, K, T> Snapshot<'a, K, T> where T: Serialize + DeserializeOwned + Clone, K: PrimaryKey<'a> + Prefixer<'a>, { - pub fn key(&self, k: K) -> Path { - self.primary.key(k) - } - - pub fn prefix(&self, p: K::Prefix) -> Prefix { - self.primary.prefix(p) - } - - pub fn sub_prefix(&self, p: K::SubPrefix) -> Prefix { - self.primary.sub_prefix(p) - } - /// should_checkpoint looks at the strategy and determines if we want to checkpoint - fn should_checkpoint(&self, store: &dyn Storage, k: &K) -> StdResult { + pub fn should_checkpoint(&self, store: &dyn Storage, k: &K) -> StdResult { match self.strategy { Strategy::EveryBlock => Ok(true), Strategy::Never => Ok(false), @@ -131,46 +90,35 @@ where Ok(false) } - /// load old value and store changelog - fn write_change(&self, store: &mut dyn Storage, k: K, height: u64) -> StdResult<()> { - // if there is already data in the changelog for this key and block, do not write more - if self - .changelog - .may_load(store, (k.clone(), U64Key::from(height)))? - .is_some() - { - return Ok(()); - } - // otherwise, store the previous value - let old = self.primary.may_load(store, k.clone())?; - self.changelog - .save(store, (k, U64Key::from(height)), &ChangeSet { old }) - } - - pub fn save(&self, store: &mut dyn Storage, k: K, data: &T, height: u64) -> StdResult<()> { - if self.should_checkpoint(store, &k)? { - self.write_change(store, k.clone(), height)?; - } - self.primary.save(store, k, data) - } - - pub fn remove(&self, store: &mut dyn Storage, k: K, height: u64) -> StdResult<()> { - if self.should_checkpoint(store, &k)? { - self.write_change(store, k.clone(), height)?; + // If there is no checkpoint for that height, then we return StdError::NotFound + pub fn assert_checkpointed(&self, store: &dyn Storage, height: u64) -> StdResult<()> { + let has = match self.strategy { + Strategy::EveryBlock => true, + Strategy::Never => false, + Strategy::Selected => self.checkpoints.may_load(store, height.into())?.is_some(), + }; + match has { + true => Ok(()), + false => Err(StdError::not_found("checkpoint")), } - self.primary.remove(store, k); - Ok(()) } - /// load will return an error if no data is set at the given key, or on parse error - pub fn load(&self, store: &dyn Storage, k: K) -> StdResult { - self.primary.load(store, k) + pub fn has_changelog(&self, store: &mut dyn Storage, key: K, height: u64) -> StdResult { + Ok(self + .changelog + .may_load(store, (key, U64Key::from(height)))? + .is_some()) } - /// may_load will parse the data stored at the key if present, returns Ok(None) if no data there. - /// returns an error on issues parsing - pub fn may_load(&self, store: &dyn Storage, k: K) -> StdResult> { - self.primary.may_load(store, k) + pub fn write_changelog( + &self, + store: &mut dyn Storage, + key: K, + height: u64, + old: Option, + ) -> StdResult<()> { + self.changelog + .save(store, (key, U64Key::from(height)), &ChangeSet { old }) } // may_load_at_height reads historical data from given checkpoints. @@ -181,9 +129,9 @@ where pub fn may_load_at_height( &self, store: &dyn Storage, - k: K, + key: K, height: u64, - ) -> StdResult> { + ) -> StdResult>> { self.assert_checkpointed(store, height)?; // this will look for the first snapshot of the given address >= given height @@ -191,272 +139,32 @@ where let start = Bound::inclusive(U64Key::new(height)); let first = self .changelog - .prefix(k.clone()) + .prefix(key) .range(store, Some(start), None, Order::Ascending) .next(); if let Some(r) = first { // if we found a match, return this last one - r.map(|(_, v)| v.old) + r.map(|(_, v)| Some(v.old)) } else { - // otherwise, return current value - self.may_load(store, k) - } - } - - // If there is no checkpoint for that height, then we return StdError::NotFound - pub fn assert_checkpointed(&self, store: &dyn Storage, height: u64) -> StdResult<()> { - let has = match self.strategy { - Strategy::EveryBlock => true, - Strategy::Never => false, - Strategy::Selected => self.checkpoints.may_load(store, height.into())?.is_some(), - }; - match has { - true => Ok(()), - false => Err(StdError::not_found("checkpoint")), + Ok(None) } } - - /// Loads the data, perform the specified action, and store the result - /// in the database. This is shorthand for some common sequences, which may be useful. - /// - /// If the data exists, `action(Some(value))` is called. Otherwise `action(None)` is called. - /// - /// This is a bit more customized than needed to only read "old" value 1 time, not 2 per naive approach - pub fn update( - &self, - store: &mut dyn Storage, - k: K, - height: u64, - action: A, - ) -> Result - where - A: FnOnce(Option) -> Result, - E: From, - { - let input = self.may_load(store, k.clone())?; - let output = action(input)?; - self.save(store, k, &output, height)?; - Ok(output) - } } -// short-cut for simple keys, rather than .prefix(()).range(...) -#[cfg(feature = "iterator")] -impl<'a, K, T> SnapshotMap<'a, K, T> -where - T: Serialize + DeserializeOwned + Clone, - K: PrimaryKey<'a> + Prefixer<'a>, - K::SubPrefix: EmptyPrefix, -{ - // I would prefer not to copy code from Prefix, but no other way - // with lifetimes (create Prefix inside function and return ref = no no) - pub fn range<'c>( - &self, - store: &'c dyn Storage, - min: Option, - max: Option, - order: cosmwasm_std::Order, - ) -> Box>> + 'c> - where - T: 'c, - { - self.sub_prefix(K::SubPrefix::new()) - .range(store, min, max, order) - } +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +pub enum Strategy { + EveryBlock, + Never, + /// Only writes for linked blocks - does a few more reads to save some writes. + /// Probably uses more gas, but less total disk usage. + /// + /// Note that you need a trusted source (eg. own contract) to set/remove checkpoints. + /// Useful when the checkpoint setting happens in the same contract as the snapshotting. + Selected, } #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] -struct ChangeSet { +pub(crate) struct ChangeSet { pub old: Option, } - -#[cfg(test)] -mod tests { - use super::*; - use cosmwasm_std::testing::MockStorage; - - type TestMap = SnapshotMap<'static, &'static [u8], u64>; - const NEVER: TestMap = - SnapshotMap::new("never", "never__check", "never__change", Strategy::Never); - const EVERY: TestMap = SnapshotMap::new( - "every", - "every__check", - "every__change", - Strategy::EveryBlock, - ); - const SELECT: TestMap = SnapshotMap::new( - "select", - "select__check", - "select__change", - Strategy::Selected, - ); - - // Fills a map &[u8] -> u64 with the following writes: - // 1: A = 5 - // 2: B = 7 - // 3: C = 1, A = 8 - // 4: B = None, C = 13 - // 5: A = None, D = 22 - // Final values -> C = 13, D = 22 - // Values at beginning of 3 -> A = 5, B = 7 - // Values at beginning of 5 -> A = 8, C = 13 - fn init_data(map: &TestMap, storage: &mut dyn Storage) { - map.save(storage, b"A", &5, 1).unwrap(); - map.save(storage, b"B", &7, 2).unwrap(); - - // checkpoint 3 - map.add_checkpoint(storage, 3).unwrap(); - - // also use update to set - to ensure this works - map.save(storage, b"C", &1, 3).unwrap(); - map.update(storage, b"A", 3, |_| -> StdResult { Ok(8) }) - .unwrap(); - - map.remove(storage, b"B", 4).unwrap(); - map.save(storage, b"C", &13, 4).unwrap(); - - // checkpoint 5 - map.add_checkpoint(storage, 5).unwrap(); - map.remove(storage, b"A", 5).unwrap(); - map.update(storage, b"D", 5, |_| -> StdResult { Ok(22) }) - .unwrap(); - // and delete it later (unknown if all data present) - map.remove_checkpoint(storage, 5).unwrap(); - } - - const FINAL_VALUES: &[(&[u8], Option)] = &[ - (b"A", None), - (b"B", None), - (b"C", Some(13)), - (b"D", Some(22)), - ]; - - const VALUES_START_3: &[(&[u8], Option)] = - &[(b"A", Some(5)), (b"B", Some(7)), (b"C", None), (b"D", None)]; - - const VALUES_START_5: &[(&[u8], Option)] = &[ - (b"A", Some(8)), - (b"B", None), - (b"C", Some(13)), - (b"D", None), - ]; - - fn assert_final_values(map: &TestMap, storage: &dyn Storage) { - for (k, v) in FINAL_VALUES.iter().cloned() { - assert_eq!(v, map.may_load(storage, k).unwrap()); - } - } - - fn assert_values_at_height( - map: &TestMap, - storage: &dyn Storage, - height: u64, - values: &[(&[u8], Option)], - ) { - for (k, v) in values.iter().cloned() { - assert_eq!(v, map.may_load_at_height(storage, k, height).unwrap()); - } - } - - fn assert_missing_checkpoint(map: &TestMap, storage: &dyn Storage, height: u64) { - for k in &[b"A", b"B", b"C", b"D"] { - assert!(map.may_load_at_height(storage, *k, height).is_err()); - } - } - - #[test] - fn never_works_like_normal_map() { - let mut storage = MockStorage::new(); - init_data(&NEVER, &mut storage); - assert_final_values(&NEVER, &storage); - - // historical queries return error - assert_missing_checkpoint(&NEVER, &storage, 3); - assert_missing_checkpoint(&NEVER, &storage, 5); - } - - #[test] - fn every_blocks_stores_present_and_past() { - let mut storage = MockStorage::new(); - init_data(&EVERY, &mut storage); - assert_final_values(&EVERY, &storage); - - // historical queries return historical values - assert_values_at_height(&EVERY, &storage, 3, VALUES_START_3); - assert_values_at_height(&EVERY, &storage, 5, VALUES_START_5); - } - - #[test] - fn selected_shows_3_not_5() { - let mut storage = MockStorage::new(); - init_data(&SELECT, &mut storage); - assert_final_values(&SELECT, &storage); - - // historical queries return historical values - assert_values_at_height(&SELECT, &storage, 3, VALUES_START_3); - // never checkpointed - assert_missing_checkpoint(&NEVER, &storage, 1); - // deleted checkpoint - assert_missing_checkpoint(&NEVER, &storage, 5); - } - - #[test] - fn handle_multiple_writes_in_one_block() { - let mut storage = MockStorage::new(); - - println!("SETUP"); - EVERY.save(&mut storage, b"A", &5, 1).unwrap(); - EVERY.save(&mut storage, b"B", &7, 2).unwrap(); - EVERY.save(&mut storage, b"C", &2, 2).unwrap(); - - // update and save - A query at 3 => 5, at 4 => 12 - EVERY - .update(&mut storage, b"A", 3, |_| -> StdResult { Ok(9) }) - .unwrap(); - EVERY.save(&mut storage, b"A", &12, 3).unwrap(); - assert_eq!( - Some(5), - EVERY.may_load_at_height(&storage, b"A", 2).unwrap() - ); - assert_eq!( - Some(5), - EVERY.may_load_at_height(&storage, b"A", 3).unwrap() - ); - assert_eq!( - Some(12), - EVERY.may_load_at_height(&storage, b"A", 4).unwrap() - ); - - // save and remove - B query at 4 => 7, at 5 => None - EVERY.save(&mut storage, b"B", &17, 4).unwrap(); - EVERY.remove(&mut storage, b"B", 4).unwrap(); - assert_eq!( - Some(7), - EVERY.may_load_at_height(&storage, b"B", 3).unwrap() - ); - assert_eq!( - Some(7), - EVERY.may_load_at_height(&storage, b"B", 4).unwrap() - ); - assert_eq!(None, EVERY.may_load_at_height(&storage, b"B", 5).unwrap()); - - // remove and update - C query at 5 => 2, at 6 => 16 - EVERY.remove(&mut storage, b"C", 5).unwrap(); - EVERY - .update(&mut storage, b"C", 5, |_| -> StdResult { Ok(16) }) - .unwrap(); - assert_eq!( - Some(2), - EVERY.may_load_at_height(&storage, b"C", 4).unwrap() - ); - assert_eq!( - Some(2), - EVERY.may_load_at_height(&storage, b"C", 5).unwrap() - ); - assert_eq!( - Some(16), - EVERY.may_load_at_height(&storage, b"C", 6).unwrap() - ); - } -} diff --git a/packages/storage-plus/src/snapshot_item.rs b/packages/storage-plus/src/snapshot_item.rs new file mode 100644 index 000000000..7c1356394 --- /dev/null +++ b/packages/storage-plus/src/snapshot_item.rs @@ -0,0 +1,267 @@ +#![cfg(feature = "iterator")] + +use serde::de::DeserializeOwned; +use serde::Serialize; + +use cosmwasm_std::{StdError, StdResult, Storage}; + +use crate::snapshot::Snapshot; +use crate::{Item, Strategy}; + +/// Item that maintains a snapshot of one or more checkpoints. +/// We can query historical data as well as current state. +/// What data is snapshotted depends on the Strategy. +pub struct SnapshotItem<'a, T> { + primary: Item<'a, T>, + snapshots: Snapshot<'a, (), T>, +} + +impl<'a, T> SnapshotItem<'a, T> { + /// Usage: SnapshotItem::new(snapshot_names!("foobar"), Strategy::EveryBlock) + pub const fn new( + storage_key: &'a str, + checkpoints: &'a str, + changelog: &'a str, + strategy: Strategy, + ) -> Self { + SnapshotItem { + primary: Item::new(storage_key), + snapshots: Snapshot::new(checkpoints, changelog, strategy), + } + } + + pub fn add_checkpoint(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { + self.snapshots.add_checkpoint(store, height) + } + + pub fn remove_checkpoint(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { + self.snapshots.remove_checkpoint(store, height) + } +} + +impl<'a, T> SnapshotItem<'a, T> +where + T: Serialize + DeserializeOwned + Clone, +{ + /// load old value and store changelog + fn write_change(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { + // if there is already data in the changelog for this block, do not write more + if self.snapshots.has_changelog(store, (), height)? { + return Ok(()); + } + // otherwise, store the previous value + let old = self.primary.may_load(store)?; + self.snapshots.write_changelog(store, (), height, old) + } + + pub fn save(&self, store: &mut dyn Storage, data: &T, height: u64) -> StdResult<()> { + if self.snapshots.should_checkpoint(store, &())? { + self.write_change(store, height)?; + } + self.primary.save(store, data) + } + + pub fn remove(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { + if self.snapshots.should_checkpoint(store, &())? { + self.write_change(store, height)?; + } + self.primary.remove(store); + Ok(()) + } + + /// load will return an error if no data is set, or on parse error + pub fn load(&self, store: &dyn Storage) -> StdResult { + self.primary.load(store) + } + + /// may_load will parse the data stored if present, returns Ok(None) if no data there. + /// returns an error on parsing issues + pub fn may_load(&self, store: &dyn Storage) -> StdResult> { + self.primary.may_load(store) + } + + pub fn may_load_at_height(&self, store: &dyn Storage, height: u64) -> StdResult> { + let snapshot = self.snapshots.may_load_at_height(store, (), height)?; + + if let Some(r) = snapshot { + Ok(r) + } else { + // otherwise, return current value + self.may_load(store) + } + } + + // If there is no checkpoint for that height, then we return StdError::NotFound + pub fn assert_checkpointed(&self, store: &dyn Storage, height: u64) -> StdResult<()> { + self.snapshots.assert_checkpointed(store, height) + } + + /// Loads the data, perform the specified action, and store the result in the database. + /// This is a shorthand for some common sequences, which may be useful. + /// + /// If the data exists, `action(Some(value))` is called. Otherwise `action(None)` is called. + /// + /// This is a bit more customized than needed to only read "old" value 1 time, not 2 per naive approach + pub fn update(&self, store: &mut dyn Storage, height: u64, action: A) -> Result + where + A: FnOnce(Option) -> Result, + E: From, + { + let input = self.may_load(store)?; + let output = action(input)?; + self.save(store, &output, height)?; + Ok(output) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::MockStorage; + + type TestItem = SnapshotItem<'static, u64>; + + const NEVER: TestItem = + SnapshotItem::new("never", "never__check", "never__change", Strategy::Never); + const EVERY: TestItem = SnapshotItem::new( + "every", + "every__check", + "every__change", + Strategy::EveryBlock, + ); + const SELECT: TestItem = SnapshotItem::new( + "select", + "select__check", + "select__change", + Strategy::Selected, + ); + + // Fills an item (u64) with the following writes: + // 1: 5 + // 2: 7 + // 3: 8 + // 4: 1 + // 5: None + // 6: 13 + // 7: None + // 8: 22 + // Final value: 22 + // Value at beginning of 3 -> 7 + // Value at beginning of 5 -> 1 + fn init_data(item: &TestItem, storage: &mut dyn Storage) { + item.save(storage, &5, 1).unwrap(); + item.save(storage, &7, 2).unwrap(); + + // checkpoint 3 + item.add_checkpoint(storage, 3).unwrap(); + + // also use update to set - to ensure this works + item.save(storage, &1, 3).unwrap(); + item.update(storage, 3, |_| -> StdResult { Ok(8) }) + .unwrap(); + + item.remove(storage, 4).unwrap(); + item.save(storage, &13, 4).unwrap(); + + // checkpoint 5 + item.add_checkpoint(storage, 5).unwrap(); + item.remove(storage, 5).unwrap(); + item.update(storage, 5, |_| -> StdResult { Ok(22) }) + .unwrap(); + // and delete it later (unknown if all data present) + item.remove_checkpoint(storage, 5).unwrap(); + } + + const FINAL_VALUE: Option = Some(22); + + const VALUE_START_3: Option = Some(7); + + const VALUE_START_5: Option = Some(13); + + fn assert_final_value(item: &TestItem, storage: &dyn Storage) { + assert_eq!(FINAL_VALUE, item.may_load(storage).unwrap()); + } + + #[track_caller] + fn assert_value_at_height( + item: &TestItem, + storage: &dyn Storage, + height: u64, + value: Option, + ) { + assert_eq!(value, item.may_load_at_height(storage, height).unwrap()); + } + + fn assert_missing_checkpoint(item: &TestItem, storage: &dyn Storage, height: u64) { + assert!(item.may_load_at_height(storage, height).is_err()); + } + + #[test] + fn never_works_like_normal_item() { + let mut storage = MockStorage::new(); + init_data(&NEVER, &mut storage); + assert_final_value(&NEVER, &storage); + + // historical queries return error + assert_missing_checkpoint(&NEVER, &storage, 3); + assert_missing_checkpoint(&NEVER, &storage, 5); + } + + #[test] + fn every_blocks_stores_present_and_past() { + let mut storage = MockStorage::new(); + init_data(&EVERY, &mut storage); + assert_final_value(&EVERY, &storage); + + // historical queries return historical values + assert_value_at_height(&EVERY, &storage, 3, VALUE_START_3); + assert_value_at_height(&EVERY, &storage, 5, VALUE_START_5); + } + + #[test] + fn selected_shows_3_not_5() { + let mut storage = MockStorage::new(); + init_data(&SELECT, &mut storage); + assert_final_value(&SELECT, &storage); + + // historical queries return historical values + assert_value_at_height(&SELECT, &storage, 3, VALUE_START_3); + // never checkpointed + assert_missing_checkpoint(&NEVER, &storage, 1); + // deleted checkpoint + assert_missing_checkpoint(&NEVER, &storage, 5); + } + + #[test] + fn handle_multiple_writes_in_one_block() { + let mut storage = MockStorage::new(); + + println!("SETUP"); + EVERY.save(&mut storage, &5, 1).unwrap(); + EVERY.save(&mut storage, &7, 2).unwrap(); + EVERY.save(&mut storage, &2, 2).unwrap(); + + // update and save - query at 3 => 2, at 4 => 12 + EVERY + .update(&mut storage, 3, |_| -> StdResult { Ok(9) }) + .unwrap(); + EVERY.save(&mut storage, &12, 3).unwrap(); + assert_eq!(Some(5), EVERY.may_load_at_height(&storage, 2).unwrap()); + assert_eq!(Some(2), EVERY.may_load_at_height(&storage, 3).unwrap()); + assert_eq!(Some(12), EVERY.may_load_at_height(&storage, 4).unwrap()); + + // save and remove - query at 4 => 1, at 5 => None + EVERY.save(&mut storage, &17, 4).unwrap(); + EVERY.remove(&mut storage, 4).unwrap(); + assert_eq!(Some(12), EVERY.may_load_at_height(&storage, 4).unwrap()); + assert_eq!(None, EVERY.may_load_at_height(&storage, 5).unwrap()); + + // remove and update - query at 5 => 2, at 6 => 13 + EVERY.remove(&mut storage, 5).unwrap(); + EVERY + .update(&mut storage, 5, |_| -> StdResult { Ok(2) }) + .unwrap(); + assert_eq!(None, EVERY.may_load_at_height(&storage, 5).unwrap()); + assert_eq!(Some(2), EVERY.may_load_at_height(&storage, 6).unwrap()); + } +} diff --git a/packages/storage-plus/src/snapshot_map.rs b/packages/storage-plus/src/snapshot_map.rs new file mode 100644 index 000000000..1ef0b66dd --- /dev/null +++ b/packages/storage-plus/src/snapshot_map.rs @@ -0,0 +1,365 @@ +#![cfg(feature = "iterator")] + +use serde::de::DeserializeOwned; +use serde::Serialize; + +use cosmwasm_std::{StdError, StdResult, Storage}; + +use crate::keys::{EmptyPrefix, PrimaryKey}; +use crate::map::Map; +use crate::path::Path; +use crate::prefix::Prefix; +use crate::snapshot::Snapshot; +use crate::{Bound, Prefixer, Strategy}; + +/// Map that maintains a snapshots of one or more checkpoints. +/// We can query historical data as well as current state. +/// What data is snapshotted depends on the Strategy. +pub struct SnapshotMap<'a, K, T> { + primary: Map<'a, K, T>, + snapshots: Snapshot<'a, K, T>, +} + +impl<'a, K, T> SnapshotMap<'a, K, T> { + /// Usage: SnapshotMap::new(snapshot_names!("foobar"), Strategy::EveryBlock) + pub const fn new( + pk: &'a str, + checkpoints: &'a str, + changelog: &'a str, + strategy: Strategy, + ) -> Self { + SnapshotMap { + primary: Map::new(pk), + snapshots: Snapshot::new(checkpoints, changelog, strategy), + } + } +} + +impl<'a, K, T> SnapshotMap<'a, K, T> +where + T: Serialize + DeserializeOwned + Clone, + K: PrimaryKey<'a> + Prefixer<'a>, +{ + pub fn add_checkpoint(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { + self.snapshots.add_checkpoint(store, height) + } + + pub fn remove_checkpoint(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { + self.snapshots.remove_checkpoint(store, height) + } +} + +impl<'a, K, T> SnapshotMap<'a, K, T> +where + T: Serialize + DeserializeOwned + Clone, + K: PrimaryKey<'a> + Prefixer<'a>, +{ + pub fn key(&self, k: K) -> Path { + self.primary.key(k) + } + + pub fn prefix(&self, p: K::Prefix) -> Prefix { + self.primary.prefix(p) + } + + pub fn sub_prefix(&self, p: K::SubPrefix) -> Prefix { + self.primary.sub_prefix(p) + } + + /// load old value and store changelog + fn write_change(&self, store: &mut dyn Storage, k: K, height: u64) -> StdResult<()> { + // if there is already data in the changelog for this key and block, do not write more + if self.snapshots.has_changelog(store, k.clone(), height)? { + return Ok(()); + } + // otherwise, store the previous value + let old = self.primary.may_load(store, k.clone())?; + self.snapshots.write_changelog(store, k, height, old) + } + + pub fn save(&self, store: &mut dyn Storage, k: K, data: &T, height: u64) -> StdResult<()> { + if self.snapshots.should_checkpoint(store, &k)? { + self.write_change(store, k.clone(), height)?; + } + self.primary.save(store, k, data) + } + + pub fn remove(&self, store: &mut dyn Storage, k: K, height: u64) -> StdResult<()> { + if self.snapshots.should_checkpoint(store, &k)? { + self.write_change(store, k.clone(), height)?; + } + self.primary.remove(store, k); + Ok(()) + } + + /// load will return an error if no data is set at the given key, or on parse error + pub fn load(&self, store: &dyn Storage, k: K) -> StdResult { + self.primary.load(store, k) + } + + /// may_load will parse the data stored at the key if present, returns Ok(None) if no data there. + /// returns an error on issues parsing + pub fn may_load(&self, store: &dyn Storage, k: K) -> StdResult> { + self.primary.may_load(store, k) + } + + pub fn may_load_at_height( + &self, + store: &dyn Storage, + k: K, + height: u64, + ) -> StdResult> { + let snapshot = self + .snapshots + .may_load_at_height(store, k.clone(), height)?; + + if let Some(r) = snapshot { + Ok(r) + } else { + // otherwise, return current value + self.may_load(store, k) + } + } + + pub fn assert_checkpointed(&self, store: &dyn Storage, height: u64) -> StdResult<()> { + self.snapshots.assert_checkpointed(store, height) + } + + /// Loads the data, perform the specified action, and store the result + /// in the database. This is shorthand for some common sequences, which may be useful. + /// + /// If the data exists, `action(Some(value))` is called. Otherwise `action(None)` is called. + /// + /// This is a bit more customized than needed to only read "old" value 1 time, not 2 per naive approach + pub fn update( + &self, + store: &mut dyn Storage, + k: K, + height: u64, + action: A, + ) -> Result + where + A: FnOnce(Option) -> Result, + E: From, + { + let input = self.may_load(store, k.clone())?; + let output = action(input)?; + self.save(store, k, &output, height)?; + Ok(output) + } +} + +// short-cut for simple keys, rather than .prefix(()).range(...) +#[cfg(feature = "iterator")] +impl<'a, K, T> SnapshotMap<'a, K, T> +where + T: Serialize + DeserializeOwned + Clone, + K: PrimaryKey<'a> + Prefixer<'a>, + K::SubPrefix: EmptyPrefix, +{ + // I would prefer not to copy code from Prefix, but no other way + // with lifetimes (create Prefix inside function and return ref = no no) + pub fn range<'c>( + &self, + store: &'c dyn Storage, + min: Option, + max: Option, + order: cosmwasm_std::Order, + ) -> Box>> + 'c> + where + T: 'c, + { + self.sub_prefix(K::SubPrefix::new()) + .range(store, min, max, order) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::MockStorage; + + type TestMap = SnapshotMap<'static, &'static [u8], u64>; + const NEVER: TestMap = + SnapshotMap::new("never", "never__check", "never__change", Strategy::Never); + const EVERY: TestMap = SnapshotMap::new( + "every", + "every__check", + "every__change", + Strategy::EveryBlock, + ); + const SELECT: TestMap = SnapshotMap::new( + "select", + "select__check", + "select__change", + Strategy::Selected, + ); + + // Fills a map &[u8] -> u64 with the following writes: + // 1: A = 5 + // 2: B = 7 + // 3: C = 1, A = 8 + // 4: B = None, C = 13 + // 5: A = None, D = 22 + // Final values -> C = 13, D = 22 + // Values at beginning of 3 -> A = 5, B = 7 + // Values at beginning of 5 -> A = 8, C = 13 + fn init_data(map: &TestMap, storage: &mut dyn Storage) { + map.save(storage, b"A", &5, 1).unwrap(); + map.save(storage, b"B", &7, 2).unwrap(); + + // checkpoint 3 + map.add_checkpoint(storage, 3).unwrap(); + + // also use update to set - to ensure this works + map.save(storage, b"C", &1, 3).unwrap(); + map.update(storage, b"A", 3, |_| -> StdResult { Ok(8) }) + .unwrap(); + + map.remove(storage, b"B", 4).unwrap(); + map.save(storage, b"C", &13, 4).unwrap(); + + // checkpoint 5 + map.add_checkpoint(storage, 5).unwrap(); + map.remove(storage, b"A", 5).unwrap(); + map.update(storage, b"D", 5, |_| -> StdResult { Ok(22) }) + .unwrap(); + // and delete it later (unknown if all data present) + map.remove_checkpoint(storage, 5).unwrap(); + } + + const FINAL_VALUES: &[(&[u8], Option)] = &[ + (b"A", None), + (b"B", None), + (b"C", Some(13)), + (b"D", Some(22)), + ]; + + const VALUES_START_3: &[(&[u8], Option)] = + &[(b"A", Some(5)), (b"B", Some(7)), (b"C", None), (b"D", None)]; + + const VALUES_START_5: &[(&[u8], Option)] = &[ + (b"A", Some(8)), + (b"B", None), + (b"C", Some(13)), + (b"D", None), + ]; + + fn assert_final_values(map: &TestMap, storage: &dyn Storage) { + for (k, v) in FINAL_VALUES.iter().cloned() { + assert_eq!(v, map.may_load(storage, k).unwrap()); + } + } + + fn assert_values_at_height( + map: &TestMap, + storage: &dyn Storage, + height: u64, + values: &[(&[u8], Option)], + ) { + for (k, v) in values.iter().cloned() { + assert_eq!(v, map.may_load_at_height(storage, k, height).unwrap()); + } + } + + fn assert_missing_checkpoint(map: &TestMap, storage: &dyn Storage, height: u64) { + for k in &[b"A", b"B", b"C", b"D"] { + assert!(map.may_load_at_height(storage, *k, height).is_err()); + } + } + + #[test] + fn never_works_like_normal_map() { + let mut storage = MockStorage::new(); + init_data(&NEVER, &mut storage); + assert_final_values(&NEVER, &storage); + + // historical queries return error + assert_missing_checkpoint(&NEVER, &storage, 3); + assert_missing_checkpoint(&NEVER, &storage, 5); + } + + #[test] + fn every_blocks_stores_present_and_past() { + let mut storage = MockStorage::new(); + init_data(&EVERY, &mut storage); + assert_final_values(&EVERY, &storage); + + // historical queries return historical values + assert_values_at_height(&EVERY, &storage, 3, VALUES_START_3); + assert_values_at_height(&EVERY, &storage, 5, VALUES_START_5); + } + + #[test] + fn selected_shows_3_not_5() { + let mut storage = MockStorage::new(); + init_data(&SELECT, &mut storage); + assert_final_values(&SELECT, &storage); + + // historical queries return historical values + assert_values_at_height(&SELECT, &storage, 3, VALUES_START_3); + // never checkpointed + assert_missing_checkpoint(&NEVER, &storage, 1); + // deleted checkpoint + assert_missing_checkpoint(&NEVER, &storage, 5); + } + + #[test] + fn handle_multiple_writes_in_one_block() { + let mut storage = MockStorage::new(); + + println!("SETUP"); + EVERY.save(&mut storage, b"A", &5, 1).unwrap(); + EVERY.save(&mut storage, b"B", &7, 2).unwrap(); + EVERY.save(&mut storage, b"C", &2, 2).unwrap(); + + // update and save - A query at 3 => 5, at 4 => 12 + EVERY + .update(&mut storage, b"A", 3, |_| -> StdResult { Ok(9) }) + .unwrap(); + EVERY.save(&mut storage, b"A", &12, 3).unwrap(); + assert_eq!( + Some(5), + EVERY.may_load_at_height(&storage, b"A", 2).unwrap() + ); + assert_eq!( + Some(5), + EVERY.may_load_at_height(&storage, b"A", 3).unwrap() + ); + assert_eq!( + Some(12), + EVERY.may_load_at_height(&storage, b"A", 4).unwrap() + ); + + // save and remove - B query at 4 => 7, at 5 => None + EVERY.save(&mut storage, b"B", &17, 4).unwrap(); + EVERY.remove(&mut storage, b"B", 4).unwrap(); + assert_eq!( + Some(7), + EVERY.may_load_at_height(&storage, b"B", 3).unwrap() + ); + assert_eq!( + Some(7), + EVERY.may_load_at_height(&storage, b"B", 4).unwrap() + ); + assert_eq!(None, EVERY.may_load_at_height(&storage, b"B", 5).unwrap()); + + // remove and update - C query at 5 => 2, at 6 => 16 + EVERY.remove(&mut storage, b"C", 5).unwrap(); + EVERY + .update(&mut storage, b"C", 5, |_| -> StdResult { Ok(16) }) + .unwrap(); + assert_eq!( + Some(2), + EVERY.may_load_at_height(&storage, b"C", 4).unwrap() + ); + assert_eq!( + Some(2), + EVERY.may_load_at_height(&storage, b"C", 5).unwrap() + ); + assert_eq!( + Some(16), + EVERY.may_load_at_height(&storage, b"C", 6).unwrap() + ); + } +}