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)) diff --git a/Cargo.toml b/Cargo.toml index d32711f97..a98277f75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "ibc", "ibc-query", "ibc-testkit", + "ibc-testkit/store", ] exclude = [ "ci/cw-check", @@ -57,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 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 } 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..776dc0f8a --- /dev/null +++ b/ibc-testkit/store/src/avl/node.rs @@ -0,0 +1,139 @@ +use std::borrow::Borrow; +use std::mem; + +use sha2::{Digest, Sha256}; +use tendermint::hash::Hash; + +use crate::avl::as_bytes::AsBytes; +use crate::avl::{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..9b229a2d6 --- /dev/null +++ b/ibc-testkit/store/src/avl/tests.rs @@ -0,0 +1,223 @@ +//! # Test suite of tendermock AVL Tree. + +use ics23::commitment_proof::Proof; +use ics23::{verify_membership, HostFunctionsManager}; +use sha2::{Digest, Sha256}; + +use crate::avl::node::{as_node_ref, NodeRef}; +use crate::avl::tree::AvlTree; +use crate::avl::*; + +#[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..1a646abcc --- /dev/null +++ b/ibc-testkit/store/src/avl/tree.rs @@ -0,0 +1,215 @@ +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}; +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. +#[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..aa70144a3 --- /dev/null +++ b/ibc-testkit/store/src/context.rs @@ -0,0 +1,53 @@ +use std::fmt::Debug; + +use ics23::CommitmentProof; + +use crate::types::{Height, Path, RawHeight}; +use crate::utils::Async; + +/// 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..0c98340e2 --- /dev/null +++ b/ibc-testkit/store/src/impls/growing.rs @@ -0,0 +1,137 @@ +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 [] +/// 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..2602762bf --- /dev/null +++ b/ibc-testkit/store/src/impls/in_memory.rs @@ -0,0 +1,108 @@ +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}; + +/// 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..0b5ea7a36 --- /dev/null +++ b/ibc-testkit/store/src/impls/revertible.rs @@ -0,0 +1,128 @@ +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 { + /// 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..c5eb95ed9 --- /dev/null +++ b/ibc-testkit/store/src/impls/shared.rs @@ -0,0 +1,107 @@ +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}; + +/// 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..d53d0e18b --- /dev/null +++ b/ibc-testkit/store/src/types/identifier.rs @@ -0,0 +1,27 @@ +use std::fmt::{Debug, Display, Formatter}; +use std::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..6b321a95a --- /dev/null +++ b/ibc-testkit/store/src/types/path.rs @@ -0,0 +1,108 @@ +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 super::Identifier; +use crate::avl::{AsBytes, ByteSlice}; + +#[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 rstest::rstest; + + use super::*; + + #[rstest] + #[case(b"hello/world")] + fn happy_test(#[case] path: &[u8]) { + assert!(Path::try_from(path).is_ok()); + } + + #[rstest] + #[case(b"hello/\xf0\x28\x8c\xbc")] + fn sad_test(#[case] path: &[u8]) { + 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..3f034488f --- /dev/null +++ b/ibc-testkit/store/src/types/store.rs @@ -0,0 +1,97 @@ +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; + +// 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..f01745bb6 --- /dev/null +++ b/ibc-testkit/store/src/utils/codec.rs @@ -0,0 +1,104 @@ +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; + 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..abb6a8383 --- /dev/null +++ b/ibc-testkit/store/src/utils/macros.rs @@ -0,0 +1,32 @@ +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 { + 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), + } + } +}