#![cfg(feature = "iterator")]
mod item;
mod map;
pub use item::SnapshotItem;
pub use map::SnapshotMap;
use crate::bound::Bound;
use crate::de::KeyDeserialize;
use crate::namespace::Namespace;
use crate::{Map, Prefixer, PrimaryKey};
use cosmwasm_std::{Order, StdError, StdResult, Storage};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub(crate) struct Snapshot<K, T> {
checkpoints: Map<u64, u32>,
pub changelog: Map<(K, u64), ChangeSet<T>>,
strategy: Strategy,
}
impl<K, T> Snapshot<K, T> {
pub const fn new(
checkpoints: &'static str,
changelog: &'static str,
strategy: Strategy,
) -> Snapshot<K, T> {
Snapshot {
checkpoints: Map::new(checkpoints),
changelog: Map::new(changelog),
strategy,
}
}
pub fn new_dyn(
checkpoints: impl Into<Namespace>,
changelog: impl Into<Namespace>,
strategy: Strategy,
) -> Snapshot<K, T> {
Snapshot {
checkpoints: Map::new_dyn(checkpoints),
changelog: Map::new_dyn(changelog),
strategy,
}
}
pub fn add_checkpoint(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> {
self.checkpoints
.update::<_, StdError>(store, height, |count| Ok(count.unwrap_or_default() + 1))?;
Ok(())
}
pub fn remove_checkpoint(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> {
let count = self
.checkpoints
.may_load(store, height)?
.unwrap_or_default();
if count <= 1 {
self.checkpoints.remove(store, height);
Ok(())
} else {
self.checkpoints.save(store, height, &(count - 1))
}
}
}
impl<'a, K, T> Snapshot<K, T>
where
T: Serialize + DeserializeOwned + Clone,
K: PrimaryKey<'a> + Prefixer<'a> + KeyDeserialize,
{
pub fn should_checkpoint(&self, store: &dyn Storage, k: &K) -> StdResult<bool> {
match self.strategy {
Strategy::EveryBlock => Ok(true),
Strategy::Never => Ok(false),
Strategy::Selected => self.should_checkpoint_selected(store, k),
}
}
fn should_checkpoint_selected(&self, store: &dyn Storage, k: &K) -> StdResult<bool> {
let checkpoint = self
.checkpoints
.range(store, None, None, Order::Descending)
.next()
.transpose()?;
if let Some((height, _)) = checkpoint {
let start = Bound::inclusive(height);
let first = self
.changelog
.prefix(k.clone())
.range_raw(store, Some(start), None, Order::Ascending)
.next()
.transpose()?;
if first.is_none() {
return Ok(true);
}
}
Ok(false)
}
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)?.is_some(),
};
match has {
true => Ok(()),
false => Err(StdError::not_found("checkpoint")),
}
}
pub fn has_changelog(&self, store: &mut dyn Storage, key: K, height: u64) -> StdResult<bool> {
Ok(self.changelog.may_load(store, (key, height))?.is_some())
}
pub fn write_changelog(
&self,
store: &mut dyn Storage,
key: K,
height: u64,
old: Option<T>,
) -> StdResult<()> {
self.changelog
.save(store, (key, height), &ChangeSet { old })
}
pub fn may_load_at_height(
&self,
store: &dyn Storage,
key: K,
height: u64,
) -> StdResult<Option<Option<T>>> {
self.assert_checkpointed(store, height)?;
let start = Bound::inclusive(height);
let first = self
.changelog
.prefix(key)
.range_raw(store, Some(start), None, Order::Ascending)
.next();
if let Some(r) = first {
r.map(|(_, v)| Some(v.old))
} else {
Ok(None)
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Strategy {
EveryBlock,
Never,
Selected,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct ChangeSet<T> {
pub old: Option<T>,
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::MockStorage;
type TestSnapshot = Snapshot<&'static str, u64>;
const NEVER: TestSnapshot = Snapshot::new("never__check", "never__change", Strategy::Never);
const EVERY: TestSnapshot =
Snapshot::new("every__check", "every__change", Strategy::EveryBlock);
const SELECT: TestSnapshot =
Snapshot::new("select__check", "select__change", Strategy::Selected);
const DUMMY_KEY: &str = "dummy";
#[test]
fn should_checkpoint() {
let storage = MockStorage::new();
assert_eq!(NEVER.should_checkpoint(&storage, &DUMMY_KEY), Ok(false));
assert_eq!(EVERY.should_checkpoint(&storage, &DUMMY_KEY), Ok(true));
assert_eq!(SELECT.should_checkpoint(&storage, &DUMMY_KEY), Ok(false));
}
#[test]
fn assert_checkpointed() {
let mut storage = MockStorage::new();
assert_eq!(
NEVER.assert_checkpointed(&storage, 1),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(EVERY.assert_checkpointed(&storage, 1), Ok(()));
assert_eq!(
SELECT.assert_checkpointed(&storage, 1),
Err(StdError::not_found("checkpoint"))
);
NEVER.add_checkpoint(&mut storage, 1).unwrap();
EVERY.add_checkpoint(&mut storage, 1).unwrap();
SELECT.add_checkpoint(&mut storage, 1).unwrap();
assert_eq!(
NEVER.assert_checkpointed(&storage, 1),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(EVERY.assert_checkpointed(&storage, 1), Ok(()));
assert_eq!(SELECT.assert_checkpointed(&storage, 1), Ok(()));
NEVER.remove_checkpoint(&mut storage, 1).unwrap();
EVERY.remove_checkpoint(&mut storage, 1).unwrap();
SELECT.remove_checkpoint(&mut storage, 1).unwrap();
assert_eq!(
NEVER.assert_checkpointed(&storage, 1),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(EVERY.assert_checkpointed(&storage, 1), Ok(()));
assert_eq!(
SELECT.assert_checkpointed(&storage, 1),
Err(StdError::not_found("checkpoint"))
);
}
#[test]
fn has_changelog() {
let mut storage = MockStorage::new();
assert_eq!(NEVER.has_changelog(&mut storage, DUMMY_KEY, 1), Ok(false));
assert_eq!(EVERY.has_changelog(&mut storage, DUMMY_KEY, 1), Ok(false));
assert_eq!(SELECT.has_changelog(&mut storage, DUMMY_KEY, 1), Ok(false));
assert_eq!(NEVER.has_changelog(&mut storage, DUMMY_KEY, 2), Ok(false));
assert_eq!(EVERY.has_changelog(&mut storage, DUMMY_KEY, 2), Ok(false));
assert_eq!(SELECT.has_changelog(&mut storage, DUMMY_KEY, 2), Ok(false));
assert_eq!(NEVER.has_changelog(&mut storage, DUMMY_KEY, 3), Ok(false));
assert_eq!(EVERY.has_changelog(&mut storage, DUMMY_KEY, 3), Ok(false));
assert_eq!(SELECT.has_changelog(&mut storage, DUMMY_KEY, 3), Ok(false));
NEVER
.write_changelog(&mut storage, DUMMY_KEY, 2, Some(3))
.unwrap();
EVERY
.write_changelog(&mut storage, DUMMY_KEY, 2, Some(4))
.unwrap();
SELECT
.write_changelog(&mut storage, DUMMY_KEY, 2, Some(5))
.unwrap();
assert_eq!(NEVER.has_changelog(&mut storage, DUMMY_KEY, 1), Ok(false));
assert_eq!(EVERY.has_changelog(&mut storage, DUMMY_KEY, 1), Ok(false));
assert_eq!(SELECT.has_changelog(&mut storage, DUMMY_KEY, 1), Ok(false));
assert_eq!(NEVER.has_changelog(&mut storage, DUMMY_KEY, 2), Ok(true));
assert_eq!(EVERY.has_changelog(&mut storage, DUMMY_KEY, 2), Ok(true));
assert_eq!(SELECT.has_changelog(&mut storage, DUMMY_KEY, 2), Ok(true));
assert_eq!(NEVER.has_changelog(&mut storage, DUMMY_KEY, 3), Ok(false));
assert_eq!(EVERY.has_changelog(&mut storage, DUMMY_KEY, 3), Ok(false));
assert_eq!(SELECT.has_changelog(&mut storage, DUMMY_KEY, 3), Ok(false));
}
#[test]
fn may_load_at_height() {
let mut storage = MockStorage::new();
assert_eq!(
NEVER.may_load_at_height(&storage, DUMMY_KEY, 3),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(EVERY.may_load_at_height(&storage, DUMMY_KEY, 3), Ok(None));
assert_eq!(
SELECT.may_load_at_height(&storage, DUMMY_KEY, 3),
Err(StdError::not_found("checkpoint"))
);
NEVER.add_checkpoint(&mut storage, 3).unwrap();
EVERY.add_checkpoint(&mut storage, 3).unwrap();
SELECT.add_checkpoint(&mut storage, 3).unwrap();
assert_eq!(
NEVER.may_load_at_height(&storage, DUMMY_KEY, 3),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(EVERY.may_load_at_height(&storage, DUMMY_KEY, 3), Ok(None));
assert_eq!(SELECT.may_load_at_height(&storage, DUMMY_KEY, 3), Ok(None));
NEVER
.write_changelog(&mut storage, DUMMY_KEY, 3, Some(100))
.unwrap();
EVERY
.write_changelog(&mut storage, DUMMY_KEY, 3, Some(101))
.unwrap();
SELECT
.write_changelog(&mut storage, DUMMY_KEY, 3, Some(102))
.unwrap();
assert_eq!(
NEVER.may_load_at_height(&storage, DUMMY_KEY, 3),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(
EVERY.may_load_at_height(&storage, DUMMY_KEY, 3),
Ok(Some(Some(101)))
);
assert_eq!(
SELECT.may_load_at_height(&storage, DUMMY_KEY, 3),
Ok(Some(Some(102)))
);
assert_eq!(
NEVER.may_load_at_height(&storage, DUMMY_KEY, 2),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(
EVERY.may_load_at_height(&storage, DUMMY_KEY, 2),
Ok(Some(Some(101)))
);
assert_eq!(
SELECT.may_load_at_height(&storage, DUMMY_KEY, 2),
Err(StdError::not_found("checkpoint"))
);
NEVER
.write_changelog(&mut storage, DUMMY_KEY, 4, None)
.unwrap();
EVERY
.write_changelog(&mut storage, DUMMY_KEY, 4, None)
.unwrap();
SELECT
.write_changelog(&mut storage, DUMMY_KEY, 4, None)
.unwrap();
NEVER.add_checkpoint(&mut storage, 4).unwrap();
EVERY.add_checkpoint(&mut storage, 4).unwrap();
SELECT.add_checkpoint(&mut storage, 4).unwrap();
assert_eq!(
NEVER.may_load_at_height(&storage, DUMMY_KEY, 4),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(
EVERY.may_load_at_height(&storage, DUMMY_KEY, 4),
Ok(Some(None))
);
assert_eq!(
SELECT.may_load_at_height(&storage, DUMMY_KEY, 4),
Ok(Some(None))
);
assert_eq!(
NEVER.may_load_at_height(&storage, DUMMY_KEY, 3),
Err(StdError::not_found("checkpoint"))
);
assert_eq!(
EVERY.may_load_at_height(&storage, DUMMY_KEY, 3),
Ok(Some(Some(101)))
);
assert_eq!(
SELECT.may_load_at_height(&storage, DUMMY_KEY, 3),
Ok(Some(Some(102)))
);
}
}