From 3c26e8bbc638b0d53dfe3e03b9e429cb3b371a09 Mon Sep 17 00:00:00 2001 From: aeryz Date: Wed, 27 Dec 2023 19:30:03 +0300 Subject: [PATCH 01/14] feat(ics23): introduce the library Signed-off-by: aeryz --- Cargo.lock | 13 + Cargo.toml | 2 +- lib/ics23/Cargo.toml | 13 + lib/ics23/src/commitment_proof.rs | 12 + lib/ics23/src/compressed_batch_entry.rs | 19 ++ lib/ics23/src/compressed_batch_proof.rs | 17 ++ lib/ics23/src/compressed_existence_proof.rs | 23 ++ .../src/compressed_nonexistence_proof.rs | 23 ++ lib/ics23/src/existence_proof.rs | 80 ++++++ lib/ics23/src/hash_op.rs | 43 ++++ lib/ics23/src/inner_op.rs | 84 +++++++ lib/ics23/src/leaf_op.rs | 94 ++++++++ lib/ics23/src/length_op.rs | 41 ++++ lib/ics23/src/lib.rs | 12 + lib/unionlabs/src/cosmos/ics23/proof_spec.rs | 15 ++ .../cometbls-light-client/Cargo.toml | 2 + .../cometbls-light-client/src/client.rs | 228 +++++++++++++++++- 17 files changed, 719 insertions(+), 2 deletions(-) create mode 100644 lib/ics23/Cargo.toml create mode 100644 lib/ics23/src/commitment_proof.rs create mode 100644 lib/ics23/src/compressed_batch_entry.rs create mode 100644 lib/ics23/src/compressed_batch_proof.rs create mode 100644 lib/ics23/src/compressed_existence_proof.rs create mode 100644 lib/ics23/src/compressed_nonexistence_proof.rs create mode 100644 lib/ics23/src/existence_proof.rs create mode 100644 lib/ics23/src/hash_op.rs create mode 100644 lib/ics23/src/inner_op.rs create mode 100644 lib/ics23/src/leaf_op.rs create mode 100644 lib/ics23/src/length_op.rs create mode 100644 lib/ics23/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 611a35f27b..d5e3eb6ecb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -986,10 +986,12 @@ dependencies = [ "ics008-wasm-client", "prost 0.11.9", "protos", + "ripemd", "schemars", "serde", "serde-json-wasm 1.0.0", "serde_json", + "sha2 0.10.8", "sha3", "thiserror", "unionlabs", @@ -2918,6 +2920,17 @@ dependencies = [ "unionlabs", ] +[[package]] +name = "ics23" +version = "0.1.0" +dependencies = [ + "prost 0.11.9", + "ripemd", + "sha2 0.10.8", + "thiserror-core", + "unionlabs", +] + [[package]] name = "ident_case" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 05266ea850..d50c0275b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ "cosmwasm/*", "hubble", "tools/parse-wasm-client-type", - "zerg", + "zerg", "lib/ics23", ] resolver = "2" diff --git a/lib/ics23/Cargo.toml b/lib/ics23/Cargo.toml new file mode 100644 index 0000000000..f8b5ef8f32 --- /dev/null +++ b/lib/ics23/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ics23" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +unionlabs = { workspace = true, default-features = false } +thiserror = { version = "1.0", package = "thiserror-core", default-features = false } +prost = { version = "0.11.9", default-features = false, features = ["std"] } +sha2 = { version = "0.10.7", default-features = false } +ripemd = { version = "0.1.3", default-features = false } diff --git a/lib/ics23/src/commitment_proof.rs b/lib/ics23/src/commitment_proof.rs new file mode 100644 index 0000000000..a8dd318dc3 --- /dev/null +++ b/lib/ics23/src/commitment_proof.rs @@ -0,0 +1,12 @@ +use unionlabs::cosmos::ics23::commitment_proof::CommitmentProof; + +use super::compressed_batch_proof; + +pub fn decompress(commitment_proof: CommitmentProof) -> CommitmentProof { + match commitment_proof { + CommitmentProof::CompressedBatch(comp) => { + CommitmentProof::Batch(compressed_batch_proof::decompress(comp)) + } + proof => proof, + } +} diff --git a/lib/ics23/src/compressed_batch_entry.rs b/lib/ics23/src/compressed_batch_entry.rs new file mode 100644 index 0000000000..33dd313b75 --- /dev/null +++ b/lib/ics23/src/compressed_batch_entry.rs @@ -0,0 +1,19 @@ +use unionlabs::cosmos::ics23::{ + batch_entry::BatchEntry, compressed_batch_entry::CompressedBatchEntry, inner_op::InnerOp, +}; + +use crate::{compressed_existence_proof, compressed_nonexistence_proof}; + +pub fn decompress( + compressed_batch_entry: CompressedBatchEntry, + lookup: Vec, +) -> BatchEntry { + match compressed_batch_entry { + CompressedBatchEntry::Exist(exist) => { + BatchEntry::Exist(compressed_existence_proof::decompress(exist, lookup)) + } + CompressedBatchEntry::Nonexist(nonexist) => { + BatchEntry::Nonexist(compressed_nonexistence_proof::decompress(nonexist, lookup)) + } + } +} diff --git a/lib/ics23/src/compressed_batch_proof.rs b/lib/ics23/src/compressed_batch_proof.rs new file mode 100644 index 0000000000..42b98e81ba --- /dev/null +++ b/lib/ics23/src/compressed_batch_proof.rs @@ -0,0 +1,17 @@ +use unionlabs::cosmos::ics23::{ + batch_proof::BatchProof, compressed_batch_proof::CompressedBatchProof, +}; + +use super::compressed_batch_entry; + +pub fn decompress(compressed_batch_proof: CompressedBatchProof) -> BatchProof { + let lookup = compressed_batch_proof.lookup_inners; + + BatchProof { + entries: compressed_batch_proof + .entries + .into_iter() + .map(|entry| compressed_batch_entry::decompress(entry, lookup.clone())) + .collect(), + } +} diff --git a/lib/ics23/src/compressed_existence_proof.rs b/lib/ics23/src/compressed_existence_proof.rs new file mode 100644 index 0000000000..10876c30ff --- /dev/null +++ b/lib/ics23/src/compressed_existence_proof.rs @@ -0,0 +1,23 @@ +use unionlabs::cosmos::ics23::{ + compressed_existence_proof::CompressedExistenceProof, existence_proof::ExistenceProof, + inner_op::InnerOp, +}; + +// TODO(aeryz): Should this function be fallible? We do unsafe indexing here +// by assuming that the indices in the path will always be within the bounds. +// Is this a correct assumption? +pub fn decompress( + compressed_existence_proof: CompressedExistenceProof, + lookup: Vec, +) -> ExistenceProof { + ExistenceProof { + key: compressed_existence_proof.key, + value: compressed_existence_proof.value, + leaf: compressed_existence_proof.leaf, + path: compressed_existence_proof + .path + .iter() + .map(|i| lookup[i.inner() as usize].clone()) + .collect(), + } +} diff --git a/lib/ics23/src/compressed_nonexistence_proof.rs b/lib/ics23/src/compressed_nonexistence_proof.rs new file mode 100644 index 0000000000..4d9b1a038d --- /dev/null +++ b/lib/ics23/src/compressed_nonexistence_proof.rs @@ -0,0 +1,23 @@ +use unionlabs::cosmos::ics23::{ + compressed_non_existence_proof::CompressedNonExistenceProof, inner_op::InnerOp, + non_existence_proof::NonExistenceProof, +}; + +use super::compressed_existence_proof; + +pub fn decompress( + compressed_nonexistence_proof: CompressedNonExistenceProof, + lookup: Vec, +) -> NonExistenceProof { + NonExistenceProof { + key: compressed_nonexistence_proof.key, + left: compressed_existence_proof::decompress( + compressed_nonexistence_proof.left, + lookup.clone(), + ), + right: compressed_existence_proof::decompress( + compressed_nonexistence_proof.right, + lookup.clone(), + ), + } +} diff --git a/lib/ics23/src/existence_proof.rs b/lib/ics23/src/existence_proof.rs new file mode 100644 index 0000000000..3f1247167d --- /dev/null +++ b/lib/ics23/src/existence_proof.rs @@ -0,0 +1,80 @@ +use unionlabs::cosmos::ics23::{existence_proof::ExistenceProof, proof_spec::ProofSpec}; + +use crate::{inner_op, leaf_op}; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum SpecMismatchError { + #[error("spec mismatch ({0})")] + LeafSpecMismatch(super::leaf_op::SpecMismatchError), + #[error("inner op spec mismatch ({0})")] + InnerOpSpecMismatch(super::inner_op::SpecMismatchError), + #[error("inner path depth too short, got ({path_len}) while the min depth is ({min_depth})")] + InnerDepthTooShort { path_len: usize, min_depth: i32 }, + #[error("inner path depth too long, got ({path_len}) while the max depth is ({max_depth})")] + InnerDepthTooLong { path_len: usize, max_depth: i32 }, +} + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum CalculateRootError { + #[error("leaf op hash ({0})")] + LeafOpHash(super::leaf_op::ApplyError), + #[error("inner op hash ({0})")] + InnerOpHash(super::inner_op::ApplyError), + #[error("inner op hash does not match the spec")] + InnerOpHashAndSpecMismatch, +} + +pub fn check_against_spec( + existence_proof: &ExistenceProof, + spec: &ProofSpec, + iavl_spec: &ProofSpec, +) -> Result<(), SpecMismatchError> { + leaf_op::check_against_spec(&existence_proof.leaf, spec, iavl_spec) + .map_err(SpecMismatchError::LeafSpecMismatch)?; + + if spec.min_depth > 0 && existence_proof.path.len() < spec.min_depth as usize { + return Err(SpecMismatchError::InnerDepthTooShort { + path_len: existence_proof.path.len(), + min_depth: spec.min_depth, + }); + } + if spec.max_depth > 0 && existence_proof.path.len() > spec.max_depth as usize { + return Err(SpecMismatchError::InnerDepthTooLong { + path_len: existence_proof.path.len(), + max_depth: spec.max_depth, + }); + } + + for (index, inner) in existence_proof.path.iter().enumerate() { + inner_op::check_against_spec(inner, spec, index as i32 + 1, iavl_spec) + .map_err(SpecMismatchError::InnerOpSpecMismatch)?; + } + + Ok(()) +} + +pub fn calculate_root( + existence_proof: &ExistenceProof, + spec: Option, +) -> Result, CalculateRootError> { + let res = leaf_op::apply( + &existence_proof.leaf, + &existence_proof.key, + &existence_proof.value, + ) + .map_err(CalculateRootError::LeafOpHash)?; + + for step in &existence_proof.path { + let res = inner_op::apply(step, &res).map_err(CalculateRootError::InnerOpHash)?; + + if let Some(ref proof_spec) = spec { + if res.len() > proof_spec.inner_spec.child_size as usize + && proof_spec.inner_spec.child_size >= 32 + { + return Err(CalculateRootError::InnerOpHashAndSpecMismatch); + } + } + } + + Ok(res) +} diff --git a/lib/ics23/src/hash_op.rs b/lib/ics23/src/hash_op.rs new file mode 100644 index 0000000000..8044b10196 --- /dev/null +++ b/lib/ics23/src/hash_op.rs @@ -0,0 +1,43 @@ +use sha2::Digest; +use unionlabs::cosmos::ics23::hash_op::HashOp; + +// TODO(aeryz): move these to `HashOp` +pub fn do_hash_or_noop(hash_op: HashOp, preimage: &[u8]) -> Vec { + if hash_op != HashOp::NoHash { + return preimage.into(); + } + + do_hash(hash_op, preimage) +} + +pub fn do_hash(hash_op: HashOp, preimage: &[u8]) -> Vec { + match hash_op { + // TODO(aeryz): why not keccak + HashOp::Sha256 => sha2::Sha256::new() + .chain_update(preimage) + .finalize() + .to_vec(), + HashOp::Sha512 => sha2::Sha512::new() + .chain_update(preimage) + .finalize() + .to_vec(), + HashOp::Ripemd160 => ripemd::Ripemd160::new() + .chain_update(preimage) + .finalize() + .to_vec(), + HashOp::Bitcoin => ripemd::Ripemd160::new() + .chain_update( + sha2::Sha256::new() + .chain_update(preimage) + .finalize() + .to_vec(), + ) + .finalize() + .to_vec(), + HashOp::Sha512256 => sha2::Sha512_256::new() + .chain_update(preimage) + .finalize() + .to_vec(), + _ => panic!("unsupported"), + } +} diff --git a/lib/ics23/src/inner_op.rs b/lib/ics23/src/inner_op.rs new file mode 100644 index 0000000000..208e7ab229 --- /dev/null +++ b/lib/ics23/src/inner_op.rs @@ -0,0 +1,84 @@ +use unionlabs::cosmos::ics23::{hash_op::HashOp, inner_op::InnerOp, proof_spec::ProofSpec}; + +use crate::hash_op; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum SpecMismatchError { + #[error("unexpected hash op ({0:?})")] + UnexpectedHashOp(HashOp), + #[error("prefix ({prefix:?}) is not the prefix of ({full:?})")] + PrefixMismatch { full: Vec, prefix: Vec }, + #[error("inner prefix too short, got ({prefix_len}) while the min length is ({min_len})")] + InnerOpPrefixTooShort { prefix_len: usize, min_len: i32 }, + #[error("inner prefix too long, got ({prefix_len}) while the max length is ({max_len})")] + InnerOpPrefixTooLong { prefix_len: usize, max_len: i32 }, + #[error("malformed inner op suffix ({0:?})")] + InnerOpSuffixMalformed(usize), +} + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum ApplyError { + #[error("inner op needs a child value")] + InnerOpNeedsChildValue, +} + +pub fn check_against_spec( + inner_op: &InnerOp, + spec: &ProofSpec, + b: i32, + iavl_spec: &ProofSpec, +) -> Result<(), SpecMismatchError> { + if inner_op.hash != spec.inner_spec.hash { + return Err(SpecMismatchError::UnexpectedHashOp(inner_op.hash)); + } + + if spec.compatible(iavl_spec) { + // TODO(aeryz): validateIavlOps + } + + if inner_op.prefix.starts_with(&spec.leaf_spec.prefix) { + return Err(SpecMismatchError::PrefixMismatch { + full: inner_op.prefix.clone(), + prefix: spec.leaf_spec.prefix.clone(), + }); + } + + // TODO(aeryz): check if min_prefix_length > 0 + if inner_op.prefix.len() < spec.inner_spec.min_prefix_length as usize { + return Err(SpecMismatchError::InnerOpPrefixTooShort { + prefix_len: inner_op.prefix.len(), + min_len: spec.inner_spec.min_prefix_length, + }); + } + + // TODO(aeryz): check if max_prefix_length > 0 + let max_prefix_length = (spec.inner_spec.min_prefix_length as usize + + (spec.inner_spec.child_order.len() - 1) * spec.inner_spec.child_size as usize) + as usize; + if inner_op.prefix.len() > max_prefix_length { + return Err(SpecMismatchError::InnerOpPrefixTooLong { + prefix_len: inner_op.prefix.len(), + max_len: spec.inner_spec.max_prefix_length, + }); + } + + if inner_op.suffix.len() % (spec.inner_spec.child_size as usize) != 0 { + return Err(SpecMismatchError::InnerOpSuffixMalformed( + inner_op.suffix.len(), + )); + } + + Ok(()) +} + +pub fn apply(inner_op: &InnerOp, child: &[u8]) -> Result, ApplyError> { + if child.is_empty() { + return Err(ApplyError::InnerOpNeedsChildValue); + } + + let mut preimage = inner_op.prefix.clone(); + preimage.extend_from_slice(child); + preimage.extend_from_slice(&inner_op.suffix); + + Ok(hash_op::do_hash(inner_op.hash, &preimage)) +} diff --git a/lib/ics23/src/leaf_op.rs b/lib/ics23/src/leaf_op.rs new file mode 100644 index 0000000000..2727ff725b --- /dev/null +++ b/lib/ics23/src/leaf_op.rs @@ -0,0 +1,94 @@ +use unionlabs::cosmos::ics23::{ + hash_op::HashOp, leaf_op::LeafOp, length_op::LengthOp, proof_spec::ProofSpec, +}; + +use super::{hash_op, length_op}; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum SpecMismatchError { + #[error("unexpected hash op ({0:?})")] + UnexpectedHashOp(HashOp), + #[error("unexpected prehash key ({0:?})")] + UnexpectedPrehashKey(HashOp), + #[error("unexpected prehash value ({0:?})")] + UnexpectedPrehashValue(HashOp), + #[error("unexpected length op ({0:?})")] + UnexpectedLengthOp(LengthOp), + #[error("prefix ({prefix:?}) is not the prefix of ({full:?})")] + PrefixMismatch { full: Vec, prefix: Vec }, +} + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum ApplyError { + #[error("key needed")] + KeyNeeded, + #[error("value needed")] + ValueNeeded, + #[error("apply leaf ({0:?})")] + LeafData(super::length_op::ApplyError), +} + +pub fn check_against_spec( + leaf_op: &LeafOp, + spec: &ProofSpec, + iavl_spec: &ProofSpec, +) -> Result<(), SpecMismatchError> { + let lspec = &spec.leaf_spec; + + if spec.compatible(iavl_spec) { + // TODO(aeryz): validate iavl opts + } + + if leaf_op.hash != lspec.hash { + return Err(SpecMismatchError::UnexpectedHashOp(lspec.hash)); + } + + if leaf_op.prehash_key != lspec.prehash_key { + return Err(SpecMismatchError::UnexpectedPrehashKey(lspec.prehash_key)); + } + + if leaf_op.prehash_value != lspec.prehash_value { + return Err(SpecMismatchError::UnexpectedPrehashValue( + lspec.prehash_value, + )); + } + + if leaf_op.length != lspec.length { + return Err(SpecMismatchError::UnexpectedLengthOp(lspec.length)); + } + + if !leaf_op.prefix.starts_with(&lspec.prefix) { + return Err(SpecMismatchError::PrefixMismatch { + full: leaf_op.prefix.clone(), + prefix: lspec.prefix.clone(), + }); + } + + Ok(()) +} + +/// Calculate the leaf hash given the key and value being proven +pub fn apply(leaf_op: &LeafOp, key: &[u8], value: &[u8]) -> Result, ApplyError> { + if key.is_empty() { + return Err(ApplyError::KeyNeeded); + } + + if value.is_empty() { + return Err(ApplyError::ValueNeeded); + } + + let pkey = prepare_data(leaf_op, leaf_op.prehash_key, key)?; + + let pvalue = prepare_data(leaf_op, leaf_op.prehash_value, value)?; + + let mut data = leaf_op.prefix.clone(); + data.extend_from_slice(&pkey); + data.extend_from_slice(&pvalue); + + Ok(hash_op::do_hash(leaf_op.hash, &data)) +} + +fn prepare_data(leaf_op: &LeafOp, hash_op: HashOp, data: &[u8]) -> Result, ApplyError> { + let hdata = hash_op::do_hash_or_noop(hash_op, data); + length_op::apply(&leaf_op.length, &hdata).map_err(ApplyError::LeafData) +} diff --git a/lib/ics23/src/length_op.rs b/lib/ics23/src/length_op.rs new file mode 100644 index 0000000000..8b8fc063f7 --- /dev/null +++ b/lib/ics23/src/length_op.rs @@ -0,0 +1,41 @@ +use unionlabs::cosmos::ics23::length_op::LengthOp; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum ApplyError { + #[error("required 32 bytes, got ({0:?})")] + Required32Bytes(usize), + #[error("required 64 bytes, ({0:?})")] + Required64Bytes(usize), + #[error("unsupported op ({0:?})")] + UnsupportedOp(LengthOp), +} + +pub fn apply(length_op: &LengthOp, data: &[u8]) -> Result, ApplyError> { + match length_op { + LengthOp::NoPrefix => Ok(data.to_vec()), + LengthOp::VarProto => { + let mut len = Vec::new(); + prost::encoding::encode_varint(data.len() as u64, &mut len); + len.extend_from_slice(data); + Ok(len) + } + LengthOp::Require32Bytes => { + if data.len() != 32 { + return Err(ApplyError::Required32Bytes(data.len()))?; + } + Ok(data.into()) + } + LengthOp::Require64Bytes => { + if data.len() != 64 { + return Err(ApplyError::Required64Bytes(data.len())); + } + Ok(data.into()) + } + LengthOp::Fixed32Little => { + let mut d = data.len().to_le_bytes().to_vec(); + d.extend_from_slice(data); + Ok(d) + } + op => Err(ApplyError::UnsupportedOp(*op)), + } +} diff --git a/lib/ics23/src/lib.rs b/lib/ics23/src/lib.rs new file mode 100644 index 0000000000..cd109d64b9 --- /dev/null +++ b/lib/ics23/src/lib.rs @@ -0,0 +1,12 @@ +#![feature(error_in_core)] + +pub mod commitment_proof; +pub mod compressed_batch_entry; +pub mod compressed_batch_proof; +pub mod compressed_existence_proof; +pub mod compressed_nonexistence_proof; +pub mod existence_proof; +pub mod hash_op; +pub mod inner_op; +pub mod leaf_op; +pub mod length_op; diff --git a/lib/unionlabs/src/cosmos/ics23/proof_spec.rs b/lib/unionlabs/src/cosmos/ics23/proof_spec.rs index 2f758f8447..07bda583f4 100644 --- a/lib/unionlabs/src/cosmos/ics23/proof_spec.rs +++ b/lib/unionlabs/src/cosmos/ics23/proof_spec.rs @@ -17,6 +17,21 @@ pub struct ProofSpec { pub min_depth: i32, } +impl ProofSpec { + // TODO(aeryz): what the hell is this + pub fn compatible(&self, spec: &ProofSpec) -> bool { + self.leaf_spec.hash == spec.leaf_spec.hash + && self.leaf_spec.prehash_key == spec.leaf_spec.prehash_key + && self.leaf_spec.prehash_value == spec.leaf_spec.prehash_value + && self.leaf_spec.length == spec.leaf_spec.length + && self.inner_spec.hash == spec.inner_spec.hash + && self.inner_spec.min_prefix_length == spec.inner_spec.min_prefix_length + && self.inner_spec.max_prefix_length == spec.inner_spec.max_prefix_length + && self.inner_spec.child_size == spec.inner_spec.child_size + && self.inner_spec.child_order.len() == spec.inner_spec.child_order.len() + } +} + impl TypeUrl for protos::cosmos::ics23::v1::ProofSpec { const TYPE_URL: &'static str = "/cosmos.ics23.v1.ProofSpec"; } diff --git a/light-clients/cometbls-light-client/Cargo.toml b/light-clients/cometbls-light-client/Cargo.toml index 300e5023f6..3fba727053 100644 --- a/light-clients/cometbls-light-client/Cargo.toml +++ b/light-clients/cometbls-light-client/Cargo.toml @@ -18,6 +18,8 @@ schemars = { version = "0.8.3", default-features = false } serde = { version = "1.0.103", default-features = false, features = ["derive"] } serde-json-wasm = { version = "*", default-features = false } sha3 = { version = "0.10.8", default-features = false } +sha2 = { version = "0.10.8", default-features = false } +ripemd = { version = "0.1.3", default-features = false } thiserror = { version = "1.0.26", default-features = false } protos = { workspace = true, default-features = false, features = ["proto_full", "std"] } cometbls-groth16-verifier = { workspace = true, default-features = false } diff --git a/light-clients/cometbls-light-client/src/client.rs b/light-clients/cometbls-light-client/src/client.rs index 0f4fa642c5..0ac20dd8b5 100644 --- a/light-clients/cometbls-light-client/src/client.rs +++ b/light-clients/cometbls-light-client/src/client.rs @@ -7,9 +7,24 @@ use ics008_wasm_client::{ }; use prost::Message; use protos::ibc::core::client::v1::GenesisMetadata; +use sha2::Digest; use unionlabs::{ + cosmos::ics23::{ + batch_proof::BatchProof, + commitment_proof::CommitmentProof, + existence_proof::{ExistenceProof, SpecMismatchError}, + hash_op::HashOp, + inner_op::InnerOp, + inner_spec::InnerSpec, + leaf_op::LeafOp, + length_op::LengthOp, + proof_spec::ProofSpec, + }, ibc::{ - core::{client::height::Height, commitment::merkle_root::MerkleRoot}, + core::{ + client::height::Height, + commitment::{merkle_proof::MerkleProof, merkle_root::MerkleRoot}, + }, lightclients::cometbls::{ client_state::ClientState, consensus_state::ConsensusState, header::Header, }, @@ -315,3 +330,214 @@ fn canonical_vote( chain_id, } } + +#[derive(Debug)] +pub enum VerifyMembershipError { + SpecMismatch(SpecMismatchError), + KeyAndExistenceProofKeyMismatch { + key: Vec, + existence_proof_key: Vec, + }, + ValueAndExistenceProofValueMismatch { + value: Vec, + existence_proof_value: Vec, + }, + RootCalculation(unionlabs::cosmos::ics23::existence_proof::CalculateRootError), + CalculatedAndGivenRootMismatch { + calculated_root: Vec, + given_root: Vec, + }, + ProofDoesNotExist, +} + +fn verify_membership( + proof: MerkleProof, + consensus_root: &MerkleRoot, + path: MerklePath, + value: &[u8], +) -> Result<(), VerifyMembershipError> { + // TODO(aeryz): check if this supposed to be embedded or configurable + if proof.proofs.len() != 2 { + panic!("Proof length needs to match the spec"); + } + + // TODO(aeryz): Make this spec a global constant if it's going to be embedded + if path.key_path.len() != 2 { + panic!("Path length needs to match the spec"); + } + + verify_chained_membership_proof(consensus_root.hash.as_ref(), &proof.proofs, path, value, 0) +} + +fn verify_chained_membership_proof( + root: &[u8], + proofs: &[CommitmentProof], + keys: MerklePath, + value: &[u8], + mut index: usize, +) -> Result<(), VerifyMembershipError> { + let specs = [iavl_spec(), tendermint_proof_spec()]; + + // FIXME(aeryz): ugly change + let mut tmp_value = value.to_vec(); + + while index < proofs.len() { + match &proofs[index] { + CommitmentProof::Exist(proof) => { + let subroot = proof + .calculate_root(None) + .map_err(VerifyMembershipError::RootCalculation)?; + + if let Some(key) = keys.key_path.get(keys.key_path.len() - 1 - index) { + do_verify_membership( + specs[index].clone(), // TODO(aeryz): I'm about to throw up + &subroot, + CommitmentProof::Exist(proof.clone()), // TODO(aeryz): disgusting + key.as_bytes(), // TODO(aeryz): weird? is this really like this? + &tmp_value, + )?; + } else { + panic!("could not retrieve key bytes for key"); + }; + + tmp_value = subroot; + } + CommitmentProof::Nonexist(_) => { + panic!("chained membership proof contains nonexistence proof at index %d"); + } + _ => { + panic!("invalid proof type"); + } + } + + index += 1; + } + + Ok(()) +} + +fn do_verify_membership( + spec: ProofSpec, + root: &[u8], + proof: CommitmentProof, + key: &[u8], + value: &[u8], +) -> Result<(), VerifyMembershipError> { + let proof = proof.decompress(); + + if let Some(ep) = get_exist_proof_for_key(proof, key) { + verify_existence_proof(ep, spec, root, key, value) + } else { + Err(VerifyMembershipError::ProofDoesNotExist) + } +} + +fn verify_existence_proof( + existence_proof: ExistenceProof, + spec: ProofSpec, + root: &[u8], + key: &[u8], + value: &[u8], +) -> Result<(), VerifyMembershipError> { + existence_proof + .check_against_spec(&spec, &iavl_spec()) + .map_err(VerifyMembershipError::SpecMismatch)?; + + if key != existence_proof.key { + return Err(VerifyMembershipError::KeyAndExistenceProofKeyMismatch { + key: key.into(), + existence_proof_key: existence_proof.key, + }); + } + + if value != existence_proof.value { + return Err(VerifyMembershipError::ValueAndExistenceProofValueMismatch { + value: value.into(), + existence_proof_value: existence_proof.value, + }); + } + + let calc = existence_proof + .calculate_root(Some(spec)) + .map_err(VerifyMembershipError::RootCalculation)?; + + if root != calc { + return Err(VerifyMembershipError::CalculatedAndGivenRootMismatch { + calculated_root: calc, + given_root: root.into(), + }); + } + + Ok(()) +} + +fn get_exist_proof_for_key(proof: CommitmentProof, key: &[u8]) -> Option { + match proof { + CommitmentProof::Exist(exist) => { + if exist.key.as_slice() == key { + return Some(exist); + } + + None + } + CommitmentProof::Batch(batch) => { + for sub in batch.entries { + match sub { + unionlabs::cosmos::ics23::batch_entry::BatchEntry::Exist(exist) => { + if exist.key.as_slice() == key { + return Some(exist); + } + } + _ => {} + } + } + + None + } + _ => None, + } +} + +fn iavl_spec() -> ProofSpec { + ProofSpec { + leaf_spec: LeafOp { + hash: HashOp::Sha256, + prehash_key: HashOp::NoHash, + prehash_value: HashOp::Sha256, + length: LengthOp::VarProto, + prefix: vec![0], + }, + inner_spec: InnerSpec { + child_order: vec![0, 1], + child_size: 33, + min_prefix_length: 4, + max_prefix_length: 12, + empty_child: vec![], + hash: HashOp::Sha256, + }, + max_depth: 0, + min_depth: 0, + } +} + +fn tendermint_proof_spec() -> ProofSpec { + ProofSpec { + leaf_spec: LeafOp { + hash: HashOp::Sha256, + prehash_key: HashOp::NoHash, + prehash_value: HashOp::Sha256, + length: LengthOp::VarProto, + prefix: [0].into(), + }, + inner_spec: InnerSpec { + child_order: [0, 1].into(), + child_size: 32, + min_prefix_length: 1, + max_prefix_length: 1, + empty_child: [].into(), + hash: HashOp::Sha256, + }, + max_depth: 0, + min_depth: 0, + } +} From 85b4d320295649b184bb7355fdd7b6de42aee085 Mon Sep 17 00:00:00 2001 From: aeryz Date: Mon, 1 Jan 2024 00:26:23 +0300 Subject: [PATCH 02/14] chore(ics23): move verify function here Signed-off-by: aeryz --- lib/ics23/src/existence_proof.rs | 75 ++++++++++++++++- lib/ics23/src/lib.rs | 49 +++++++++++ .../cometbls-light-client/src/client.rs | 83 ------------------- 3 files changed, 121 insertions(+), 86 deletions(-) diff --git a/lib/ics23/src/existence_proof.rs b/lib/ics23/src/existence_proof.rs index 3f1247167d..452dbb24ee 100644 --- a/lib/ics23/src/existence_proof.rs +++ b/lib/ics23/src/existence_proof.rs @@ -1,10 +1,10 @@ use unionlabs::cosmos::ics23::{existence_proof::ExistenceProof, proof_spec::ProofSpec}; -use crate::{inner_op, leaf_op}; +use crate::{iavl_spec, inner_op, leaf_op}; #[derive(Debug, PartialEq, thiserror::Error)] pub enum SpecMismatchError { - #[error("spec mismatch ({0})")] + #[error("leaf spec mismatch ({0})")] LeafSpecMismatch(super::leaf_op::SpecMismatchError), #[error("inner op spec mismatch ({0})")] InnerOpSpecMismatch(super::inner_op::SpecMismatchError), @@ -24,6 +24,31 @@ pub enum CalculateRootError { InnerOpHashAndSpecMismatch, } +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum VerifyError { + #[error("spec mismatch ({0})")] + SpecMismatch(SpecMismatchError), + #[error("key and existence proof value doesn't match ({key:?}, {existence_proof_key:?})")] + KeyAndExistenceProofKeyMismatch { + key: Vec, + existence_proof_key: Vec, + }, + #[error( + "value and existence proof value doesn't match ({value:?}, {existence_proof_value:?})" + )] + ValueAndExistenceProofValueMismatch { + value: Vec, + existence_proof_value: Vec, + }, + #[error("root calculation ({0})")] + RootCalculation(CalculateRootError), + #[error("calculated and given root doesn't match ({calculated_root:?}, {given_root:?})")] + CalculatedAndGivenRootMismatch { + calculated_root: Vec, + given_root: Vec, + }, +} + pub fn check_against_spec( existence_proof: &ExistenceProof, spec: &ProofSpec, @@ -53,7 +78,14 @@ pub fn check_against_spec( Ok(()) } -pub fn calculate_root( +/// Calculate determines the root hash that matches the given proof. +/// You must validate the result is what you have in a header. +/// Returns error if the calculations cannot be performed. +pub fn calculate_root(existence_proof: &ExistenceProof) -> Result, CalculateRootError> { + calculate(existence_proof, None) +} + +fn calculate( existence_proof: &ExistenceProof, spec: Option, ) -> Result, CalculateRootError> { @@ -78,3 +110,40 @@ pub fn calculate_root( Ok(res) } + +/// Verify does all checks to ensure this proof proves this key, value -> root +/// and matches the spec. +pub fn verify( + existence_proof: &ExistenceProof, + spec: ProofSpec, + root: &[u8], + key: &[u8], + value: &[u8], +) -> Result<(), VerifyError> { + check_against_spec(existence_proof, &spec, &iavl_spec()).map_err(VerifyError::SpecMismatch)?; + + if key != existence_proof.key { + return Err(VerifyError::KeyAndExistenceProofKeyMismatch { + key: key.into(), + existence_proof_key: existence_proof.key.clone(), + }); + } + + if value != existence_proof.value { + return Err(VerifyError::ValueAndExistenceProofValueMismatch { + value: value.into(), + existence_proof_value: existence_proof.value.clone(), + }); + } + + let calc = calculate(existence_proof, Some(spec)).map_err(VerifyError::RootCalculation)?; + + if root != calc { + return Err(VerifyError::CalculatedAndGivenRootMismatch { + calculated_root: calc, + given_root: root.into(), + }); + } + + Ok(()) +} diff --git a/lib/ics23/src/lib.rs b/lib/ics23/src/lib.rs index cd109d64b9..9e5697d782 100644 --- a/lib/ics23/src/lib.rs +++ b/lib/ics23/src/lib.rs @@ -1,5 +1,10 @@ #![feature(error_in_core)] +use unionlabs::cosmos::ics23::{ + hash_op::HashOp, inner_spec::InnerSpec, leaf_op::LeafOp, length_op::LengthOp, + proof_spec::ProofSpec, +}; + pub mod commitment_proof; pub mod compressed_batch_entry; pub mod compressed_batch_proof; @@ -10,3 +15,47 @@ pub mod hash_op; pub mod inner_op; pub mod leaf_op; pub mod length_op; + +pub fn iavl_spec() -> ProofSpec { + ProofSpec { + leaf_spec: LeafOp { + hash: HashOp::Sha256, + prehash_key: HashOp::NoHash, + prehash_value: HashOp::Sha256, + length: LengthOp::VarProto, + prefix: vec![0], + }, + inner_spec: InnerSpec { + child_order: vec![0, 1], + child_size: 33, + min_prefix_length: 4, + max_prefix_length: 12, + empty_child: vec![], + hash: HashOp::Sha256, + }, + max_depth: 0, + min_depth: 0, + } +} + +pub fn tendermint_proof_spec() -> ProofSpec { + ProofSpec { + leaf_spec: LeafOp { + hash: HashOp::Sha256, + prehash_key: HashOp::NoHash, + prehash_value: HashOp::Sha256, + length: LengthOp::VarProto, + prefix: vec![0], + }, + inner_spec: InnerSpec { + child_order: vec![0, 1], + child_size: 32, + min_prefix_length: 1, + max_prefix_length: 1, + empty_child: vec![], + hash: HashOp::Sha256, + }, + max_depth: 0, + min_depth: 0, + } +} diff --git a/light-clients/cometbls-light-client/src/client.rs b/light-clients/cometbls-light-client/src/client.rs index 0ac20dd8b5..8f1abef5c9 100644 --- a/light-clients/cometbls-light-client/src/client.rs +++ b/light-clients/cometbls-light-client/src/client.rs @@ -432,45 +432,6 @@ fn do_verify_membership( } } -fn verify_existence_proof( - existence_proof: ExistenceProof, - spec: ProofSpec, - root: &[u8], - key: &[u8], - value: &[u8], -) -> Result<(), VerifyMembershipError> { - existence_proof - .check_against_spec(&spec, &iavl_spec()) - .map_err(VerifyMembershipError::SpecMismatch)?; - - if key != existence_proof.key { - return Err(VerifyMembershipError::KeyAndExistenceProofKeyMismatch { - key: key.into(), - existence_proof_key: existence_proof.key, - }); - } - - if value != existence_proof.value { - return Err(VerifyMembershipError::ValueAndExistenceProofValueMismatch { - value: value.into(), - existence_proof_value: existence_proof.value, - }); - } - - let calc = existence_proof - .calculate_root(Some(spec)) - .map_err(VerifyMembershipError::RootCalculation)?; - - if root != calc { - return Err(VerifyMembershipError::CalculatedAndGivenRootMismatch { - calculated_root: calc, - given_root: root.into(), - }); - } - - Ok(()) -} - fn get_exist_proof_for_key(proof: CommitmentProof, key: &[u8]) -> Option { match proof { CommitmentProof::Exist(exist) => { @@ -497,47 +458,3 @@ fn get_exist_proof_for_key(proof: CommitmentProof, key: &[u8]) -> Option None, } } - -fn iavl_spec() -> ProofSpec { - ProofSpec { - leaf_spec: LeafOp { - hash: HashOp::Sha256, - prehash_key: HashOp::NoHash, - prehash_value: HashOp::Sha256, - length: LengthOp::VarProto, - prefix: vec![0], - }, - inner_spec: InnerSpec { - child_order: vec![0, 1], - child_size: 33, - min_prefix_length: 4, - max_prefix_length: 12, - empty_child: vec![], - hash: HashOp::Sha256, - }, - max_depth: 0, - min_depth: 0, - } -} - -fn tendermint_proof_spec() -> ProofSpec { - ProofSpec { - leaf_spec: LeafOp { - hash: HashOp::Sha256, - prehash_key: HashOp::NoHash, - prehash_value: HashOp::Sha256, - length: LengthOp::VarProto, - prefix: [0].into(), - }, - inner_spec: InnerSpec { - child_order: [0, 1].into(), - child_size: 32, - min_prefix_length: 1, - max_prefix_length: 1, - empty_child: [].into(), - hash: HashOp::Sha256, - }, - max_depth: 0, - min_depth: 0, - } -} From 4b6ffabfe7c449680222c0423ca9a380dfd3caab Mon Sep 17 00:00:00 2001 From: aeryz Date: Tue, 2 Jan 2024 14:05:17 +0300 Subject: [PATCH 03/14] feat(cometbls-lc): membership verification working Signed-off-by: aeryz --- Cargo.lock | 1 + Cargo.toml | 2 + lib/ics23/src/commitment_proof.rs | 2 +- lib/ics23/src/existence_proof.rs | 4 +- lib/ics23/src/hash_op.rs | 2 +- lib/ics23/src/inner_op.rs | 7 +- lib/ics23/src/length_op.rs | 2 +- lib/ics23/src/lib.rs | 3 + lib/ics23/src/verify.rs | 58 +++++++++ .../cometbls-light-client/Cargo.toml | 1 + .../cometbls-light-client/src/client.rs | 120 ++++++------------ .../cometbls-light-client/src/errors.rs | 3 + 12 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 lib/ics23/src/verify.rs diff --git a/Cargo.lock b/Cargo.lock index d5e3eb6ecb..c679107019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -984,6 +984,7 @@ dependencies = [ "cosmwasm-std", "hex", "ics008-wasm-client", + "ics23", "prost 0.11.9", "protos", "ripemd", diff --git a/Cargo.toml b/Cargo.toml index d50c0275b6..72bfeaf07b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "lib/pg-queue", "lib/serde-utils", "lib/unionlabs", + "lib/ics23", "lib/*/fuzz", "tools/generate-rust-sol-bindings", "light-clients/*", @@ -64,6 +65,7 @@ ucs01-relay-api = { path = "cosmwasm/ucs01-relay-api", default-features = false voyager-message = { path = "voyager/voyager-message", default-features = false } derive_more = { version = "0.99.17", default-features = false } ucs01-relay = { path = "cosmwasm/ucs01-relay", default-features = false } +ics23 = { path = "lib/ics23" } # external dependencies diff --git a/lib/ics23/src/commitment_proof.rs b/lib/ics23/src/commitment_proof.rs index a8dd318dc3..e9ab9d59f1 100644 --- a/lib/ics23/src/commitment_proof.rs +++ b/lib/ics23/src/commitment_proof.rs @@ -1,6 +1,6 @@ use unionlabs::cosmos::ics23::commitment_proof::CommitmentProof; -use super::compressed_batch_proof; +use crate::compressed_batch_proof; pub fn decompress(commitment_proof: CommitmentProof) -> CommitmentProof { match commitment_proof { diff --git a/lib/ics23/src/existence_proof.rs b/lib/ics23/src/existence_proof.rs index 452dbb24ee..1a510d3926 100644 --- a/lib/ics23/src/existence_proof.rs +++ b/lib/ics23/src/existence_proof.rs @@ -89,7 +89,7 @@ fn calculate( existence_proof: &ExistenceProof, spec: Option, ) -> Result, CalculateRootError> { - let res = leaf_op::apply( + let mut res = leaf_op::apply( &existence_proof.leaf, &existence_proof.key, &existence_proof.value, @@ -97,7 +97,7 @@ fn calculate( .map_err(CalculateRootError::LeafOpHash)?; for step in &existence_proof.path { - let res = inner_op::apply(step, &res).map_err(CalculateRootError::InnerOpHash)?; + res = inner_op::apply(step, res).map_err(CalculateRootError::InnerOpHash)?; if let Some(ref proof_spec) = spec { if res.len() > proof_spec.inner_spec.child_size as usize diff --git a/lib/ics23/src/hash_op.rs b/lib/ics23/src/hash_op.rs index 8044b10196..314ae55a17 100644 --- a/lib/ics23/src/hash_op.rs +++ b/lib/ics23/src/hash_op.rs @@ -3,7 +3,7 @@ use unionlabs::cosmos::ics23::hash_op::HashOp; // TODO(aeryz): move these to `HashOp` pub fn do_hash_or_noop(hash_op: HashOp, preimage: &[u8]) -> Vec { - if hash_op != HashOp::NoHash { + if hash_op == HashOp::NoHash { return preimage.into(); } diff --git a/lib/ics23/src/inner_op.rs b/lib/ics23/src/inner_op.rs index 208e7ab229..99e24e5b61 100644 --- a/lib/ics23/src/inner_op.rs +++ b/lib/ics23/src/inner_op.rs @@ -52,9 +52,10 @@ pub fn check_against_spec( } // TODO(aeryz): check if max_prefix_length > 0 - let max_prefix_length = (spec.inner_spec.min_prefix_length as usize + let max_prefix_length = (spec.inner_spec.max_prefix_length as usize + (spec.inner_spec.child_order.len() - 1) * spec.inner_spec.child_size as usize) as usize; + if inner_op.prefix.len() > max_prefix_length { return Err(SpecMismatchError::InnerOpPrefixTooLong { prefix_len: inner_op.prefix.len(), @@ -71,13 +72,13 @@ pub fn check_against_spec( Ok(()) } -pub fn apply(inner_op: &InnerOp, child: &[u8]) -> Result, ApplyError> { +pub fn apply(inner_op: &InnerOp, child: Vec) -> Result, ApplyError> { if child.is_empty() { return Err(ApplyError::InnerOpNeedsChildValue); } let mut preimage = inner_op.prefix.clone(); - preimage.extend_from_slice(child); + preimage.extend_from_slice(&child); preimage.extend_from_slice(&inner_op.suffix); Ok(hash_op::do_hash(inner_op.hash, &preimage)) diff --git a/lib/ics23/src/length_op.rs b/lib/ics23/src/length_op.rs index 8b8fc063f7..01a9d8507f 100644 --- a/lib/ics23/src/length_op.rs +++ b/lib/ics23/src/length_op.rs @@ -32,7 +32,7 @@ pub fn apply(length_op: &LengthOp, data: &[u8]) -> Result, ApplyError> { Ok(data.into()) } LengthOp::Fixed32Little => { - let mut d = data.len().to_le_bytes().to_vec(); + let mut d = (data.len() as u32).to_le_bytes().to_vec(); d.extend_from_slice(data); Ok(d) } diff --git a/lib/ics23/src/lib.rs b/lib/ics23/src/lib.rs index 9e5697d782..0e13e3b6d2 100644 --- a/lib/ics23/src/lib.rs +++ b/lib/ics23/src/lib.rs @@ -15,6 +15,9 @@ pub mod hash_op; pub mod inner_op; pub mod leaf_op; pub mod length_op; +mod verify; + +pub use verify::*; pub fn iavl_spec() -> ProofSpec { ProofSpec { diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs new file mode 100644 index 0000000000..8528284b05 --- /dev/null +++ b/lib/ics23/src/verify.rs @@ -0,0 +1,58 @@ +use unionlabs::cosmos::ics23::{ + batch_entry::BatchEntry, commitment_proof::CommitmentProof, existence_proof::ExistenceProof, + proof_spec::ProofSpec, +}; + +use crate::{commitment_proof, existence_proof}; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum VerifyMembershipError { + #[error("existence proof verification failed, ({0})")] + ExistenceProofVerify(existence_proof::VerifyError), + #[error("proof does not exist")] + ProofDoesNotExist, +} + +pub fn verify_membership( + spec: ProofSpec, + root: &[u8], + proof: CommitmentProof, + key: &[u8], + value: &[u8], +) -> Result<(), VerifyMembershipError> { + let proof = commitment_proof::decompress(proof); + + if let Some(ep) = get_exist_proof_for_key(proof, key) { + existence_proof::verify(&ep, spec, root, key, value) + .map_err(VerifyMembershipError::ExistenceProofVerify) + } else { + Err(VerifyMembershipError::ProofDoesNotExist) + } +} + +fn get_exist_proof_for_key(proof: CommitmentProof, key: &[u8]) -> Option { + match proof { + CommitmentProof::Exist(exist) => { + if exist.key.as_slice() == key { + return Some(exist); + } + + None + } + CommitmentProof::Batch(batch) => { + for sub in batch.entries { + match sub { + BatchEntry::Exist(exist) => { + if exist.key.as_slice() == key { + return Some(exist); + } + } + _ => {} + } + } + + None + } + _ => None, + } +} diff --git a/light-clients/cometbls-light-client/Cargo.toml b/light-clients/cometbls-light-client/Cargo.toml index 3fba727053..255d49a751 100644 --- a/light-clients/cometbls-light-client/Cargo.toml +++ b/light-clients/cometbls-light-client/Cargo.toml @@ -26,6 +26,7 @@ cometbls-groth16-verifier = { workspace = true, default-features = false } ics008-wasm-client.workspace = true unionlabs = { workspace = true, default-features = false } +ics23.workspace = true [dev-dependencies] base64 = "0.21" diff --git a/light-clients/cometbls-light-client/src/client.rs b/light-clients/cometbls-light-client/src/client.rs index 8f1abef5c9..66ffcf2dd6 100644 --- a/light-clients/cometbls-light-client/src/client.rs +++ b/light-clients/cometbls-light-client/src/client.rs @@ -5,21 +5,11 @@ use ics008_wasm_client::{ }, IbcClient, MerklePath, Status, StorageState, }; +use ics23::{iavl_spec, tendermint_proof_spec}; use prost::Message; use protos::ibc::core::client::v1::GenesisMetadata; -use sha2::Digest; use unionlabs::{ - cosmos::ics23::{ - batch_proof::BatchProof, - commitment_proof::CommitmentProof, - existence_proof::{ExistenceProof, SpecMismatchError}, - hash_op::HashOp, - inner_op::InnerOp, - inner_spec::InnerSpec, - leaf_op::LeafOp, - length_op::LengthOp, - proof_spec::ProofSpec, - }, + cosmos::ics23::commitment_proof::CommitmentProof, ibc::{ core::{ client::height::Height, @@ -30,6 +20,7 @@ use unionlabs::{ }, }, tendermint::types::commit::Commit, + TryFromProto, }; use crate::{errors::Error, zkp_verifier::verify_zkp_v2}; @@ -55,15 +46,30 @@ impl IbcClient for CometblsLightClient { type ConsensusState = ConsensusState; fn verify_membership( - _deps: Deps, - _height: Height, + deps: Deps, + height: Height, _delay_time_period: u64, _delay_block_period: u64, - _proof: Binary, - _path: MerklePath, - _value: StorageState, + proof: Binary, + path: MerklePath, + value: StorageState, ) -> Result<(), Self::Error> { - Ok(()) + let consensus_state: WasmConsensusState = + read_consensus_state(deps, &height)?.ok_or(Error::ConsensusStateNotFound(height))?; + + let merkle_proof = MerkleProof::try_from_proto_bytes(proof.as_ref()).map_err(|e| { + Error::DecodeFromProto { + reason: format!("{:?}", e), + } + })?; + + match value { + StorageState::Occupied(value) => { + verify_membership(merkle_proof, &consensus_state.data.root, path, &value) + .map_err(Error::VerifyMembership) + } + StorageState::Empty => Ok(()), + } } fn verify_header( @@ -331,23 +337,14 @@ fn canonical_vote( } } -#[derive(Debug)] +#[derive(Debug, PartialEq, thiserror::Error)] pub enum VerifyMembershipError { - SpecMismatch(SpecMismatchError), - KeyAndExistenceProofKeyMismatch { - key: Vec, - existence_proof_key: Vec, - }, - ValueAndExistenceProofValueMismatch { - value: Vec, - existence_proof_value: Vec, - }, - RootCalculation(unionlabs::cosmos::ics23::existence_proof::CalculateRootError), - CalculatedAndGivenRootMismatch { - calculated_root: Vec, - given_root: Vec, - }, - ProofDoesNotExist, + #[error("root calculation ({0})")] + RootCalculation(ics23::existence_proof::CalculateRootError), + #[error("{0}")] + Ics23(ics23::VerifyMembershipError), + #[error("invalid root top level")] // TODO(aeryz): beautify + InvalidRoot, } fn verify_membership( @@ -378,24 +375,22 @@ fn verify_chained_membership_proof( ) -> Result<(), VerifyMembershipError> { let specs = [iavl_spec(), tendermint_proof_spec()]; - // FIXME(aeryz): ugly change let mut tmp_value = value.to_vec(); while index < proofs.len() { match &proofs[index] { CommitmentProof::Exist(proof) => { - let subroot = proof - .calculate_root(None) + let subroot = ics23::existence_proof::calculate_root(proof) .map_err(VerifyMembershipError::RootCalculation)?; - if let Some(key) = keys.key_path.get(keys.key_path.len() - 1 - index) { - do_verify_membership( + ics23::verify_membership( specs[index].clone(), // TODO(aeryz): I'm about to throw up &subroot, CommitmentProof::Exist(proof.clone()), // TODO(aeryz): disgusting key.as_bytes(), // TODO(aeryz): weird? is this really like this? &tmp_value, - )?; + ) + .map_err(VerifyMembershipError::Ics23)?; } else { panic!("could not retrieve key bytes for key"); }; @@ -413,48 +408,9 @@ fn verify_chained_membership_proof( index += 1; } - Ok(()) -} - -fn do_verify_membership( - spec: ProofSpec, - root: &[u8], - proof: CommitmentProof, - key: &[u8], - value: &[u8], -) -> Result<(), VerifyMembershipError> { - let proof = proof.decompress(); - - if let Some(ep) = get_exist_proof_for_key(proof, key) { - verify_existence_proof(ep, spec, root, key, value) - } else { - Err(VerifyMembershipError::ProofDoesNotExist) + if tmp_value.as_slice() != root { + return Err(VerifyMembershipError::InvalidRoot); } -} -fn get_exist_proof_for_key(proof: CommitmentProof, key: &[u8]) -> Option { - match proof { - CommitmentProof::Exist(exist) => { - if exist.key.as_slice() == key { - return Some(exist); - } - - None - } - CommitmentProof::Batch(batch) => { - for sub in batch.entries { - match sub { - unionlabs::cosmos::ics23::batch_entry::BatchEntry::Exist(exist) => { - if exist.key.as_slice() == key { - return Some(exist); - } - } - _ => {} - } - } - - None - } - _ => None, - } + Ok(()) } diff --git a/light-clients/cometbls-light-client/src/errors.rs b/light-clients/cometbls-light-client/src/errors.rs index 2745f504d6..cb4221533a 100644 --- a/light-clients/cometbls-light-client/src/errors.rs +++ b/light-clients/cometbls-light-client/src/errors.rs @@ -105,6 +105,9 @@ pub enum Error { #[error("Wasm client error: {0}")] Wasm(String), + + #[error("verify membership error: {0}")] + VerifyMembership(crate::client::VerifyMembershipError), } impl Error { From 18912af5796718425b1f3d36ff7a4c9cbf27871c Mon Sep 17 00:00:00 2001 From: aeryz Date: Tue, 2 Jan 2024 19:10:39 +0300 Subject: [PATCH 04/14] feat(unionlabs): add ibc/core/commitment/merkle_path Signed-off-by: aeryz --- lib/unionlabs/src/ibc/core/commitment.rs | 1 + .../src/ibc/core/commitment/merkle_path.rs | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 lib/unionlabs/src/ibc/core/commitment/merkle_path.rs diff --git a/lib/unionlabs/src/ibc/core/commitment.rs b/lib/unionlabs/src/ibc/core/commitment.rs index d8d575d255..47be19b2ce 100644 --- a/lib/unionlabs/src/ibc/core/commitment.rs +++ b/lib/unionlabs/src/ibc/core/commitment.rs @@ -1,3 +1,4 @@ +pub mod merkle_path; pub mod merkle_prefix; pub mod merkle_proof; pub mod merkle_root; diff --git a/lib/unionlabs/src/ibc/core/commitment/merkle_path.rs b/lib/unionlabs/src/ibc/core/commitment/merkle_path.rs new file mode 100644 index 0000000000..95ef059a0d --- /dev/null +++ b/lib/unionlabs/src/ibc/core/commitment/merkle_path.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +use crate::{Proto, TypeUrl}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MerklePath { + pub key_path: Vec, +} + +impl From for protos::ibc::core::commitment::v1::MerklePath { + fn from(value: MerklePath) -> Self { + Self { + key_path: value.key_path, + } + } +} + +impl From for MerklePath { + fn from(value: protos::ibc::core::commitment::v1::MerklePath) -> Self { + Self { + key_path: value.key_path, + } + } +} + +impl Proto for MerklePath { + type Proto = protos::ibc::core::commitment::v1::MerklePath; +} + +impl TypeUrl for protos::ibc::core::commitment::v1::MerklePath { + const TYPE_URL: &'static str = "/ibc.core.commitment.v1.MerklePath"; +} From 49f4887aa3242e0f00719fb838fcd2d0027852c7 Mon Sep 17 00:00:00 2001 From: aeryz Date: Wed, 3 Jan 2024 00:02:24 +0300 Subject: [PATCH 05/14] chore: use `unionlabs::MerklePath` Signed-off-by: aeryz --- lib/ics-008-wasm-client/src/ibc_client.rs | 7 +- lib/ics-008-wasm-client/src/msg.rs | 7 +- .../cometbls-light-client/src/client.rs | 84 +------------------ .../ethereum-light-client/src/client.rs | 4 +- 4 files changed, 10 insertions(+), 92 deletions(-) diff --git a/lib/ics-008-wasm-client/src/ibc_client.rs b/lib/ics-008-wasm-client/src/ibc_client.rs index d5f556337e..8fcf6d096e 100644 --- a/lib/ics-008-wasm-client/src/ibc_client.rs +++ b/lib/ics-008-wasm-client/src/ibc_client.rs @@ -4,16 +4,15 @@ use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, StdError}; use protos::ibc::core::client::v1::GenesisMetadata; use unionlabs::{ ibc::{ - core::client::height::Height, + core::{client::height::Height, commitment::merkle_path::MerklePath}, lightclients::wasm::{client_state::ClientState, consensus_state::ConsensusState}, }, Proto, TryFromProto, TryFromProtoBytesError, TryFromProtoErrorOf, }; use crate::{ - msg::{MerklePath, QueryMsg}, - CheckForMisbehaviourResult, EmptyResult, Error, ExportMetadataResult, Status, StatusResult, - SudoMsg, TimestampAtHeightResult, UpdateStateResult, + msg::QueryMsg, CheckForMisbehaviourResult, EmptyResult, Error, ExportMetadataResult, Status, + StatusResult, SudoMsg, TimestampAtHeightResult, UpdateStateResult, }; pub enum StorageState { diff --git a/lib/ics-008-wasm-client/src/msg.rs b/lib/ics-008-wasm-client/src/msg.rs index b0fc2dec9d..4073268bfd 100644 --- a/lib/ics-008-wasm-client/src/msg.rs +++ b/lib/ics-008-wasm-client/src/msg.rs @@ -8,7 +8,7 @@ use protos::ibc::{ }, }; use serde::{Deserialize, Serialize}; -use unionlabs::ibc::core::client::height::Height; +use unionlabs::ibc::core::{client::height::Height, commitment::merkle_path::MerklePath}; // TODO(aeryz): Handle all these properly #[derive(Debug, Serialize, Deserialize)] @@ -23,11 +23,6 @@ pub struct ClientMessage { pub data: Binary, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct MerklePath { - pub key_path: Vec, -} - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct EmptyResult {} diff --git a/light-clients/cometbls-light-client/src/client.rs b/light-clients/cometbls-light-client/src/client.rs index 66ffcf2dd6..ef5462a8c9 100644 --- a/light-clients/cometbls-light-client/src/client.rs +++ b/light-clients/cometbls-light-client/src/client.rs @@ -3,7 +3,7 @@ use ics008_wasm_client::{ storage_utils::{ read_client_state, read_consensus_state, save_client_state, save_consensus_state, }, - IbcClient, MerklePath, Status, StorageState, + IbcClient, Status, StorageState, }; use ics23::{iavl_spec, tendermint_proof_spec}; use prost::Message; @@ -13,7 +13,9 @@ use unionlabs::{ ibc::{ core::{ client::height::Height, - commitment::{merkle_proof::MerkleProof, merkle_root::MerkleRoot}, + commitment::{ + merkle_path::MerklePath, merkle_proof::MerkleProof, merkle_root::MerkleRoot, + }, }, lightclients::cometbls::{ client_state::ClientState, consensus_state::ConsensusState, header::Header, @@ -336,81 +338,3 @@ fn canonical_vote( chain_id, } } - -#[derive(Debug, PartialEq, thiserror::Error)] -pub enum VerifyMembershipError { - #[error("root calculation ({0})")] - RootCalculation(ics23::existence_proof::CalculateRootError), - #[error("{0}")] - Ics23(ics23::VerifyMembershipError), - #[error("invalid root top level")] // TODO(aeryz): beautify - InvalidRoot, -} - -fn verify_membership( - proof: MerkleProof, - consensus_root: &MerkleRoot, - path: MerklePath, - value: &[u8], -) -> Result<(), VerifyMembershipError> { - // TODO(aeryz): check if this supposed to be embedded or configurable - if proof.proofs.len() != 2 { - panic!("Proof length needs to match the spec"); - } - - // TODO(aeryz): Make this spec a global constant if it's going to be embedded - if path.key_path.len() != 2 { - panic!("Path length needs to match the spec"); - } - - verify_chained_membership_proof(consensus_root.hash.as_ref(), &proof.proofs, path, value, 0) -} - -fn verify_chained_membership_proof( - root: &[u8], - proofs: &[CommitmentProof], - keys: MerklePath, - value: &[u8], - mut index: usize, -) -> Result<(), VerifyMembershipError> { - let specs = [iavl_spec(), tendermint_proof_spec()]; - - let mut tmp_value = value.to_vec(); - - while index < proofs.len() { - match &proofs[index] { - CommitmentProof::Exist(proof) => { - let subroot = ics23::existence_proof::calculate_root(proof) - .map_err(VerifyMembershipError::RootCalculation)?; - if let Some(key) = keys.key_path.get(keys.key_path.len() - 1 - index) { - ics23::verify_membership( - specs[index].clone(), // TODO(aeryz): I'm about to throw up - &subroot, - CommitmentProof::Exist(proof.clone()), // TODO(aeryz): disgusting - key.as_bytes(), // TODO(aeryz): weird? is this really like this? - &tmp_value, - ) - .map_err(VerifyMembershipError::Ics23)?; - } else { - panic!("could not retrieve key bytes for key"); - }; - - tmp_value = subroot; - } - CommitmentProof::Nonexist(_) => { - panic!("chained membership proof contains nonexistence proof at index %d"); - } - _ => { - panic!("invalid proof type"); - } - } - - index += 1; - } - - if tmp_value.as_slice() != root { - return Err(VerifyMembershipError::InvalidRoot); - } - - Ok(()) -} diff --git a/light-clients/ethereum-light-client/src/client.rs b/light-clients/ethereum-light-client/src/client.rs index a40a3649d0..48fedc3ec4 100644 --- a/light-clients/ethereum-light-client/src/client.rs +++ b/light-clients/ethereum-light-client/src/client.rs @@ -16,7 +16,7 @@ use unionlabs::{ google::protobuf::any::Any, hash::H256, ibc::{ - core::client::height::Height, + core::{client::height::Height, commitment::merkle_path::MerklePath}, lightclients::{ cometbls, ethereum::{ @@ -66,7 +66,7 @@ impl IbcClient for EthereumLightClient { _delay_time_period: u64, _delay_block_period: u64, proof: Binary, - mut path: ics008_wasm_client::MerklePath, + mut path: MerklePath, value: ics008_wasm_client::StorageState, ) -> Result<(), Self::Error> { let consensus_state: WasmConsensusState = From 7c614724a020a2c5e4f4e8cde130c75ef845fa0a Mon Sep 17 00:00:00 2001 From: aeryz Date: Wed, 3 Jan 2024 00:03:04 +0300 Subject: [PATCH 06/14] chore(ics23): improve the code and reduce copies Signed-off-by: aeryz --- lib/ics23/src/commitment_proof.rs | 2 +- lib/ics23/src/compressed_batch_entry.rs | 2 +- lib/ics23/src/compressed_batch_proof.rs | 2 +- lib/ics23/src/compressed_existence_proof.rs | 2 +- .../src/compressed_nonexistence_proof.rs | 12 +-- lib/ics23/src/existence_proof.rs | 11 ++- lib/ics23/src/ibc_api.rs | 99 +++++++++++++++++++ lib/ics23/src/inner_op.rs | 2 +- lib/ics23/src/leaf_op.rs | 6 +- lib/ics23/src/lib.rs | 50 +--------- lib/ics23/src/proof_specs.rs | 46 +++++++++ lib/ics23/src/verify.rs | 2 +- lib/unionlabs/src/cosmos/ics23/inner_spec.rs | 14 +-- lib/unionlabs/src/cosmos/ics23/leaf_op.rs | 10 +- 14 files changed, 180 insertions(+), 80 deletions(-) create mode 100644 lib/ics23/src/ibc_api.rs create mode 100644 lib/ics23/src/proof_specs.rs diff --git a/lib/ics23/src/commitment_proof.rs b/lib/ics23/src/commitment_proof.rs index e9ab9d59f1..a8dd318dc3 100644 --- a/lib/ics23/src/commitment_proof.rs +++ b/lib/ics23/src/commitment_proof.rs @@ -1,6 +1,6 @@ use unionlabs::cosmos::ics23::commitment_proof::CommitmentProof; -use crate::compressed_batch_proof; +use super::compressed_batch_proof; pub fn decompress(commitment_proof: CommitmentProof) -> CommitmentProof { match commitment_proof { diff --git a/lib/ics23/src/compressed_batch_entry.rs b/lib/ics23/src/compressed_batch_entry.rs index 33dd313b75..595121f7c7 100644 --- a/lib/ics23/src/compressed_batch_entry.rs +++ b/lib/ics23/src/compressed_batch_entry.rs @@ -6,7 +6,7 @@ use crate::{compressed_existence_proof, compressed_nonexistence_proof}; pub fn decompress( compressed_batch_entry: CompressedBatchEntry, - lookup: Vec, + lookup: &Vec, ) -> BatchEntry { match compressed_batch_entry { CompressedBatchEntry::Exist(exist) => { diff --git a/lib/ics23/src/compressed_batch_proof.rs b/lib/ics23/src/compressed_batch_proof.rs index 42b98e81ba..2b84b7a72f 100644 --- a/lib/ics23/src/compressed_batch_proof.rs +++ b/lib/ics23/src/compressed_batch_proof.rs @@ -11,7 +11,7 @@ pub fn decompress(compressed_batch_proof: CompressedBatchProof) -> BatchProof { entries: compressed_batch_proof .entries .into_iter() - .map(|entry| compressed_batch_entry::decompress(entry, lookup.clone())) + .map(|entry| compressed_batch_entry::decompress(entry, &lookup)) .collect(), } } diff --git a/lib/ics23/src/compressed_existence_proof.rs b/lib/ics23/src/compressed_existence_proof.rs index 10876c30ff..fe387d50ce 100644 --- a/lib/ics23/src/compressed_existence_proof.rs +++ b/lib/ics23/src/compressed_existence_proof.rs @@ -8,7 +8,7 @@ use unionlabs::cosmos::ics23::{ // Is this a correct assumption? pub fn decompress( compressed_existence_proof: CompressedExistenceProof, - lookup: Vec, + lookup: &Vec, ) -> ExistenceProof { ExistenceProof { key: compressed_existence_proof.key, diff --git a/lib/ics23/src/compressed_nonexistence_proof.rs b/lib/ics23/src/compressed_nonexistence_proof.rs index 4d9b1a038d..b2180c4262 100644 --- a/lib/ics23/src/compressed_nonexistence_proof.rs +++ b/lib/ics23/src/compressed_nonexistence_proof.rs @@ -7,17 +7,11 @@ use super::compressed_existence_proof; pub fn decompress( compressed_nonexistence_proof: CompressedNonExistenceProof, - lookup: Vec, + lookup: &Vec, ) -> NonExistenceProof { NonExistenceProof { key: compressed_nonexistence_proof.key, - left: compressed_existence_proof::decompress( - compressed_nonexistence_proof.left, - lookup.clone(), - ), - right: compressed_existence_proof::decompress( - compressed_nonexistence_proof.right, - lookup.clone(), - ), + left: compressed_existence_proof::decompress(compressed_nonexistence_proof.left, lookup), + right: compressed_existence_proof::decompress(compressed_nonexistence_proof.right, lookup), } } diff --git a/lib/ics23/src/existence_proof.rs b/lib/ics23/src/existence_proof.rs index 1a510d3926..a4fbcc62d6 100644 --- a/lib/ics23/src/existence_proof.rs +++ b/lib/ics23/src/existence_proof.rs @@ -1,6 +1,6 @@ use unionlabs::cosmos::ics23::{existence_proof::ExistenceProof, proof_spec::ProofSpec}; -use crate::{iavl_spec, inner_op, leaf_op}; +use crate::{inner_op, leaf_op, proof_specs::IAVL_PROOF_SPEC}; #[derive(Debug, PartialEq, thiserror::Error)] pub enum SpecMismatchError { @@ -87,7 +87,7 @@ pub fn calculate_root(existence_proof: &ExistenceProof) -> Result, Calcu fn calculate( existence_proof: &ExistenceProof, - spec: Option, + spec: Option<&ProofSpec>, ) -> Result, CalculateRootError> { let mut res = leaf_op::apply( &existence_proof.leaf, @@ -99,7 +99,7 @@ fn calculate( for step in &existence_proof.path { res = inner_op::apply(step, res).map_err(CalculateRootError::InnerOpHash)?; - if let Some(ref proof_spec) = spec { + if let Some(proof_spec) = spec { if res.len() > proof_spec.inner_spec.child_size as usize && proof_spec.inner_spec.child_size >= 32 { @@ -115,12 +115,13 @@ fn calculate( /// and matches the spec. pub fn verify( existence_proof: &ExistenceProof, - spec: ProofSpec, + spec: &ProofSpec, root: &[u8], key: &[u8], value: &[u8], ) -> Result<(), VerifyError> { - check_against_spec(existence_proof, &spec, &iavl_spec()).map_err(VerifyError::SpecMismatch)?; + check_against_spec(existence_proof, spec, &IAVL_PROOF_SPEC) + .map_err(VerifyError::SpecMismatch)?; if key != existence_proof.key { return Err(VerifyError::KeyAndExistenceProofKeyMismatch { diff --git a/lib/ics23/src/ibc_api.rs b/lib/ics23/src/ibc_api.rs new file mode 100644 index 0000000000..1831a14c32 --- /dev/null +++ b/lib/ics23/src/ibc_api.rs @@ -0,0 +1,99 @@ +use unionlabs::{ + cosmos::ics23::{commitment_proof::CommitmentProof, proof_spec::ProofSpec}, + ibc::core::commitment::{ + merkle_path::MerklePath, merkle_proof::MerkleProof, merkle_root::MerkleRoot, + }, +}; + +pub use crate::proof_specs::{IAVL_PROOF_SPEC, TENDERMINT_PROOF_SPEC}; +use crate::{existence_proof, verify}; + +pub const SDK_SPECS: [&'static ProofSpec; 2] = [&IAVL_PROOF_SPEC, &TENDERMINT_PROOF_SPEC]; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum VerifyMembershipError { + #[error("root calculation ({0})")] + RootCalculation(existence_proof::CalculateRootError), + #[error("{0}")] + InnerVerification(verify::VerifyMembershipError), + #[error("invalid root top level")] // TODO(aeryz): beautify + InvalidRoot, + #[error("expected the size of proofs to be ({expected}), got ({got})")] + InvalidProofsLength { expected: usize, got: usize }, + #[error("expected the size of key path to be ({expected}), got ({got})")] + InvalidKeyPathLength { expected: usize, got: usize }, + #[error("proof type is expected to be `Exist`")] + InvalidProofType, + #[error("could not retrieve the key due to invalid indexing")] + InvalidIndexing, +} + +pub fn verify_membership( + proof: MerkleProof, + specs: &[&ProofSpec], + consensus_root: &MerkleRoot, + path: MerklePath, + value: Vec, +) -> Result<(), VerifyMembershipError> { + if proof.proofs.len() != specs.len() { + return Err(VerifyMembershipError::InvalidProofsLength { + expected: specs.len(), + got: proof.proofs.len(), + }); + } + + if path.key_path.len() != specs.len() { + return Err(VerifyMembershipError::InvalidKeyPathLength { + expected: specs.len(), + got: path.key_path.len(), + }); + } + + verify_chained_membership_proof( + consensus_root.hash.as_ref(), + specs, + proof.proofs, + path, + value, + 0, + ) +} + +fn verify_chained_membership_proof( + root: &[u8], + specs: &[&ProofSpec], + proofs: Vec, + keys: MerklePath, + value: Vec, + index: usize, +) -> Result<(), VerifyMembershipError> { + proofs + .into_iter() + .skip(index) + .enumerate() + .try_fold(value, |value, (i, proof)| { + let CommitmentProof::Exist(ref existence_proof) = proof else { + return Err(VerifyMembershipError::InvalidProofType); + }; + + let subroot = existence_proof::calculate_root(existence_proof) + .map_err(VerifyMembershipError::RootCalculation)?; + + let key = keys + .key_path + .get(keys.key_path.len() - 1 - i) + .ok_or(VerifyMembershipError::InvalidIndexing)?; + + verify::verify_membership(specs[index], &subroot, proof, key.as_bytes(), &value) + .map_err(VerifyMembershipError::InnerVerification)?; + + Ok(subroot) + }) + .and_then(|value| { + if value.as_slice() == root { + Ok(()) + } else { + Err(VerifyMembershipError::InvalidRoot) + } + }) +} diff --git a/lib/ics23/src/inner_op.rs b/lib/ics23/src/inner_op.rs index 99e24e5b61..bbfc955657 100644 --- a/lib/ics23/src/inner_op.rs +++ b/lib/ics23/src/inner_op.rs @@ -39,7 +39,7 @@ pub fn check_against_spec( if inner_op.prefix.starts_with(&spec.leaf_spec.prefix) { return Err(SpecMismatchError::PrefixMismatch { full: inner_op.prefix.clone(), - prefix: spec.leaf_spec.prefix.clone(), + prefix: spec.leaf_spec.prefix.clone().into_owned(), }); } diff --git a/lib/ics23/src/leaf_op.rs b/lib/ics23/src/leaf_op.rs index 2727ff725b..775c0e99cb 100644 --- a/lib/ics23/src/leaf_op.rs +++ b/lib/ics23/src/leaf_op.rs @@ -59,8 +59,8 @@ pub fn check_against_spec( if !leaf_op.prefix.starts_with(&lspec.prefix) { return Err(SpecMismatchError::PrefixMismatch { - full: leaf_op.prefix.clone(), - prefix: lspec.prefix.clone(), + full: leaf_op.prefix.clone().into_owned(), + prefix: lspec.prefix.clone().into_owned(), }); } @@ -81,7 +81,7 @@ pub fn apply(leaf_op: &LeafOp, key: &[u8], value: &[u8]) -> Result, Appl let pvalue = prepare_data(leaf_op, leaf_op.prehash_value, value)?; - let mut data = leaf_op.prefix.clone(); + let mut data = leaf_op.prefix.clone().into_owned(); data.extend_from_slice(&pkey); data.extend_from_slice(&pvalue); diff --git a/lib/ics23/src/lib.rs b/lib/ics23/src/lib.rs index 0e13e3b6d2..ab76444f97 100644 --- a/lib/ics23/src/lib.rs +++ b/lib/ics23/src/lib.rs @@ -12,53 +12,9 @@ pub mod compressed_existence_proof; pub mod compressed_nonexistence_proof; pub mod existence_proof; pub mod hash_op; +pub mod ibc_api; pub mod inner_op; pub mod leaf_op; pub mod length_op; -mod verify; - -pub use verify::*; - -pub fn iavl_spec() -> ProofSpec { - ProofSpec { - leaf_spec: LeafOp { - hash: HashOp::Sha256, - prehash_key: HashOp::NoHash, - prehash_value: HashOp::Sha256, - length: LengthOp::VarProto, - prefix: vec![0], - }, - inner_spec: InnerSpec { - child_order: vec![0, 1], - child_size: 33, - min_prefix_length: 4, - max_prefix_length: 12, - empty_child: vec![], - hash: HashOp::Sha256, - }, - max_depth: 0, - min_depth: 0, - } -} - -pub fn tendermint_proof_spec() -> ProofSpec { - ProofSpec { - leaf_spec: LeafOp { - hash: HashOp::Sha256, - prehash_key: HashOp::NoHash, - prehash_value: HashOp::Sha256, - length: LengthOp::VarProto, - prefix: vec![0], - }, - inner_spec: InnerSpec { - child_order: vec![0, 1], - child_size: 32, - min_prefix_length: 1, - max_prefix_length: 1, - empty_child: vec![], - hash: HashOp::Sha256, - }, - max_depth: 0, - min_depth: 0, - } -} +pub mod proof_specs; +pub mod verify; diff --git a/lib/ics23/src/proof_specs.rs b/lib/ics23/src/proof_specs.rs new file mode 100644 index 0000000000..d28e7d0f00 --- /dev/null +++ b/lib/ics23/src/proof_specs.rs @@ -0,0 +1,46 @@ +use std::borrow::Cow; + +use unionlabs::cosmos::ics23::{ + hash_op::HashOp, inner_spec::InnerSpec, leaf_op::LeafOp, length_op::LengthOp, + proof_spec::ProofSpec, +}; + +pub const IAVL_PROOF_SPEC: ProofSpec = ProofSpec { + leaf_spec: LeafOp { + hash: HashOp::Sha256, + prehash_key: HashOp::NoHash, + prehash_value: HashOp::Sha256, + length: LengthOp::VarProto, + prefix: Cow::Borrowed(&[0]), + }, + inner_spec: InnerSpec { + child_order: Cow::Borrowed(&[0, 1]), + child_size: 33, + min_prefix_length: 4, + max_prefix_length: 12, + empty_child: Cow::Borrowed(&[]), + hash: HashOp::Sha256, + }, + max_depth: 0, + min_depth: 0, +}; + +pub const TENDERMINT_PROOF_SPEC: ProofSpec = ProofSpec { + leaf_spec: LeafOp { + hash: HashOp::Sha256, + prehash_key: HashOp::NoHash, + prehash_value: HashOp::Sha256, + length: LengthOp::VarProto, + prefix: Cow::Borrowed(&[0]), + }, + inner_spec: InnerSpec { + child_order: Cow::Borrowed(&[0, 1]), + child_size: 32, + min_prefix_length: 1, + max_prefix_length: 1, + empty_child: Cow::Borrowed(&[]), + hash: HashOp::Sha256, + }, + max_depth: 0, + min_depth: 0, +}; diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs index 8528284b05..9ef61791ab 100644 --- a/lib/ics23/src/verify.rs +++ b/lib/ics23/src/verify.rs @@ -14,7 +14,7 @@ pub enum VerifyMembershipError { } pub fn verify_membership( - spec: ProofSpec, + spec: &ProofSpec, root: &[u8], proof: CommitmentProof, key: &[u8], diff --git a/lib/unionlabs/src/cosmos/ics23/inner_spec.rs b/lib/unionlabs/src/cosmos/ics23/inner_spec.rs index cc2f2ef31c..316f045561 100644 --- a/lib/unionlabs/src/cosmos/ics23/inner_spec.rs +++ b/lib/unionlabs/src/cosmos/ics23/inner_spec.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use serde::{Deserialize, Serialize}; use super::hash_op::HashOp; @@ -6,12 +8,12 @@ use crate::{errors::UnknownEnumVariant, Proto, TypeUrl}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct InnerSpec { - pub child_order: Vec, + pub child_order: Cow<'static, [i32]>, pub child_size: i32, pub min_prefix_length: i32, pub max_prefix_length: i32, #[serde(with = "::serde_utils::hex_string")] - pub empty_child: Vec, + pub empty_child: Cow<'static, [u8]>, pub hash: HashOp, } @@ -26,11 +28,11 @@ impl Proto for InnerSpec { impl From for protos::cosmos::ics23::v1::InnerSpec { fn from(value: InnerSpec) -> Self { Self { - child_order: value.child_order, + child_order: value.child_order.into(), child_size: value.child_size, min_prefix_length: value.min_prefix_length, max_prefix_length: value.max_prefix_length, - empty_child: value.empty_child, + empty_child: value.empty_child.into(), hash: value.hash.into(), } } @@ -46,11 +48,11 @@ impl TryFrom for InnerSpec { fn try_from(value: protos::cosmos::ics23::v1::InnerSpec) -> Result { Ok(Self { - child_order: value.child_order, + child_order: value.child_order.into(), child_size: value.child_size, min_prefix_length: value.min_prefix_length, max_prefix_length: value.max_prefix_length, - empty_child: value.empty_child, + empty_child: value.empty_child.into(), hash: value.hash.try_into().map_err(TryFromInnerSpecError::Hash)?, }) } diff --git a/lib/unionlabs/src/cosmos/ics23/leaf_op.rs b/lib/unionlabs/src/cosmos/ics23/leaf_op.rs index 5df170d686..7c577096eb 100644 --- a/lib/unionlabs/src/cosmos/ics23/leaf_op.rs +++ b/lib/unionlabs/src/cosmos/ics23/leaf_op.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use serde::{Deserialize, Serialize}; use super::{hash_op::HashOp, length_op::LengthOp}; @@ -11,7 +13,7 @@ pub struct LeafOp { pub prehash_value: HashOp, pub length: LengthOp, #[serde(with = "::serde_utils::hex_string")] - pub prefix: Vec, + pub prefix: Cow<'static, [u8]>, } impl TypeUrl for protos::cosmos::ics23::v1::LeafOp { @@ -29,7 +31,7 @@ impl From for protos::cosmos::ics23::v1::LeafOp { prehash_key: value.prehash_key.into(), prehash_value: value.prehash_value.into(), length: value.length.into(), - prefix: value.prefix, + prefix: value.prefix.into(), } } } @@ -60,7 +62,7 @@ impl TryFrom for LeafOp { .length .try_into() .map_err(TryFromLeafOpError::Length)?, - prefix: value.prefix, + prefix: value.prefix.into(), }) } } @@ -73,7 +75,7 @@ impl From for contracts::glue::CosmosIcs23V1LeafOpData { prehash_key: value.prehash_key.into(), prehash_value: value.prehash_value.into(), length: value.length.into(), - prefix: value.prefix.into(), + prefix: value.prefix.into_owned().into(), } } } From d6d668dd8cc1b3945c2bdc074b2601c6cbdea333 Mon Sep 17 00:00:00 2001 From: aeryz Date: Wed, 3 Jan 2024 10:46:12 +0300 Subject: [PATCH 07/14] feat(ics23): add `validate_iavl_ops` Signed-off-by: aeryz --- lib/ics23/src/existence_proof.rs | 10 ++-- lib/ics23/src/lib.rs | 11 +--- lib/ics23/src/{ => ops}/hash_op.rs | 1 - lib/ics23/src/{ => ops}/inner_op.rs | 24 +++++++-- lib/ics23/src/{ => ops}/leaf_op.rs | 24 ++++++--- lib/ics23/src/{ => ops}/length_op.rs | 0 lib/ics23/src/ops/mod.rs | 55 ++++++++++++++++++++ lib/unionlabs/src/cosmos/ics23/proof_spec.rs | 2 +- 8 files changed, 97 insertions(+), 30 deletions(-) rename lib/ics23/src/{ => ops}/hash_op.rs (96%) rename lib/ics23/src/{ => ops}/inner_op.rs (77%) rename lib/ics23/src/{ => ops}/leaf_op.rs (77%) rename lib/ics23/src/{ => ops}/length_op.rs (100%) create mode 100644 lib/ics23/src/ops/mod.rs diff --git a/lib/ics23/src/existence_proof.rs b/lib/ics23/src/existence_proof.rs index a4fbcc62d6..6f1a0d764e 100644 --- a/lib/ics23/src/existence_proof.rs +++ b/lib/ics23/src/existence_proof.rs @@ -1,6 +1,6 @@ use unionlabs::cosmos::ics23::{existence_proof::ExistenceProof, proof_spec::ProofSpec}; -use crate::{inner_op, leaf_op, proof_specs::IAVL_PROOF_SPEC}; +use crate::{inner_op, leaf_op}; #[derive(Debug, PartialEq, thiserror::Error)] pub enum SpecMismatchError { @@ -52,9 +52,8 @@ pub enum VerifyError { pub fn check_against_spec( existence_proof: &ExistenceProof, spec: &ProofSpec, - iavl_spec: &ProofSpec, ) -> Result<(), SpecMismatchError> { - leaf_op::check_against_spec(&existence_proof.leaf, spec, iavl_spec) + leaf_op::check_against_spec(&existence_proof.leaf, spec) .map_err(SpecMismatchError::LeafSpecMismatch)?; if spec.min_depth > 0 && existence_proof.path.len() < spec.min_depth as usize { @@ -71,7 +70,7 @@ pub fn check_against_spec( } for (index, inner) in existence_proof.path.iter().enumerate() { - inner_op::check_against_spec(inner, spec, index as i32 + 1, iavl_spec) + inner_op::check_against_spec(inner, spec, index as i32 + 1) .map_err(SpecMismatchError::InnerOpSpecMismatch)?; } @@ -120,8 +119,7 @@ pub fn verify( key: &[u8], value: &[u8], ) -> Result<(), VerifyError> { - check_against_spec(existence_proof, spec, &IAVL_PROOF_SPEC) - .map_err(VerifyError::SpecMismatch)?; + check_against_spec(existence_proof, spec).map_err(VerifyError::SpecMismatch)?; if key != existence_proof.key { return Err(VerifyError::KeyAndExistenceProofKeyMismatch { diff --git a/lib/ics23/src/lib.rs b/lib/ics23/src/lib.rs index ab76444f97..fe11635a21 100644 --- a/lib/ics23/src/lib.rs +++ b/lib/ics23/src/lib.rs @@ -1,20 +1,13 @@ #![feature(error_in_core)] -use unionlabs::cosmos::ics23::{ - hash_op::HashOp, inner_spec::InnerSpec, leaf_op::LeafOp, length_op::LengthOp, - proof_spec::ProofSpec, -}; - pub mod commitment_proof; pub mod compressed_batch_entry; pub mod compressed_batch_proof; pub mod compressed_existence_proof; pub mod compressed_nonexistence_proof; pub mod existence_proof; -pub mod hash_op; pub mod ibc_api; -pub mod inner_op; -pub mod leaf_op; -pub mod length_op; +mod ops; pub mod proof_specs; pub mod verify; +pub use ops::*; diff --git a/lib/ics23/src/hash_op.rs b/lib/ics23/src/ops/hash_op.rs similarity index 96% rename from lib/ics23/src/hash_op.rs rename to lib/ics23/src/ops/hash_op.rs index 314ae55a17..90907d80ac 100644 --- a/lib/ics23/src/hash_op.rs +++ b/lib/ics23/src/ops/hash_op.rs @@ -1,7 +1,6 @@ use sha2::Digest; use unionlabs::cosmos::ics23::hash_op::HashOp; -// TODO(aeryz): move these to `HashOp` pub fn do_hash_or_noop(hash_op: HashOp, preimage: &[u8]) -> Vec { if hash_op == HashOp::NoHash { return preimage.into(); diff --git a/lib/ics23/src/inner_op.rs b/lib/ics23/src/ops/inner_op.rs similarity index 77% rename from lib/ics23/src/inner_op.rs rename to lib/ics23/src/ops/inner_op.rs index bbfc955657..3327070168 100644 --- a/lib/ics23/src/inner_op.rs +++ b/lib/ics23/src/ops/inner_op.rs @@ -1,6 +1,7 @@ use unionlabs::cosmos::ics23::{hash_op::HashOp, inner_op::InnerOp, proof_spec::ProofSpec}; -use crate::hash_op; +use super::{hash_op, validate_iavl_ops}; +use crate::proof_specs::IAVL_PROOF_SPEC; #[derive(Debug, PartialEq, thiserror::Error)] pub enum SpecMismatchError { @@ -14,6 +15,10 @@ pub enum SpecMismatchError { InnerOpPrefixTooLong { prefix_len: usize, max_len: i32 }, #[error("malformed inner op suffix ({0:?})")] InnerOpSuffixMalformed(usize), + #[error("validate iavl ops ({0})")] + ValidateIavlOps(super::ValidateIavlOpsError), + #[error("bad prefix remaining {0} bytes after reading")] + BadPrefix(usize), } #[derive(Debug, PartialEq, thiserror::Error)] @@ -26,14 +31,24 @@ pub fn check_against_spec( inner_op: &InnerOp, spec: &ProofSpec, b: i32, - iavl_spec: &ProofSpec, ) -> Result<(), SpecMismatchError> { if inner_op.hash != spec.inner_spec.hash { return Err(SpecMismatchError::UnexpectedHashOp(inner_op.hash)); } - if spec.compatible(iavl_spec) { - // TODO(aeryz): validateIavlOps + if spec.compatible(&IAVL_PROOF_SPEC) { + match validate_iavl_ops(&inner_op.prefix, b) { + Ok(remaining) => { + if remaining != 1 && remaining != 34 { + return Err(SpecMismatchError::BadPrefix(remaining)); + } + + if inner_op.hash != HashOp::Sha256 { + return Err(SpecMismatchError::UnexpectedHashOp(inner_op.hash)); + } + } + Err(e) => return Err(SpecMismatchError::ValidateIavlOps(e)), + } } if inner_op.prefix.starts_with(&spec.leaf_spec.prefix) { @@ -51,7 +66,6 @@ pub fn check_against_spec( }); } - // TODO(aeryz): check if max_prefix_length > 0 let max_prefix_length = (spec.inner_spec.max_prefix_length as usize + (spec.inner_spec.child_order.len() - 1) * spec.inner_spec.child_size as usize) as usize; diff --git a/lib/ics23/src/leaf_op.rs b/lib/ics23/src/ops/leaf_op.rs similarity index 77% rename from lib/ics23/src/leaf_op.rs rename to lib/ics23/src/ops/leaf_op.rs index 775c0e99cb..3f7720b294 100644 --- a/lib/ics23/src/leaf_op.rs +++ b/lib/ics23/src/ops/leaf_op.rs @@ -2,7 +2,8 @@ use unionlabs::cosmos::ics23::{ hash_op::HashOp, leaf_op::LeafOp, length_op::LengthOp, proof_spec::ProofSpec, }; -use super::{hash_op, length_op}; +use super::{hash_op, length_op, validate_iavl_ops}; +use crate::{proof_specs::IAVL_PROOF_SPEC, ValidateIavlOpsError}; #[derive(Debug, PartialEq, thiserror::Error)] pub enum SpecMismatchError { @@ -14,8 +15,12 @@ pub enum SpecMismatchError { UnexpectedPrehashValue(HashOp), #[error("unexpected length op ({0:?})")] UnexpectedLengthOp(LengthOp), + #[error("bad prefix remaining {0} bytes after reading")] + BadPrefix(usize), #[error("prefix ({prefix:?}) is not the prefix of ({full:?})")] PrefixMismatch { full: Vec, prefix: Vec }, + #[error("validate iavl ops ({0})")] + ValidateIavlOps(ValidateIavlOpsError), } #[derive(Debug, PartialEq, thiserror::Error)] @@ -28,15 +33,18 @@ pub enum ApplyError { LeafData(super::length_op::ApplyError), } -pub fn check_against_spec( - leaf_op: &LeafOp, - spec: &ProofSpec, - iavl_spec: &ProofSpec, -) -> Result<(), SpecMismatchError> { +pub fn check_against_spec(leaf_op: &LeafOp, spec: &ProofSpec) -> Result<(), SpecMismatchError> { let lspec = &spec.leaf_spec; - if spec.compatible(iavl_spec) { - // TODO(aeryz): validate iavl opts + if spec.compatible(&IAVL_PROOF_SPEC) { + match validate_iavl_ops(&leaf_op.prefix, 0) { + Ok(remaining) => { + if remaining > 0 { + return Err(SpecMismatchError::BadPrefix(remaining)); + } + } + Err(e) => return Err(SpecMismatchError::ValidateIavlOps(e)), + } } if leaf_op.hash != lspec.hash { diff --git a/lib/ics23/src/length_op.rs b/lib/ics23/src/ops/length_op.rs similarity index 100% rename from lib/ics23/src/length_op.rs rename to lib/ics23/src/ops/length_op.rs diff --git a/lib/ics23/src/ops/mod.rs b/lib/ics23/src/ops/mod.rs new file mode 100644 index 0000000000..0abc16c1ce --- /dev/null +++ b/lib/ics23/src/ops/mod.rs @@ -0,0 +1,55 @@ +pub mod hash_op; +pub mod inner_op; +pub mod leaf_op; +pub mod length_op; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum ValidateIavlOpsError { + #[error("height ({height}) is smaller than the min. height ({min_height})")] + HeightTooShort { height: i64, min_height: i32 }, + #[error("size ({0}) is expected to be non-negative integer")] + NegativeSize(i64), + #[error("version ({0}) is expected to be non-negative integer")] + NegativeVersion(i64), + #[error("min. height cannot be negative")] + NegativeMinHeight(i32), + #[error("prost decode ({0:?})")] + Decode(prost::DecodeError), +} + +pub(crate) fn read_varint(mut buffer: &[u8]) -> Result { + let ux = prost::encoding::decode_varint(&mut buffer)?; + let mut x = (ux >> 1) as i64; + if ux & 1 != 0 { + x = !x; + } + Ok(x) +} + +pub(crate) fn validate_iavl_ops( + prefix: &[u8], + min_height: i32, +) -> Result { + let mut prefix = prefix.to_vec(); + + if min_height < 0 { + return Err(ValidateIavlOpsError::NegativeMinHeight(min_height)); + } + + let height = read_varint(&mut prefix).map_err(ValidateIavlOpsError::Decode)?; + if height < min_height as i64 { + return Err(ValidateIavlOpsError::HeightTooShort { height, min_height }); + } + + let size = read_varint(&mut prefix).map_err(ValidateIavlOpsError::Decode)?; + if size < 0 { + return Err(ValidateIavlOpsError::NegativeSize(size)); + } + + let version = read_varint(&mut prefix).map_err(ValidateIavlOpsError::Decode)?; + if version < 0 { + return Err(ValidateIavlOpsError::NegativeVersion(size)); + } + + Ok(prefix.len()) +} diff --git a/lib/unionlabs/src/cosmos/ics23/proof_spec.rs b/lib/unionlabs/src/cosmos/ics23/proof_spec.rs index 07bda583f4..aef23ab666 100644 --- a/lib/unionlabs/src/cosmos/ics23/proof_spec.rs +++ b/lib/unionlabs/src/cosmos/ics23/proof_spec.rs @@ -18,7 +18,7 @@ pub struct ProofSpec { } impl ProofSpec { - // TODO(aeryz): what the hell is this + // TODO(aeryz): move this to ics23 pub fn compatible(&self, spec: &ProofSpec) -> bool { self.leaf_spec.hash == spec.leaf_spec.hash && self.leaf_spec.prehash_key == spec.leaf_spec.prehash_key From a8e707c79aead4f4736c13e5b209429bb8c2a416 Mon Sep 17 00:00:00 2001 From: aeryz Date: Wed, 3 Jan 2024 13:37:00 +0300 Subject: [PATCH 08/14] feat(ics23): implement non-membership verification Signed-off-by: aeryz --- .../src/compressed_nonexistence_proof.rs | 8 +- lib/ics23/src/existence_proof.rs | 64 +-- lib/ics23/src/ibc_api.rs | 63 ++- lib/ics23/src/non_existence_proof.rs | 41 ++ lib/ics23/src/verify.rs | 367 ++++++++++++++++-- .../ics23/compressed_non_existence_proof.rs | 27 +- .../src/cosmos/ics23/non_existence_proof.rs | 28 +- 7 files changed, 474 insertions(+), 124 deletions(-) create mode 100644 lib/ics23/src/non_existence_proof.rs diff --git a/lib/ics23/src/compressed_nonexistence_proof.rs b/lib/ics23/src/compressed_nonexistence_proof.rs index b2180c4262..f7e8c36911 100644 --- a/lib/ics23/src/compressed_nonexistence_proof.rs +++ b/lib/ics23/src/compressed_nonexistence_proof.rs @@ -11,7 +11,11 @@ pub fn decompress( ) -> NonExistenceProof { NonExistenceProof { key: compressed_nonexistence_proof.key, - left: compressed_existence_proof::decompress(compressed_nonexistence_proof.left, lookup), - right: compressed_existence_proof::decompress(compressed_nonexistence_proof.right, lookup), + left: compressed_nonexistence_proof + .left + .map(|proof| compressed_existence_proof::decompress(proof, lookup)), + right: compressed_nonexistence_proof + .right + .map(|proof| compressed_existence_proof::decompress(proof, lookup)), } } diff --git a/lib/ics23/src/existence_proof.rs b/lib/ics23/src/existence_proof.rs index 6f1a0d764e..7a1ddf522d 100644 --- a/lib/ics23/src/existence_proof.rs +++ b/lib/ics23/src/existence_proof.rs @@ -24,31 +24,6 @@ pub enum CalculateRootError { InnerOpHashAndSpecMismatch, } -#[derive(Debug, PartialEq, thiserror::Error)] -pub enum VerifyError { - #[error("spec mismatch ({0})")] - SpecMismatch(SpecMismatchError), - #[error("key and existence proof value doesn't match ({key:?}, {existence_proof_key:?})")] - KeyAndExistenceProofKeyMismatch { - key: Vec, - existence_proof_key: Vec, - }, - #[error( - "value and existence proof value doesn't match ({value:?}, {existence_proof_value:?})" - )] - ValueAndExistenceProofValueMismatch { - value: Vec, - existence_proof_value: Vec, - }, - #[error("root calculation ({0})")] - RootCalculation(CalculateRootError), - #[error("calculated and given root doesn't match ({calculated_root:?}, {given_root:?})")] - CalculatedAndGivenRootMismatch { - calculated_root: Vec, - given_root: Vec, - }, -} - pub fn check_against_spec( existence_proof: &ExistenceProof, spec: &ProofSpec, @@ -84,7 +59,7 @@ pub fn calculate_root(existence_proof: &ExistenceProof) -> Result, Calcu calculate(existence_proof, None) } -fn calculate( +pub(crate) fn calculate( existence_proof: &ExistenceProof, spec: Option<&ProofSpec>, ) -> Result, CalculateRootError> { @@ -109,40 +84,3 @@ fn calculate( Ok(res) } - -/// Verify does all checks to ensure this proof proves this key, value -> root -/// and matches the spec. -pub fn verify( - existence_proof: &ExistenceProof, - spec: &ProofSpec, - root: &[u8], - key: &[u8], - value: &[u8], -) -> Result<(), VerifyError> { - check_against_spec(existence_proof, spec).map_err(VerifyError::SpecMismatch)?; - - if key != existence_proof.key { - return Err(VerifyError::KeyAndExistenceProofKeyMismatch { - key: key.into(), - existence_proof_key: existence_proof.key.clone(), - }); - } - - if value != existence_proof.value { - return Err(VerifyError::ValueAndExistenceProofValueMismatch { - value: value.into(), - existence_proof_value: existence_proof.value.clone(), - }); - } - - let calc = calculate(existence_proof, Some(spec)).map_err(VerifyError::RootCalculation)?; - - if root != calc { - return Err(VerifyError::CalculatedAndGivenRootMismatch { - calculated_root: calc, - given_root: root.into(), - }); - } - - Ok(()) -} diff --git a/lib/ics23/src/ibc_api.rs b/lib/ics23/src/ibc_api.rs index 1831a14c32..33cb178279 100644 --- a/lib/ics23/src/ibc_api.rs +++ b/lib/ics23/src/ibc_api.rs @@ -26,6 +26,8 @@ pub enum VerifyMembershipError { InvalidProofType, #[error("could not retrieve the key due to invalid indexing")] InvalidIndexing, + #[error("nonexistence proof has empty left and right proof")] + EmptyNonExistenceProof, } pub fn verify_membership( @@ -59,6 +61,57 @@ pub fn verify_membership( ) } +pub fn verify_non_membership( + proof: MerkleProof, + specs: &[&ProofSpec], + consensus_root: &MerkleRoot, + path: MerklePath, +) -> Result<(), VerifyMembershipError> { + if proof.proofs.len() != specs.len() { + return Err(VerifyMembershipError::InvalidProofsLength { + expected: specs.len(), + got: proof.proofs.len(), + }); + } + + if path.key_path.len() != specs.len() { + return Err(VerifyMembershipError::InvalidKeyPathLength { + expected: specs.len(), + got: path.key_path.len(), + }); + } + + let CommitmentProof::Nonexist(nonexist) = &proof.proofs[0] else { + return Err(VerifyMembershipError::InvalidProofType); + }; + + let subroot = if let Some(ep) = &nonexist.left { + existence_proof::calculate_root(ep) + } else if let Some(ep) = &nonexist.right { + existence_proof::calculate_root(ep) + } else { + return Err(VerifyMembershipError::EmptyNonExistenceProof); + } + .map_err(VerifyMembershipError::RootCalculation)?; + + let key = path + .key_path + .get(path.key_path.len() - 1) + .ok_or(VerifyMembershipError::InvalidIndexing)?; + + verify::verify_non_membership(specs[0], &subroot, &nonexist, key.as_bytes()) + .map_err(VerifyMembershipError::InnerVerification)?; + + verify_chained_membership_proof( + consensus_root.hash.as_ref(), + specs, + proof.proofs, + path, + subroot, + 1, + ) +} + fn verify_chained_membership_proof( root: &[u8], specs: &[&ProofSpec], @@ -84,8 +137,14 @@ fn verify_chained_membership_proof( .get(keys.key_path.len() - 1 - i) .ok_or(VerifyMembershipError::InvalidIndexing)?; - verify::verify_membership(specs[index], &subroot, proof, key.as_bytes(), &value) - .map_err(VerifyMembershipError::InnerVerification)?; + verify::verify_membership( + specs[index], + &subroot, + existence_proof, + key.as_bytes(), + &value, + ) + .map_err(VerifyMembershipError::InnerVerification)?; Ok(subroot) }) diff --git a/lib/ics23/src/non_existence_proof.rs b/lib/ics23/src/non_existence_proof.rs new file mode 100644 index 0000000000..e19ea73880 --- /dev/null +++ b/lib/ics23/src/non_existence_proof.rs @@ -0,0 +1,41 @@ +use unionlabs::cosmos::ics23::non_existence_proof::NonExistenceProof; + +use crate::existence_proof; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum VerifyError { + #[error("spec mismatch ({0})")] + SpecMismatch(SpecMismatchError), + #[error("key and existence proof value doesn't match ({key:?}, {existence_proof_key:?})")] + KeyAndExistenceProofKeyMismatch { + key: Vec, + existence_proof_key: Vec, + }, + #[error( + "value and existence proof value doesn't match ({value:?}, {existence_proof_value:?})" + )] + ValueAndExistenceProofValueMismatch { + value: Vec, + existence_proof_value: Vec, + }, + #[error("root calculation ({0})")] + RootCalculation(CalculateRootError), + #[error("calculated and given root doesn't match ({calculated_root:?}, {given_root:?})")] + CalculatedAndGivenRootMismatch { + calculated_root: Vec, + given_root: Vec, + }, +} + +pub fn verify( + non_existence_proof: &NonExistenceProof, + spec: &ProofSpec, + root: &[u8], + key: &[u8], +) -> Result<(), VerifyError> { + if let Some(left) = non_existence_proof.left { + existence_proof::verify(&left, spec, root, &left.key, &left.value)?; + } + + Ok(()) +} diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs index 9ef61791ab..1f5ca018a9 100644 --- a/lib/ics23/src/verify.rs +++ b/lib/ics23/src/verify.rs @@ -1,58 +1,361 @@ use unionlabs::cosmos::ics23::{ - batch_entry::BatchEntry, commitment_proof::CommitmentProof, existence_proof::ExistenceProof, - proof_spec::ProofSpec, + existence_proof::ExistenceProof, inner_op::InnerOp, inner_spec::InnerSpec, + non_existence_proof::NonExistenceProof, proof_spec::ProofSpec, }; -use crate::{commitment_proof, existence_proof}; +use crate::{ + existence_proof::{self, CalculateRootError, SpecMismatchError}, + hash_op::do_hash_or_noop, +}; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum VerifyError { + #[error("spec mismatch ({0})")] + SpecMismatch(SpecMismatchError), + #[error("key and existence proof value doesn't match ({key:?}, {existence_proof_key:?})")] + KeyAndExistenceProofKeyMismatch { + key: Vec, + existence_proof_key: Vec, + }, + #[error( + "value and existence proof value doesn't match ({value:?}, {existence_proof_value:?})" + )] + ValueAndExistenceProofValueMismatch { + value: Vec, + existence_proof_value: Vec, + }, + #[error("root calculation ({0})")] + RootCalculation(CalculateRootError), + #[error("calculated and given root doesn't match ({calculated_root:?}, {given_root:?})")] + CalculatedAndGivenRootMismatch { + calculated_root: Vec, + given_root: Vec, + }, + #[error("key is not left of right proof")] + KeyIsNotLeftOfRightProof, + #[error("key is not right of left proof")] + KeyIsNotRightOfLeftProof, + #[error("left proof missing, right proof must be left-most")] + LeftProofMissing, + #[error("right proof missing, left proof must be right-most")] + RightProofMissing, + #[error("both left and right proofs are missing")] + BothProofsMissing, +} #[derive(Debug, PartialEq, thiserror::Error)] pub enum VerifyMembershipError { #[error("existence proof verification failed, ({0})")] - ExistenceProofVerify(existence_proof::VerifyError), + ExistenceProofVerify(VerifyError), #[error("proof does not exist")] ProofDoesNotExist, } +pub fn verify_non_membership( + spec: &ProofSpec, + root: &[u8], + proof: &NonExistenceProof, + key: &[u8], +) -> Result<(), VerifyMembershipError> { + verify_non_existence(&proof, spec, root, key) + .map_err(VerifyMembershipError::ExistenceProofVerify) +} + pub fn verify_membership( spec: &ProofSpec, root: &[u8], - proof: CommitmentProof, + proof: &ExistenceProof, key: &[u8], value: &[u8], ) -> Result<(), VerifyMembershipError> { - let proof = commitment_proof::decompress(proof); + // TODO(aeryz): push proof.key and proof.value checks to here + verify_existence_proof(proof, spec, root, key, value) + .map_err(VerifyMembershipError::ExistenceProofVerify) +} + +fn verify_non_existence( + non_existence_proof: &NonExistenceProof, + spec: &ProofSpec, + root: &[u8], + key: &[u8], +) -> Result<(), VerifyError> { + let key_for_comparison = |spec: &ProofSpec, key: &[u8]| -> Vec { + // TODO(aeryz): we don't have prehash_key_before_comparison, why? + do_hash_or_noop(spec.leaf_spec.prehash_key, key) + }; + + let left_ops = |left: &ExistenceProof| -> Result<(), VerifyError> { + verify_existence_proof(&left, spec, root, &left.key, &left.value)?; + + if key_for_comparison(spec, key) == key_for_comparison(spec, &left.key) { + return Err(VerifyError::KeyIsNotRightOfLeftProof); + } + + if !is_right_most(&spec.inner_spec, &left.path) { + return Err(VerifyError::RightProofMissing); + } + + Ok(()) + }; + + let right_ops = |right: &ExistenceProof| -> Result<(), VerifyError> { + verify_existence_proof(&right, spec, root, &right.key, &right.value)?; + + if key_for_comparison(spec, key) == key_for_comparison(spec, &right.key) { + return Err(VerifyError::KeyIsNotLeftOfRightProof); + } + + if !is_left_most(&spec.inner_spec, &right.path) { + return Err(VerifyError::LeftProofMissing); + } + + Ok(()) + }; - if let Some(ep) = get_exist_proof_for_key(proof, key) { - existence_proof::verify(&ep, spec, root, key, value) - .map_err(VerifyMembershipError::ExistenceProofVerify) - } else { - Err(VerifyMembershipError::ProofDoesNotExist) + match (&non_existence_proof.left, &non_existence_proof.right) { + (None, Some(right)) => right_ops(right)?, + (Some(left), None) => left_ops(left)?, + (Some(left), Some(right)) => { + if !is_left_neighbor(&spec.inner_spec, &left.path, &right.path) { + return Err(VerifyError::RightProofMissing); + } + } + (None, None) => return Err(VerifyError::BothProofsMissing), } + + Ok(()) } -fn get_exist_proof_for_key(proof: CommitmentProof, key: &[u8]) -> Option { - match proof { - CommitmentProof::Exist(exist) => { - if exist.key.as_slice() == key { - return Some(exist); - } +fn is_left_neighbor(spec: &InnerSpec, left: &[InnerOp], right: &[InnerOp]) -> bool { + let Some((mut topleft, mut left)) = left.split_last() else { + return false; + }; - None - } - CommitmentProof::Batch(batch) => { - for sub in batch.entries { - match sub { - BatchEntry::Exist(exist) => { - if exist.key.as_slice() == key { - return Some(exist); - } - } - _ => {} - } - } + let Some((mut topright, mut right)) = right.split_last() else { + return false; + }; + + while topleft.prefix == topright.prefix && topleft.suffix == topright.suffix { + (topleft, left) = if let Some(l) = left.split_last() { + l + } else { + return false; + }; + + (topright, right) = if let Some(r) = right.split_last() { + r + } else { + return false; + }; + } + + if !is_left_step(spec, topleft, topright) + || !is_right_most(spec, left) + || !is_left_most(spec, right) + { + return false; + } + + true +} + +fn is_left_step(spec: &InnerSpec, left: &InnerOp, right: &InnerOp) -> bool { + let Ok(leftidx) = order_from_padding(spec, left) else { + // TODO(aeryz) + panic!("err") + }; - None + let Ok(rightidx) = order_from_padding(spec, right) else { + // TODO(aeryz) + panic!("err") + }; + + return rightidx == leftidx + 1; +} + +fn is_right_most(spec: &InnerSpec, path: &[InnerOp]) -> bool { + let (min_prefix, max_prefix, suffix) = get_padding(spec, (spec.child_order.len() - 1) as i32); + + for step in path { + if !has_padding(step, min_prefix, max_prefix, suffix) + && !right_branches_are_empty(spec, step) + { + return false; } - _ => None, } + + true } + +fn is_left_most(spec: &InnerSpec, path: &[InnerOp]) -> bool { + let (min_prefix, max_prefix, suffix) = get_padding(spec, 0); + + for step in path { + if !has_padding(step, min_prefix, max_prefix, suffix) + && !left_branches_are_empty(spec, step) + { + return false; + } + } + + true +} + +fn right_branches_are_empty(spec: &InnerSpec, op: &InnerOp) -> bool { + let Ok(idx) = order_from_padding(spec, op) else { + return false; + }; + + let right_branches = (spec.child_order.len() as i32) - 1 - idx; + if right_branches == 0 { + return false; + } + + if (op.suffix.len() as i32) != right_branches * spec.child_size { + return false; + } + + for i in 0..right_branches { + let idx = get_position(&spec.child_order, i); + let from = (idx * spec.child_size) as usize; + if spec.empty_child != &op.suffix[from..from + (spec.child_size as usize)] { + return false; + } + } + + true +} + +fn left_branches_are_empty(spec: &InnerSpec, op: &InnerOp) -> bool { + let Ok(left_branches) = order_from_padding(spec, op) else { + return false; + }; + + if left_branches == 0 { + return false; + } + + let actual_prefix = (op.prefix.len() as i32) - left_branches * spec.child_size; + if actual_prefix < 0 { + return false; + } + + for i in 0..left_branches { + let idx = get_position(&spec.child_order, i); + let from = (actual_prefix + idx * spec.child_size) as usize; + if spec.empty_child != &op.prefix[from..from + (spec.child_size as usize)] { + return false; + } + } + + true +} + +fn order_from_padding(spec: &InnerSpec, inner: &InnerOp) -> Result { + let branch = (0..spec.child_order.len()) + .find(|&branch| { + let (minp, maxp, suffix) = get_padding(spec, branch as i32); + has_padding(inner, minp, maxp, suffix) + }) + .map(|branch| branch as i32); + + branch.ok_or(()) +} + +fn has_padding(op: &InnerOp, min_prefix: i32, max_prefix: i32, suffix: i32) -> bool { + if (op.prefix.len() as i32) < min_prefix || (op.prefix.len() as i32) > max_prefix { + return false; + } + + (op.suffix.len() as i32) == suffix +} + +fn get_padding(spec: &InnerSpec, branch: i32) -> (i32, i32, i32) { + let idx = get_position(&spec.child_order, branch); + + let prefix = idx * spec.child_size; + let min_prefix = prefix + spec.min_prefix_length; + let max_prefix = prefix + spec.max_prefix_length; + + let suffix = (spec.child_order.len() as i32 - 1 - idx) * spec.child_size; + + (min_prefix, max_prefix, suffix) +} + +fn get_position(order: &[i32], branch: i32) -> i32 { + if branch < 0 || branch as usize >= order.len() { + // TODO(aeryz): + panic!("invalid branch") + } + + match order.iter().enumerate().find(|(_, &elem)| elem == branch) { + Some((i, _)) => i as i32, + None => panic!("branch not found"), // TODO(aeryz) + } +} + +/// Verify does all checks to ensure this proof proves this key, value -> root +/// and matches the spec. +fn verify_existence_proof( + existence_proof: &ExistenceProof, + spec: &ProofSpec, + root: &[u8], + key: &[u8], + value: &[u8], +) -> Result<(), VerifyError> { + existence_proof::check_against_spec(existence_proof, spec) + .map_err(VerifyError::SpecMismatch)?; + + if key != existence_proof.key { + return Err(VerifyError::KeyAndExistenceProofKeyMismatch { + key: key.into(), + existence_proof_key: existence_proof.key.clone(), + }); + } + + if value != existence_proof.value { + return Err(VerifyError::ValueAndExistenceProofValueMismatch { + value: value.into(), + existence_proof_value: existence_proof.value.clone(), + }); + } + + let calc = existence_proof::calculate(existence_proof, Some(spec)) + .map_err(VerifyError::RootCalculation)?; + + if root != calc { + return Err(VerifyError::CalculatedAndGivenRootMismatch { + calculated_root: calc, + given_root: root.into(), + }); + } + + Ok(()) +} + +// fn get_exist_proof_for_key(proof: CommitmentProof, key: &[u8]) -> Option { +// match proof { +// CommitmentProof::Exist(exist) => { +// if exist.key.as_slice() == key { +// return Some(exist); +// } + +// None +// } +// CommitmentProof::Batch(batch) => { +// for sub in batch.entries { +// match sub { +// BatchEntry::Exist(exist) => { +// if exist.key.as_slice() == key { +// return Some(exist); +// } +// } +// _ => {} +// } +// } + +// None +// } +// _ => None, +// } +// } diff --git a/lib/unionlabs/src/cosmos/ics23/compressed_non_existence_proof.rs b/lib/unionlabs/src/cosmos/ics23/compressed_non_existence_proof.rs index 0d3830f189..1a4e817a9f 100644 --- a/lib/unionlabs/src/cosmos/ics23/compressed_non_existence_proof.rs +++ b/lib/unionlabs/src/cosmos/ics23/compressed_non_existence_proof.rs @@ -1,8 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::{ - cosmos::ics23::compressed_existence_proof::CompressedExistenceProof, - errors::{required, MissingField}, + cosmos::ics23::compressed_existence_proof::CompressedExistenceProof, errors::MissingField, TryFromProtoErrorOf, }; @@ -11,8 +10,8 @@ use crate::{ pub struct CompressedNonExistenceProof { #[serde(with = "::serde_utils::hex_string")] pub key: Vec, - pub left: CompressedExistenceProof, - pub right: CompressedExistenceProof, + pub left: Option, + pub right: Option, } impl crate::Proto for CompressedNonExistenceProof { @@ -36,11 +35,15 @@ impl TryFrom ) -> Result { Ok(Self { key: value.key, - left: required!(value.left)? - .try_into() + left: value + .left + .map(|proof| proof.try_into()) + .transpose() .map_err(TryFromCompressedNonExistenceProofError::Left)?, - right: required!(value.right)? - .try_into() + right: value + .right + .map(|proof| proof.try_into()) + .transpose() .map_err(TryFromCompressedNonExistenceProofError::Right)?, }) } @@ -53,8 +56,8 @@ impl From fn from(value: CompressedNonExistenceProof) -> Self { Self { key: value.key.into(), - left: value.left.into(), - right: value.right.into(), + left: value.left.map(Into::into).unwrap_or_default(), + right: value.right.map(Into::into).unwrap_or_default(), } } } @@ -63,8 +66,8 @@ impl From for protos::cosmos::ics23::v1::Compressed fn from(value: CompressedNonExistenceProof) -> Self { Self { key: value.key, - left: Some(value.left.into()), - right: Some(value.right.into()), + left: value.left.map(Into::into), + right: value.right.map(Into::into), } } } diff --git a/lib/unionlabs/src/cosmos/ics23/non_existence_proof.rs b/lib/unionlabs/src/cosmos/ics23/non_existence_proof.rs index 5d56f1a057..ca48d06191 100644 --- a/lib/unionlabs/src/cosmos/ics23/non_existence_proof.rs +++ b/lib/unionlabs/src/cosmos/ics23/non_existence_proof.rs @@ -1,9 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::{ - cosmos::ics23::existence_proof::ExistenceProof, - errors::{required, MissingField}, - TryFromProtoErrorOf, + cosmos::ics23::existence_proof::ExistenceProof, errors::MissingField, TryFromProtoErrorOf, }; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -11,8 +9,8 @@ use crate::{ pub struct NonExistenceProof { #[serde(with = "::serde_utils::hex_string")] pub key: Vec, - pub left: ExistenceProof, - pub right: ExistenceProof, + pub left: Option, + pub right: Option, } impl crate::Proto for NonExistenceProof { @@ -32,11 +30,15 @@ impl TryFrom for NonExistenceProof fn try_from(value: protos::cosmos::ics23::v1::NonExistenceProof) -> Result { Ok(Self { key: value.key, - left: required!(value.left)? - .try_into() + left: value + .left + .map(|proof| proof.try_into()) + .transpose() .map_err(TryFromNonExistenceProofError::Left)?, - right: required!(value.right)? - .try_into() + right: value + .right + .map(|proof| proof.try_into()) + .transpose() .map_err(TryFromNonExistenceProofError::Right)?, }) } @@ -47,8 +49,8 @@ impl From for contracts::glue::CosmosIcs23V1NonExistenceProof fn from(value: NonExistenceProof) -> Self { Self { key: value.key.into(), - left: value.left.into(), - right: value.right.into(), + left: value.left.map(Into::into).unwrap_or_default(), + right: value.right.map(Into::into).unwrap_or_default(), } } } @@ -57,8 +59,8 @@ impl From for protos::cosmos::ics23::v1::NonExistenceProof { fn from(value: NonExistenceProof) -> Self { Self { key: value.key, - left: Some(value.left.into()), - right: Some(value.right.into()), + left: value.left.map(Into::into), + right: value.right.map(Into::into), } } } From bf67aa0fd9accd80f1119b3316823ef0c84aaff7 Mon Sep 17 00:00:00 2001 From: aeryz Date: Wed, 3 Jan 2024 13:45:49 +0300 Subject: [PATCH 09/14] chore(ics23): remove unnecessary `non_existence_proof` Signed-off-by: aeryz --- lib/ics23/src/non_existence_proof.rs | 41 ---------------------------- lib/ics23/src/verify.rs | 27 ------------------ 2 files changed, 68 deletions(-) delete mode 100644 lib/ics23/src/non_existence_proof.rs diff --git a/lib/ics23/src/non_existence_proof.rs b/lib/ics23/src/non_existence_proof.rs deleted file mode 100644 index e19ea73880..0000000000 --- a/lib/ics23/src/non_existence_proof.rs +++ /dev/null @@ -1,41 +0,0 @@ -use unionlabs::cosmos::ics23::non_existence_proof::NonExistenceProof; - -use crate::existence_proof; - -#[derive(Debug, PartialEq, thiserror::Error)] -pub enum VerifyError { - #[error("spec mismatch ({0})")] - SpecMismatch(SpecMismatchError), - #[error("key and existence proof value doesn't match ({key:?}, {existence_proof_key:?})")] - KeyAndExistenceProofKeyMismatch { - key: Vec, - existence_proof_key: Vec, - }, - #[error( - "value and existence proof value doesn't match ({value:?}, {existence_proof_value:?})" - )] - ValueAndExistenceProofValueMismatch { - value: Vec, - existence_proof_value: Vec, - }, - #[error("root calculation ({0})")] - RootCalculation(CalculateRootError), - #[error("calculated and given root doesn't match ({calculated_root:?}, {given_root:?})")] - CalculatedAndGivenRootMismatch { - calculated_root: Vec, - given_root: Vec, - }, -} - -pub fn verify( - non_existence_proof: &NonExistenceProof, - spec: &ProofSpec, - root: &[u8], - key: &[u8], -) -> Result<(), VerifyError> { - if let Some(left) = non_existence_proof.left { - existence_proof::verify(&left, spec, root, &left.key, &left.value)?; - } - - Ok(()) -} diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs index 1f5ca018a9..53a908c129 100644 --- a/lib/ics23/src/verify.rs +++ b/lib/ics23/src/verify.rs @@ -332,30 +332,3 @@ fn verify_existence_proof( Ok(()) } - -// fn get_exist_proof_for_key(proof: CommitmentProof, key: &[u8]) -> Option { -// match proof { -// CommitmentProof::Exist(exist) => { -// if exist.key.as_slice() == key { -// return Some(exist); -// } - -// None -// } -// CommitmentProof::Batch(batch) => { -// for sub in batch.entries { -// match sub { -// BatchEntry::Exist(exist) => { -// if exist.key.as_slice() == key { -// return Some(exist); -// } -// } -// _ => {} -// } -// } - -// None -// } -// _ => None, -// } -// } From 9a7cf23f6bbcc7ae1ad1d2af0d13a9b87c745d1a Mon Sep 17 00:00:00 2001 From: aeryz Date: Wed, 3 Jan 2024 16:56:31 +0300 Subject: [PATCH 10/14] chore(ics23): remove unused decompression logic Signed-off-by: aeryz --- lib/ics23/src/commitment_proof.rs | 12 ---------- lib/ics23/src/compressed_batch_entry.rs | 19 --------------- lib/ics23/src/compressed_batch_proof.rs | 17 -------------- lib/ics23/src/compressed_existence_proof.rs | 23 ------------------- .../src/compressed_nonexistence_proof.rs | 21 ----------------- lib/ics23/src/ibc_api.rs | 10 +++++--- lib/ics23/src/lib.rs | 5 ---- lib/ics23/src/verify.rs | 1 - .../cometbls-light-client/src/client.rs | 22 ++++++++++++------ 9 files changed, 22 insertions(+), 108 deletions(-) delete mode 100644 lib/ics23/src/commitment_proof.rs delete mode 100644 lib/ics23/src/compressed_batch_entry.rs delete mode 100644 lib/ics23/src/compressed_batch_proof.rs delete mode 100644 lib/ics23/src/compressed_existence_proof.rs delete mode 100644 lib/ics23/src/compressed_nonexistence_proof.rs diff --git a/lib/ics23/src/commitment_proof.rs b/lib/ics23/src/commitment_proof.rs deleted file mode 100644 index a8dd318dc3..0000000000 --- a/lib/ics23/src/commitment_proof.rs +++ /dev/null @@ -1,12 +0,0 @@ -use unionlabs::cosmos::ics23::commitment_proof::CommitmentProof; - -use super::compressed_batch_proof; - -pub fn decompress(commitment_proof: CommitmentProof) -> CommitmentProof { - match commitment_proof { - CommitmentProof::CompressedBatch(comp) => { - CommitmentProof::Batch(compressed_batch_proof::decompress(comp)) - } - proof => proof, - } -} diff --git a/lib/ics23/src/compressed_batch_entry.rs b/lib/ics23/src/compressed_batch_entry.rs deleted file mode 100644 index 595121f7c7..0000000000 --- a/lib/ics23/src/compressed_batch_entry.rs +++ /dev/null @@ -1,19 +0,0 @@ -use unionlabs::cosmos::ics23::{ - batch_entry::BatchEntry, compressed_batch_entry::CompressedBatchEntry, inner_op::InnerOp, -}; - -use crate::{compressed_existence_proof, compressed_nonexistence_proof}; - -pub fn decompress( - compressed_batch_entry: CompressedBatchEntry, - lookup: &Vec, -) -> BatchEntry { - match compressed_batch_entry { - CompressedBatchEntry::Exist(exist) => { - BatchEntry::Exist(compressed_existence_proof::decompress(exist, lookup)) - } - CompressedBatchEntry::Nonexist(nonexist) => { - BatchEntry::Nonexist(compressed_nonexistence_proof::decompress(nonexist, lookup)) - } - } -} diff --git a/lib/ics23/src/compressed_batch_proof.rs b/lib/ics23/src/compressed_batch_proof.rs deleted file mode 100644 index 2b84b7a72f..0000000000 --- a/lib/ics23/src/compressed_batch_proof.rs +++ /dev/null @@ -1,17 +0,0 @@ -use unionlabs::cosmos::ics23::{ - batch_proof::BatchProof, compressed_batch_proof::CompressedBatchProof, -}; - -use super::compressed_batch_entry; - -pub fn decompress(compressed_batch_proof: CompressedBatchProof) -> BatchProof { - let lookup = compressed_batch_proof.lookup_inners; - - BatchProof { - entries: compressed_batch_proof - .entries - .into_iter() - .map(|entry| compressed_batch_entry::decompress(entry, &lookup)) - .collect(), - } -} diff --git a/lib/ics23/src/compressed_existence_proof.rs b/lib/ics23/src/compressed_existence_proof.rs deleted file mode 100644 index fe387d50ce..0000000000 --- a/lib/ics23/src/compressed_existence_proof.rs +++ /dev/null @@ -1,23 +0,0 @@ -use unionlabs::cosmos::ics23::{ - compressed_existence_proof::CompressedExistenceProof, existence_proof::ExistenceProof, - inner_op::InnerOp, -}; - -// TODO(aeryz): Should this function be fallible? We do unsafe indexing here -// by assuming that the indices in the path will always be within the bounds. -// Is this a correct assumption? -pub fn decompress( - compressed_existence_proof: CompressedExistenceProof, - lookup: &Vec, -) -> ExistenceProof { - ExistenceProof { - key: compressed_existence_proof.key, - value: compressed_existence_proof.value, - leaf: compressed_existence_proof.leaf, - path: compressed_existence_proof - .path - .iter() - .map(|i| lookup[i.inner() as usize].clone()) - .collect(), - } -} diff --git a/lib/ics23/src/compressed_nonexistence_proof.rs b/lib/ics23/src/compressed_nonexistence_proof.rs deleted file mode 100644 index f7e8c36911..0000000000 --- a/lib/ics23/src/compressed_nonexistence_proof.rs +++ /dev/null @@ -1,21 +0,0 @@ -use unionlabs::cosmos::ics23::{ - compressed_non_existence_proof::CompressedNonExistenceProof, inner_op::InnerOp, - non_existence_proof::NonExistenceProof, -}; - -use super::compressed_existence_proof; - -pub fn decompress( - compressed_nonexistence_proof: CompressedNonExistenceProof, - lookup: &Vec, -) -> NonExistenceProof { - NonExistenceProof { - key: compressed_nonexistence_proof.key, - left: compressed_nonexistence_proof - .left - .map(|proof| compressed_existence_proof::decompress(proof, lookup)), - right: compressed_nonexistence_proof - .right - .map(|proof| compressed_existence_proof::decompress(proof, lookup)), - } -} diff --git a/lib/ics23/src/ibc_api.rs b/lib/ics23/src/ibc_api.rs index 33cb178279..c2956dd48d 100644 --- a/lib/ics23/src/ibc_api.rs +++ b/lib/ics23/src/ibc_api.rs @@ -16,8 +16,9 @@ pub enum VerifyMembershipError { RootCalculation(existence_proof::CalculateRootError), #[error("{0}")] InnerVerification(verify::VerifyMembershipError), - #[error("invalid root top level")] // TODO(aeryz): beautify - InvalidRoot, + // TODO(aeryz): print hex + #[error("calculated root ({calculated:?}) does not match the given ({given:?}) value")] + InvalidRoot { given: Vec, calculated: Vec }, #[error("expected the size of proofs to be ({expected}), got ({got})")] InvalidProofsLength { expected: usize, got: usize }, #[error("expected the size of key path to be ({expected}), got ({got})")] @@ -152,7 +153,10 @@ fn verify_chained_membership_proof( if value.as_slice() == root { Ok(()) } else { - Err(VerifyMembershipError::InvalidRoot) + Err(VerifyMembershipError::InvalidRoot { + given: value, + calculated: root.into(), + }) } }) } diff --git a/lib/ics23/src/lib.rs b/lib/ics23/src/lib.rs index fe11635a21..5b975cc424 100644 --- a/lib/ics23/src/lib.rs +++ b/lib/ics23/src/lib.rs @@ -1,10 +1,5 @@ #![feature(error_in_core)] -pub mod commitment_proof; -pub mod compressed_batch_entry; -pub mod compressed_batch_proof; -pub mod compressed_existence_proof; -pub mod compressed_nonexistence_proof; pub mod existence_proof; pub mod ibc_api; mod ops; diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs index 53a908c129..933e4c159e 100644 --- a/lib/ics23/src/verify.rs +++ b/lib/ics23/src/verify.rs @@ -68,7 +68,6 @@ pub fn verify_membership( key: &[u8], value: &[u8], ) -> Result<(), VerifyMembershipError> { - // TODO(aeryz): push proof.key and proof.value checks to here verify_existence_proof(proof, spec, root, key, value) .map_err(VerifyMembershipError::ExistenceProofVerify) } diff --git a/light-clients/cometbls-light-client/src/client.rs b/light-clients/cometbls-light-client/src/client.rs index ef5462a8c9..3a22e7b09f 100644 --- a/light-clients/cometbls-light-client/src/client.rs +++ b/light-clients/cometbls-light-client/src/client.rs @@ -5,11 +5,10 @@ use ics008_wasm_client::{ }, IbcClient, Status, StorageState, }; -use ics23::{iavl_spec, tendermint_proof_spec}; +use ics23::ibc_api::SDK_SPECS; use prost::Message; use protos::ibc::core::client::v1::GenesisMetadata; use unionlabs::{ - cosmos::ics23::commitment_proof::CommitmentProof, ibc::{ core::{ client::height::Height, @@ -66,12 +65,21 @@ impl IbcClient for CometblsLightClient { })?; match value { - StorageState::Occupied(value) => { - verify_membership(merkle_proof, &consensus_state.data.root, path, &value) - .map_err(Error::VerifyMembership) - } - StorageState::Empty => Ok(()), + StorageState::Occupied(value) => ics23::ibc_api::verify_membership( + merkle_proof, + &SDK_SPECS, + &consensus_state.data.root, + path, + value, + ), + StorageState::Empty => ics23::ibc_api::verify_non_membership( + merkle_proof, + &SDK_SPECS, + &consensus_state.data.root, + path, + ), } + .map_err(Error::VerifyMembership) } fn verify_header( From 8d9b3a1a06ed23921d710d9589e0cf4f90641e72 Mon Sep 17 00:00:00 2001 From: aeryz Date: Wed, 3 Jan 2024 18:43:59 +0300 Subject: [PATCH 11/14] chore(ics23): implement api tests Signed-off-by: aeryz --- Cargo.lock | 1 + lib/ics23/Cargo.toml | 3 + lib/ics23/src/ibc_api.rs | 200 +++++++++++++++++++++++++++++++++++++-- lib/ics23/src/ops/mod.rs | 12 +-- lib/ics23/src/verify.rs | 104 ++++++++++++++++++++ 5 files changed, 304 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c679107019..a6d5800a66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2925,6 +2925,7 @@ dependencies = [ name = "ics23" version = "0.1.0" dependencies = [ + "hex-literal", "prost 0.11.9", "ripemd", "sha2 0.10.8", diff --git a/lib/ics23/Cargo.toml b/lib/ics23/Cargo.toml index f8b5ef8f32..ab583e0585 100644 --- a/lib/ics23/Cargo.toml +++ b/lib/ics23/Cargo.toml @@ -11,3 +11,6 @@ thiserror = { version = "1.0", package = "thiserror-core", default-features = fa prost = { version = "0.11.9", default-features = false, features = ["std"] } sha2 = { version = "0.10.7", default-features = false } ripemd = { version = "0.1.3", default-features = false } + +[dev-dependencies] +hex-literal = "0.4.1" diff --git a/lib/ics23/src/ibc_api.rs b/lib/ics23/src/ibc_api.rs index c2956dd48d..1489f9eb29 100644 --- a/lib/ics23/src/ibc_api.rs +++ b/lib/ics23/src/ibc_api.rs @@ -3,10 +3,14 @@ use unionlabs::{ ibc::core::commitment::{ merkle_path::MerklePath, merkle_proof::MerkleProof, merkle_root::MerkleRoot, }, + TryFromProto, }; pub use crate::proof_specs::{IAVL_PROOF_SPEC, TENDERMINT_PROOF_SPEC}; -use crate::{existence_proof, verify}; +use crate::{ + existence_proof, + verify::{self}, +}; pub const SDK_SPECS: [&'static ProofSpec; 2] = [&IAVL_PROOF_SPEC, &TENDERMINT_PROOF_SPEC]; @@ -123,8 +127,8 @@ fn verify_chained_membership_proof( ) -> Result<(), VerifyMembershipError> { proofs .into_iter() - .skip(index) .enumerate() + .skip(index) .try_fold(value, |value, (i, proof)| { let CommitmentProof::Exist(ref existence_proof) = proof else { return Err(VerifyMembershipError::InvalidProofType); @@ -138,14 +142,8 @@ fn verify_chained_membership_proof( .get(keys.key_path.len() - 1 - i) .ok_or(VerifyMembershipError::InvalidIndexing)?; - verify::verify_membership( - specs[index], - &subroot, - existence_proof, - key.as_bytes(), - &value, - ) - .map_err(VerifyMembershipError::InnerVerification)?; + verify::verify_membership(specs[i], &subroot, existence_proof, key.as_bytes(), &value) + .map_err(VerifyMembershipError::InnerVerification)?; Ok(subroot) }) @@ -160,3 +158,185 @@ fn verify_chained_membership_proof( } }) } + +#[cfg(test)] +mod tests { + use hex_literal::hex; + use unionlabs::{ + hash::H256, + ibc::core::commitment::{ + merkle_path::MerklePath, merkle_proof::MerkleProof, merkle_root::MerkleRoot, + }, + TryFromProto, + }; + + use super::{ + verify_chained_membership_proof, verify_membership, VerifyMembershipError, SDK_SPECS, + }; + use crate::{ibc_api::verify_non_membership, verify}; + + fn chained_membership( + proof: &[u8], + root: &[u8], + value: &[u8], + path: &[&str], + ) -> Result<(), VerifyMembershipError> { + let path = MerklePath { + key_path: path.into_iter().map(|item| item.to_string()).collect(), + }; + let proofs = MerkleProof::try_from_proto_bytes(&proof).unwrap(); + verify_membership( + proofs, + &SDK_SPECS, + &MerkleRoot { + hash: H256::try_from(root).unwrap(), + }, + path, + value.into(), + ) + } + + // union-testnet-2 + // key = 0x01 + take(20, sha256(account)) + // proof, value = nix run .#uniond -- genstateproof 345 "014152090B0C95C948EDC407995560FEED4A9DF81E" "/store/acc/key" --node https://rpc.0xc0dejug.uno:443 + // path = split('/', '/acc/${key}') + // root = nix run .#uniond -- query block 346 --node https://rpc.0xc0dejug.uno:443 | jq .block.header.app_hash + #[test] + fn testnet_exist() { + let root = hex!("B802C903BEFE08624832AAF853DBEA4EDE1F7D50E88BEDD6317831F45CC74A3D"); + let proof = hex!("0afa020af7020a15014152090b0c95c948edc407995560feed4a9df81e129e010a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74127a0a2c756e696f6e31673966716a7a63766a687935336d77797137763432633837613439666d37713772646568386312460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103820c4b94dccd7d74706216c426fe884d9a4404410df69d6421899595c5a9c122180120011a0b0801180120012a0300020222290801122502040220170c890f01b9fa9ab803511bbc7be7c25359309f04d021a72e0a9b93b8ff72c020222b08011204040802201a2120a89a7b1aedf861a8c6316009af3d19448bfe8834dfb5546c7e1af7f95c3000b4222b08011204061002201a212029347d33c119e85fc1335f43ad17c4a1986ad44c71837158ceffd36e2f38f986222b080112040a3002201a2120e284b7ed0385d018b1ffcd6f33bf6ac575fb7731704d0ae71be278bd8bf5e0b50a80020afd010a03616363122082d7d632a58654a81bb6764379eff4b6e641e96620a12dac0e250e6caf94f7761a090801180120012a010022250801122101ba30cf8122e71a87fea08d0da9499e0373495a64e1648de8f08ca1a73e1fc1a8222708011201011a208a19e0585632ebada293099d24f28707d453266ae7ded6e854dfd8a025c7ce71222708011201011a204a22410f42f7706402b38c460e74d712c95cea8e6e370c691f43c0abf3f4e104222708011201011a20b999d9a62cbd36a843f207580c4802d194e6441f7f3715ddce55d5194d46e57a222708011201011a2022ecbf124eff995ecf01998dd8346b71810af164e192feeb4d4287085128b9df"); + let value = hex!("0a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74127a0a2c756e696f6e31673966716a7a63766a687935336d77797137763432633837613439666d37713772646568386312460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103820c4b94dccd7d74706216c426fe884d9a4404410df69d6421899595c5a9c12218012001"); + chained_membership( + &proof, + &root, + &value, + &["acc", unsafe { + &String::from_utf8_unchecked( + hex!("014152090B0C95C948EDC407995560FEED4A9DF81E").to_vec(), + ) + }], + ) + .unwrap(); + // let path: Vec> = vec![ + // b"acc".into(), + // hex!("014152090B0C95C948EDC407995560FEED4A9DF81E").into(), + // ]; + + // MerklePath::try_from_proto_bytes() + } + + #[test] + fn connection_exists() { + let root = hex!("899CD0B55A4FEDE9AF3C959C43ED3AE6805293642590A81CD95B4C97F89CC424"); + let proof = hex!("0abc020ab9020a18636f6e6e656374696f6e732f636f6e6e656374696f6e2d31125b0a0930382d7761736d2d3112230a0131120d4f524445525f4f524445524544120f4f524445525f554e4f524445524544180222250a0e636f6d6574626c732d6e65772d30120c636f6e6e656374696f6e2d301a050a0369626328061a0c0801180120012a040002f006222c080112050204f006201a212075c4910f51207d3c65960120fe931f138e2624668d75869f51b8442593dd5eab222a080112260408de0a2002b6fcf07091245d162f1196b003c555c564980e02c4d4a9fa0a249798f4b25e20222c08011205060ede0a201a2120ff6b0a04e076eecbabfee4e751c0523cbedba898211b5847404e2d954a2203e3222a08011226081ede0a20635053419cfb6a81c839860d99f3ed002840124a790ddd9f066d8bce63f9df54200afc010af9010a03696263122024b15e198bcf648dee62c7ca1fd8c3950c85c3d898833180c3e3c412ccbc559d1a090801180120012a01002225080112210106b99c0d8119ff1edbcbe165d0f19337dbbc080e677c88e57aa2ae767ebf0f0f222708011201011a20aa650406ea0d76e39dd43d2ea6a91e3fdaa1c908fc21a7ca68e5e62cc8115639222508011221016ac3182364d7cdaa1f52a77b6081e070aa29b2d253f3642169693cde336e2bdc222508011221016376cbd7b917c7105ddac35bdeddd79e6c9cbbc66dd227941599de2b9bc8b3de222708011201011a200d68ac7c3e8daf94c65ccdfe5b7397f50e80325240ef9b2a0ec483afaea30544"); + let value = hex!("0a0930382d7761736d2d3112230a0131120d4f524445525f4f524445524544120f4f524445525f554e4f524445524544180222250a0e636f6d6574626c732d6e65772d30120c636f6e6e656374696f6e2d301a050a036962632806"); + + assert_eq!( + chained_membership(&proof, &root, &value, &["ibc", "connections/connection-1"]), + Ok(()) + ); + } + + #[test] + fn client_state_exists() { + let root = hex!("971AF378C1F256110B3BA2EFD90325D5B5AFC8185997F2C12A7C4638B906CC2F"); + let proof = hex!("0ab0030aad030a1d636c69656e74732f30382d7761736d2d312f636c69656e74537461746512c7010a252f6962632e6c69676874636c69656e74732e7761736d2e76312e436c69656e745374617465129d010a720a20d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b807818e0fac1950622310a04900000691a060a049000007022060a04900000712a060a049000007232110a040400000010ffffffffffffffffff01280c30203880024204080110034880c2d72f50a0f4a4011220e8dcc770de5a013041588233812f73ac797ec6078b0011cbcbfe49d474f4c1191a051081f2e7011a0c0801180120012a040002ae06222c080112050204b006201a2120980ab410769397da376376a2756754b225f34cc0eea404b068924f64180abcc4222c080112050408b006201a21209d79cf7fc2f248ea0a56cff266ac54cfbc06687e25ffee99aec2884856d0104f222a080112260610b006203e808c2bc895d44d05d7af6d8b0424fabb1d9ab6f53b10cdb084b2996f75bfa620222c08011205081eb006201a212095bb7de983d8ea1282a2d60e2f6c675bec25f82be86aa874ff0f15827c1ab3ed0afc010af9010a036962631220859b7ac80b1c0ca82504e0d8e9de460d42ca66a03e708cbd09869e5216c73a591a090801180120012a01002225080112210106b99c0d8119ff1edbcbe165d0f19337dbbc080e677c88e57aa2ae767ebf0f0f222708011201011a20aa650406ea0d76e39dd43d2ea6a91e3fdaa1c908fc21a7ca68e5e62cc8115639222508011221016ac3182364d7cdaa1f52a77b6081e070aa29b2d253f3642169693cde336e2bdc22250801122101c9d0a585c82dc572f3fcedc70302d4c3fbbc9e84f0618c6b446a70efa312e8dc222708011201011a20952029410a533cf530124179204303bea59a86f5b4993291c5b8ca406412c5f7"); + let value = hex!("0a252f6962632e6c69676874636c69656e74732e7761736d2e76312e436c69656e745374617465129d010a720a20d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b807818e0fac1950622310a04900000691a060a049000007022060a04900000712a060a049000007232110a040400000010ffffffffffffffffff01280c30203880024204080110034880c2d72f50a0f4a4011220e8dcc770de5a013041588233812f73ac797ec6078b0011cbcbfe49d474f4c1191a051081f2e701"); + + assert_eq!( + chained_membership( + &proof, + &root, + &value, + &["ibc", "clients/08-wasm-1/clientState"] + ), + Ok(()) + ); + } + + #[test] + fn existence_proof_root_mismatch() { + let root = hex!("971AF378C1F256110B3BA2EFD90325D5B5AFC8185997F2C12A7C4638B906CC22"); + let proof = hex!("0ab0030aad030a1d636c69656e74732f30382d7761736d2d312f636c69656e74537461746512c7010a252f6962632e6c69676874636c69656e74732e7761736d2e76312e436c69656e745374617465129d010a720a20d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b807818e0fac1950622310a04900000691a060a049000007022060a04900000712a060a049000007232110a040400000010ffffffffffffffffff01280c30203880024204080110034880c2d72f50a0f4a4011220e8dcc770de5a013041588233812f73ac797ec6078b0011cbcbfe49d474f4c1191a051081f2e7011a0c0801180120012a040002ae06222c080112050204b006201a2120980ab410769397da376376a2756754b225f34cc0eea404b068924f64180abcc4222c080112050408b006201a21209d79cf7fc2f248ea0a56cff266ac54cfbc06687e25ffee99aec2884856d0104f222a080112260610b006203e808c2bc895d44d05d7af6d8b0424fabb1d9ab6f53b10cdb084b2996f75bfa620222c08011205081eb006201a212095bb7de983d8ea1282a2d60e2f6c675bec25f82be86aa874ff0f15827c1ab3ed0afc010af9010a036962631220859b7ac80b1c0ca82504e0d8e9de460d42ca66a03e708cbd09869e5216c73a591a090801180120012a01002225080112210106b99c0d8119ff1edbcbe165d0f19337dbbc080e677c88e57aa2ae767ebf0f0f222708011201011a20aa650406ea0d76e39dd43d2ea6a91e3fdaa1c908fc21a7ca68e5e62cc8115639222508011221016ac3182364d7cdaa1f52a77b6081e070aa29b2d253f3642169693cde336e2bdc22250801122101c9d0a585c82dc572f3fcedc70302d4c3fbbc9e84f0618c6b446a70efa312e8dc222708011201011a20952029410a533cf530124179204303bea59a86f5b4993291c5b8ca406412c5f7"); + let value = hex!("0a252f6962632e6c69676874636c69656e74732e7761736d2e76312e436c69656e745374617465129d010a720a20d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b807818e0fac1950622310a04900000691a060a049000007022060a04900000712a060a049000007232110a040400000010ffffffffffffffffff01280c30203880024204080110034880c2d72f50a0f4a4011220e8dcc770de5a013041588233812f73ac797ec6078b0011cbcbfe49d474f4c1191a051081f2e701"); + + assert!(matches!( + chained_membership( + &proof, + &root, + &value, + &["ibc", "clients/08-wasm-1/clientState"] + ), + Err(VerifyMembershipError::InvalidRoot { .. }) + )); + } + + #[test] + fn existence_proof_key_mismatch() { + let root = hex!("971AF378C1F256110B3BA2EFD90325D5B5AFC8185997F2C12A7C4638B906CC2F"); + let proof = hex!("0ab0030aad030a1d636c69656e74732f30382d7761736d2d312f636c69656e74537461746512c7010a252f6962632e6c69676874636c69656e74732e7761736d2e76312e436c69656e745374617465129d010a720a20d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b807818e0fac1950622310a04900000691a060a049000007022060a04900000712a060a049000007232110a040400000010ffffffffffffffffff01280c30203880024204080110034880c2d72f50a0f4a4011220e8dcc770de5a013041588233812f73ac797ec6078b0011cbcbfe49d474f4c1191a051081f2e7011a0c0801180120012a040002ae06222c080112050204b006201a2120980ab410769397da376376a2756754b225f34cc0eea404b068924f64180abcc4222c080112050408b006201a21209d79cf7fc2f248ea0a56cff266ac54cfbc06687e25ffee99aec2884856d0104f222a080112260610b006203e808c2bc895d44d05d7af6d8b0424fabb1d9ab6f53b10cdb084b2996f75bfa620222c08011205081eb006201a212095bb7de983d8ea1282a2d60e2f6c675bec25f82be86aa874ff0f15827c1ab3ed0afc010af9010a036962631220859b7ac80b1c0ca82504e0d8e9de460d42ca66a03e708cbd09869e5216c73a591a090801180120012a01002225080112210106b99c0d8119ff1edbcbe165d0f19337dbbc080e677c88e57aa2ae767ebf0f0f222708011201011a20aa650406ea0d76e39dd43d2ea6a91e3fdaa1c908fc21a7ca68e5e62cc8115639222508011221016ac3182364d7cdaa1f52a77b6081e070aa29b2d253f3642169693cde336e2bdc22250801122101c9d0a585c82dc572f3fcedc70302d4c3fbbc9e84f0618c6b446a70efa312e8dc222708011201011a20952029410a533cf530124179204303bea59a86f5b4993291c5b8ca406412c5f7"); + let value = hex!("0a252f6962632e6c69676874636c69656e74732e7761736d2e76312e436c69656e745374617465129d010a720a20d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b807818e0fac1950622310a04900000691a060a049000007022060a04900000712a060a049000007232110a040400000010ffffffffffffffffff01280c30203880024204080110034880c2d72f50a0f4a4011220e8dcc770de5a013041588233812f73ac797ec6078b0011cbcbfe49d474f4c1191a051081f2e701"); + + assert!(matches!( + chained_membership( + &proof, + &root, + &value, + &["ibc", "clients/08-wasm-2/clientState"] + ), + Err(VerifyMembershipError::InnerVerification( + verify::VerifyMembershipError::ExistenceProofVerify( + verify::VerifyError::KeyAndExistenceProofKeyMismatch { .. } + ) + )) + )); + } + + #[test] + fn existence_proof_value_mismatch() { + let root = hex!("971AF378C1F256110B3BA2EFD90325D5B5AFC8185997F2C12A7C4638B906CC2F"); + let proof = hex!("0ab0030aad030a1d636c69656e74732f30382d7761736d2d312f636c69656e74537461746512c7010a252f6962632e6c69676874636c69656e74732e7761736d2e76312e436c69656e745374617465129d010a720a20d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b807818e0fac1950622310a04900000691a060a049000007022060a04900000712a060a049000007232110a040400000010ffffffffffffffffff01280c30203880024204080110034880c2d72f50a0f4a4011220e8dcc770de5a013041588233812f73ac797ec6078b0011cbcbfe49d474f4c1191a051081f2e7011a0c0801180120012a040002ae06222c080112050204b006201a2120980ab410769397da376376a2756754b225f34cc0eea404b068924f64180abcc4222c080112050408b006201a21209d79cf7fc2f248ea0a56cff266ac54cfbc06687e25ffee99aec2884856d0104f222a080112260610b006203e808c2bc895d44d05d7af6d8b0424fabb1d9ab6f53b10cdb084b2996f75bfa620222c08011205081eb006201a212095bb7de983d8ea1282a2d60e2f6c675bec25f82be86aa874ff0f15827c1ab3ed0afc010af9010a036962631220859b7ac80b1c0ca82504e0d8e9de460d42ca66a03e708cbd09869e5216c73a591a090801180120012a01002225080112210106b99c0d8119ff1edbcbe165d0f19337dbbc080e677c88e57aa2ae767ebf0f0f222708011201011a20aa650406ea0d76e39dd43d2ea6a91e3fdaa1c908fc21a7ca68e5e62cc8115639222508011221016ac3182364d7cdaa1f52a77b6081e070aa29b2d253f3642169693cde336e2bdc22250801122101c9d0a585c82dc572f3fcedc70302d4c3fbbc9e84f0618c6b446a70efa312e8dc222708011201011a20952029410a533cf530124179204303bea59a86f5b4993291c5b8ca406412c5f7"); + let value = hex!("0a252f6962632e6c69676874636c69656e74732e7761736d2e76312e436c69656e745374617465129d010a720a20d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b807818e0fac1950622310a04900000691a060a049000007022060a04900000712a060a049000007232110a040400000010ffffffffffffffffff01280c30203880024204080110034880c2d72f50a0f4a4011220e8dcc770de5a013041588233812f73ac797ec6078b0011cbcbfe49d474f4c1191a051081f2e700"); + + assert!(matches!( + chained_membership( + &proof, + &root, + &value, + &["ibc", "clients/08-wasm-1/clientState"] + ), + Err(VerifyMembershipError::InnerVerification( + verify::VerifyMembershipError::ExistenceProofVerify( + verify::VerifyError::ValueAndExistenceProofValueMismatch { .. } + ) + )) + )); + } + + #[test] + fn account_non_existence() { + let root = hex!("C9A25B954FEF48EC601359591A28C9A2FD32A411421AEF2DC16DC8A68B3CFA98"); + let proof = hex!("0a96061293060a15014152090b0c95c948edc407995560feed4a9df88812fa020a15014152090b0c95c948edc407995560feed4a9df81e129e010a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74127a0a2c756e696f6e31673966716a7a63766a687935336d77797137763432633837613439666d37713772646568386312460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103820c4b94dccd7d74706216c426fe884d9a4404410df69d6421899595c5a9c122180420031a0b0801180120012a0300027822290801122502047820170c890f01b9fa9ab803511bbc7be7c25359309f04d021a72e0a9b93b8ff72c020222c0801120504089601201a21205f282a80f1d186fa1f7b237f81e8bc9a4bb40d5a03cbbdffdd421b1ad4cb16f4222c0801120506109601201a2120e9c65294b7106c7323dcabe4532232c319afc78cd373e338f12df43f8ecfa909222c080112050a309601201a2120a95af7890dba33514ea28a3db7b409f4887b058d6d1e43960c4cd45bb1d9bef81afc020a150143e46d91544517a037a8029b6c7f86f62bab389b129e010a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74127a0a2c756e696f6e3167306a786d79323567357436716461677132646b636c7578376334366b77796d38646563667712460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034611ea6606f6241fdeb0db1854a785eaa2fef5770694237daaf46057cadb3903180320031a0c0801180120012a0400029601222c0801120502049601201a2120532543090d1564b206e953fd6f97000d9b78bd5a8a424f551d483a58b3f54c57222a0801122604089601207e55a1ee8006e9c29c895a8de8ea8cdc6aaddc10e05ea3d3ee8fac786a73c02d20222c0801120506109601201a2120e9c65294b7106c7323dcabe4532232c319afc78cd373e338f12df43f8ecfa909222c080112050a309601201a2120a95af7890dba33514ea28a3db7b409f4887b058d6d1e43960c4cd45bb1d9bef80a80020afd010a0361636312205281c416bf4f80b9d99428a09f91ff311968a3b2adb199342d63c9db20a417e91a090801180120012a010022250801122101ba30cf8122e71a87fea08d0da9499e0373495a64e1648de8f08ca1a73e1fc1a8222708011201011a203489cd05a389a1d165f19003cea0994df9e55a5cb53b3d659417040be528b86d222708011201011a20e5c60ddccacb1c6b0be7957e8d7a86dc0f8bcec91c91d666d39eb1ebedd1bdf1222708011201011a2047a4c9a64496594e8b255443aa979293b2c7120150cf31e0eeeb8a2a987fd7e8222708011201011a2053bca15bed4becbdfd1b4cd0e63bd3845646022a99a2289a6678d8608f092207"); + + let proof = MerkleProof::try_from_proto_bytes(&proof).unwrap(); + let root = MerkleRoot { + hash: H256::try_from(root.as_slice()).unwrap(), + }; + + assert_eq!( + verify_non_membership( + proof, + &SDK_SPECS, + &root, + MerklePath { + key_path: vec!["acc".to_string(), unsafe { + String::from_utf8_unchecked( + hex!("014152090b0c95c948edc407995560feed4a9df888").to_vec(), + ) + }] + } + ), + Ok(()) + ); + } +} diff --git a/lib/ics23/src/ops/mod.rs b/lib/ics23/src/ops/mod.rs index 0abc16c1ce..d4334e7b2a 100644 --- a/lib/ics23/src/ops/mod.rs +++ b/lib/ics23/src/ops/mod.rs @@ -17,13 +17,13 @@ pub enum ValidateIavlOpsError { Decode(prost::DecodeError), } -pub(crate) fn read_varint(mut buffer: &[u8]) -> Result { +pub(crate) fn read_varint<'a>(mut buffer: &'a [u8]) -> Result<(&'a [u8], i64), prost::DecodeError> { let ux = prost::encoding::decode_varint(&mut buffer)?; let mut x = (ux >> 1) as i64; if ux & 1 != 0 { x = !x; } - Ok(x) + Ok((buffer, x)) } pub(crate) fn validate_iavl_ops( @@ -36,20 +36,20 @@ pub(crate) fn validate_iavl_ops( return Err(ValidateIavlOpsError::NegativeMinHeight(min_height)); } - let height = read_varint(&mut prefix).map_err(ValidateIavlOpsError::Decode)?; + let (mut buffer, height) = read_varint(&mut prefix).map_err(ValidateIavlOpsError::Decode)?; if height < min_height as i64 { return Err(ValidateIavlOpsError::HeightTooShort { height, min_height }); } - let size = read_varint(&mut prefix).map_err(ValidateIavlOpsError::Decode)?; + let (mut buffer, size) = read_varint(&mut buffer).map_err(ValidateIavlOpsError::Decode)?; if size < 0 { return Err(ValidateIavlOpsError::NegativeSize(size)); } - let version = read_varint(&mut prefix).map_err(ValidateIavlOpsError::Decode)?; + let (buffer, version) = read_varint(&mut buffer).map_err(ValidateIavlOpsError::Decode)?; if version < 0 { return Err(ValidateIavlOpsError::NegativeVersion(size)); } - Ok(prefix.len()) + Ok(buffer.len()) } diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs index 933e4c159e..86ebfee642 100644 --- a/lib/ics23/src/verify.rs +++ b/lib/ics23/src/verify.rs @@ -331,3 +331,107 @@ fn verify_existence_proof( Ok(()) } + +#[cfg(test)] +mod tests { + use hex_literal::hex; + use unionlabs::{cosmos::ics23::commitment_proof::CommitmentProof, TryFromProto}; + + use super::*; + use crate::proof_specs::TENDERMINT_PROOF_SPEC; + + fn ensure_existant( + proof: &[u8], + root: &[u8], + key: &[u8], + value: &[u8], + ) -> Result<(), VerifyMembershipError> { + let CommitmentProof::Exist(commitment_proof) = + CommitmentProof::try_from_proto_bytes(&proof).unwrap() + else { + panic!("unexpected proof type"); + }; + + super::verify_membership( + &TENDERMINT_PROOF_SPEC, + &root, + &commitment_proof, + &key, + &value, + ) + } + + fn ensure_nonexistant( + proof: &[u8], + root: &[u8], + key: &[u8], + ) -> Result<(), VerifyMembershipError> { + let CommitmentProof::Nonexist(commitment_proof) = + CommitmentProof::try_from_proto_bytes(&proof).unwrap() + else { + panic!("unexpected proof type"); + }; + + verify_non_membership(&TENDERMINT_PROOF_SPEC, &root, &commitment_proof, &key) + } + + #[test] + fn verify_membership_left() { + let proof = hex!("0adb030a14303142424373615a55715146735259436c6a5767121e76616c75655f666f725f303142424373615a55715146735259436c6a57671a090801180120012a0100222708011201011a20cb3131cd98b069efcc0e8c7e68da47370adbff32266d7fcd1b0580fdf3961266222708011201011a2021d1205c1f8537205e8fb4b176f960b459d9131669968d59c456442f7673b68b222708011201011a20b82a0e7f4434b3cedb87ea83eb5a70c7dc664c77b2fe21c6245f315e58fdf745222708011201011a20bf0657a0e6fbd8f2043eb2cf751561adcf50547d16201224133eeb8d38145229222708011201011a206d47c03df91a4a0252055d116439d34b5b73f3a24d5cb3cf0d4b08caa540cac4222708011201011a20d5d2926993fa15c7410ac4ee1f1d81afddfb0ab5f6f4706b05f407bc01638149222708011201011a20540719b26a7301ad012ac45ebe716679e5595e5570d78be9b6da8d8591afb374222708011201011a20fccaaa9950730e80b9ccf75ad2cfeab26ae750b8bd6ac1ff1c7a7502f3c64be2222708011201011a20ecb61a6d70accb79c2325fb0b51677ed1561c91af5e10578c8294002fbb3c21e222708011201011a201b3bc1bd8d08af9f6199de84e95d646570cbd9b306a632a5acf617cbd7d1ab0a"); + let root = hex!("c569a38a5775bbda2051c34ae00894186f837c39d11dca55495b9aed14f17ddf"); + let key = hex!("303142424373615a55715146735259436c6a5767"); + let value = hex!("76616c75655f666f725f303142424373615a55715146735259436c6a5767"); + + assert_eq!(ensure_existant(&proof, &root, &key, &value), Ok(())); + } + + #[test] + fn verify_membership_middle() { + let proof = hex!("0ad1030a14513334656d766f39447145585735325257523835121e76616c75655f666f725f513334656d766f394471455857353252575238351a090801180120012a010022250801122101e231d775380f2d663651e213cc726660e2ce0a2f2e9ee12cbb7df32294104a8c222708011201011a2014af194c63500236e52cc290ab24244fab39a520ece7e20fa93f4c9ff80c6626222508011221017966d2ead34418db2eaa04c0dffb9316805e8a0d421d1270c8954c35ee3221382225080112210172339e20a49bb16795a99bd905b47f99c45e5e5a9e6b7fb223dc8fe6751e1bda222708011201011a2053dd1ecc25ff906a0ef4db37ee068f3d8ad6d1d49913eefb847a675a681c5ffa222708011201011a20de90f9951a19497be7e389e02aa79e26faf77080e740e8743249a17a537f287d22250801122101ad4e53e981afc5a71e34ab0c4ffbccf1b468414d9d0939bd08edbd2461bc944a222708011201011a209b4cf89c3995b9dd66d58ab088846b2c6b59c52c6d10ec1d759ca9e9aa5eef5c222508011221013928a078bd66ab3949f5b1846b6d354dbdc1968a416607c7d91555ca26716667222708011201011a20d2d82cf8915b9ae6f92c7eae343e37d312ace05e654ce47acdf57d0a5490b873"); + let root = hex!("494b16e3a64a85df143b2881bdd3ec94c3f8e18b343e8ff9c2d61afd05d040c8"); + let key = hex!("513334656d766f39447145585735325257523835"); + let value = hex!("76616c75655f666f725f513334656d766f39447145585735325257523835"); + + assert_eq!(ensure_existant(&proof, &root, &key, &value), Ok(())); + } + + #[test] + fn verify_membership_right() { + let proof = hex!("0aab020a147a785a4e6b534c64634d655657526c7658456644121e76616c75655f666f725f7a785a4e6b534c64634d655657526c76584566441a090801180120012a0100222508011221012634b831468dbafb1fc61a979c348ff8462da9a7d550191a6afc916ade16cc9922250801122101ab814d419bfc94ee9920d0ce993ce5da011e43613daf4b6f302855760083d7dd222508011221015a1568c73eaeaba567a6b2b2944b0e9a0228c931884cb5942f58ed835b8a7ac522250801122101a171412db5ee84835ef247768914e835ff80b7711e4aa8060871c2667ec3ea2922250801122101f9c2491884de24fb61ba8f358a56b306a8989bd35f1f8a4c8dabce22f703cc14222508011221012f12a6aa6270eff8a1628052938ff5e36cfcc5bf2eaedc0941ee46398ebc7c38"); + let root = hex!("f54227f1a7d90aa2bf7931066196fd3072b7fe6b1fbd49d1e26e85a90d9541bb"); + let key = hex!("7a785a4e6b534c64634d655657526c7658456644"); + let value = hex!("76616c75655f666f725f7a785a4e6b534c64634d655657526c7658456644"); + + assert_eq!(ensure_existant(&proof, &root, &key, &value), Ok(())); + } + + // https://github.com/cosmos/ics23/blob/b1abd8678aab07165efd453c96796a179eb3131f/testdata/tendermint/nonexist_left.json + #[test] + fn verify_non_membership_left() { + let proof = hex!("12e4030a04010101011adb030a143032615465366472483456706f4f583245507137121e76616c75655f666f725f3032615465366472483456706f4f5832455071371a090801180120012a0100222708011201011a20b843481496dc10561056b63ec8f726f3357395b610355b25082f5768b2073e91222708011201011a20d5281fdd872060e89173d4de1100fa6c96f778467df66abb10cf3b1f5821f182222708011201011a20eb981020433d929c6275ad772accf2e6aa916db97e31d2f26d0b6b07b444bbef222708011201011a204a40e813132aff60b64ba9d109548ab39459ad48a203ab8d3455dd842a7ab1da222708011201011a208f354a84ce1476e0b9cca92e65301a6435b1f242c2f53f943b764a4f326a71c7222708011201011a20ac6451617a6406005035dddad36657fde5312cc4d67d69ca1464611847c10cfb222708011201011a2023c1d1dd62002a0e2efcc679196589a4337234dcd209cb449cc3ac10773b60e0222708011201011a203b11c267328ba761ddc630dd5ef7642aeda05f180539fe93c0ca57729705bc46222708011201011a205ff2e1933be704539463c264b157ff2b8d9960813bd36c69c5208d57e3b1e07e222708011201011a20c4a79e6c0cbf60fb8e5bf940db4c444b7e442951b69c840db38cf28c8aa008be"); + let root = hex!("4e2e78d2da505b7d0b00fda55a4b048eed9a23a7f7fc3d801f20ce4851b442aa"); + let key = hex!("01010101"); + + assert_eq!(ensure_nonexistant(&proof, &root, &key), Ok(())) + } + + // https://github.com/cosmos/ics23/blob/b1abd8678aab07165efd453c96796a179eb3131f/testdata/tendermint/nonexist_middle.json + #[test] + fn verify_non_membership_middle() { + let proof = hex!("12c0070a14544f31483668784a4b667136547a56767649ffff12cf030a14544f31483668784a4b667136547a567676497747121e76616c75655f666f725f544f31483668784a4b667136547a5676764977471a090801180120012a01002225080112210143e19cb5e5dab017734caa78a2e2bccbb4797b7dc5a91abeab630c66fa6b162522250801122101b575404a1bb42b0fef8ae7f217af88aec769f7d66b5bc4b2913e74d651365473222508011221017c22dc50e866f9a1dce517ea01621161cecd70f4bdcd024b5a392746a1c8dc2622250801122101578105344f2c98c323ba0b8ca31e75aaa2b865cc389681e300b14d1c20713796222708011201011a20895c070c14546ecef7f5cb3a4bda1fd436a0ff99190f90bd037cbeaf52b2ffc1222708011201011a20f7571fca06ac4387c3eae5469c152427b797abb55fa98727eacbd5c1c91b5fb4222508011221015056e6472f8e5c5c9b8881c5f0e49601e9eca31f3e1766aa69c2dc9c6d9112be222708011201011a206c74439556c5edb5aa693af410d3718dbb613d37799f2f4e8ff304a8bfe3351b22250801122101253014334c7b8cd78436979554f7890f3dc1c971925ea31b48fc729cd179c701222708011201011a20b81c19ad4b5d8d15f716b91519bf7ad3d6e2289f9061fd2592a8431ea97806fe1ad5030a14544f433344683150664f76657538585166635778121e76616c75655f666f725f544f433344683150664f766575385851666357781a090801180120012a0100222708011201011a20415d4cfaed0bfc98ac32acc219a8517bfa1983a15cc742e8b2f860167484bd46222708011201011a2098d853d9cc0ee1d2162527f660f2b90ab55b13e5534f1b7753ec481d7901d3ec222708011201011a20b5113e6000c5411b7cfa6fd09b6752a43de0fcd3951ed3b154d162deb53224a2222708011201011a208ce18cd72cc83511cb8ff706433f2fa4208c85b9f4c8d0ed71a614f24b89ae6c22250801122101c611244fe6b5fda4257615902eb24c14efcd9708c7c875d1ac5e867767aa1eab222708011201011a20f7571fca06ac4387c3eae5469c152427b797abb55fa98727eacbd5c1c91b5fb4222508011221015056e6472f8e5c5c9b8881c5f0e49601e9eca31f3e1766aa69c2dc9c6d9112be222708011201011a206c74439556c5edb5aa693af410d3718dbb613d37799f2f4e8ff304a8bfe3351b22250801122101253014334c7b8cd78436979554f7890f3dc1c971925ea31b48fc729cd179c701222708011201011a20b81c19ad4b5d8d15f716b91519bf7ad3d6e2289f9061fd2592a8431ea97806fe"); + let root = hex!("4bf28d948566078c5ebfa86db7471c1541eab834f539037075b9f9e3b1c72cfc"); + let key = hex!("544f31483668784a4b667136547a56767649ffff"); + + assert_eq!(ensure_nonexistant(&proof, &root, &key), Ok(())) + } + + // https://github.com/cosmos/ics23/blob/b1abd8678aab07165efd453c96796a179eb3131f/testdata/tendermint/nonexist_right.json + #[test] + fn verify_non_membership_right() { + let proof = hex!("12a9030a04ffffffff12a0030a147a774e4d4a456f7932674253586277666e63504a121e76616c75655f666f725f7a774e4d4a456f7932674253586277666e63504a1a090801180120012a01002225080112210178a215355c17371583418df95773476b347a853f6eae317677721e0c24e78ad2222508011221015e2cf893e7cd70251eb4debd855c8c9a92f6e0a1fd931cf41e0575846ab174e822250801122101414bae883f8133f0201a2791dafeaef3daa24a6631b3f9402de3a4dc658fd035222508011221012e2829beee266a814af4db08046f4575b011e5ec9d2d93c1510c3cc7d8219edc22250801122101f8286597078491ae0ef61264c218c6e167e4e03f1de47945d9ba75bb41deb81a22250801122101dea6a53098d11ce2138cbcae26b392959f05d7e1e24b9547584571012280f289222508011221010a8e535094d18b2120c38454b445d9accf3f1b255690e6f3d48164ae73b4c775222508011221012cbb518f52ec1f8e26dd36587f29a6890a11c0dd3f94e7a28546e695f296d3a722250801122101839d9ddd9dadf41c0ecfc3f7e20f57833b8fb5bcb703bef4f97910bbe5b579b9"); + let root = hex!("83952b0b17e64c862628bcc1277e7f8847589af794ed5a855339281d395ec04f"); + let key = hex!("ffffffff"); + + assert_eq!(ensure_nonexistant(&proof, &root, &key), Ok(())) + } +} From 3e7582e5a36bd037b15bdc17a09573b2ffecb491 Mon Sep 17 00:00:00 2001 From: aeryz Date: Fri, 5 Jan 2024 13:16:54 +0300 Subject: [PATCH 12/14] feat(ics23): upgrade ics23 to include `prehash_key_before_comparison` Signed-off-by: aeryz --- Cargo.lock | 1 + flake.lock | 8 +- flake.nix | 2 +- generated/rust/src/cosmos.ics23.v1.rs | 5 + lib/chain-utils/Cargo.toml | 1 + lib/chain-utils/src/cosmos.rs | 44 +---- lib/ics23/src/ibc_api.rs | 7 +- lib/ics23/src/ops/hash_op.rs | 1 - lib/ics23/src/ops/inner_op.rs | 4 +- lib/ics23/src/ops/leaf_op.rs | 7 +- lib/ics23/src/proof_specs.rs | 14 ++ lib/ics23/src/verify.rs | 195 +++++++++++-------- lib/unionlabs/src/cosmos/ics23/proof_spec.rs | 18 +- 13 files changed, 151 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6d5800a66..7cdd9388a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -785,6 +785,7 @@ dependencies = [ "ethers", "futures", "hex", + "ics23", "num_enum", "prost 0.11.9", "protos", diff --git a/flake.lock b/flake.lock index 66cc5f17eb..4ad71f91a6 100644 --- a/flake.lock +++ b/flake.lock @@ -294,17 +294,17 @@ "ics23": { "flake": false, "locked": { - "lastModified": 1670407596, - "narHash": "sha256-O7oZI+29xKAbMHssg5HhxlssedSfejCuzHNHYX7WwBc=", + "lastModified": 1681373664, + "narHash": "sha256-vxlzs69D6Jscal9bSBeuVcX0Xibp69pcclWt5NldLtM=", "owner": "cosmos", "repo": "ics23", - "rev": "b1abd8678aab07165efd453c96796a179eb3131f", + "rev": "74ce807b7be39a7e0afb4e2efb8e28a57965f57b", "type": "github" }, "original": { "owner": "cosmos", "repo": "ics23", - "rev": "b1abd8678aab07165efd453c96796a179eb3131f", + "rev": "74ce807b7be39a7e0afb4e2efb8e28a57965f57b", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 0ca35f3df2..515ff38b47 100644 --- a/flake.nix +++ b/flake.nix @@ -45,7 +45,7 @@ flake = false; }; ics23 = { - url = "github:cosmos/ics23?rev=b1abd8678aab07165efd453c96796a179eb3131f"; + url = "github:cosmos/ics23?rev=74ce807b7be39a7e0afb4e2efb8e28a57965f57b"; flake = false; }; cosmosproto = { diff --git a/generated/rust/src/cosmos.ics23.v1.rs b/generated/rust/src/cosmos.ics23.v1.rs index c3a259ff27..51c4830022 100644 --- a/generated/rust/src/cosmos.ics23.v1.rs +++ b/generated/rust/src/cosmos.ics23.v1.rs @@ -190,6 +190,11 @@ pub struct ProofSpec { #[prost(int32, tag = "4")] #[cfg_attr(feature = "serde", serde(default))] pub min_depth: i32, + /// prehash_key_before_comparison is a flag that indicates whether to use the + /// prehash_key specified by LeafOp to compare lexical ordering of keys for + /// non-existence proofs. + #[prost(bool, tag = "5")] + pub prehash_key_before_comparison: bool, } /// InnerSpec contains all store-specific structure info to determine if two proofs from a /// given store are neighbors. diff --git a/lib/chain-utils/Cargo.toml b/lib/chain-utils/Cargo.toml index 4d0cd62e7c..751685faa6 100644 --- a/lib/chain-utils/Cargo.toml +++ b/lib/chain-utils/Cargo.toml @@ -30,3 +30,4 @@ thiserror = "1.0.49" crossbeam-queue = "0.3.8" num_enum = "0.7.0" dashmap = "5.5.3" +ics23.workspace = true diff --git a/lib/chain-utils/src/cosmos.rs b/lib/chain-utils/src/cosmos.rs index 615398234f..0d7e737cb2 100644 --- a/lib/chain-utils/src/cosmos.rs +++ b/lib/chain-utils/src/cosmos.rs @@ -6,10 +6,6 @@ use serde::{Deserialize, Serialize}; use tendermint_rpc::{Client, WebSocketClient, WebSocketClientUrl}; use unionlabs::{ bounded::{BoundedI32, BoundedI64}, - cosmos::ics23::{ - hash_op::HashOp, inner_spec::InnerSpec, leaf_op::LeafOp, length_op::LengthOp, - proof_spec::ProofSpec, - }, encoding::Proto, events::{IbcEvent, TryFromTendermintEventError, WriteAcknowledgement}, google::protobuf::{any::Any, duration::NANOS_PER_SECOND}, @@ -209,44 +205,8 @@ impl Chain for Cosmos { revision_height: height.value(), }, proof_specs: [ - ProofSpec { - leaf_spec: LeafOp { - hash: HashOp::Sha256, - prehash_key: HashOp::NoHash, - prehash_value: HashOp::Sha256, - length: LengthOp::VarProto, - prefix: vec![0], - }, - inner_spec: InnerSpec { - child_order: vec![0, 1], - child_size: 33, - min_prefix_length: 4, - max_prefix_length: 12, - empty_child: vec![], - hash: HashOp::Sha256, - }, - max_depth: 0, - min_depth: 0, - }, - ProofSpec { - leaf_spec: LeafOp { - hash: HashOp::Sha256, - prehash_key: HashOp::NoHash, - prehash_value: HashOp::Sha256, - length: LengthOp::VarProto, - prefix: [0].into(), - }, - inner_spec: InnerSpec { - child_order: [0, 1].into(), - child_size: 32, - min_prefix_length: 1, - max_prefix_length: 1, - empty_child: [].into(), - hash: HashOp::Sha256, - }, - max_depth: 0, - min_depth: 0, - }, + ics23::ibc_api::IAVL_PROOF_SPEC, + ics23::ibc_api::TENDERMINT_PROOF_SPEC, ] .into(), upgrade_path: vec!["upgrade".into(), "upgradedIBCState".into()], diff --git a/lib/ics23/src/ibc_api.rs b/lib/ics23/src/ibc_api.rs index 1489f9eb29..c3d5f3bbb2 100644 --- a/lib/ics23/src/ibc_api.rs +++ b/lib/ics23/src/ibc_api.rs @@ -3,7 +3,6 @@ use unionlabs::{ ibc::core::commitment::{ merkle_path::MerklePath, merkle_proof::MerkleProof, merkle_root::MerkleRoot, }, - TryFromProto, }; pub use crate::proof_specs::{IAVL_PROOF_SPEC, TENDERMINT_PROOF_SPEC}; @@ -170,10 +169,8 @@ mod tests { TryFromProto, }; - use super::{ - verify_chained_membership_proof, verify_membership, VerifyMembershipError, SDK_SPECS, - }; - use crate::{ibc_api::verify_non_membership, verify}; + use super::{verify_membership, verify_non_membership, VerifyMembershipError, SDK_SPECS}; + use crate::verify; fn chained_membership( proof: &[u8], diff --git a/lib/ics23/src/ops/hash_op.rs b/lib/ics23/src/ops/hash_op.rs index 90907d80ac..0b684b8345 100644 --- a/lib/ics23/src/ops/hash_op.rs +++ b/lib/ics23/src/ops/hash_op.rs @@ -11,7 +11,6 @@ pub fn do_hash_or_noop(hash_op: HashOp, preimage: &[u8]) -> Vec { pub fn do_hash(hash_op: HashOp, preimage: &[u8]) -> Vec { match hash_op { - // TODO(aeryz): why not keccak HashOp::Sha256 => sha2::Sha256::new() .chain_update(preimage) .finalize() diff --git a/lib/ics23/src/ops/inner_op.rs b/lib/ics23/src/ops/inner_op.rs index 3327070168..54db288bf3 100644 --- a/lib/ics23/src/ops/inner_op.rs +++ b/lib/ics23/src/ops/inner_op.rs @@ -1,7 +1,7 @@ use unionlabs::cosmos::ics23::{hash_op::HashOp, inner_op::InnerOp, proof_spec::ProofSpec}; use super::{hash_op, validate_iavl_ops}; -use crate::proof_specs::IAVL_PROOF_SPEC; +use crate::proof_specs::{self, IAVL_PROOF_SPEC}; #[derive(Debug, PartialEq, thiserror::Error)] pub enum SpecMismatchError { @@ -36,7 +36,7 @@ pub fn check_against_spec( return Err(SpecMismatchError::UnexpectedHashOp(inner_op.hash)); } - if spec.compatible(&IAVL_PROOF_SPEC) { + if proof_specs::compatible(spec, &IAVL_PROOF_SPEC) { match validate_iavl_ops(&inner_op.prefix, b) { Ok(remaining) => { if remaining != 1 && remaining != 34 { diff --git a/lib/ics23/src/ops/leaf_op.rs b/lib/ics23/src/ops/leaf_op.rs index 3f7720b294..90653b0e54 100644 --- a/lib/ics23/src/ops/leaf_op.rs +++ b/lib/ics23/src/ops/leaf_op.rs @@ -3,7 +3,10 @@ use unionlabs::cosmos::ics23::{ }; use super::{hash_op, length_op, validate_iavl_ops}; -use crate::{proof_specs::IAVL_PROOF_SPEC, ValidateIavlOpsError}; +use crate::{ + proof_specs::{self, IAVL_PROOF_SPEC}, + ValidateIavlOpsError, +}; #[derive(Debug, PartialEq, thiserror::Error)] pub enum SpecMismatchError { @@ -36,7 +39,7 @@ pub enum ApplyError { pub fn check_against_spec(leaf_op: &LeafOp, spec: &ProofSpec) -> Result<(), SpecMismatchError> { let lspec = &spec.leaf_spec; - if spec.compatible(&IAVL_PROOF_SPEC) { + if proof_specs::compatible(spec, &IAVL_PROOF_SPEC) { match validate_iavl_ops(&leaf_op.prefix, 0) { Ok(remaining) => { if remaining > 0 { diff --git a/lib/ics23/src/proof_specs.rs b/lib/ics23/src/proof_specs.rs index d28e7d0f00..2d4d5f9732 100644 --- a/lib/ics23/src/proof_specs.rs +++ b/lib/ics23/src/proof_specs.rs @@ -23,6 +23,7 @@ pub const IAVL_PROOF_SPEC: ProofSpec = ProofSpec { }, max_depth: 0, min_depth: 0, + prehash_key_before_comparison: false, }; pub const TENDERMINT_PROOF_SPEC: ProofSpec = ProofSpec { @@ -43,4 +44,17 @@ pub const TENDERMINT_PROOF_SPEC: ProofSpec = ProofSpec { }, max_depth: 0, min_depth: 0, + prehash_key_before_comparison: false, }; + +pub fn compatible(lhs: &ProofSpec, rhs: &ProofSpec) -> bool { + lhs.leaf_spec.hash == rhs.leaf_spec.hash + && lhs.leaf_spec.prehash_key == rhs.leaf_spec.prehash_key + && lhs.leaf_spec.prehash_value == rhs.leaf_spec.prehash_value + && lhs.leaf_spec.length == rhs.leaf_spec.length + && lhs.inner_spec.hash == rhs.inner_spec.hash + && lhs.inner_spec.min_prefix_length == rhs.inner_spec.min_prefix_length + && lhs.inner_spec.max_prefix_length == rhs.inner_spec.max_prefix_length + && lhs.inner_spec.child_size == rhs.inner_spec.child_size + && lhs.inner_spec.child_order.len() == rhs.inner_spec.child_order.len() +} diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs index 86ebfee642..9a247a676b 100644 --- a/lib/ics23/src/verify.rs +++ b/lib/ics23/src/verify.rs @@ -41,6 +41,8 @@ pub enum VerifyError { RightProofMissing, #[error("both left and right proofs are missing")] BothProofsMissing, + #[error("neighbor search failure ({0})")] + NeighborSearch(NeighborSearchError), } #[derive(Debug, PartialEq, thiserror::Error)] @@ -51,6 +53,18 @@ pub enum VerifyMembershipError { ProofDoesNotExist, } +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum NeighborSearchError { + #[error("")] + InvalidBranch { branch: i32, order_len: usize }, + #[error("branch ({branch}) not found in ({order:?})")] + BranchNotFoundInOrder { branch: i32, order: Vec }, + #[error("cannot find any valid spacing for this node")] + CannotFindValidSpacing, + #[error("invalid path provided for proof")] + InvalidPath, +} + pub fn verify_non_membership( spec: &ProofSpec, root: &[u8], @@ -79,18 +93,20 @@ fn verify_non_existence( key: &[u8], ) -> Result<(), VerifyError> { let key_for_comparison = |spec: &ProofSpec, key: &[u8]| -> Vec { - // TODO(aeryz): we don't have prehash_key_before_comparison, why? + if !spec.prehash_key_before_comparison { + return key.to_vec(); + } do_hash_or_noop(spec.leaf_spec.prehash_key, key) }; let left_ops = |left: &ExistenceProof| -> Result<(), VerifyError> { verify_existence_proof(&left, spec, root, &left.key, &left.value)?; - if key_for_comparison(spec, key) == key_for_comparison(spec, &left.key) { + if key_for_comparison(spec, key) <= key_for_comparison(spec, &left.key) { return Err(VerifyError::KeyIsNotRightOfLeftProof); } - if !is_right_most(&spec.inner_spec, &left.path) { + if !is_right_most(&spec.inner_spec, &left.path).map_err(VerifyError::NeighborSearch)? { return Err(VerifyError::RightProofMissing); } @@ -100,11 +116,11 @@ fn verify_non_existence( let right_ops = |right: &ExistenceProof| -> Result<(), VerifyError> { verify_existence_proof(&right, spec, root, &right.key, &right.value)?; - if key_for_comparison(spec, key) == key_for_comparison(spec, &right.key) { + if key_for_comparison(spec, key) >= key_for_comparison(spec, &right.key) { return Err(VerifyError::KeyIsNotLeftOfRightProof); } - if !is_left_most(&spec.inner_spec, &right.path) { + if !is_left_most(&spec.inner_spec, &right.path).map_err(VerifyError::NeighborSearch)? { return Err(VerifyError::LeftProofMissing); } @@ -115,7 +131,9 @@ fn verify_non_existence( (None, Some(right)) => right_ops(right)?, (Some(left), None) => left_ops(left)?, (Some(left), Some(right)) => { - if !is_left_neighbor(&spec.inner_spec, &left.path, &right.path) { + if !is_left_neighbor(&spec.inner_spec, &left.path, &right.path) + .map_err(VerifyError::NeighborSearch)? + { return Err(VerifyError::RightProofMissing); } } @@ -125,142 +143,143 @@ fn verify_non_existence( Ok(()) } -fn is_left_neighbor(spec: &InnerSpec, left: &[InnerOp], right: &[InnerOp]) -> bool { - let Some((mut topleft, mut left)) = left.split_last() else { - return false; - }; - - let Some((mut topright, mut right)) = right.split_last() else { - return false; - }; +/// returns true if `right` is the next possible path right of `left` +/// +/// Find the common suffix from the Left.Path and Right.Path and remove it. We have LPath and RPath now, which must be neighbors. +/// Validate that LPath[len-1] is the left neighbor of RPath[len-1] +/// For step in LPath[0..len-1], validate step is right-most node +/// For step in RPath[0..len-1], validate step is left-most node +fn is_left_neighbor( + spec: &InnerSpec, + left: &[InnerOp], + right: &[InnerOp], +) -> Result { + let (mut topleft, mut left) = left.split_last().ok_or(NeighborSearchError::InvalidPath)?; + let (mut topright, mut right) = right.split_last().ok_or(NeighborSearchError::InvalidPath)?; while topleft.prefix == topright.prefix && topleft.suffix == topright.suffix { - (topleft, left) = if let Some(l) = left.split_last() { - l - } else { - return false; - }; - - (topright, right) = if let Some(r) = right.split_last() { - r - } else { - return false; - }; + (topleft, left) = left.split_last().ok_or(NeighborSearchError::InvalidPath)?; + (topright, right) = right.split_last().ok_or(NeighborSearchError::InvalidPath)?; } - if !is_left_step(spec, topleft, topright) - || !is_right_most(spec, left) - || !is_left_most(spec, right) + if !is_left_step(spec, topleft, topright)? + || !is_right_most(spec, left)? + || !is_left_most(spec, right)? { - return false; + return Ok(false); } - true + Ok(true) } -fn is_left_step(spec: &InnerSpec, left: &InnerOp, right: &InnerOp) -> bool { - let Ok(leftidx) = order_from_padding(spec, left) else { - // TODO(aeryz) - panic!("err") - }; +/// assumes left and right have common parents +/// checks if left is exactly one slot to the left of right +fn is_left_step( + spec: &InnerSpec, + left: &InnerOp, + right: &InnerOp, +) -> Result { + let leftidx = order_from_padding(spec, left)?; - let Ok(rightidx) = order_from_padding(spec, right) else { - // TODO(aeryz) - panic!("err") - }; + let rightidx = order_from_padding(spec, right)?; - return rightidx == leftidx + 1; + Ok(rightidx == leftidx + 1) } -fn is_right_most(spec: &InnerSpec, path: &[InnerOp]) -> bool { - let (min_prefix, max_prefix, suffix) = get_padding(spec, (spec.child_order.len() - 1) as i32); +/// returns true if this is the right-most path in the tree, excluding placeholder (empty child) nodes +fn is_right_most(spec: &InnerSpec, path: &[InnerOp]) -> Result { + let (min_prefix, max_prefix, suffix) = get_padding(spec, (spec.child_order.len() - 1) as i32)?; for step in path { if !has_padding(step, min_prefix, max_prefix, suffix) - && !right_branches_are_empty(spec, step) + && !right_branches_are_empty(spec, step)? { - return false; + return Ok(false); } } - true + Ok(true) } -fn is_left_most(spec: &InnerSpec, path: &[InnerOp]) -> bool { - let (min_prefix, max_prefix, suffix) = get_padding(spec, 0); +/// returns true if this is the left-most path in the tree, excluding placeholder (empty child) nodes +fn is_left_most(spec: &InnerSpec, path: &[InnerOp]) -> Result { + let (min_prefix, max_prefix, suffix) = get_padding(spec, 0)?; for step in path { if !has_padding(step, min_prefix, max_prefix, suffix) - && !left_branches_are_empty(spec, step) + && !left_branches_are_empty(spec, step)? { - return false; + return Ok(false); } } - true + Ok(true) } -fn right_branches_are_empty(spec: &InnerSpec, op: &InnerOp) -> bool { - let Ok(idx) = order_from_padding(spec, op) else { - return false; - }; +/// returns true if the padding bytes correspond to all empty siblings +/// on the right side of a branch, ie. it's a valid placeholder on a rightmost path +fn right_branches_are_empty(spec: &InnerSpec, op: &InnerOp) -> Result { + let idx = order_from_padding(spec, op)?; let right_branches = (spec.child_order.len() as i32) - 1 - idx; if right_branches == 0 { - return false; + return Ok(false); } if (op.suffix.len() as i32) != right_branches * spec.child_size { - return false; + return Ok(false); } for i in 0..right_branches { - let idx = get_position(&spec.child_order, i); + let idx = get_position(&spec.child_order, i)?; let from = (idx * spec.child_size) as usize; if spec.empty_child != &op.suffix[from..from + (spec.child_size as usize)] { - return false; + return Ok(false); } } - true + Ok(true) } -fn left_branches_are_empty(spec: &InnerSpec, op: &InnerOp) -> bool { - let Ok(left_branches) = order_from_padding(spec, op) else { - return false; - }; +/// returns true if the padding bytes correspond to all empty siblings +/// on the left side of a branch, ie. it's a valid placeholder on a leftmost path +fn left_branches_are_empty(spec: &InnerSpec, op: &InnerOp) -> Result { + let left_branches = order_from_padding(spec, op)?; if left_branches == 0 { - return false; + return Ok(false); } let actual_prefix = (op.prefix.len() as i32) - left_branches * spec.child_size; if actual_prefix < 0 { - return false; + return Ok(false); } for i in 0..left_branches { - let idx = get_position(&spec.child_order, i); + let idx = get_position(&spec.child_order, i)?; let from = (actual_prefix + idx * spec.child_size) as usize; if spec.empty_child != &op.prefix[from..from + (spec.child_size as usize)] { - return false; + return Ok(false); } } - true + Ok(true) } -fn order_from_padding(spec: &InnerSpec, inner: &InnerOp) -> Result { - let branch = (0..spec.child_order.len()) - .find(|&branch| { - let (minp, maxp, suffix) = get_padding(spec, branch as i32); - has_padding(inner, minp, maxp, suffix) - }) - .map(|branch| branch as i32); +/// will look at the proof and determine which order it is... +/// So we can see if it is branch 0, 1, 2 etc... to determine neighbors +fn order_from_padding(spec: &InnerSpec, inner: &InnerOp) -> Result { + for branch in 0..spec.child_order.len() { + let (minp, maxp, suffix) = get_padding(spec, branch as i32)?; + if has_padding(inner, minp, maxp, suffix) { + return Ok(branch as i32); + } + } - branch.ok_or(()) + Err(NeighborSearchError::CannotFindValidSpacing) } +/// checks if an op has the expected padding fn has_padding(op: &InnerOp, min_prefix: i32, max_prefix: i32, suffix: i32) -> bool { if (op.prefix.len() as i32) < min_prefix || (op.prefix.len() as i32) > max_prefix { return false; @@ -269,8 +288,9 @@ fn has_padding(op: &InnerOp, min_prefix: i32, max_prefix: i32, suffix: i32) -> b (op.suffix.len() as i32) == suffix } -fn get_padding(spec: &InnerSpec, branch: i32) -> (i32, i32, i32) { - let idx = get_position(&spec.child_order, branch); +/// determines prefix and suffix with the given spec and position in the tree +fn get_padding(spec: &InnerSpec, branch: i32) -> Result<(i32, i32, i32), NeighborSearchError> { + let idx = get_position(&spec.child_order, branch)?; let prefix = idx * spec.child_size; let min_prefix = prefix + spec.min_prefix_length; @@ -278,18 +298,25 @@ fn get_padding(spec: &InnerSpec, branch: i32) -> (i32, i32, i32) { let suffix = (spec.child_order.len() as i32 - 1 - idx) * spec.child_size; - (min_prefix, max_prefix, suffix) + Ok((min_prefix, max_prefix, suffix)) } -fn get_position(order: &[i32], branch: i32) -> i32 { +/// checks where the branch is in the order and returns +/// the index of this branch +fn get_position(order: &[i32], branch: i32) -> Result { if branch < 0 || branch as usize >= order.len() { - // TODO(aeryz): - panic!("invalid branch") + return Err(NeighborSearchError::InvalidBranch { + branch, + order_len: order.len(), + }); } match order.iter().enumerate().find(|(_, &elem)| elem == branch) { - Some((i, _)) => i as i32, - None => panic!("branch not found"), // TODO(aeryz) + Some((i, _)) => Ok(i as i32), + None => Err(NeighborSearchError::BranchNotFoundInOrder { + branch, + order: order.to_vec(), + }), } } diff --git a/lib/unionlabs/src/cosmos/ics23/proof_spec.rs b/lib/unionlabs/src/cosmos/ics23/proof_spec.rs index aef23ab666..8991b9cfa6 100644 --- a/lib/unionlabs/src/cosmos/ics23/proof_spec.rs +++ b/lib/unionlabs/src/cosmos/ics23/proof_spec.rs @@ -15,21 +15,7 @@ pub struct ProofSpec { pub max_depth: i32, // REVIEW: > 0? pub min_depth: i32, -} - -impl ProofSpec { - // TODO(aeryz): move this to ics23 - pub fn compatible(&self, spec: &ProofSpec) -> bool { - self.leaf_spec.hash == spec.leaf_spec.hash - && self.leaf_spec.prehash_key == spec.leaf_spec.prehash_key - && self.leaf_spec.prehash_value == spec.leaf_spec.prehash_value - && self.leaf_spec.length == spec.leaf_spec.length - && self.inner_spec.hash == spec.inner_spec.hash - && self.inner_spec.min_prefix_length == spec.inner_spec.min_prefix_length - && self.inner_spec.max_prefix_length == spec.inner_spec.max_prefix_length - && self.inner_spec.child_size == spec.inner_spec.child_size - && self.inner_spec.child_order.len() == spec.inner_spec.child_order.len() - } + pub prehash_key_before_comparison: bool, } impl TypeUrl for protos::cosmos::ics23::v1::ProofSpec { @@ -47,6 +33,7 @@ impl From for protos::cosmos::ics23::v1::ProofSpec { inner_spec: Some(value.inner_spec.into()), max_depth: value.max_depth, min_depth: value.min_depth, + prehash_key_before_comparison: value.prehash_key_before_comparison, } } } @@ -71,6 +58,7 @@ impl TryFrom for ProofSpec { .map_err(TryFromProofSpecError::InnerSpec)?, max_depth: value.max_depth, min_depth: value.min_depth, + prehash_key_before_comparison: value.prehash_key_before_comparison, }) } } From 993cc1a52dd6ead087417d430aa52055e0d94be8 Mon Sep 17 00:00:00 2001 From: aeryz Date: Fri, 5 Jan 2024 13:32:51 +0300 Subject: [PATCH 13/14] chore: make `sha2` a workspace dependency Signed-off-by: aeryz --- Cargo.lock | 1 + Cargo.toml | 1 + cosmwasm/ucs01-relay/Cargo.toml | 2 +- dictionary.txt | 7 ++++ lib/chain-utils/Cargo.toml | 2 +- lib/ethereum-verifier/Cargo.toml | 2 +- lib/ics23/Cargo.toml | 3 +- lib/ics23/src/ibc_api.rs | 9 +++-- lib/ics23/src/ops/inner_op.rs | 1 - lib/ics23/src/ops/leaf_op.rs | 6 ++-- lib/ics23/src/verify.rs | 34 +++++++++---------- lib/unionlabs/Cargo.toml | 2 +- .../cometbls-light-client/Cargo.toml | 2 +- voyager/Cargo.toml | 2 +- 14 files changed, 41 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cdd9388a4..55336a1059 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2926,6 +2926,7 @@ dependencies = [ name = "ics23" version = "0.1.0" dependencies = [ + "hex", "hex-literal", "prost 0.11.9", "ripemd", diff --git a/Cargo.toml b/Cargo.toml index 72bfeaf07b..39c8c2d423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ ethers = { git = "https://github.com/unionlabs/ethers-rs", branch = "ethers-core ethers-contract-derive = { git = "https://github.com/unionlabs/ethers-rs", branch = "ethers-core-wasm", default-features = false } ethers-core = { git = "https://github.com/unionlabs/ethers-rs", branch = "ethers-core-wasm", default-features = false } ethers-contract-abigen = { git = "https://github.com/unionlabs/ethers-rs", branch = "ethers-core-wasm", default-features = false } +sha2 = { version = "0.10.7", default-features = false } # ethers = { path = "/home/ben/projects/union/ethers-rs/ethers", default-features = false } # ethers-contract-derive = { path = "/home/ben/projects/union/ethers-rs/ethers-contract/ethers-contract-derive", default-features = false } diff --git a/cosmwasm/ucs01-relay/Cargo.toml b/cosmwasm/ucs01-relay/Cargo.toml index 50fd1cfb19..0093d6e4bd 100644 --- a/cosmwasm/ucs01-relay/Cargo.toml +++ b/cosmwasm/ucs01-relay/Cargo.toml @@ -22,7 +22,7 @@ schemars = "0.8" semver = "1" serde = { version = "1.0", default-features = false, features = ["derive"] } thiserror = { version = "1.0" } -sha2 = { version = "0.10", default-features = false } +sha2.workspace = true hex = { version = "0.4", default-features = false } token-factory-api = { workspace = true } ucs01-relay-api = { workspace = true } diff --git a/dictionary.txt b/dictionary.txt index fd5ce0a441..f664ac4e2e 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -325,6 +325,7 @@ groupkeeper groupmodule hasher hasura +hdata healthcheck healthz heredocs @@ -420,9 +421,11 @@ lockb logformat logline logtostderr +lspec lzrndrpkvsjqgk mapstructure maxdome +maxp maxpeers mdsvex merkle @@ -433,6 +436,7 @@ milagro mimc miminum miniserve +minp mintkeeper minttypes miri @@ -531,6 +535,7 @@ pubkey pubkey's pubkeys publint +pvalue pxplusqx pxqx pxrx @@ -620,6 +625,7 @@ subdetail submessage submessages submsg +subroot subsec substituters substores @@ -730,6 +736,7 @@ valnode valoper valoperpub valset +varint vercel verison verkle diff --git a/lib/chain-utils/Cargo.toml b/lib/chain-utils/Cargo.toml index 751685faa6..8de4d1298d 100644 --- a/lib/chain-utils/Cargo.toml +++ b/lib/chain-utils/Cargo.toml @@ -22,7 +22,7 @@ tracing = "0.1.37" tokio = { version = "1.32.0", default-features = false } typenum = { version = "1.16.0", default-features = false, features = ["const-generics", "no_std"] } prost = "0.11.0" -sha2 = "0.10.6" +sha2.workspace = true chrono = { version = "0.4.26", default-features = false, features = ["alloc"] } reqwest = "0.11.20" serde_json = "1.0.107" diff --git a/lib/ethereum-verifier/Cargo.toml b/lib/ethereum-verifier/Cargo.toml index 2964026b4f..52fc4ddabc 100644 --- a/lib/ethereum-verifier/Cargo.toml +++ b/lib/ethereum-verifier/Cargo.toml @@ -21,7 +21,7 @@ milagro_bls = { workspace = true, default-features = false } primitive-types = { version = "0.12.1", default-features = false, features = ["rlp"] } rlp = { version = "0.5.2", default-features = false, features = ["derive"] } serde = { version = "1.0.103", default-features = false, features = ["derive", "alloc"] } -sha2 = { version = "0.10", default-features = false } +sha2.workspace = true sha3 = { version = "0.10", default-features = false } trie-db = { version = "0.27.1", default-features = false } typenum = { version = "1.16.0", features = ["const-generics", "no_std"] } diff --git a/lib/ics23/Cargo.toml b/lib/ics23/Cargo.toml index ab583e0585..8dc1368f63 100644 --- a/lib/ics23/Cargo.toml +++ b/lib/ics23/Cargo.toml @@ -9,8 +9,9 @@ edition = "2021" unionlabs = { workspace = true, default-features = false } thiserror = { version = "1.0", package = "thiserror-core", default-features = false } prost = { version = "0.11.9", default-features = false, features = ["std"] } -sha2 = { version = "0.10.7", default-features = false } +sha2.workspace = true ripemd = { version = "0.1.3", default-features = false } +hex = { version = "0.4.3" } [dev-dependencies] hex-literal = "0.4.1" diff --git a/lib/ics23/src/ibc_api.rs b/lib/ics23/src/ibc_api.rs index c3d5f3bbb2..cc12546e0d 100644 --- a/lib/ics23/src/ibc_api.rs +++ b/lib/ics23/src/ibc_api.rs @@ -19,9 +19,8 @@ pub enum VerifyMembershipError { RootCalculation(existence_proof::CalculateRootError), #[error("{0}")] InnerVerification(verify::VerifyMembershipError), - // TODO(aeryz): print hex - #[error("calculated root ({calculated:?}) does not match the given ({given:?}) value")] - InvalidRoot { given: Vec, calculated: Vec }, + #[error("calculated root ({calculated}) does not match the given ({given}) value")] + InvalidRoot { given: String, calculated: String }, #[error("expected the size of proofs to be ({expected}), got ({got})")] InvalidProofsLength { expected: usize, got: usize }, #[error("expected the size of key path to be ({expected}), got ({got})")] @@ -151,8 +150,8 @@ fn verify_chained_membership_proof( Ok(()) } else { Err(VerifyMembershipError::InvalidRoot { - given: value, - calculated: root.into(), + given: hex::encode(&value), + calculated: hex::encode(root), }) } }) diff --git a/lib/ics23/src/ops/inner_op.rs b/lib/ics23/src/ops/inner_op.rs index 54db288bf3..7381dba89f 100644 --- a/lib/ics23/src/ops/inner_op.rs +++ b/lib/ics23/src/ops/inner_op.rs @@ -58,7 +58,6 @@ pub fn check_against_spec( }); } - // TODO(aeryz): check if min_prefix_length > 0 if inner_op.prefix.len() < spec.inner_spec.min_prefix_length as usize { return Err(SpecMismatchError::InnerOpPrefixTooShort { prefix_len: inner_op.prefix.len(), diff --git a/lib/ics23/src/ops/leaf_op.rs b/lib/ics23/src/ops/leaf_op.rs index 90653b0e54..1fc6a0fd56 100644 --- a/lib/ics23/src/ops/leaf_op.rs +++ b/lib/ics23/src/ops/leaf_op.rs @@ -21,7 +21,7 @@ pub enum SpecMismatchError { #[error("bad prefix remaining {0} bytes after reading")] BadPrefix(usize), #[error("prefix ({prefix:?}) is not the prefix of ({full:?})")] - PrefixMismatch { full: Vec, prefix: Vec }, + PrefixMismatch { full: String, prefix: String }, #[error("validate iavl ops ({0})")] ValidateIavlOps(ValidateIavlOpsError), } @@ -70,8 +70,8 @@ pub fn check_against_spec(leaf_op: &LeafOp, spec: &ProofSpec) -> Result<(), Spec if !leaf_op.prefix.starts_with(&lspec.prefix) { return Err(SpecMismatchError::PrefixMismatch { - full: leaf_op.prefix.clone().into_owned(), - prefix: lspec.prefix.clone().into_owned(), + full: hex::encode(&leaf_op.prefix), + prefix: hex::encode(&lspec.prefix), }); } diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs index 9a247a676b..ad25f60e9c 100644 --- a/lib/ics23/src/verify.rs +++ b/lib/ics23/src/verify.rs @@ -154,15 +154,15 @@ fn is_left_neighbor( left: &[InnerOp], right: &[InnerOp], ) -> Result { - let (mut topleft, mut left) = left.split_last().ok_or(NeighborSearchError::InvalidPath)?; - let (mut topright, mut right) = right.split_last().ok_or(NeighborSearchError::InvalidPath)?; + let (mut top_left, mut left) = left.split_last().ok_or(NeighborSearchError::InvalidPath)?; + let (mut top_right, mut right) = right.split_last().ok_or(NeighborSearchError::InvalidPath)?; - while topleft.prefix == topright.prefix && topleft.suffix == topright.suffix { - (topleft, left) = left.split_last().ok_or(NeighborSearchError::InvalidPath)?; - (topright, right) = right.split_last().ok_or(NeighborSearchError::InvalidPath)?; + while top_left.prefix == top_right.prefix && top_left.suffix == top_right.suffix { + (top_left, left) = left.split_last().ok_or(NeighborSearchError::InvalidPath)?; + (top_right, right) = right.split_last().ok_or(NeighborSearchError::InvalidPath)?; } - if !is_left_step(spec, topleft, topright)? + if !is_left_step(spec, top_left, top_right)? || !is_right_most(spec, left)? || !is_left_most(spec, right)? { @@ -179,11 +179,11 @@ fn is_left_step( left: &InnerOp, right: &InnerOp, ) -> Result { - let leftidx = order_from_padding(spec, left)?; + let left_idx = order_from_padding(spec, left)?; - let rightidx = order_from_padding(spec, right)?; + let right_idx = order_from_padding(spec, right)?; - Ok(rightidx == leftidx + 1) + Ok(right_idx == left_idx + 1) } /// returns true if this is the right-most path in the tree, excluding placeholder (empty child) nodes @@ -367,7 +367,7 @@ mod tests { use super::*; use crate::proof_specs::TENDERMINT_PROOF_SPEC; - fn ensure_existant( + fn ensure_existent( proof: &[u8], root: &[u8], key: &[u8], @@ -388,7 +388,7 @@ mod tests { ) } - fn ensure_nonexistant( + fn ensure_non_existent( proof: &[u8], root: &[u8], key: &[u8], @@ -409,7 +409,7 @@ mod tests { let key = hex!("303142424373615a55715146735259436c6a5767"); let value = hex!("76616c75655f666f725f303142424373615a55715146735259436c6a5767"); - assert_eq!(ensure_existant(&proof, &root, &key, &value), Ok(())); + assert_eq!(ensure_existent(&proof, &root, &key, &value), Ok(())); } #[test] @@ -419,7 +419,7 @@ mod tests { let key = hex!("513334656d766f39447145585735325257523835"); let value = hex!("76616c75655f666f725f513334656d766f39447145585735325257523835"); - assert_eq!(ensure_existant(&proof, &root, &key, &value), Ok(())); + assert_eq!(ensure_existent(&proof, &root, &key, &value), Ok(())); } #[test] @@ -429,7 +429,7 @@ mod tests { let key = hex!("7a785a4e6b534c64634d655657526c7658456644"); let value = hex!("76616c75655f666f725f7a785a4e6b534c64634d655657526c7658456644"); - assert_eq!(ensure_existant(&proof, &root, &key, &value), Ok(())); + assert_eq!(ensure_existent(&proof, &root, &key, &value), Ok(())); } // https://github.com/cosmos/ics23/blob/b1abd8678aab07165efd453c96796a179eb3131f/testdata/tendermint/nonexist_left.json @@ -439,7 +439,7 @@ mod tests { let root = hex!("4e2e78d2da505b7d0b00fda55a4b048eed9a23a7f7fc3d801f20ce4851b442aa"); let key = hex!("01010101"); - assert_eq!(ensure_nonexistant(&proof, &root, &key), Ok(())) + assert_eq!(ensure_non_existent(&proof, &root, &key), Ok(())) } // https://github.com/cosmos/ics23/blob/b1abd8678aab07165efd453c96796a179eb3131f/testdata/tendermint/nonexist_middle.json @@ -449,7 +449,7 @@ mod tests { let root = hex!("4bf28d948566078c5ebfa86db7471c1541eab834f539037075b9f9e3b1c72cfc"); let key = hex!("544f31483668784a4b667136547a56767649ffff"); - assert_eq!(ensure_nonexistant(&proof, &root, &key), Ok(())) + assert_eq!(ensure_non_existent(&proof, &root, &key), Ok(())) } // https://github.com/cosmos/ics23/blob/b1abd8678aab07165efd453c96796a179eb3131f/testdata/tendermint/nonexist_right.json @@ -459,6 +459,6 @@ mod tests { let root = hex!("83952b0b17e64c862628bcc1277e7f8847589af794ed5a855339281d395ec04f"); let key = hex!("ffffffff"); - assert_eq!(ensure_nonexistant(&proof, &root, &key), Ok(())) + assert_eq!(ensure_non_existent(&proof, &root, &key), Ok(())) } } diff --git a/lib/unionlabs/Cargo.toml b/lib/unionlabs/Cargo.toml index cef0e9cf2c..98229ee2f6 100644 --- a/lib/unionlabs/Cargo.toml +++ b/lib/unionlabs/Cargo.toml @@ -21,7 +21,7 @@ k256 = { version = "0.13.1", default-features = false, features = ["schnorr"] } subtle-encoding = { version = "0.5.1", default-features = false, features = ["bech32-preview"] } ripemd = { version = "0.1.3", default-features = false } rs_merkle = "1.4.1" -sha2 = { version = "0.10.7", default-features = false } +sha2.workspace = true serde = { version = "1.0.164", default-features = false, features = ["derive"] } primitive-types = { version = "0.12.1", default-features = false, features = ["serde_no_std"] } hex = { version = "0.4.3", default-features = false } diff --git a/light-clients/cometbls-light-client/Cargo.toml b/light-clients/cometbls-light-client/Cargo.toml index 255d49a751..c8ab545e4d 100644 --- a/light-clients/cometbls-light-client/Cargo.toml +++ b/light-clients/cometbls-light-client/Cargo.toml @@ -18,7 +18,7 @@ schemars = { version = "0.8.3", default-features = false } serde = { version = "1.0.103", default-features = false, features = ["derive"] } serde-json-wasm = { version = "*", default-features = false } sha3 = { version = "0.10.8", default-features = false } -sha2 = { version = "0.10.8", default-features = false } +sha2.workspace = true ripemd = { version = "0.1.3", default-features = false } thiserror = { version = "1.0.26", default-features = false } protos = { workspace = true, default-features = false, features = ["proto_full", "std"] } diff --git a/voyager/Cargo.toml b/voyager/Cargo.toml index 81d45c1d30..8bc8e097f0 100644 --- a/voyager/Cargo.toml +++ b/voyager/Cargo.toml @@ -16,7 +16,7 @@ prost = "0.11.0" reqwest = { version = "0.11.17", default-features = false, features = ["tokio-rustls"] } ripemd = "0.1.3" serde_json = "1.0.96" -sha2 = "0.10.6" +sha2.workspace = true subtle-encoding = { version = "0.5.1", features = ["bech32-preview"] } tendermint = { workspace = true } tendermint-proto = { workspace = true } From 37eab01d06a0bef7ba8224356466a6c471f7a6d6 Mon Sep 17 00:00:00 2001 From: aeryz Date: Sat, 6 Jan 2024 14:19:26 +0300 Subject: [PATCH 14/14] chore(ics23): more improvements Signed-off-by: aeryz --- Cargo.lock | 2 +- Cargo.toml | 4 +- lib/chain-utils/src/cosmos.rs | 7 +- lib/ics23/Cargo.toml | 2 +- lib/ics23/src/existence_proof.rs | 10 +-- lib/ics23/src/ibc_api.rs | 80 ++++++++++--------- lib/ics23/src/ops/hash_op.rs | 29 +++---- lib/ics23/src/ops/inner_op.rs | 18 +++-- lib/ics23/src/ops/leaf_op.rs | 33 ++++++-- lib/ics23/src/ops/length_op.rs | 24 +++--- lib/ics23/src/ops/mod.rs | 18 ++--- lib/ics23/src/verify.rs | 57 ++++++++----- .../ics23/compressed_non_existence_proof.rs | 4 +- .../src/cosmos/ics23/non_existence_proof.rs | 4 +- .../cometbls-light-client/src/client.rs | 4 +- .../cometbls-light-client/src/errors.rs | 2 +- 16 files changed, 166 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55336a1059..7675f16924 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2926,10 +2926,10 @@ dependencies = [ name = "ics23" version = "0.1.0" dependencies = [ - "hex", "hex-literal", "prost 0.11.9", "ripemd", + "serde-utils", "sha2 0.10.8", "thiserror-core", "unionlabs", diff --git a/Cargo.toml b/Cargo.toml index 39c8c2d423..e8ce01a102 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ members = [ "cosmwasm/*", "hubble", "tools/parse-wasm-client-type", - "zerg", "lib/ics23", + "zerg", ] resolver = "2" @@ -65,7 +65,7 @@ ucs01-relay-api = { path = "cosmwasm/ucs01-relay-api", default-features = false voyager-message = { path = "voyager/voyager-message", default-features = false } derive_more = { version = "0.99.17", default-features = false } ucs01-relay = { path = "cosmwasm/ucs01-relay", default-features = false } -ics23 = { path = "lib/ics23" } +ics23 = { path = "lib/ics23", default-features = false } # external dependencies diff --git a/lib/chain-utils/src/cosmos.rs b/lib/chain-utils/src/cosmos.rs index 0d7e737cb2..2c1d7e3f07 100644 --- a/lib/chain-utils/src/cosmos.rs +++ b/lib/chain-utils/src/cosmos.rs @@ -2,6 +2,7 @@ use std::{num::ParseIntError, sync::Arc}; use bip32::secp256k1::ecdsa; use futures::{stream, Future, FutureExt, Stream, StreamExt}; +use ics23::ibc_api::SDK_SPECS; use serde::{Deserialize, Serialize}; use tendermint_rpc::{Client, WebSocketClient, WebSocketClientUrl}; use unionlabs::{ @@ -204,11 +205,7 @@ impl Chain for Cosmos { revision_number: self.chain_revision, revision_height: height.value(), }, - proof_specs: [ - ics23::ibc_api::IAVL_PROOF_SPEC, - ics23::ibc_api::TENDERMINT_PROOF_SPEC, - ] - .into(), + proof_specs: SDK_SPECS.into(), upgrade_path: vec!["upgrade".into(), "upgradedIBCState".into()], allow_update_after_expiry: false, allow_update_after_misbehavior: false, diff --git a/lib/ics23/Cargo.toml b/lib/ics23/Cargo.toml index 8dc1368f63..6ead8b3332 100644 --- a/lib/ics23/Cargo.toml +++ b/lib/ics23/Cargo.toml @@ -11,7 +11,7 @@ thiserror = { version = "1.0", package = "thiserror-core", default-features = fa prost = { version = "0.11.9", default-features = false, features = ["std"] } sha2.workspace = true ripemd = { version = "0.1.3", default-features = false } -hex = { version = "0.4.3" } +serde-utils.workspace = true [dev-dependencies] hex-literal = "0.4.1" diff --git a/lib/ics23/src/existence_proof.rs b/lib/ics23/src/existence_proof.rs index 7a1ddf522d..0935c8aac4 100644 --- a/lib/ics23/src/existence_proof.rs +++ b/lib/ics23/src/existence_proof.rs @@ -63,15 +63,15 @@ pub(crate) fn calculate( existence_proof: &ExistenceProof, spec: Option<&ProofSpec>, ) -> Result, CalculateRootError> { - let mut res = leaf_op::apply( + let res = leaf_op::apply( &existence_proof.leaf, &existence_proof.key, &existence_proof.value, ) .map_err(CalculateRootError::LeafOpHash)?; - for step in &existence_proof.path { - res = inner_op::apply(step, res).map_err(CalculateRootError::InnerOpHash)?; + existence_proof.path.iter().try_fold(res, |res, step| { + let res = inner_op::apply(step, res).map_err(CalculateRootError::InnerOpHash)?; if let Some(proof_spec) = spec { if res.len() > proof_spec.inner_spec.child_size as usize @@ -80,7 +80,7 @@ pub(crate) fn calculate( return Err(CalculateRootError::InnerOpHashAndSpecMismatch); } } - } - Ok(res) + Ok(res) + }) } diff --git a/lib/ics23/src/ibc_api.rs b/lib/ics23/src/ibc_api.rs index cc12546e0d..6844c9ddb6 100644 --- a/lib/ics23/src/ibc_api.rs +++ b/lib/ics23/src/ibc_api.rs @@ -11,7 +11,7 @@ use crate::{ verify::{self}, }; -pub const SDK_SPECS: [&'static ProofSpec; 2] = [&IAVL_PROOF_SPEC, &TENDERMINT_PROOF_SPEC]; +pub const SDK_SPECS: [ProofSpec; 2] = [IAVL_PROOF_SPEC, TENDERMINT_PROOF_SPEC]; #[derive(Debug, PartialEq, thiserror::Error)] pub enum VerifyMembershipError { @@ -19,23 +19,25 @@ pub enum VerifyMembershipError { RootCalculation(existence_proof::CalculateRootError), #[error("{0}")] InnerVerification(verify::VerifyMembershipError), - #[error("calculated root ({calculated}) does not match the given ({given}) value")] - InvalidRoot { given: String, calculated: String }, - #[error("expected the size of proofs to be ({expected}), got ({got})")] - InvalidProofsLength { expected: usize, got: usize }, - #[error("expected the size of key path to be ({expected}), got ({got})")] - InvalidKeyPathLength { expected: usize, got: usize }, + #[error("calculated root ({calculated}) does not match the given ({given}) value", calculated = serde_utils::to_hex(calculated), given = serde_utils::to_hex(found))] + InvalidRoot { found: Vec, calculated: Vec }, + #[error("expected the size of proofs to be ({expected}), found ({found})")] + InvalidProofsLength { expected: usize, found: usize }, + #[error("expected the size of key path to be ({expected}), found ({found})")] + InvalidKeyPathLength { expected: usize, found: usize }, #[error("proof type is expected to be `Exist`")] InvalidProofType, #[error("could not retrieve the key due to invalid indexing")] InvalidIndexing, #[error("nonexistence proof has empty left and right proof")] EmptyNonExistenceProof, + #[error("given proof is empty")] + EmptyProof, } pub fn verify_membership( - proof: MerkleProof, - specs: &[&ProofSpec], + proof: &MerkleProof, + specs: &[ProofSpec], consensus_root: &MerkleRoot, path: MerklePath, value: Vec, @@ -43,21 +45,21 @@ pub fn verify_membership( if proof.proofs.len() != specs.len() { return Err(VerifyMembershipError::InvalidProofsLength { expected: specs.len(), - got: proof.proofs.len(), + found: proof.proofs.len(), }); } if path.key_path.len() != specs.len() { return Err(VerifyMembershipError::InvalidKeyPathLength { expected: specs.len(), - got: path.key_path.len(), + found: path.key_path.len(), }); } verify_chained_membership_proof( consensus_root.hash.as_ref(), specs, - proof.proofs, + &proof.proofs, path, value, 0, @@ -65,22 +67,27 @@ pub fn verify_membership( } pub fn verify_non_membership( - proof: MerkleProof, - specs: &[&ProofSpec], + proof: &MerkleProof, + specs: &[ProofSpec], consensus_root: &MerkleRoot, path: MerklePath, ) -> Result<(), VerifyMembershipError> { + // this will also assert `specs` and `path.key_path` is not empty + if proof.proofs.is_empty() { + return Err(VerifyMembershipError::EmptyProof); + } + if proof.proofs.len() != specs.len() { return Err(VerifyMembershipError::InvalidProofsLength { expected: specs.len(), - got: proof.proofs.len(), + found: proof.proofs.len(), }); } if path.key_path.len() != specs.len() { return Err(VerifyMembershipError::InvalidKeyPathLength { expected: specs.len(), - got: path.key_path.len(), + found: path.key_path.len(), }); } @@ -88,27 +95,22 @@ pub fn verify_non_membership( return Err(VerifyMembershipError::InvalidProofType); }; - let subroot = if let Some(ep) = &nonexist.left { - existence_proof::calculate_root(ep) - } else if let Some(ep) = &nonexist.right { - existence_proof::calculate_root(ep) - } else { - return Err(VerifyMembershipError::EmptyNonExistenceProof); - } - .map_err(VerifyMembershipError::RootCalculation)?; - - let key = path - .key_path - .get(path.key_path.len() - 1) - .ok_or(VerifyMembershipError::InvalidIndexing)?; + // Even both are `Some`, still calculate the left branch + let subroot = match (&nonexist.left, &nonexist.right) { + (Some(ep), None) | (None, Some(ep)) | (Some(ep), Some(_)) => { + existence_proof::calculate_root(ep).map_err(VerifyMembershipError::RootCalculation)? + } + _ => return Err(VerifyMembershipError::EmptyNonExistenceProof), + }; - verify::verify_non_membership(specs[0], &subroot, &nonexist, key.as_bytes()) + let key = path.key_path[path.key_path.len() - 1].as_bytes(); + verify::verify_non_membership(&specs[0], &subroot, nonexist, key) .map_err(VerifyMembershipError::InnerVerification)?; verify_chained_membership_proof( consensus_root.hash.as_ref(), specs, - proof.proofs, + &proof.proofs, path, subroot, 1, @@ -117,14 +119,14 @@ pub fn verify_non_membership( fn verify_chained_membership_proof( root: &[u8], - specs: &[&ProofSpec], - proofs: Vec, + specs: &[ProofSpec], + proofs: &[CommitmentProof], keys: MerklePath, value: Vec, index: usize, ) -> Result<(), VerifyMembershipError> { proofs - .into_iter() + .iter() .enumerate() .skip(index) .try_fold(value, |value, (i, proof)| { @@ -140,7 +142,7 @@ fn verify_chained_membership_proof( .get(keys.key_path.len() - 1 - i) .ok_or(VerifyMembershipError::InvalidIndexing)?; - verify::verify_membership(specs[i], &subroot, existence_proof, key.as_bytes(), &value) + verify::verify_membership(&specs[i], &subroot, existence_proof, key.as_bytes(), &value) .map_err(VerifyMembershipError::InnerVerification)?; Ok(subroot) @@ -150,8 +152,8 @@ fn verify_chained_membership_proof( Ok(()) } else { Err(VerifyMembershipError::InvalidRoot { - given: hex::encode(&value), - calculated: hex::encode(root), + found: value, + calculated: root.to_vec(), }) } }) @@ -182,7 +184,7 @@ mod tests { }; let proofs = MerkleProof::try_from_proto_bytes(&proof).unwrap(); verify_membership( - proofs, + &proofs, &SDK_SPECS, &MerkleRoot { hash: H256::try_from(root).unwrap(), @@ -321,7 +323,7 @@ mod tests { assert_eq!( verify_non_membership( - proof, + &proof, &SDK_SPECS, &root, MerklePath { diff --git a/lib/ics23/src/ops/hash_op.rs b/lib/ics23/src/ops/hash_op.rs index 0b684b8345..72ff968288 100644 --- a/lib/ics23/src/ops/hash_op.rs +++ b/lib/ics23/src/ops/hash_op.rs @@ -1,16 +1,16 @@ use sha2::Digest; use unionlabs::cosmos::ics23::hash_op::HashOp; -pub fn do_hash_or_noop(hash_op: HashOp, preimage: &[u8]) -> Vec { - if hash_op == HashOp::NoHash { - return preimage.into(); - } - - do_hash(hash_op, preimage) +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum HashError { + #[error( + "supported hash ops are ([Sha256, Sha512, Ripemd160, Bitcoin, Sha512256]) but found ({0})" + )] + UnsupportedOp(HashOp), } -pub fn do_hash(hash_op: HashOp, preimage: &[u8]) -> Vec { - match hash_op { +pub fn do_hash(hash_op: HashOp, preimage: &[u8]) -> Result, HashError> { + let hash = match hash_op { HashOp::Sha256 => sha2::Sha256::new() .chain_update(preimage) .finalize() @@ -24,18 +24,15 @@ pub fn do_hash(hash_op: HashOp, preimage: &[u8]) -> Vec { .finalize() .to_vec(), HashOp::Bitcoin => ripemd::Ripemd160::new() - .chain_update( - sha2::Sha256::new() - .chain_update(preimage) - .finalize() - .to_vec(), - ) + .chain_update(sha2::Sha256::new().chain_update(preimage).finalize()) .finalize() .to_vec(), HashOp::Sha512256 => sha2::Sha512_256::new() .chain_update(preimage) .finalize() .to_vec(), - _ => panic!("unsupported"), - } + op => return Err(HashError::UnsupportedOp(op)), + }; + + Ok(hash) } diff --git a/lib/ics23/src/ops/inner_op.rs b/lib/ics23/src/ops/inner_op.rs index 7381dba89f..ea1cc7987e 100644 --- a/lib/ics23/src/ops/inner_op.rs +++ b/lib/ics23/src/ops/inner_op.rs @@ -1,19 +1,22 @@ use unionlabs::cosmos::ics23::{hash_op::HashOp, inner_op::InnerOp, proof_spec::ProofSpec}; use super::{hash_op, validate_iavl_ops}; -use crate::proof_specs::{self, IAVL_PROOF_SPEC}; +use crate::{ + hash_op::HashError, + proof_specs::{self, IAVL_PROOF_SPEC}, +}; #[derive(Debug, PartialEq, thiserror::Error)] pub enum SpecMismatchError { #[error("unexpected hash op ({0:?})")] UnexpectedHashOp(HashOp), - #[error("prefix ({prefix:?}) is not the prefix of ({full:?})")] + #[error("prefix ({prefix}) is not the prefix of ({full})", prefix = serde_utils::to_hex(prefix), full = serde_utils::to_hex(full))] PrefixMismatch { full: Vec, prefix: Vec }, #[error("inner prefix too short, got ({prefix_len}) while the min length is ({min_len})")] InnerOpPrefixTooShort { prefix_len: usize, min_len: i32 }, #[error("inner prefix too long, got ({prefix_len}) while the max length is ({max_len})")] InnerOpPrefixTooLong { prefix_len: usize, max_len: i32 }, - #[error("malformed inner op suffix ({0:?})")] + #[error("malformed inner op suffix ({0})")] InnerOpSuffixMalformed(usize), #[error("validate iavl ops ({0})")] ValidateIavlOps(super::ValidateIavlOpsError), @@ -25,6 +28,8 @@ pub enum SpecMismatchError { pub enum ApplyError { #[error("inner op needs a child value")] InnerOpNeedsChildValue, + #[error(transparent)] + Hash(#[from] HashError), } pub fn check_against_spec( @@ -65,9 +70,8 @@ pub fn check_against_spec( }); } - let max_prefix_length = (spec.inner_spec.max_prefix_length as usize - + (spec.inner_spec.child_order.len() - 1) * spec.inner_spec.child_size as usize) - as usize; + let max_prefix_length = spec.inner_spec.max_prefix_length as usize + + (spec.inner_spec.child_order.len() - 1) * spec.inner_spec.child_size as usize; if inner_op.prefix.len() > max_prefix_length { return Err(SpecMismatchError::InnerOpPrefixTooLong { @@ -94,5 +98,5 @@ pub fn apply(inner_op: &InnerOp, child: Vec) -> Result, ApplyError> preimage.extend_from_slice(&child); preimage.extend_from_slice(&inner_op.suffix); - Ok(hash_op::do_hash(inner_op.hash, &preimage)) + Ok(hash_op::do_hash(inner_op.hash, &preimage)?) } diff --git a/lib/ics23/src/ops/leaf_op.rs b/lib/ics23/src/ops/leaf_op.rs index 1fc6a0fd56..6af8135814 100644 --- a/lib/ics23/src/ops/leaf_op.rs +++ b/lib/ics23/src/ops/leaf_op.rs @@ -1,9 +1,12 @@ +use std::borrow::Cow; + use unionlabs::cosmos::ics23::{ hash_op::HashOp, leaf_op::LeafOp, length_op::LengthOp, proof_spec::ProofSpec, }; use super::{hash_op, length_op, validate_iavl_ops}; use crate::{ + hash_op::HashError, proof_specs::{self, IAVL_PROOF_SPEC}, ValidateIavlOpsError, }; @@ -20,8 +23,12 @@ pub enum SpecMismatchError { UnexpectedLengthOp(LengthOp), #[error("bad prefix remaining {0} bytes after reading")] BadPrefix(usize), - #[error("prefix ({prefix:?}) is not the prefix of ({full:?})")] - PrefixMismatch { full: String, prefix: String }, + #[error( + "prefix ({prefix}) is not the prefix of ({full})", + prefix = serde_utils::to_hex(prefix), + full = serde_utils::to_hex(full) + )] + PrefixMismatch { full: Vec, prefix: Vec }, #[error("validate iavl ops ({0})")] ValidateIavlOps(ValidateIavlOpsError), } @@ -34,6 +41,8 @@ pub enum ApplyError { ValueNeeded, #[error("apply leaf ({0:?})")] LeafData(super::length_op::ApplyError), + #[error(transparent)] + Hash(#[from] HashError), } pub fn check_against_spec(leaf_op: &LeafOp, spec: &ProofSpec) -> Result<(), SpecMismatchError> { @@ -70,8 +79,8 @@ pub fn check_against_spec(leaf_op: &LeafOp, spec: &ProofSpec) -> Result<(), Spec if !leaf_op.prefix.starts_with(&lspec.prefix) { return Err(SpecMismatchError::PrefixMismatch { - full: hex::encode(&leaf_op.prefix), - prefix: hex::encode(&lspec.prefix), + full: leaf_op.prefix.to_vec(), + prefix: lspec.prefix.to_vec(), }); } @@ -96,10 +105,18 @@ pub fn apply(leaf_op: &LeafOp, key: &[u8], value: &[u8]) -> Result, Appl data.extend_from_slice(&pkey); data.extend_from_slice(&pvalue); - Ok(hash_op::do_hash(leaf_op.hash, &data)) + Ok(hash_op::do_hash(leaf_op.hash, &data)?) } -fn prepare_data(leaf_op: &LeafOp, hash_op: HashOp, data: &[u8]) -> Result, ApplyError> { - let hdata = hash_op::do_hash_or_noop(hash_op, data); - length_op::apply(&leaf_op.length, &hdata).map_err(ApplyError::LeafData) +fn prepare_data<'a>( + leaf_op: &LeafOp, + hash_op: HashOp, + data: &'a [u8], +) -> Result, ApplyError> { + let hashed_data = if hash_op == HashOp::NoHash { + Cow::Borrowed(data) + } else { + Cow::Owned(hash_op::do_hash(hash_op, data)?) + }; + length_op::apply(&leaf_op.length, hashed_data).map_err(ApplyError::LeafData) } diff --git a/lib/ics23/src/ops/length_op.rs b/lib/ics23/src/ops/length_op.rs index 01a9d8507f..0618dce548 100644 --- a/lib/ics23/src/ops/length_op.rs +++ b/lib/ics23/src/ops/length_op.rs @@ -1,40 +1,42 @@ +use std::borrow::Cow; + use unionlabs::cosmos::ics23::length_op::LengthOp; #[derive(Debug, PartialEq, thiserror::Error)] pub enum ApplyError { - #[error("required 32 bytes, got ({0:?})")] + #[error("required 32 bytes, found ({0})")] Required32Bytes(usize), - #[error("required 64 bytes, ({0:?})")] + #[error("required 64 bytes, ({0})")] Required64Bytes(usize), - #[error("unsupported op ({0:?})")] + #[error("unsupported op ({0})")] UnsupportedOp(LengthOp), } -pub fn apply(length_op: &LengthOp, data: &[u8]) -> Result, ApplyError> { +pub fn apply<'a>(length_op: &LengthOp, data: Cow<'a, [u8]>) -> Result, ApplyError> { match length_op { - LengthOp::NoPrefix => Ok(data.to_vec()), + LengthOp::NoPrefix => Ok(data), LengthOp::VarProto => { let mut len = Vec::new(); prost::encoding::encode_varint(data.len() as u64, &mut len); - len.extend_from_slice(data); - Ok(len) + len.extend_from_slice(&data); + Ok(len.into()) } LengthOp::Require32Bytes => { if data.len() != 32 { return Err(ApplyError::Required32Bytes(data.len()))?; } - Ok(data.into()) + Ok(data) } LengthOp::Require64Bytes => { if data.len() != 64 { return Err(ApplyError::Required64Bytes(data.len())); } - Ok(data.into()) + Ok(data) } LengthOp::Fixed32Little => { let mut d = (data.len() as u32).to_le_bytes().to_vec(); - d.extend_from_slice(data); - Ok(d) + d.extend_from_slice(&data); + Ok(d.into()) } op => Err(ApplyError::UnsupportedOp(*op)), } diff --git a/lib/ics23/src/ops/mod.rs b/lib/ics23/src/ops/mod.rs index d4334e7b2a..b796f23344 100644 --- a/lib/ics23/src/ops/mod.rs +++ b/lib/ics23/src/ops/mod.rs @@ -5,19 +5,19 @@ pub mod length_op; #[derive(Debug, PartialEq, thiserror::Error)] pub enum ValidateIavlOpsError { - #[error("height ({height}) is smaller than the min. height ({min_height})")] + #[error("height ({height}) is smaller than the minimum height ({min_height})")] HeightTooShort { height: i64, min_height: i32 }, #[error("size ({0}) is expected to be non-negative integer")] NegativeSize(i64), #[error("version ({0}) is expected to be non-negative integer")] NegativeVersion(i64), - #[error("min. height cannot be negative")] + #[error("minimum height cannot be negative")] NegativeMinHeight(i32), - #[error("prost decode ({0:?})")] - Decode(prost::DecodeError), + #[error(transparent)] + Decode(#[from] prost::DecodeError), } -pub(crate) fn read_varint<'a>(mut buffer: &'a [u8]) -> Result<(&'a [u8], i64), prost::DecodeError> { +pub(crate) fn read_varint(mut buffer: &[u8]) -> Result<(&[u8], i64), prost::DecodeError> { let ux = prost::encoding::decode_varint(&mut buffer)?; let mut x = (ux >> 1) as i64; if ux & 1 != 0 { @@ -30,23 +30,23 @@ pub(crate) fn validate_iavl_ops( prefix: &[u8], min_height: i32, ) -> Result { - let mut prefix = prefix.to_vec(); + let prefix = prefix.to_vec(); if min_height < 0 { return Err(ValidateIavlOpsError::NegativeMinHeight(min_height)); } - let (mut buffer, height) = read_varint(&mut prefix).map_err(ValidateIavlOpsError::Decode)?; + let (buffer, height) = read_varint(&prefix)?; if height < min_height as i64 { return Err(ValidateIavlOpsError::HeightTooShort { height, min_height }); } - let (mut buffer, size) = read_varint(&mut buffer).map_err(ValidateIavlOpsError::Decode)?; + let (buffer, size) = read_varint(buffer)?; if size < 0 { return Err(ValidateIavlOpsError::NegativeSize(size)); } - let (buffer, version) = read_varint(&mut buffer).map_err(ValidateIavlOpsError::Decode)?; + let (buffer, version) = read_varint(buffer)?; if version < 0 { return Err(ValidateIavlOpsError::NegativeVersion(size)); } diff --git a/lib/ics23/src/verify.rs b/lib/ics23/src/verify.rs index ad25f60e9c..b9ec3bf355 100644 --- a/lib/ics23/src/verify.rs +++ b/lib/ics23/src/verify.rs @@ -1,11 +1,13 @@ +use std::borrow::Cow; + use unionlabs::cosmos::ics23::{ - existence_proof::ExistenceProof, inner_op::InnerOp, inner_spec::InnerSpec, + existence_proof::ExistenceProof, hash_op::HashOp, inner_op::InnerOp, inner_spec::InnerSpec, non_existence_proof::NonExistenceProof, proof_spec::ProofSpec, }; use crate::{ existence_proof::{self, CalculateRootError, SpecMismatchError}, - hash_op::do_hash_or_noop, + hash_op::{do_hash, HashError}, }; #[derive(Debug, PartialEq, thiserror::Error)] @@ -26,7 +28,7 @@ pub enum VerifyError { }, #[error("root calculation ({0})")] RootCalculation(CalculateRootError), - #[error("calculated and given root doesn't match ({calculated_root:?}, {given_root:?})")] + #[error("calculated and given root doesn't match ({calculated_root}, {given_root})", calculated_root = serde_utils::to_hex(calculated_root), given_root = serde_utils::to_hex(given_root))] CalculatedAndGivenRootMismatch { calculated_root: Vec, given_root: Vec, @@ -43,6 +45,8 @@ pub enum VerifyError { BothProofsMissing, #[error("neighbor search failure ({0})")] NeighborSearch(NeighborSearchError), + #[error(transparent)] + Hash(#[from] HashError), } #[derive(Debug, PartialEq, thiserror::Error)] @@ -71,7 +75,7 @@ pub fn verify_non_membership( proof: &NonExistenceProof, key: &[u8], ) -> Result<(), VerifyMembershipError> { - verify_non_existence(&proof, spec, root, key) + verify_non_existence(proof, spec, root, key) .map_err(VerifyMembershipError::ExistenceProofVerify) } @@ -92,17 +96,10 @@ fn verify_non_existence( root: &[u8], key: &[u8], ) -> Result<(), VerifyError> { - let key_for_comparison = |spec: &ProofSpec, key: &[u8]| -> Vec { - if !spec.prehash_key_before_comparison { - return key.to_vec(); - } - do_hash_or_noop(spec.leaf_spec.prehash_key, key) - }; - let left_ops = |left: &ExistenceProof| -> Result<(), VerifyError> { - verify_existence_proof(&left, spec, root, &left.key, &left.value)?; + verify_existence_proof(left, spec, root, &left.key, &left.value)?; - if key_for_comparison(spec, key) <= key_for_comparison(spec, &left.key) { + if key_for_comparison(spec, key)? <= key_for_comparison(spec, &left.key)? { return Err(VerifyError::KeyIsNotRightOfLeftProof); } @@ -114,9 +111,9 @@ fn verify_non_existence( }; let right_ops = |right: &ExistenceProof| -> Result<(), VerifyError> { - verify_existence_proof(&right, spec, root, &right.key, &right.value)?; + verify_existence_proof(right, spec, root, &right.key, &right.value)?; - if key_for_comparison(spec, key) >= key_for_comparison(spec, &right.key) { + if key_for_comparison(spec, key)? >= key_for_comparison(spec, &right.key)? { return Err(VerifyError::KeyIsNotLeftOfRightProof); } @@ -143,12 +140,23 @@ fn verify_non_existence( Ok(()) } +fn key_for_comparison<'a>(spec: &ProofSpec, key: &'a [u8]) -> Result, HashError> { + if !spec.prehash_key_before_comparison { + return Ok(Cow::Borrowed(key)); + } + if spec.leaf_spec.prehash_key == HashOp::NoHash { + Ok(Cow::Borrowed(key)) + } else { + Ok(Cow::Owned(do_hash(spec.leaf_spec.prehash_key, key)?)) + } +} + /// returns true if `right` is the next possible path right of `left` /// -/// Find the common suffix from the Left.Path and Right.Path and remove it. We have LPath and RPath now, which must be neighbors. -/// Validate that LPath[len-1] is the left neighbor of RPath[len-1] -/// For step in LPath[0..len-1], validate step is right-most node -/// For step in RPath[0..len-1], validate step is left-most node +/// Find the common suffix from the Left.Path and Right.Path and remove it. We have LPath and RPath now, which must be neighbors. +/// Validate that LPath[len-1] is the left neighbor of RPath[len-1] +/// For step in LPath[0..len-1], validate step is right-most node +/// For step in RPath[0..len-1], validate step is left-most node fn is_left_neighbor( spec: &InnerSpec, left: &[InnerOp], @@ -233,7 +241,12 @@ fn right_branches_are_empty(spec: &InnerSpec, op: &InnerOp) -> Result Result= op.suffix.len() + || spec.empty_child != &op.prefix[from..from + (spec.child_size as usize)] + { return Ok(false); } } diff --git a/lib/unionlabs/src/cosmos/ics23/compressed_non_existence_proof.rs b/lib/unionlabs/src/cosmos/ics23/compressed_non_existence_proof.rs index 1a4e817a9f..874d432bbc 100644 --- a/lib/unionlabs/src/cosmos/ics23/compressed_non_existence_proof.rs +++ b/lib/unionlabs/src/cosmos/ics23/compressed_non_existence_proof.rs @@ -37,12 +37,12 @@ impl TryFrom key: value.key, left: value .left - .map(|proof| proof.try_into()) + .map(TryInto::try_into) .transpose() .map_err(TryFromCompressedNonExistenceProofError::Left)?, right: value .right - .map(|proof| proof.try_into()) + .map(TryInto::try_into) .transpose() .map_err(TryFromCompressedNonExistenceProofError::Right)?, }) diff --git a/lib/unionlabs/src/cosmos/ics23/non_existence_proof.rs b/lib/unionlabs/src/cosmos/ics23/non_existence_proof.rs index ca48d06191..23e2935d16 100644 --- a/lib/unionlabs/src/cosmos/ics23/non_existence_proof.rs +++ b/lib/unionlabs/src/cosmos/ics23/non_existence_proof.rs @@ -32,12 +32,12 @@ impl TryFrom for NonExistenceProof key: value.key, left: value .left - .map(|proof| proof.try_into()) + .map(TryInto::try_into) .transpose() .map_err(TryFromNonExistenceProofError::Left)?, right: value .right - .map(|proof| proof.try_into()) + .map(TryInto::try_into) .transpose() .map_err(TryFromNonExistenceProofError::Right)?, }) diff --git a/light-clients/cometbls-light-client/src/client.rs b/light-clients/cometbls-light-client/src/client.rs index 3a22e7b09f..eed9fe0aed 100644 --- a/light-clients/cometbls-light-client/src/client.rs +++ b/light-clients/cometbls-light-client/src/client.rs @@ -66,14 +66,14 @@ impl IbcClient for CometblsLightClient { match value { StorageState::Occupied(value) => ics23::ibc_api::verify_membership( - merkle_proof, + &merkle_proof, &SDK_SPECS, &consensus_state.data.root, path, value, ), StorageState::Empty => ics23::ibc_api::verify_non_membership( - merkle_proof, + &merkle_proof, &SDK_SPECS, &consensus_state.data.root, path, diff --git a/light-clients/cometbls-light-client/src/errors.rs b/light-clients/cometbls-light-client/src/errors.rs index cb4221533a..5de5c41bc7 100644 --- a/light-clients/cometbls-light-client/src/errors.rs +++ b/light-clients/cometbls-light-client/src/errors.rs @@ -107,7 +107,7 @@ pub enum Error { Wasm(String), #[error("verify membership error: {0}")] - VerifyMembership(crate::client::VerifyMembershipError), + VerifyMembership(#[from] ics23::ibc_api::VerifyMembershipError), } impl Error {