From d8bae310f36e4cec37d87bce91b60b9505cb0548 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 14:40:51 +0100 Subject: [PATCH 1/9] copy basecoin-store src to ibc-testkit --- ibc-testkit/store/src/avl/as_bytes.rs | 63 ++++++ ibc-testkit/store/src/avl/mod.rs | 29 +++ ibc-testkit/store/src/avl/node.rs | 137 +++++++++++++ ibc-testkit/store/src/avl/proof.rs | 38 ++++ ibc-testkit/store/src/avl/tests.rs | 224 ++++++++++++++++++++++ ibc-testkit/store/src/avl/tree.rs | 222 +++++++++++++++++++++ ibc-testkit/store/src/context.rs | 52 +++++ ibc-testkit/store/src/impls/growing.rs | 139 ++++++++++++++ ibc-testkit/store/src/impls/in_memory.rs | 107 +++++++++++ ibc-testkit/store/src/impls/mod.rs | 9 + ibc-testkit/store/src/impls/revertible.rs | 130 +++++++++++++ ibc-testkit/store/src/impls/shared.rs | 106 ++++++++++ ibc-testkit/store/src/lib.rs | 5 + ibc-testkit/store/src/types/height.rs | 31 +++ ibc-testkit/store/src/types/identifier.rs | 29 +++ ibc-testkit/store/src/types/mod.rs | 9 + ibc-testkit/store/src/types/path.rs | 183 ++++++++++++++++++ ibc-testkit/store/src/types/store.rs | 95 +++++++++ ibc-testkit/store/src/utils/codec.rs | 102 ++++++++++ ibc-testkit/store/src/utils/macros.rs | 32 ++++ ibc-testkit/store/src/utils/mod.rs | 6 + ibc-testkit/store/src/utils/sync.rs | 29 +++ 22 files changed, 1777 insertions(+) create mode 100644 ibc-testkit/store/src/avl/as_bytes.rs create mode 100644 ibc-testkit/store/src/avl/mod.rs create mode 100644 ibc-testkit/store/src/avl/node.rs create mode 100644 ibc-testkit/store/src/avl/proof.rs create mode 100644 ibc-testkit/store/src/avl/tests.rs create mode 100644 ibc-testkit/store/src/avl/tree.rs create mode 100644 ibc-testkit/store/src/context.rs create mode 100644 ibc-testkit/store/src/impls/growing.rs create mode 100644 ibc-testkit/store/src/impls/in_memory.rs create mode 100644 ibc-testkit/store/src/impls/mod.rs create mode 100644 ibc-testkit/store/src/impls/revertible.rs create mode 100644 ibc-testkit/store/src/impls/shared.rs create mode 100644 ibc-testkit/store/src/lib.rs create mode 100644 ibc-testkit/store/src/types/height.rs create mode 100644 ibc-testkit/store/src/types/identifier.rs create mode 100644 ibc-testkit/store/src/types/mod.rs create mode 100644 ibc-testkit/store/src/types/path.rs create mode 100644 ibc-testkit/store/src/types/store.rs create mode 100644 ibc-testkit/store/src/utils/codec.rs create mode 100644 ibc-testkit/store/src/utils/macros.rs create mode 100644 ibc-testkit/store/src/utils/mod.rs create mode 100644 ibc-testkit/store/src/utils/sync.rs diff --git a/ibc-testkit/store/src/avl/as_bytes.rs b/ibc-testkit/store/src/avl/as_bytes.rs new file mode 100644 index 000000000..f141518f2 --- /dev/null +++ b/ibc-testkit/store/src/avl/as_bytes.rs @@ -0,0 +1,63 @@ +//! # AsBytes trait definition +//! +//! This module hosts the `AsBytes` trait, which is used by the AVL Tree to convert value to raw +//! bytes. This is helpful for making the AVL Tree generic over a wide range of data types for its +//! keys (the values still need to implement `Borrow<[u8]>), as long as they can be interpreted as +//! a slice of bytes. +//! +//! To add support for a new type in the AVL Tree, simply implement the `AsByte` trait for that type. + +pub enum ByteSlice<'a> { + Slice(&'a [u8]), + Vector(Vec), +} + +impl AsRef<[u8]> for ByteSlice<'_> { + fn as_ref(&self) -> &[u8] { + match self { + ByteSlice::Slice(s) => s, + ByteSlice::Vector(v) => v.as_slice(), + } + } +} + +/// A trait for objects that can be interpreted as a slice of bytes. +pub trait AsBytes { + fn as_bytes(&self) -> ByteSlice<'_>; +} + +impl AsBytes for Vec { + fn as_bytes(&self) -> ByteSlice<'_> { + ByteSlice::Slice(self) + } +} + +impl AsBytes for [u8] { + fn as_bytes(&self) -> ByteSlice<'_> { + ByteSlice::Slice(self) + } +} + +impl AsBytes for str { + fn as_bytes(&self) -> ByteSlice<'_> { + ByteSlice::Slice(self.as_bytes()) + } +} + +impl AsBytes for &str { + fn as_bytes(&self) -> ByteSlice<'_> { + ByteSlice::Slice((*self).as_bytes()) + } +} + +impl AsBytes for String { + fn as_bytes(&self) -> ByteSlice<'_> { + ByteSlice::Slice(self.as_bytes()) + } +} + +impl AsBytes for [u8; 1] { + fn as_bytes(&self) -> ByteSlice<'_> { + ByteSlice::Slice(self) + } +} diff --git a/ibc-testkit/store/src/avl/mod.rs b/ibc-testkit/store/src/avl/mod.rs new file mode 100644 index 000000000..2925e587b --- /dev/null +++ b/ibc-testkit/store/src/avl/mod.rs @@ -0,0 +1,29 @@ +//! # AVL Tree +//! +//! This module hosts a simple implementation of an AVL Merkle Tree that support the `get` and +//! `insert` instructions (no delete yet, it's not needed as the on-chain store is supposed to be +//! immutable). +//! +//! Proof of existence are supported using [ICS23](https://github.com/confio/ics23), but proof of +//! non-existence are not yet implemented. +//! +//! Keys needs to implement `Ord` and `AsBytes` (see `as_bytes` module), while values are required +//! to implement `Borrow<[u8]>`. +//! +//! For more info, see [AVL Tree on wikipedia](https://en.wikipedia.org/wiki/AVL_tree), + +pub use as_bytes::{AsBytes, ByteSlice}; +pub use node::AvlNode; +pub use proof::get_proof_spec; +use tendermint::hash::Algorithm; +pub use tree::AvlTree; + +mod as_bytes; +mod node; +mod proof; +mod tree; + +#[cfg(test)] +mod tests; + +const HASH_ALGO: Algorithm = Algorithm::Sha256; diff --git a/ibc-testkit/store/src/avl/node.rs b/ibc-testkit/store/src/avl/node.rs new file mode 100644 index 000000000..ab2e9e712 --- /dev/null +++ b/ibc-testkit/store/src/avl/node.rs @@ -0,0 +1,137 @@ +use std::{borrow::Borrow, mem}; + +use sha2::{Digest, Sha256}; +use tendermint::hash::Hash; + +use crate::avl::{as_bytes::AsBytes, proof, HASH_ALGO}; + +pub type NodeRef = Option>>; + +/// A node in the AVL Tree. +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct AvlNode { + pub key: K, + pub value: V, + pub hash: Hash, + pub merkle_hash: Hash, + pub height: u32, + pub left: NodeRef, + pub right: NodeRef, +} + +/// Wrap a key + value couple into a `NodeRef`. +#[allow(clippy::unnecessary_wraps)] +pub fn as_node_ref(key: K, value: V) -> NodeRef +where + V: Borrow<[u8]>, +{ + Some(Box::new(AvlNode::new(key, value))) +} + +impl AvlNode +where + V: Borrow<[u8]>, +{ + fn new(key: K, value: V) -> Self { + let mut sha = Sha256::new(); + sha.update(proof::LEAF_PREFIX); + sha.update(key.as_bytes().as_ref()); + sha.update(value.borrow()); + let hash = sha.finalize(); + let merkle_hash = Hash::from_bytes(HASH_ALGO, &Sha256::digest(hash)).unwrap(); + let hash = Hash::from_bytes(HASH_ALGO, &hash).unwrap(); + + AvlNode { + key, + value, + hash, + merkle_hash, + height: 0, + left: None, + right: None, + } + } + + /// Set the value of the current node. + pub(crate) fn set_value(&mut self, value: V) -> V { + let hash = Self::local_hash(&self.key, &value); + self.hash = hash; + mem::replace(&mut self.value, value) + } + + /// The left height, or `None` if there is no left child. + fn left_height(&self) -> Option { + self.left.as_ref().map(|left| left.height) + } + + /// The right height, or `None` if there is no right child. + fn right_height(&self) -> Option { + self.right.as_ref().map(|right| right.height) + } + + /// Compute the local hash for a given key and value. + fn local_hash(key: &K, value: &V) -> Hash { + let mut sha = Sha256::new(); + sha.update(proof::LEAF_PREFIX); + sha.update(key.as_bytes()); + sha.update(value.borrow()); + let hash = sha.finalize(); + Hash::from_bytes(HASH_ALGO, &hash).unwrap() + } + + /// The left merkle hash, if any + pub fn left_hash(&self) -> Option<&[u8]> { + Some(self.left.as_ref()?.merkle_hash.as_bytes()) + } + + /// The right merkle hash, if any + pub fn right_hash(&self) -> Option<&[u8]> { + Some(self.right.as_ref()?.merkle_hash.as_bytes()) + } + + /// Update the height of this node by looking at the height of its two children. + /// The height of this node is computed as the maximum among the height of its two children, and + /// incremented by 1. + fn update_height(&mut self) { + match &self.right { + None => match &self.left { + None => self.height = 0, + Some(left) => self.height = left.height + 1, + }, + Some(right) => match &self.left { + None => self.height = right.height + 1, + Some(left) => self.height = std::cmp::max(left.height, right.height) + 1, + }, + } + } + + /// Update the node's merkle hash by looking at the hashes of its two children. + fn update_hashes(&mut self) { + let mut sha = Sha256::new(); + if let Some(left) = &self.left { + sha.update(left.merkle_hash.as_bytes()); + } + sha.update(self.hash.as_bytes()); + if let Some(right) = &self.right { + sha.update(right.merkle_hash.as_bytes()) + } + self.merkle_hash = Hash::from_bytes(HASH_ALGO, sha.finalize().as_slice()).unwrap(); + } + + /// Update node meta data, such as its height and merkle hash, by looking at its two + /// children. + pub fn update(&mut self) { + self.update_hashes(); + self.update_height(); + } + + /// Returns the node's balance factor (left_height - right_height). + pub fn balance_factor(&self) -> i32 { + match (self.left_height(), self.right_height()) { + (None, None) => 0, + (None, Some(h)) => -(h as i32), + (Some(h), None) => h as i32, + (Some(h_l), Some(h_r)) => (h_l as i32) - (h_r as i32), + } + } +} diff --git a/ibc-testkit/store/src/avl/proof.rs b/ibc-testkit/store/src/avl/proof.rs new file mode 100644 index 000000000..17e58ba21 --- /dev/null +++ b/ibc-testkit/store/src/avl/proof.rs @@ -0,0 +1,38 @@ +//! # ICS23 Proof +//! +//! This module provides the ICS23 proof spec, which can be used to verify the existence of a value +//! in the AVL Tree. +use ics23::{HashOp, InnerSpec, LeafOp, LengthOp, ProofSpec}; + +pub const LEAF_PREFIX: [u8; 64] = [0; 64]; // 64 bytes of zeroes. + +#[allow(dead_code)] +/// Return the `ProofSpec` of tendermock AVL Tree. +pub fn get_proof_spec() -> ProofSpec { + ProofSpec { + leaf_spec: Some(LeafOp { + hash: HashOp::Sha256.into(), + prehash_key: HashOp::NoHash.into(), + prehash_value: HashOp::NoHash.into(), + length: LengthOp::NoPrefix.into(), + prefix: LEAF_PREFIX.to_vec(), + }), + inner_spec: Some(InnerSpec { + child_order: vec![0, 1, 2], + child_size: 32, + min_prefix_length: 0, + max_prefix_length: 64, + empty_child: vec![0, 32], + hash: HashOp::Sha256.into(), + }), + max_depth: 0, + min_depth: 0, + prehash_key_before_comparison: false, + } +} + +#[cfg(test)] +mod test { + #[test] + fn proof() {} +} diff --git a/ibc-testkit/store/src/avl/tests.rs b/ibc-testkit/store/src/avl/tests.rs new file mode 100644 index 000000000..2d23ff88b --- /dev/null +++ b/ibc-testkit/store/src/avl/tests.rs @@ -0,0 +1,224 @@ +//! # Test suite of tendermock AVL Tree. + +use ics23::{commitment_proof::Proof, verify_membership, HostFunctionsManager}; +use sha2::{Digest, Sha256}; + +use crate::avl::{ + node::{as_node_ref, NodeRef}, + tree::AvlTree, + *, +}; + +#[test] +fn insert() { + let data = [42]; + let mut tree = AvlTree::new(); + let target = AvlTree { + root: build_node([1], data, as_node_ref([0], data), as_node_ref([2], data)), + }; + tree.insert([1], data); + tree.insert([0], data); + tree.insert([2], data); + assert_eq!(tree, target); +} + +#[test] +fn get() { + let mut tree = AvlTree::new(); + tree.insert([1], [1]); + tree.insert([2], [2]); + tree.insert([0], [0]); + tree.insert([5], [5]); + + assert_eq!(tree.get(&[0]), Some(&[0])); + assert_eq!(tree.get(&[1]), Some(&[1])); + assert_eq!(tree.get(&[2]), Some(&[2])); + assert_eq!(tree.get(&[5]), Some(&[5])); + assert_eq!(tree.get(&[4]), None); +} + +#[test] +fn rotate_right() { + let mut before = AvlTree { + root: build_node( + [5], + [5], + build_node([3], [3], as_node_ref([2], [2]), as_node_ref([4], [4])), + as_node_ref([6], [6]), + ), + }; + let after = AvlTree { + root: build_node( + [3], + [3], + as_node_ref([2], [2]), + build_node([5], [5], as_node_ref([4], [4]), as_node_ref([6], [6])), + ), + }; + AvlTree::rotate_right(&mut before.root); + assert_eq!(before, after); +} + +#[test] +fn rotate_left() { + let mut before = AvlTree { + root: build_node( + [1], + [1], + as_node_ref([0], [0]), + build_node([3], [3], as_node_ref([2], [2]), as_node_ref([4], [4])), + ), + }; + let after = AvlTree { + root: build_node( + [3], + [3], + build_node([1], [1], as_node_ref([0], [0]), as_node_ref([2], [2])), + as_node_ref([4], [4]), + ), + }; + AvlTree::rotate_left(&mut before.root); + assert_eq!(before, after); +} + +#[test] +fn proof() { + let mut tree = AvlTree::new(); + tree.insert("A", [0]); + tree.insert("B", [1]); + let node_a = tree.root.as_ref().unwrap(); + let node_b = node_a.right.as_ref().unwrap(); + let root = tree.root_hash().expect("Unable to retrieve root hash"); + let ics_proof = tree + .get_proof("B") + .expect("Unable to retrieve proof for 'B'"); + let proof = match &ics_proof.proof.as_ref().unwrap() { + Proof::Exist(proof) => proof, + _ => panic!("Should return an existence proof"), + }; + assert_eq!(proof.path.len(), 2); + // Apply leaf transformations + let leaf = proof + .leaf + .as_ref() + .expect("There should be a leaf in the proof"); + let mut sha = Sha256::new(); + sha.update(&leaf.prefix); + sha.update("B".as_bytes()); + sha.update([1]); + let child_hash = sha.finalize(); + // Apply first inner node transformations + let inner_b = &proof.path[0]; + let mut sha = Sha256::new(); + sha.update(&inner_b.prefix); + sha.update(child_hash); + sha.update(&inner_b.suffix); + let inner_hash_b = sha.finalize(); + assert_eq!(inner_hash_b.as_slice(), node_b.merkle_hash.as_bytes()); + // Apply second inner node transformations + let inner_a = &proof.path[1]; + let mut sha = Sha256::new(); + sha.update(&inner_a.prefix); + sha.update(inner_hash_b); + sha.update(&inner_a.suffix); + let inner_hash_a = sha.finalize(); + assert_eq!(inner_hash_a.as_slice(), node_a.merkle_hash.as_bytes()); + // Check with ics32 + let spec = get_proof_spec(); + assert!(verify_membership::( + &ics_proof, + &spec, + &root.as_bytes().to_vec(), + "B".as_bytes(), + &[1] + )); +} + +#[test] +fn integration() { + let mut tree = AvlTree::new(); + tree.insert("M", [0]); + tree.insert("N", [0]); + tree.insert("O", [0]); + tree.insert("L", [0]); + tree.insert("K", [0]); + tree.insert("Q", [0]); + tree.insert("P", [0]); + tree.insert("H", [0]); + tree.insert("I", [0]); + tree.insert("A", [0]); + assert!(check_integrity(&tree.root)); + + let root = tree + .root_hash() + .expect("Unable to retrieve root hash") + .as_bytes() + .to_vec(); + let proof = tree + .get_proof("K") + .expect("Unable to retrieve a proof for 'K'"); + let spec = get_proof_spec(); + assert!(verify_membership::( + &proof, + &spec, + &root, + "K".as_bytes(), + &[0] + )); +} + +/// Check that nodes are ordered, heights are correct and that balance factors are in {-1, 0, 1}. +fn check_integrity(node_ref: &NodeRef) -> bool { + if let Some(node) = node_ref { + let mut left_height = 0; + let mut right_height = 0; + let mut is_leaf = true; + if let Some(ref left) = node.left { + if left.key >= node.key { + println!("[AVL]: Left child should have a smaller key"); + return false; + } + left_height = left.height; + is_leaf = false; + } + if let Some(ref right) = node.right { + if right.key <= node.key { + println!("[AVL]: Right child should have a bigger key"); + return false; + } + right_height = right.height; + is_leaf = false; + } + let balance_factor = (left_height as i32) - (right_height as i32); + if balance_factor <= -2 { + println!("[AVL] Balance factor <= -2"); + return false; + } else if balance_factor >= 2 { + println!("[AVL] Balance factor >= 2"); + return false; + } + let bonus_height = u32::from(!is_leaf); + if node.height != std::cmp::max(left_height, right_height) + bonus_height { + println!("[AVL] Heights are inconsistent"); + return false; + } + check_integrity(&node.left) && check_integrity(&node.right) + } else { + true + } +} + +/// An helper function to build simple AvlNodes. +#[allow(clippy::unnecessary_wraps)] +fn build_node( + key: T, + value: [u8; 1], + left: NodeRef, + right: NodeRef, +) -> NodeRef { + let mut node = as_node_ref(key, value).unwrap(); + node.left = left; + node.right = right; + node.update(); + Some(node) +} diff --git a/ibc-testkit/store/src/avl/tree.rs b/ibc-testkit/store/src/avl/tree.rs new file mode 100644 index 000000000..0be463937 --- /dev/null +++ b/ibc-testkit/store/src/avl/tree.rs @@ -0,0 +1,222 @@ +use core::{ + borrow::Borrow, + cmp::{Ord, Ordering}, + marker::Sized, + option::{ + Option, + Option::{None, Some}, + }, +}; + +use ics23::{ + commitment_proof::Proof, CommitmentProof, ExistenceProof, HashOp, InnerOp, LeafOp, LengthOp, +}; +use tendermint::hash::Hash; + +use crate::avl::{ + node::{as_node_ref, NodeRef}, + proof, AsBytes, +}; + +/// An AVL Tree that supports `get` and `insert` operation and can be used to prove existence of a +/// given key-value couple. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct AvlTree { + pub root: NodeRef, +} + +impl> AvlTree { + /// Return an empty AVL tree. + pub fn new() -> Self { + AvlTree { root: None } + } + + #[allow(dead_code)] + /// Return the hash of the merkle tree root, if it has at least one node. + pub fn root_hash(&self) -> Option<&Hash> { + Some(&self.root.as_ref()?.merkle_hash) + } + + /// Return the value corresponding to the key, if it exists. + pub fn get(&self, key: &Q) -> Option<&V> + where + K: Borrow, + Q: Ord, + { + let mut node_ref = &self.root; + while let Some(ref node) = node_ref { + match node.key.borrow().cmp(key) { + Ordering::Greater => node_ref = &node.left, + Ordering::Less => node_ref = &node.right, + Ordering::Equal => return Some(&node.value), + } + } + None + } + + /// Insert a value into the AVL tree, this operation runs in amortized O(log(n)). + pub fn insert(&mut self, key: K, value: V) -> Option { + let node_ref = &mut self.root; + let mut old_value = None; + AvlTree::insert_rec(node_ref, key, value, &mut old_value); + old_value + } + + /// Insert a value in the tree. + fn insert_rec(node_ref: &mut NodeRef, key: K, value: V, old_value: &mut Option) { + if let Some(node) = node_ref { + match node.key.cmp(&key) { + Ordering::Greater => AvlTree::insert_rec(&mut node.left, key, value, old_value), + Ordering::Less => AvlTree::insert_rec(&mut node.right, key, value, old_value), + Ordering::Equal => *old_value = Some(node.set_value(value)), + } + node.update(); + AvlTree::balance_node(node_ref); + } else { + *node_ref = as_node_ref(key, value); + } + } + + #[allow(dead_code)] + /// Return an existence proof for the given element, if it exists. + pub fn get_proof(&self, key: &Q) -> Option + where + K: Borrow, + Q: Ord, + { + let proof = Self::get_proof_rec(key, &self.root)?; + Some(CommitmentProof { + proof: Some(Proof::Exist(proof)), + }) + } + + /// Recursively build a proof of existence for the desired value. + fn get_proof_rec(key: &Q, node: &NodeRef) -> Option + where + K: Borrow, + Q: Ord, + { + if let Some(node) = node { + let empty_hash = []; + let (mut proof, prefix, suffix) = match node.key.borrow().cmp(key) { + Ordering::Greater => { + let proof = Self::get_proof_rec(key, &node.left)?; + let prefix = vec![]; + let mut suffix = Vec::with_capacity(64); + suffix.extend(node.hash.as_bytes()); + suffix.extend(node.right_hash().unwrap_or(&empty_hash)); + (proof, prefix, suffix) + } + Ordering::Less => { + let proof = Self::get_proof_rec(key, &node.right)?; + let suffix = vec![]; + let mut prefix = Vec::with_capacity(64); + prefix.extend(node.left_hash().unwrap_or(&empty_hash)); + prefix.extend(node.hash.as_bytes()); + (proof, prefix, suffix) + } + Ordering::Equal => { + let leaf = Some(LeafOp { + hash: HashOp::Sha256.into(), + prehash_key: HashOp::NoHash.into(), + prehash_value: HashOp::NoHash.into(), + length: LengthOp::NoPrefix.into(), + prefix: proof::LEAF_PREFIX.to_vec(), + }); + let proof = ExistenceProof { + key: node.key.as_bytes().as_ref().to_owned(), + value: node.value.borrow().to_owned(), + leaf, + path: vec![], + }; + let prefix = node.left_hash().unwrap_or(&empty_hash).to_vec(); + let suffix = node.right_hash().unwrap_or(&empty_hash).to_vec(); + (proof, prefix, suffix) + } + }; + let inner = InnerOp { + hash: HashOp::Sha256.into(), + prefix, + suffix, + }; + proof.path.push(inner); + Some(proof) + } else { + None + } + } + + /// Rebalance the AVL tree by performing rotations, if needed. + fn balance_node(node_ref: &mut NodeRef) { + let node = node_ref + .as_mut() + .expect("[AVL]: Empty node in node balance"); + let balance_factor = node.balance_factor(); + if balance_factor >= 2 { + let left = node + .left + .as_mut() + .expect("[AVL]: Unexpected empty left node"); + if left.balance_factor() < 1 { + AvlTree::rotate_left(&mut node.left); + } + AvlTree::rotate_right(node_ref); + } else if balance_factor <= -2 { + let right = node + .right + .as_mut() + .expect("[AVL]: Unexpected empty right node"); + if right.balance_factor() > -1 { + AvlTree::rotate_right(&mut node.right); + } + AvlTree::rotate_left(node_ref); + } + } + + /// Performs a right rotation. + pub fn rotate_right(root: &mut NodeRef) { + let mut node = root.take().expect("[AVL]: Empty root in right rotation"); + let mut left = node.left.take().expect("[AVL]: Unexpected right rotation"); + let mut left_right = left.right.take(); + std::mem::swap(&mut node.left, &mut left_right); + node.update(); + std::mem::swap(&mut left.right, &mut Some(node)); + left.update(); + std::mem::swap(root, &mut Some(left)); + } + + /// Perform a left rotation. + pub fn rotate_left(root: &mut NodeRef) { + let mut node = root.take().expect("[AVL]: Empty root in left rotation"); + let mut right = node.right.take().expect("[AVL]: Unexpected left rotation"); + let mut right_left = right.left.take(); + std::mem::swap(&mut node.right, &mut right_left); + node.update(); + std::mem::swap(&mut right.left, &mut Some(node)); + right.update(); + std::mem::swap(root, &mut Some(right)) + } + + #[allow(dead_code)] + /// Return a list of the keys present in the tree. + pub fn get_keys(&self) -> Vec<&K> { + let mut keys = Vec::new(); + Self::get_keys_rec(&self.root, &mut keys); + keys + } + + #[allow(dead_code)] + fn get_keys_rec<'a>(node_ref: &'a NodeRef, keys: &mut Vec<&'a K>) { + if let Some(node) = node_ref { + Self::get_keys_rec(&node.left, keys); + keys.push(&node.key); + Self::get_keys_rec(&node.right, keys); + } + } +} + +impl> Default for AvlTree { + fn default() -> Self { + Self::new() + } +} diff --git a/ibc-testkit/store/src/context.rs b/ibc-testkit/store/src/context.rs new file mode 100644 index 000000000..351e7545d --- /dev/null +++ b/ibc-testkit/store/src/context.rs @@ -0,0 +1,52 @@ +use crate::types::{Height, Path, RawHeight}; +use crate::utils::Async; + +use ics23::CommitmentProof; +use std::fmt::Debug; + +/// Store trait - maybe provableStore or privateStore +pub trait Store: Async + Clone { + /// Error type - expected to envelope all possible errors in store + type Error: Debug; + + /// Set `value` for `path` + fn set(&mut self, path: Path, value: Vec) -> Result>, Self::Error>; + + /// Get associated `value` for `path` at specified `height` + fn get(&self, height: Height, path: &Path) -> Option>; + + /// Delete specified `path` + // TODO(rano): return Result to denote success or failure + fn delete(&mut self, path: &Path); + + /// Commit `Pending` block to canonical chain and create new `Pending` + fn commit(&mut self) -> Result, Self::Error>; + + /// Apply accumulated changes to `Pending` + fn apply(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + /// Reset accumulated changes + fn reset(&mut self) {} + + /// Prune historic blocks upto specified `height` + fn prune(&mut self, height: RawHeight) -> Result { + Ok(height) + } + + /// Return the current height of the chain + fn current_height(&self) -> RawHeight; + + /// Return all keys that start with specified prefix + fn get_keys(&self, key_prefix: &Path) -> Vec; // TODO(hu55a1n1): implement support for all heights +} + +/// ProvableStore trait +pub trait ProvableStore: Store { + /// Return a vector commitment + fn root_hash(&self) -> Vec; + + /// Return proof of existence for key + fn get_proof(&self, height: Height, key: &Path) -> Option; +} diff --git a/ibc-testkit/store/src/impls/growing.rs b/ibc-testkit/store/src/impls/growing.rs new file mode 100644 index 000000000..952be0b47 --- /dev/null +++ b/ibc-testkit/store/src/impls/growing.rs @@ -0,0 +1,139 @@ +use crate::context::ProvableStore; +use crate::context::Store; +use crate::types::Height; +use crate::types::Path; + +use ics23::CommitmentProof; + +/// GrowingStore does not prune any path. +/// If the path is set to v, the stored value is v +/// If the path is deleted, the stored value is [] +/// Note: we should not allow empty vec to store as +/// this would conflict with the deletion representation. +#[derive(Clone, Debug)] +pub struct GrowingStore { + store: S, +} + +impl GrowingStore { + pub fn new(store: S) -> Self { + Self { store } + } +} + +impl Default for GrowingStore +where + S: Default, +{ + fn default() -> Self { + Self::new(S::default()) + } +} + +impl Store for GrowingStore +where + S: Store, +{ + type Error = S::Error; + + #[inline] + fn set(&mut self, path: Path, value: Vec) -> Result>, Self::Error> { + if value.is_empty() { + panic!("empty vec is not allowed to store") + } + self.store.set(path, value) + } + + #[inline] + fn get(&self, height: Height, path: &Path) -> Option> { + // ignore if path is deleted + self.store.get(height, path).filter(|v| !v.is_empty()) + } + + #[inline] + fn delete(&mut self, path: &Path) { + // set value to empty vec to denote the path is deleted. + self.store.set(path.clone(), vec![]).expect("delete failed"); + } + + fn commit(&mut self) -> Result, Self::Error> { + self.store.commit() + } + + #[inline] + fn apply(&mut self) -> Result<(), Self::Error> { + self.store.apply() + } + + #[inline] + fn reset(&mut self) { + self.store.reset() + } + + #[inline] + fn prune(&mut self, height: u64) -> Result { + self.store.prune(height) + } + + #[inline] + fn current_height(&self) -> u64 { + self.store.current_height() + } + + #[inline] + fn get_keys(&self, key_prefix: &Path) -> Vec { + self.store + .get_keys(key_prefix) + .into_iter() + // ignore the deleted paths + .filter(|k| { + self.get(Height::Pending, k) + .filter(|v| !v.is_empty()) + .is_some() + }) + .collect() + } +} + +impl ProvableStore for GrowingStore +where + S: ProvableStore, +{ + #[inline] + fn root_hash(&self) -> Vec { + self.store.root_hash() + } + + #[inline] + fn get_proof(&self, height: Height, key: &Path) -> Option { + self.get(height, key) + // ignore if path is deleted + .filter(|v| !v.is_empty()) + .and_then(|_| self.store.get_proof(height, key)) + } +} + +impl GrowingStore +where + S: Store, +{ + #[inline] + pub fn is_deleted(&self, path: &Path) -> bool { + self.get(Height::Pending, path) + .filter(|v| v.is_empty()) + .is_some() + } + + #[inline] + pub fn deleted_keys(&self, key_prefix: &Path) -> Vec { + self.store + .get_keys(key_prefix) + .into_iter() + .filter(|k| { + self.get(Height::Pending, k) + .filter(|v| v.is_empty()) + .is_some() + }) + .collect() + } +} diff --git a/ibc-testkit/store/src/impls/in_memory.rs b/ibc-testkit/store/src/impls/in_memory.rs new file mode 100644 index 000000000..f1e34c835 --- /dev/null +++ b/ibc-testkit/store/src/impls/in_memory.rs @@ -0,0 +1,107 @@ +use crate::avl::{AsBytes, AvlTree}; +use crate::context::{ProvableStore, Store}; +use crate::types::{Height, Path, State}; + +use ics23::CommitmentProof; +use tendermint::{hash::Algorithm, Hash}; +use tracing::trace; + +/// An in-memory store backed by an AvlTree. +#[derive(Clone, Debug)] +pub struct InMemoryStore { + /// collection of states corresponding to every committed block height + store: Vec, + /// pending block state + pending: State, +} + +impl InMemoryStore { + #[inline] + fn get_state(&self, height: Height) -> Option<&State> { + match height { + Height::Pending => Some(&self.pending), + Height::Latest => self.store.last(), + Height::Stable(height) => { + let h = height as usize; + if h <= self.store.len() { + self.store.get(h - 1) + } else { + None + } + } + } + } +} + +impl Default for InMemoryStore { + /// The store starts out with an empty state. We also initialize the pending location as empty. + fn default() -> Self { + Self { + store: vec![], + pending: AvlTree::new(), + } + } +} + +impl Store for InMemoryStore { + type Error = (); // underlying store ops are infallible + + fn set(&mut self, path: Path, value: Vec) -> Result>, Self::Error> { + trace!("set at path = {}", path.to_string()); + Ok(self.pending.insert(path, value)) + } + + fn get(&self, height: Height, path: &Path) -> Option> { + trace!( + "get at path = {} at height = {:?}", + path.to_string(), + height + ); + self.get_state(height).and_then(|v| v.get(path).cloned()) + } + + fn delete(&mut self, _path: &Path) { + todo!() + } + + fn commit(&mut self) -> Result, Self::Error> { + trace!("committing height: {}", self.store.len()); + self.store.push(self.pending.clone()); + Ok(self.root_hash()) + } + + fn current_height(&self) -> u64 { + self.store.len() as u64 + } + + fn get_keys(&self, key_prefix: &Path) -> Vec { + let key_prefix = key_prefix.as_bytes(); + self.pending + .get_keys() + .into_iter() + .filter(|&key| key.as_bytes().as_ref().starts_with(key_prefix.as_ref())) + .cloned() + .collect() + } +} + +impl ProvableStore for InMemoryStore { + fn root_hash(&self) -> Vec { + self.pending + .root_hash() + .unwrap_or(&Hash::from_bytes(Algorithm::Sha256, &[0u8; 32]).unwrap()) + .as_bytes() + .to_vec() + } + + fn get_proof(&self, height: Height, key: &Path) -> Option { + trace!( + "get proof at path = {} at height = {:?}", + key.to_string(), + height + ); + self.get_state(height).and_then(|v| v.get_proof(key)) + } +} + +// TODO(hu55a1n1): import tests diff --git a/ibc-testkit/store/src/impls/mod.rs b/ibc-testkit/store/src/impls/mod.rs new file mode 100644 index 000000000..e011e91ac --- /dev/null +++ b/ibc-testkit/store/src/impls/mod.rs @@ -0,0 +1,9 @@ +pub(crate) mod growing; +pub(crate) mod in_memory; +pub(crate) mod revertible; +pub(crate) mod shared; + +pub use growing::GrowingStore; +pub use in_memory::InMemoryStore; +pub use revertible::RevertibleStore; +pub use shared::SharedStore; diff --git a/ibc-testkit/store/src/impls/revertible.rs b/ibc-testkit/store/src/impls/revertible.rs new file mode 100644 index 000000000..34aec72b0 --- /dev/null +++ b/ibc-testkit/store/src/impls/revertible.rs @@ -0,0 +1,130 @@ +use crate::context::ProvableStore; +use crate::context::Store; +use crate::types::Height; +use crate::types::Path; + +use ics23::CommitmentProof; +use tracing::trace; + +/// A wrapper store that implements rudimentary `apply()`/`reset()` support for other stores +#[derive(Clone, Debug)] +pub struct RevertibleStore { + /// backing store + store: S, + /// operation log for recording rollback operations in preserved order + op_log: Vec, +} + +#[derive(Clone, Debug)] +enum RevertOp { + Delete(Path), + Set(Path, Vec), +} + +impl RevertibleStore +where + S: Store, +{ + pub fn new(store: S) -> Self { + Self { + store, + op_log: vec![], + } + } +} + +impl Default for RevertibleStore +where + S: Default + Store, +{ + fn default() -> Self { + Self::new(S::default()) + } +} + +impl Store for RevertibleStore +where + S: Store, +{ + type Error = S::Error; + + #[inline] + fn set(&mut self, path: Path, value: Vec) -> Result>, Self::Error> { + let old_value = self.store.set(path.clone(), value)?; + match old_value { + // None implies this was an insert op, so we record the revert op as delete op + None => self.op_log.push(RevertOp::Delete(path)), + // Some old value implies this was an update op, so we record the revert op as a set op + // with the old value + Some(ref old_value) => self.op_log.push(RevertOp::Set(path, old_value.clone())), + } + Ok(old_value) + } + + #[inline] + fn get(&self, height: Height, path: &Path) -> Option> { + self.store.get(height, path) + } + + #[inline] + fn delete(&mut self, path: &Path) { + self.store.delete(path) + } + + #[inline] + fn commit(&mut self) -> Result, Self::Error> { + // call `apply()` before `commit()` to make sure all operations are applied + self.apply()?; + self.store.commit() + } + + #[inline] + fn apply(&mut self) -> Result<(), Self::Error> { + // note that we do NOT call the backing store's apply here - this allows users to create + // multilayered `WalStore`s + self.op_log.clear(); + Ok(()) + } + + #[inline] + fn reset(&mut self) { + // note that we do NOT call the backing store's reset here - this allows users to create + // multilayered `WalStore`s + trace!("Rollback operation log changes"); + while let Some(op) = self.op_log.pop() { + match op { + RevertOp::Delete(path) => self.delete(&path), + RevertOp::Set(path, value) => { + // FIXME: potential non-termination + // self.set() may insert a new op into the op_log + self.set(path, value).unwrap(); // safety - reset failures are unrecoverable + } + } + } + } + + #[inline] + fn current_height(&self) -> u64 { + self.store.current_height() + } + + #[inline] + fn get_keys(&self, key_prefix: &Path) -> Vec { + self.store.get_keys(key_prefix) + } +} + +impl ProvableStore for RevertibleStore +where + S: ProvableStore, +{ + #[inline] + fn root_hash(&self) -> Vec { + self.store.root_hash() + } + + #[inline] + fn get_proof(&self, height: Height, key: &Path) -> Option { + self.store.get_proof(height, key) + } +} diff --git a/ibc-testkit/store/src/impls/shared.rs b/ibc-testkit/store/src/impls/shared.rs new file mode 100644 index 000000000..110f0a03b --- /dev/null +++ b/ibc-testkit/store/src/impls/shared.rs @@ -0,0 +1,106 @@ +use crate::context::{ProvableStore, Store}; +use crate::types::{Height, Path, RawHeight}; +use crate::utils::{SharedRw, SharedRwExt}; + +use ics23::CommitmentProof; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, RwLock}; + +/// Wraps a store to make it shareable by cloning +#[derive(Clone, Debug)] +pub struct SharedStore(SharedRw); + +impl SharedStore { + pub fn new(store: S) -> Self { + Self(Arc::new(RwLock::new(store))) + } + + pub fn share(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Default for SharedStore +where + S: Default + Store, +{ + fn default() -> Self { + Self::new(S::default()) + } +} + +impl Store for SharedStore +where + S: Store, +{ + type Error = S::Error; + + #[inline] + fn set(&mut self, path: Path, value: Vec) -> Result>, Self::Error> { + self.write_access().set(path, value) + } + + #[inline] + fn get(&self, height: Height, path: &Path) -> Option> { + self.read_access().get(height, path) + } + + #[inline] + fn delete(&mut self, path: &Path) { + self.write_access().delete(path) + } + + #[inline] + fn commit(&mut self) -> Result, Self::Error> { + self.write_access().commit() + } + + #[inline] + fn apply(&mut self) -> Result<(), Self::Error> { + self.write_access().apply() + } + + #[inline] + fn reset(&mut self) { + self.write_access().reset() + } + + #[inline] + fn current_height(&self) -> RawHeight { + self.read_access().current_height() + } + + #[inline] + fn get_keys(&self, key_prefix: &Path) -> Vec { + self.read_access().get_keys(key_prefix) + } +} + +impl ProvableStore for SharedStore +where + S: ProvableStore, +{ + #[inline] + fn root_hash(&self) -> Vec { + self.read_access().root_hash() + } + + #[inline] + fn get_proof(&self, height: Height, key: &Path) -> Option { + self.read_access().get_proof(height, key) + } +} + +impl Deref for SharedStore { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SharedStore { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/ibc-testkit/store/src/lib.rs b/ibc-testkit/store/src/lib.rs new file mode 100644 index 000000000..f972b398f --- /dev/null +++ b/ibc-testkit/store/src/lib.rs @@ -0,0 +1,5 @@ +pub mod avl; +pub mod context; +pub mod impls; +pub mod types; +pub mod utils; diff --git a/ibc-testkit/store/src/types/height.rs b/ibc-testkit/store/src/types/height.rs new file mode 100644 index 000000000..897dd8248 --- /dev/null +++ b/ibc-testkit/store/src/types/height.rs @@ -0,0 +1,31 @@ +use std::fmt::{Display, Formatter}; + +/// Block height +pub type RawHeight = u64; + +/// Store height to query +#[derive(Debug, Copy, Clone, Eq, Ord, PartialEq, PartialOrd)] +pub enum Height { + Pending, + Latest, + Stable(RawHeight), // or equivalently `tendermint::block::Height` +} + +impl Display for Height { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Height::Pending => write!(f, "pending"), + Height::Latest => write!(f, "latest"), + Height::Stable(height) => write!(f, "{}", height), + } + } +} + +impl From for Height { + fn from(value: u64) -> Self { + match value { + 0 => Height::Latest, // see https://docs.tendermint.com/master/spec/abci/abci.html#query + _ => Height::Stable(value), + } + } +} diff --git a/ibc-testkit/store/src/types/identifier.rs b/ibc-testkit/store/src/types/identifier.rs new file mode 100644 index 000000000..50f011b40 --- /dev/null +++ b/ibc-testkit/store/src/types/identifier.rs @@ -0,0 +1,29 @@ +use std::{ + fmt::{Debug, Display, Formatter}, + ops::Deref, +}; + +/// A new type representing a valid ICS024 identifier. +/// Implements `Deref`. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] +pub struct Identifier(String); + +impl Deref for Identifier { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Identifier { + fn from(value: String) -> Self { + Self(value) + } +} + +impl Display for Identifier { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/ibc-testkit/store/src/types/mod.rs b/ibc-testkit/store/src/types/mod.rs new file mode 100644 index 000000000..3f5c9ce2e --- /dev/null +++ b/ibc-testkit/store/src/types/mod.rs @@ -0,0 +1,9 @@ +pub mod height; +pub mod identifier; +pub mod path; +pub mod store; + +pub use height::{Height, RawHeight}; +pub use identifier::Identifier; +pub use path::Path; +pub use store::{BinStore, JsonStore, MainStore, ProtobufStore, State, TypedSet, TypedStore}; diff --git a/ibc-testkit/store/src/types/path.rs b/ibc-testkit/store/src/types/path.rs new file mode 100644 index 000000000..316b250e9 --- /dev/null +++ b/ibc-testkit/store/src/types/path.rs @@ -0,0 +1,183 @@ +use super::Identifier; +use crate::avl::{AsBytes, ByteSlice}; +use displaydoc::Display as DisplayDoc; +use ibc::core::host::types::path::{Path as IbcPath, PathError}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use std::str::{from_utf8, Utf8Error}; + +#[derive(Debug, DisplayDoc)] +pub enum Error { + /// path isn't a valid string: `{error}` + MalformedPathString { error: Utf8Error }, + /// parse error: `{0}` + ParseError(String), +} + +/// A new type representing a valid ICS024 `Path`. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] + +pub struct Path(Vec); + +impl Path { + pub fn get(&self, index: usize) -> Option<&Identifier> { + self.0.get(index) + } +} + +impl TryFrom for Path { + type Error = Error; + + fn try_from(s: String) -> Result { + let mut identifiers = vec![]; + let parts = s.split('/'); // split will never return an empty iterator + for part in parts { + identifiers.push(Identifier::from(part.to_owned())); + } + Ok(Self(identifiers)) + } +} + +impl TryFrom<&[u8]> for Path { + type Error = Error; + + fn try_from(value: &[u8]) -> Result { + let s = from_utf8(value).map_err(|e| Error::MalformedPathString { error: e })?; + s.to_owned().try_into() + } +} + +impl From for Path { + fn from(id: Identifier) -> Self { + Self(vec![id]) + } +} + +impl Display for Path { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.0 + .iter() + .map(|iden| iden.as_str().to_owned()) + .collect::>() + .join("/") + ) + } +} + +impl AsBytes for Path { + fn as_bytes(&self) -> ByteSlice<'_> { + ByteSlice::Vector(self.to_string().into_bytes()) + } +} + +impl TryFrom for IbcPath { + type Error = PathError; + + fn try_from(path: Path) -> Result { + Self::from_str(path.to_string().as_str()) + } +} + +impl From for Path { + fn from(ibc_path: IbcPath) -> Self { + Self::try_from(ibc_path.to_string()).unwrap() // safety - `IbcPath`s are correct-by-construction + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{collections::HashSet, convert::TryFrom}; + + use lazy_static::lazy_static; + use proptest::prelude::*; + use rand::{distributions::Standard, seq::SliceRandom}; + + const ALLOWED_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + ._+-#[]<>"; + + lazy_static! { + static ref VALID_CHARS: HashSet = { + ALLOWED_CHARS + .iter() + .map(|c| char::from(*c)) + .collect::>() + }; + } + + fn gen_valid_identifier(len: usize) -> String { + let mut rng = rand::thread_rng(); + + (0..=len) + .map(|_| { + let idx = rng.gen_range(0..ALLOWED_CHARS.len()); + ALLOWED_CHARS[idx] as char + }) + .collect::() + } + + fn gen_invalid_identifier(len: usize) -> String { + let mut rng = rand::thread_rng(); + + (0..=len) + .map(|_| loop { + let c = rng.sample::(Standard); + + if c.is_ascii() && !VALID_CHARS.contains(&c) { + return c; + } + }) + .collect::() + } + + proptest! { + #[test] + fn path_with_valid_parts_is_valid(n_parts in 1usize..=10) { + let mut rng = rand::thread_rng(); + + let parts = (0..n_parts) + .map(|_| { + let len = rng.gen_range(1usize..=10); + gen_valid_identifier(len) + }) + .collect::>(); + + let path = parts.join("/"); + + assert!(Path::try_from(path).is_ok()); + } + + #[test] + #[ignore] + fn path_with_invalid_parts_is_invalid(n_parts in 1usize..=10) { + let mut rng = rand::thread_rng(); + let n_invalid_parts = rng.gen_range(1usize..=n_parts); + let n_valid_parts = n_parts - n_invalid_parts; + + let mut parts = (0..n_invalid_parts) + .map(|_| { + let len = rng.gen_range(1usize..=10); + gen_invalid_identifier(len) + }) + .collect::>(); + + let mut valid_parts = (0..n_valid_parts) + .map(|_| { + let len = rng.gen_range(1usize..=10); + gen_valid_identifier(len) + }) + .collect::>(); + + parts.append(&mut valid_parts); + parts.shuffle(&mut rng); + + let path = parts.join("/"); + + assert!(Path::try_from(path).is_err()); + } + } +} diff --git a/ibc-testkit/store/src/types/store.rs b/ibc-testkit/store/src/types/store.rs new file mode 100644 index 000000000..44187f77a --- /dev/null +++ b/ibc-testkit/store/src/types/store.rs @@ -0,0 +1,95 @@ +use crate::avl::AvlTree; +use crate::context::Store; +use crate::impls::{RevertibleStore, SharedStore}; +use crate::types::{Height, Path, RawHeight}; +use crate::utils::codec::{BinCodec, JsonCodec, NullCodec, ProtobufCodec}; +use crate::utils::Codec; +use std::{fmt::Debug, marker::PhantomData}; + +// A state type that represents a snapshot of the store at every block. +// The value is a `Vec` to allow stored types to choose their own serde. +pub type State = AvlTree>; + +pub type MainStore = SharedStore>; + +/// A `TypedStore` that uses the `JsonCodec` +pub type JsonStore = TypedStore>; + +/// A `TypedStore` that uses the `ProtobufCodec` +pub type ProtobufStore = TypedStore>; + +/// A `TypedSet` that stores only paths and no values +pub type TypedSet = TypedStore; + +/// A `TypedStore` that uses the `BinCodec` +pub type BinStore = TypedStore>; + +#[derive(Clone, Debug)] +pub struct TypedStore { + store: S, + _key: PhantomData, + _codec: PhantomData, +} + +impl TypedStore +where + S: Store, + C: Codec, + K: Into + Clone, +{ + #[inline] + pub fn new(store: S) -> Self { + Self { + store, + _codec: PhantomData, + _key: PhantomData, + } + } + + #[inline] + pub fn set(&mut self, path: K, value: V) -> Result, S::Error> { + self.store + .set(path.into(), C::encode(&value).unwrap().as_ref().to_vec()) + .map(|prev_val| prev_val.and_then(|v| C::decode(&v))) + } + + #[inline] + pub fn delete(&mut self, path: K) { + self.store.delete(&path.into()) + } + + #[inline] + pub fn get(&self, height: Height, path: &K) -> Option { + self.store + .get(height, &path.clone().into()) + .and_then(|v| C::decode(&v)) + } + + #[inline] + pub fn get_keys(&self, key_prefix: &Path) -> Vec { + self.store.get_keys(key_prefix) + } + + #[inline] + pub fn current_height(&self) -> RawHeight { + self.store.current_height() + } +} + +impl TypedStore +where + S: Store, + K: Into + Clone, +{ + #[inline] + pub fn set_path(&mut self, path: K) -> Result<(), S::Error> { + self.store + .set(path.into(), NullCodec::encode(&()).unwrap()) + .map(|_| ()) + } + + #[inline] + pub fn is_path_set(&self, height: Height, path: &K) -> bool { + self.store.get(height, &path.clone().into()).is_some() + } +} diff --git a/ibc-testkit/store/src/utils/codec.rs b/ibc-testkit/store/src/utils/codec.rs new file mode 100644 index 000000000..c4ff7d148 --- /dev/null +++ b/ibc-testkit/store/src/utils/codec.rs @@ -0,0 +1,102 @@ +use serde::{de::DeserializeOwned, Serialize}; +use std::marker::PhantomData; + +/// A trait that defines how types are decoded/encoded. +pub trait Codec { + type Type; + type Encoded: AsRef<[u8]>; + + fn encode(d: &Self::Type) -> Option; + + fn decode(bytes: &[u8]) -> Option; +} + +/// A JSON codec that uses `serde_json` to encode/decode as a JSON string +#[derive(Clone, Debug)] +pub struct JsonCodec(PhantomData); + +impl Codec for JsonCodec +where + T: Serialize + DeserializeOwned, +{ + type Type = T; + type Encoded = String; + + fn encode(d: &Self::Type) -> Option { + serde_json::to_string(d).ok() + } + + fn decode(bytes: &[u8]) -> Option { + let json_string = String::from_utf8(bytes.to_vec()).ok()?; + serde_json::from_str(&json_string).ok() + } +} + +/// A Null codec that can be used for paths that are only meant to be set/reset and do not hold any +/// typed value. +#[derive(Clone)] +pub struct NullCodec; + +impl Codec for NullCodec { + type Type = (); + type Encoded = Vec; + + fn encode(_d: &Self::Type) -> Option { + // using [0x00] to represent null + Some(vec![0x00]) + } + + fn decode(bytes: &[u8]) -> Option { + match bytes { + // the encoded bytes must be [0x00] + [0x00] => Some(()), + _ => None, + } + } +} + +/// A Protobuf codec that uses `prost` to encode/decode +#[derive(Clone, Debug)] +pub struct ProtobufCodec { + domain_type: PhantomData, + raw_type: PhantomData, +} + +impl Codec for ProtobufCodec +where + T: Into + Clone, + R: TryInto + Default + prost::Message, +{ + type Type = T; + type Encoded = Vec; + + fn encode(d: &Self::Type) -> Option { + let r = d.clone().into(); + Some(r.encode_to_vec()) + } + + fn decode(bytes: &[u8]) -> Option { + let r = R::decode(bytes).ok()?; + r.try_into().ok() + } +} + +/// A binary codec that uses `AsRef<[u8]>` and `From>` to encode and decode respectively. +#[derive(Clone, Debug)] +pub struct BinCodec(PhantomData); + +impl Codec for BinCodec +where + T: AsRef<[u8]> + From>, +{ + type Type = T; + type Encoded = Vec; + + fn encode(d: &Self::Type) -> Option { + Some(d.as_ref().to_vec()) + } + + fn decode(bytes: &[u8]) -> Option { + Some(bytes.to_vec().into()) + } +} diff --git a/ibc-testkit/store/src/utils/macros.rs b/ibc-testkit/store/src/utils/macros.rs new file mode 100644 index 000000000..a614aaf94 --- /dev/null +++ b/ibc-testkit/store/src/utils/macros.rs @@ -0,0 +1,32 @@ +use crate::types::Path; + +use ibc::core::host::types::path::{ + AckPath, ChannelEndPath, ClientConnectionPath, ClientConsensusStatePath, ClientStatePath, + CommitmentPath, ConnectionPath, ReceiptPath, SeqAckPath, SeqRecvPath, SeqSendPath, + UpgradeClientPath, +}; + +macro_rules! impl_into_path_for { + ($($path:ty),+) => { + $(impl From<$path> for Path { + fn from(ibc_path: $path) -> Self { + Self::try_from(ibc_path.to_string()).unwrap() // safety - `IbcPath`s are correct-by-construction + } + })+ + }; +} + +impl_into_path_for!( + ClientStatePath, + ClientConsensusStatePath, + ConnectionPath, + ClientConnectionPath, + ChannelEndPath, + SeqSendPath, + SeqRecvPath, + SeqAckPath, + CommitmentPath, + ReceiptPath, + AckPath, + UpgradeClientPath +); diff --git a/ibc-testkit/store/src/utils/mod.rs b/ibc-testkit/store/src/utils/mod.rs new file mode 100644 index 000000000..dc406c540 --- /dev/null +++ b/ibc-testkit/store/src/utils/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod codec; +pub mod macros; +pub(crate) mod sync; + +pub use codec::{Codec, JsonCodec}; +pub use sync::{Async, SharedRw, SharedRwExt}; diff --git a/ibc-testkit/store/src/utils/sync.rs b/ibc-testkit/store/src/utils/sync.rs new file mode 100644 index 000000000..1a6fab85d --- /dev/null +++ b/ibc-testkit/store/src/utils/sync.rs @@ -0,0 +1,29 @@ +use core::panic; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +pub trait Async: Send + Sync + 'static {} + +impl Async for A where A: Send + Sync + 'static {} + +pub type SharedRw = Arc>; + +pub trait SharedRwExt { + fn read_access(&self) -> RwLockReadGuard<'_, T>; + fn write_access(&self) -> RwLockWriteGuard<'_, T>; +} + +impl SharedRwExt for SharedRw { + fn read_access(&self) -> RwLockReadGuard<'_, T> { + match self.read() { + Ok(guard) => guard, + Err(poisoned) => panic!("poisoned lock: {:?}", poisoned), + } + } + + fn write_access(&self) -> RwLockWriteGuard<'_, T> { + match self.write() { + Ok(guard) => guard, + Err(poisoned) => panic!("poisoned lock: {:?}", poisoned), + } + } +} From 0faeabb81ff57c8da4fd93c009dba14cd61cbe29 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 14:41:21 +0100 Subject: [PATCH 2/9] cargo fmt --- ibc-testkit/store/src/avl/node.rs | 6 +++-- ibc-testkit/store/src/avl/tests.rs | 11 +++++---- ibc-testkit/store/src/avl/tree.rs | 27 +++++++++-------------- ibc-testkit/store/src/context.rs | 7 +++--- ibc-testkit/store/src/impls/growing.rs | 8 +++---- ibc-testkit/store/src/impls/in_memory.rs | 9 ++++---- ibc-testkit/store/src/impls/revertible.rs | 8 +++---- ibc-testkit/store/src/impls/shared.rs | 9 ++++---- ibc-testkit/store/src/types/identifier.rs | 6 ++--- ibc-testkit/store/src/types/path.rs | 20 ++++++++++------- ibc-testkit/store/src/types/store.rs | 4 +++- ibc-testkit/store/src/utils/codec.rs | 4 +++- ibc-testkit/store/src/utils/macros.rs | 4 ++-- 13 files changed, 61 insertions(+), 62 deletions(-) diff --git a/ibc-testkit/store/src/avl/node.rs b/ibc-testkit/store/src/avl/node.rs index ab2e9e712..776dc0f8a 100644 --- a/ibc-testkit/store/src/avl/node.rs +++ b/ibc-testkit/store/src/avl/node.rs @@ -1,9 +1,11 @@ -use std::{borrow::Borrow, mem}; +use std::borrow::Borrow; +use std::mem; use sha2::{Digest, Sha256}; use tendermint::hash::Hash; -use crate::avl::{as_bytes::AsBytes, proof, HASH_ALGO}; +use crate::avl::as_bytes::AsBytes; +use crate::avl::{proof, HASH_ALGO}; pub type NodeRef = Option>>; diff --git a/ibc-testkit/store/src/avl/tests.rs b/ibc-testkit/store/src/avl/tests.rs index 2d23ff88b..9b229a2d6 100644 --- a/ibc-testkit/store/src/avl/tests.rs +++ b/ibc-testkit/store/src/avl/tests.rs @@ -1,13 +1,12 @@ //! # Test suite of tendermock AVL Tree. -use ics23::{commitment_proof::Proof, verify_membership, HostFunctionsManager}; +use ics23::commitment_proof::Proof; +use ics23::{verify_membership, HostFunctionsManager}; use sha2::{Digest, Sha256}; -use crate::avl::{ - node::{as_node_ref, NodeRef}, - tree::AvlTree, - *, -}; +use crate::avl::node::{as_node_ref, NodeRef}; +use crate::avl::tree::AvlTree; +use crate::avl::*; #[test] fn insert() { diff --git a/ibc-testkit/store/src/avl/tree.rs b/ibc-testkit/store/src/avl/tree.rs index 0be463937..1a646abcc 100644 --- a/ibc-testkit/store/src/avl/tree.rs +++ b/ibc-testkit/store/src/avl/tree.rs @@ -1,22 +1,15 @@ -use core::{ - borrow::Borrow, - cmp::{Ord, Ordering}, - marker::Sized, - option::{ - Option, - Option::{None, Some}, - }, -}; - -use ics23::{ - commitment_proof::Proof, CommitmentProof, ExistenceProof, HashOp, InnerOp, LeafOp, LengthOp, -}; +use core::borrow::Borrow; +use core::cmp::{Ord, Ordering}; +use core::marker::Sized; +use core::option::Option; +use core::option::Option::{None, Some}; + +use ics23::commitment_proof::Proof; +use ics23::{CommitmentProof, ExistenceProof, HashOp, InnerOp, LeafOp, LengthOp}; use tendermint::hash::Hash; -use crate::avl::{ - node::{as_node_ref, NodeRef}, - proof, AsBytes, -}; +use crate::avl::node::{as_node_ref, NodeRef}; +use crate::avl::{proof, AsBytes}; /// An AVL Tree that supports `get` and `insert` operation and can be used to prove existence of a /// given key-value couple. diff --git a/ibc-testkit/store/src/context.rs b/ibc-testkit/store/src/context.rs index 351e7545d..aa70144a3 100644 --- a/ibc-testkit/store/src/context.rs +++ b/ibc-testkit/store/src/context.rs @@ -1,8 +1,9 @@ -use crate::types::{Height, Path, RawHeight}; -use crate::utils::Async; +use std::fmt::Debug; use ics23::CommitmentProof; -use std::fmt::Debug; + +use crate::types::{Height, Path, RawHeight}; +use crate::utils::Async; /// Store trait - maybe provableStore or privateStore pub trait Store: Async + Clone { diff --git a/ibc-testkit/store/src/impls/growing.rs b/ibc-testkit/store/src/impls/growing.rs index 952be0b47..0c98340e2 100644 --- a/ibc-testkit/store/src/impls/growing.rs +++ b/ibc-testkit/store/src/impls/growing.rs @@ -1,10 +1,8 @@ -use crate::context::ProvableStore; -use crate::context::Store; -use crate::types::Height; -use crate::types::Path; - use ics23::CommitmentProof; +use crate::context::{ProvableStore, Store}; +use crate::types::{Height, Path}; + /// GrowingStore does not prune any path. /// If the path is set to v, the stored value is v /// If the path is deleted, the stored value is [] diff --git a/ibc-testkit/store/src/impls/in_memory.rs b/ibc-testkit/store/src/impls/in_memory.rs index f1e34c835..2602762bf 100644 --- a/ibc-testkit/store/src/impls/in_memory.rs +++ b/ibc-testkit/store/src/impls/in_memory.rs @@ -1,11 +1,12 @@ +use ics23::CommitmentProof; +use tendermint::hash::Algorithm; +use tendermint::Hash; +use tracing::trace; + use crate::avl::{AsBytes, AvlTree}; use crate::context::{ProvableStore, Store}; use crate::types::{Height, Path, State}; -use ics23::CommitmentProof; -use tendermint::{hash::Algorithm, Hash}; -use tracing::trace; - /// An in-memory store backed by an AvlTree. #[derive(Clone, Debug)] pub struct InMemoryStore { diff --git a/ibc-testkit/store/src/impls/revertible.rs b/ibc-testkit/store/src/impls/revertible.rs index 34aec72b0..0b5ea7a36 100644 --- a/ibc-testkit/store/src/impls/revertible.rs +++ b/ibc-testkit/store/src/impls/revertible.rs @@ -1,11 +1,9 @@ -use crate::context::ProvableStore; -use crate::context::Store; -use crate::types::Height; -use crate::types::Path; - use ics23::CommitmentProof; use tracing::trace; +use crate::context::{ProvableStore, Store}; +use crate::types::{Height, Path}; + /// A wrapper store that implements rudimentary `apply()`/`reset()` support for other stores #[derive(Clone, Debug)] pub struct RevertibleStore { diff --git a/ibc-testkit/store/src/impls/shared.rs b/ibc-testkit/store/src/impls/shared.rs index 110f0a03b..c5eb95ed9 100644 --- a/ibc-testkit/store/src/impls/shared.rs +++ b/ibc-testkit/store/src/impls/shared.rs @@ -1,11 +1,12 @@ +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, RwLock}; + +use ics23::CommitmentProof; + use crate::context::{ProvableStore, Store}; use crate::types::{Height, Path, RawHeight}; use crate::utils::{SharedRw, SharedRwExt}; -use ics23::CommitmentProof; -use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, RwLock}; - /// Wraps a store to make it shareable by cloning #[derive(Clone, Debug)] pub struct SharedStore(SharedRw); diff --git a/ibc-testkit/store/src/types/identifier.rs b/ibc-testkit/store/src/types/identifier.rs index 50f011b40..d53d0e18b 100644 --- a/ibc-testkit/store/src/types/identifier.rs +++ b/ibc-testkit/store/src/types/identifier.rs @@ -1,7 +1,5 @@ -use std::{ - fmt::{Debug, Display, Formatter}, - ops::Deref, -}; +use std::fmt::{Debug, Display, Formatter}; +use std::ops::Deref; /// A new type representing a valid ICS024 identifier. /// Implements `Deref`. diff --git a/ibc-testkit/store/src/types/path.rs b/ibc-testkit/store/src/types/path.rs index 316b250e9..01b941541 100644 --- a/ibc-testkit/store/src/types/path.rs +++ b/ibc-testkit/store/src/types/path.rs @@ -1,10 +1,11 @@ -use super::Identifier; -use crate::avl::{AsBytes, ByteSlice}; +use std::fmt::{Display, Formatter}; +use std::str::{from_utf8, FromStr, Utf8Error}; + use displaydoc::Display as DisplayDoc; use ibc::core::host::types::path::{Path as IbcPath, PathError}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -use std::str::{from_utf8, Utf8Error}; + +use super::Identifier; +use crate::avl::{AsBytes, ByteSlice}; #[derive(Debug, DisplayDoc)] pub enum Error { @@ -89,12 +90,15 @@ impl From for Path { #[cfg(test)] mod tests { - use super::*; - use std::{collections::HashSet, convert::TryFrom}; + use std::collections::HashSet; + use std::convert::TryFrom; use lazy_static::lazy_static; use proptest::prelude::*; - use rand::{distributions::Standard, seq::SliceRandom}; + use rand::distributions::Standard; + use rand::seq::SliceRandom; + + use super::*; const ALLOWED_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ abcdefghijklmnopqrstuvwxyz\ diff --git a/ibc-testkit/store/src/types/store.rs b/ibc-testkit/store/src/types/store.rs index 44187f77a..3f034488f 100644 --- a/ibc-testkit/store/src/types/store.rs +++ b/ibc-testkit/store/src/types/store.rs @@ -1,10 +1,12 @@ +use std::fmt::Debug; +use std::marker::PhantomData; + use crate::avl::AvlTree; use crate::context::Store; use crate::impls::{RevertibleStore, SharedStore}; use crate::types::{Height, Path, RawHeight}; use crate::utils::codec::{BinCodec, JsonCodec, NullCodec, ProtobufCodec}; use crate::utils::Codec; -use std::{fmt::Debug, marker::PhantomData}; // A state type that represents a snapshot of the store at every block. // The value is a `Vec` to allow stored types to choose their own serde. diff --git a/ibc-testkit/store/src/utils/codec.rs b/ibc-testkit/store/src/utils/codec.rs index c4ff7d148..f01745bb6 100644 --- a/ibc-testkit/store/src/utils/codec.rs +++ b/ibc-testkit/store/src/utils/codec.rs @@ -1,6 +1,8 @@ -use serde::{de::DeserializeOwned, Serialize}; use std::marker::PhantomData; +use serde::de::DeserializeOwned; +use serde::Serialize; + /// A trait that defines how types are decoded/encoded. pub trait Codec { type Type; diff --git a/ibc-testkit/store/src/utils/macros.rs b/ibc-testkit/store/src/utils/macros.rs index a614aaf94..abb6a8383 100644 --- a/ibc-testkit/store/src/utils/macros.rs +++ b/ibc-testkit/store/src/utils/macros.rs @@ -1,11 +1,11 @@ -use crate::types::Path; - use ibc::core::host::types::path::{ AckPath, ChannelEndPath, ClientConnectionPath, ClientConsensusStatePath, ClientStatePath, CommitmentPath, ConnectionPath, ReceiptPath, SeqAckPath, SeqRecvPath, SeqSendPath, UpgradeClientPath, }; +use crate::types::Path; + macro_rules! impl_into_path_for { ($($path:ty),+) => { $(impl From<$path> for Path { From f8c16b999817088de47d52b20fee7cdb0551c5d4 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 14:42:21 +0100 Subject: [PATCH 3/9] replace proptests with table driven tests --- ibc-testkit/store/src/types/path.rs | 99 ++++------------------------- 1 file changed, 11 insertions(+), 88 deletions(-) diff --git a/ibc-testkit/store/src/types/path.rs b/ibc-testkit/store/src/types/path.rs index 01b941541..0b29e5fe0 100644 --- a/ibc-testkit/store/src/types/path.rs +++ b/ibc-testkit/store/src/types/path.rs @@ -90,98 +90,21 @@ impl From for Path { #[cfg(test)] mod tests { - use std::collections::HashSet; - use std::convert::TryFrom; - - use lazy_static::lazy_static; - use proptest::prelude::*; - use rand::distributions::Standard; - use rand::seq::SliceRandom; + use rstest::rstest; use super::*; - const ALLOWED_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ - abcdefghijklmnopqrstuvwxyz\ - ._+-#[]<>"; - - lazy_static! { - static ref VALID_CHARS: HashSet = { - ALLOWED_CHARS - .iter() - .map(|c| char::from(*c)) - .collect::>() - }; - } - - fn gen_valid_identifier(len: usize) -> String { - let mut rng = rand::thread_rng(); - - (0..=len) - .map(|_| { - let idx = rng.gen_range(0..ALLOWED_CHARS.len()); - ALLOWED_CHARS[idx] as char - }) - .collect::() - } - - fn gen_invalid_identifier(len: usize) -> String { - let mut rng = rand::thread_rng(); - - (0..=len) - .map(|_| loop { - let c = rng.sample::(Standard); - - if c.is_ascii() && !VALID_CHARS.contains(&c) { - return c; - } - }) - .collect::() + #[rstest] + #[case("hello/world")] + fn happy_test(#[case] path: &str) { + assert!(Path::try_from(path.to_owned()).is_ok()); } - proptest! { - #[test] - fn path_with_valid_parts_is_valid(n_parts in 1usize..=10) { - let mut rng = rand::thread_rng(); - - let parts = (0..n_parts) - .map(|_| { - let len = rng.gen_range(1usize..=10); - gen_valid_identifier(len) - }) - .collect::>(); - - let path = parts.join("/"); - - assert!(Path::try_from(path).is_ok()); - } - - #[test] - #[ignore] - fn path_with_invalid_parts_is_invalid(n_parts in 1usize..=10) { - let mut rng = rand::thread_rng(); - let n_invalid_parts = rng.gen_range(1usize..=n_parts); - let n_valid_parts = n_parts - n_invalid_parts; - - let mut parts = (0..n_invalid_parts) - .map(|_| { - let len = rng.gen_range(1usize..=10); - gen_invalid_identifier(len) - }) - .collect::>(); - - let mut valid_parts = (0..n_valid_parts) - .map(|_| { - let len = rng.gen_range(1usize..=10); - gen_valid_identifier(len) - }) - .collect::>(); - - parts.append(&mut valid_parts); - parts.shuffle(&mut rng); - - let path = parts.join("/"); - - assert!(Path::try_from(path).is_err()); - } + // TODO(rano): add failing case for `Path::try_from` + #[rstest] + #[ignore] + #[case("hello/@@@")] + fn sad_test(#[case] path: &str) { + assert!(Path::try_from(path.to_owned()).is_err()); } } From ee042ce1e0ba4edf76f327f1b4a7a14a06d25f8b Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 14:43:52 +0100 Subject: [PATCH 4/9] add cargo toml for ibc-testkit-store --- ibc-testkit/store/Cargo.toml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 ibc-testkit/store/Cargo.toml diff --git a/ibc-testkit/store/Cargo.toml b/ibc-testkit/store/Cargo.toml new file mode 100644 index 000000000..20032843c --- /dev/null +++ b/ibc-testkit/store/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "ibc-testkit-store" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +readme = "README.md" +keywords = ["blockchain", "store", "merkle", "avl"] +description = """ + Maintained by `ibc-rs`, a simple implementation of an AVL store tailored for the `ibc-testkit`. +""" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +displaydoc = { workspace = true } +tendermint = { workspace = true } +ibc = { workspace = true } +ics23 = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +prost = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +rstest = { workspace = true } From 5366e91cd62d28e1bde49ce4f2e0dc7c024d7e7f Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 14:44:24 +0100 Subject: [PATCH 5/9] add ibc-testkit-store to workspace --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index d32711f97..08c406086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "ibc", "ibc-query", "ibc-testkit", + "ibc-testkit/store", ] exclude = [ "ci/cw-check", From 2a79f92a2ddf61c4c3c2b7f7ad07280ebee85b39 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 14:44:45 +0100 Subject: [PATCH 6/9] add and restructure deps --- Cargo.toml | 4 ++++ ibc-clients/ics08-wasm/types/Cargo.toml | 2 +- ibc-testkit/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 08c406086..a98277f75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,10 @@ sha2 = { version = "0.10.8", default-features = false } serde = { version = "1.0", default-features = false } serde_json = { package = "serde-json-wasm", version = "1.0.0", default-features = false } subtle-encoding = { version = "0.5", default-features = false } +base64 = { version = "0.21", default-features = false } +tracing = { version = "0.1", default-features = false } +ics23 = { version = "0.11", default-features = false } +prost = { version = "0.12", default-features = false } # ibc dependencies ibc = { version = "0.49.1", path = "./ibc", default-features = false } diff --git a/ibc-clients/ics08-wasm/types/Cargo.toml b/ibc-clients/ics08-wasm/types/Cargo.toml index fe645d132..b5a78377a 100644 --- a/ibc-clients/ics08-wasm/types/Cargo.toml +++ b/ibc-clients/ics08-wasm/types/Cargo.toml @@ -8,7 +8,7 @@ edition = { workspace = true } [dependencies] # external dependencies -base64 = { version = "0.21", default-features = false, features = ["alloc"] } +base64 = { workspace = true, features = ["alloc"] } displaydoc = { workspace = true } serde = { workspace = true , optional = true } cosmwasm-schema = { version = "1.4.1", default-features = false, optional = true } diff --git a/ibc-testkit/Cargo.toml b/ibc-testkit/Cargo.toml index e020657a6..b28dc29f9 100644 --- a/ibc-testkit/Cargo.toml +++ b/ibc-testkit/Cargo.toml @@ -25,7 +25,7 @@ schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } subtle-encoding = { workspace = true } -tracing = { version = "0.1.40", default-features = false } +tracing = { workspace = true } typed-builder = { version = "0.18.0" } # ibc dependencies From 41f60cf01472ac1caade765e854ae8fc7bfee9b0 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 14:53:26 +0100 Subject: [PATCH 7/9] add changelog --- .../features/1045-add-blockchain-store-implementations.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1045-add-blockchain-store-implementations.md diff --git a/.changelog/unreleased/features/1045-add-blockchain-store-implementations.md b/.changelog/unreleased/features/1045-add-blockchain-store-implementations.md new file mode 100644 index 000000000..f336b8d66 --- /dev/null +++ b/.changelog/unreleased/features/1045-add-blockchain-store-implementations.md @@ -0,0 +1,2 @@ +- [ibc-testkit] Add blockchain store implementations. + ([\#1045](https://github.com/cosmos/ibc-rs/issues/1045)) From 49a357faff1eb33869f9245b60967a13e8d87c58 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 16:31:06 +0100 Subject: [PATCH 8/9] use byte slice instead of string --- ibc-testkit/store/src/types/path.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ibc-testkit/store/src/types/path.rs b/ibc-testkit/store/src/types/path.rs index 0b29e5fe0..f2a220cc1 100644 --- a/ibc-testkit/store/src/types/path.rs +++ b/ibc-testkit/store/src/types/path.rs @@ -95,16 +95,16 @@ mod tests { use super::*; #[rstest] - #[case("hello/world")] - fn happy_test(#[case] path: &str) { - assert!(Path::try_from(path.to_owned()).is_ok()); + #[case(b"hello/world")] + fn happy_test(#[case] path: &[u8]) { + assert!(Path::try_from(path).is_ok()); } // TODO(rano): add failing case for `Path::try_from` #[rstest] #[ignore] - #[case("hello/@@@")] - fn sad_test(#[case] path: &str) { - assert!(Path::try_from(path.to_owned()).is_err()); + #[case(b"hello/@@@")] + fn sad_test(#[case] path: &[u8]) { + assert!(Path::try_from(path).is_err()); } } From 89a0b1140c8d5c7ee99a404d8953818d77f090b9 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 19 Jan 2024 16:32:34 +0100 Subject: [PATCH 9/9] add failing test --- ibc-testkit/store/src/types/path.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ibc-testkit/store/src/types/path.rs b/ibc-testkit/store/src/types/path.rs index f2a220cc1..6b321a95a 100644 --- a/ibc-testkit/store/src/types/path.rs +++ b/ibc-testkit/store/src/types/path.rs @@ -100,10 +100,8 @@ mod tests { assert!(Path::try_from(path).is_ok()); } - // TODO(rano): add failing case for `Path::try_from` #[rstest] - #[ignore] - #[case(b"hello/@@@")] + #[case(b"hello/\xf0\x28\x8c\xbc")] fn sad_test(#[case] path: &[u8]) { assert!(Path::try_from(path).is_err()); }