diff --git a/.changelog/unreleased/features/ibc-relayer/1561-config-proof-specs.md b/.changelog/unreleased/features/ibc-relayer/1561-config-proof-specs.md new file mode 100644 index 0000000000..a9d4447b5e --- /dev/null +++ b/.changelog/unreleased/features/ibc-relayer/1561-config-proof-specs.md @@ -0,0 +1,2 @@ +- Allow custom proof-specs in chain config + ([#1561](https://github.com/informalsystems/ibc-rs/issues/1561)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index dc1589fe9c..37ed5a6f65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1336,6 +1336,7 @@ dependencies = [ "getrandom 0.2.3", "prost", "prost-types", + "serde", "tendermint-proto", "tonic", ] diff --git a/config.toml b/config.toml index dfeeaf2db1..399d4b3887 100644 --- a/config.toml +++ b/config.toml @@ -223,6 +223,60 @@ memo_prefix = '' # ['transfer', 'channel-0'], # ] +# Specify custom ICS23 proof-specs for the chain (serialized as JSON). +# Default: [`ProofSpecs::cosmos()`](modules/src/core/ics23_commitment/specs.rs) +proof-specs = ''' +[ + { + "leaf_spec": { + "hash": 1, + "prehash_key": 0, + "prehash_value": 1, + "length": 1, + "prefix": [ + 0 + ] + }, + "inner_spec": { + "child_order": [ + 0, + 1 + ], + "child_size": 33, + "min_prefix_length": 4, + "max_prefix_length": 12, + "empty_child": [], + "hash": 1 + }, + "max_depth": 0, + "min_depth": 0 + }, + { + "leaf_spec": { + "hash": 1, + "prehash_key": 0, + "prehash_value": 1, + "length": 1, + "prefix": [ + 0 + ] + }, + "inner_spec": { + "child_order": [ + 0, + 1 + ], + "child_size": 32, + "min_prefix_length": 1, + "max_prefix_length": 1, + "empty_child": [], + "hash": 1 + }, + "max_depth": 0, + "min_depth": 0 + } +] +''' [[chains]] id = 'ibc-1' diff --git a/modules/src/clients/ics07_tendermint/client_state.rs b/modules/src/clients/ics07_tendermint/client_state.rs index ac0de43cab..dfa93acbdb 100644 --- a/modules/src/clients/ics07_tendermint/client_state.rs +++ b/modules/src/clients/ics07_tendermint/client_state.rs @@ -29,7 +29,7 @@ pub struct ClientState { pub max_clock_drift: Duration, pub frozen_height: Height, pub latest_height: Height, - // pub proof_specs: ::core::vec::Vec, + pub proof_specs: ProofSpecs, pub upgrade_path: Vec, pub allow_update: AllowUpdate, } @@ -52,6 +52,7 @@ impl ClientState { max_clock_drift: Duration, latest_height: Height, frozen_height: Height, + proof_specs: ProofSpecs, upgrade_path: Vec, allow_update: AllowUpdate, ) -> Result { @@ -91,6 +92,13 @@ impl ClientState { )); } + // Disallow empty proof-specs + if proof_specs.is_empty() { + return Err(Error::validation( + "ClientState proof-specs cannot be empty".to_string(), + )); + } + Ok(Self { chain_id, trust_level, @@ -99,6 +107,7 @@ impl ClientState { max_clock_drift, frozen_height, latest_height, + proof_specs, upgrade_path, allow_update, }) @@ -228,6 +237,7 @@ impl TryFrom for ClientState { after_expiry: raw.allow_update_after_expiry, after_misbehaviour: raw.allow_update_after_misbehaviour, }, + proof_specs: raw.proof_specs.into(), }) } } @@ -242,7 +252,7 @@ impl From for RawClientState { max_clock_drift: Some(value.max_clock_drift.into()), frozen_height: Some(value.frozen_height.into()), latest_height: Some(value.latest_height.into()), - proof_specs: ProofSpecs::cosmos().into(), + proof_specs: value.proof_specs.into(), allow_update_after_expiry: value.allow_update.after_expiry, allow_update_after_misbehaviour: value.allow_update.after_misbehaviour, upgrade_path: value.upgrade_path, @@ -261,6 +271,7 @@ mod tests { use crate::clients::ics07_tendermint::client_state::{AllowUpdate, ClientState}; use crate::core::ics02_client::trust_threshold::TrustThreshold; + use crate::core::ics23_commitment::specs::ProofSpecs; use crate::core::ics24_host::identifier::ChainId; use crate::test::test_serialization_roundtrip; use crate::timestamp::ZERO_DURATION; @@ -293,6 +304,7 @@ mod tests { max_clock_drift: Duration, latest_height: Height, frozen_height: Height, + proof_specs: ProofSpecs, upgrade_path: Vec, allow_update: AllowUpdate, } @@ -306,6 +318,7 @@ mod tests { max_clock_drift: Duration::new(3, 0), latest_height: Height::new(0, 10), frozen_height: Height::default(), + proof_specs: ProofSpecs::default(), upgrade_path: vec!["".to_string()], allow_update: AllowUpdate { after_expiry: false, @@ -373,6 +386,7 @@ mod tests { p.max_clock_drift, p.latest_height, p.frozen_height, + p.proof_specs, p.upgrade_path, p.allow_update, ); @@ -414,6 +428,7 @@ pub mod test_util { u64::from(tm_header.height), ), Height::zero(), + Default::default(), vec!["".to_string()], AllowUpdate { after_expiry: false, diff --git a/modules/src/core/ics02_client/handler/create_client.rs b/modules/src/core/ics02_client/handler/create_client.rs index 06b1621042..49becdbfe6 100644 --- a/modules/src/core/ics02_client/handler/create_client.rs +++ b/modules/src/core/ics02_client/handler/create_client.rs @@ -71,6 +71,7 @@ mod tests { use crate::core::ics02_client::msgs::create_client::MsgCreateAnyClient; use crate::core::ics02_client::msgs::ClientMsg; use crate::core::ics02_client::trust_threshold::TrustThreshold; + use crate::core::ics23_commitment::specs::ProofSpecs; use crate::core::ics24_host::identifier::ClientId; use crate::events::IbcEvent; use crate::handler::HandlerOutput; @@ -230,6 +231,7 @@ mod tests { max_clock_drift: Duration::from_millis(3000), latest_height: Height::new(0, u64::from(tm_header.height)), frozen_height: Height::zero(), + proof_specs: ProofSpecs::default(), allow_update: AllowUpdate { after_expiry: false, after_misbehaviour: false, diff --git a/modules/src/core/ics23_commitment/commitment.rs b/modules/src/core/ics23_commitment/commitment.rs index d60e70f626..47034aca4d 100644 --- a/modules/src/core/ics23_commitment/commitment.rs +++ b/modules/src/core/ics23_commitment/commitment.rs @@ -157,11 +157,12 @@ impl Serialize for CommitmentPrefix { pub mod test_util { use crate::prelude::*; use ibc_proto::ibc::core::commitment::v1::MerkleProof as RawMerkleProof; + use ibc_proto::ics23::CommitmentProof; /// Returns a dummy `RawMerkleProof`, for testing only! pub fn get_dummy_merkle_proof() -> RawMerkleProof { - let parsed = ibc_proto::ics23::CommitmentProof { proof: None }; - let mproofs: Vec = vec![parsed]; + let parsed = CommitmentProof { proof: None }; + let mproofs: Vec = vec![parsed]; RawMerkleProof { proofs: mproofs } } } diff --git a/modules/src/core/ics23_commitment/specs.rs b/modules/src/core/ics23_commitment/specs.rs index 728ea63b67..53e45a5912 100644 --- a/modules/src/core/ics23_commitment/specs.rs +++ b/modules/src/core/ics23_commitment/specs.rs @@ -1,6 +1,7 @@ use crate::prelude::*; -use ibc_proto::ics23::ProofSpec as ProtoProofSpec; -use ics23::ProofSpec; +use ibc_proto::ics23::{InnerSpec as IbcInnerSpec, LeafOp as IbcLeafOp, ProofSpec as IbcProofSpec}; +use ics23::{InnerSpec as Ics23InnerSpec, LeafOp as Ics23LeafOp, ProofSpec as Ics23ProofSpec}; +use serde::{Deserialize, Serialize}; /// An array of proof specifications. /// @@ -9,38 +10,139 @@ use ics23::ProofSpec; /// Additionally, this type also aids in the conversion from `ProofSpec` types from crate `ics23` /// into proof specifications as represented in the `ibc_proto` type; see the /// `From` trait(s) below. -pub struct ProofSpecs { - specs: Vec, -} +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ProofSpecs(Vec); impl ProofSpecs { /// Returns the specification for Cosmos-SDK proofs pub fn cosmos() -> Self { - Self { - specs: vec![ - ics23::iavl_spec(), // Format of proofs-iavl (iavl merkle proofs) - ics23::tendermint_spec(), // Format of proofs-tendermint (crypto/ merkle SimpleProof) - ], + vec![ + ics23::iavl_spec(), // Format of proofs-iavl (iavl merkle proofs) + ics23::tendermint_spec(), // Format of proofs-tendermint (crypto/ merkle SimpleProof) + ] + .into() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl Default for ProofSpecs { + fn default() -> Self { + Self::cosmos() + } +} + +impl From> for ProofSpecs { + fn from(ibc_specs: Vec) -> Self { + Self(ibc_specs.into_iter().map(ProofSpec).collect()) + } +} + +impl From> for ProofSpecs { + fn from(ics23_specs: Vec) -> Self { + Self( + ics23_specs + .into_iter() + .map(|ics23_spec| ics23_spec.into()) + .collect(), + ) + } +} + +impl From for Vec { + fn from(specs: ProofSpecs) -> Self { + specs.0.into_iter().map(|spec| spec.into()).collect() + } +} + +impl From for Vec { + fn from(specs: ProofSpecs) -> Self { + specs.0.into_iter().map(|spec| spec.0).collect() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +struct ProofSpec(IbcProofSpec); + +impl From for ProofSpec { + fn from(spec: Ics23ProofSpec) -> Self { + Self(IbcProofSpec { + leaf_spec: spec.leaf_spec.map(|lop| LeafOp::from(lop).0), + inner_spec: spec.inner_spec.map(|ispec| InnerSpec::from(ispec).0), + max_depth: spec.max_depth, + min_depth: spec.min_depth, + }) + } +} + +impl From for Ics23ProofSpec { + fn from(spec: ProofSpec) -> Self { + let spec = spec.0; + Ics23ProofSpec { + leaf_spec: spec.leaf_spec.map(|lop| LeafOp(lop).into()), + inner_spec: spec.inner_spec.map(|ispec| InnerSpec(ispec).into()), + max_depth: spec.max_depth, + min_depth: spec.min_depth, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +struct LeafOp(IbcLeafOp); + +impl From for LeafOp { + fn from(leaf_op: Ics23LeafOp) -> Self { + Self(IbcLeafOp { + hash: leaf_op.hash, + prehash_key: leaf_op.prehash_key, + prehash_value: leaf_op.prehash_value, + length: leaf_op.length, + prefix: leaf_op.prefix, + }) + } +} + +impl From for Ics23LeafOp { + fn from(leaf_op: LeafOp) -> Self { + let leaf_op = leaf_op.0; + Ics23LeafOp { + hash: leaf_op.hash, + prehash_key: leaf_op.prehash_key, + prehash_value: leaf_op.prehash_value, + length: leaf_op.length, + prefix: leaf_op.prefix, } } } -/// Converts from the domain type (which is represented as a vector of `ics23::ProofSpec` -/// to the corresponding proto type (vector of `ibc_proto::ProofSpec`). -/// TODO: fix with -impl From for Vec { - fn from(domain_specs: ProofSpecs) -> Self { - let mut raw_specs = Vec::new(); - for ds in domain_specs.specs.iter() { - // Both `ProofSpec` types implement trait `prost::Message`. Convert by encoding, then - // decoding into the destination type. - // Safety note: the source and target data structures are identical, hence the - // encode/decode conversion here should be infallible. - let mut encoded = Vec::new(); - prost::Message::encode(ds, &mut encoded).unwrap(); - let decoded: ProtoProofSpec = prost::Message::decode(&*encoded).unwrap(); - raw_specs.push(decoded); +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +struct InnerSpec(IbcInnerSpec); + +impl From for InnerSpec { + fn from(inner_spec: Ics23InnerSpec) -> Self { + Self(IbcInnerSpec { + child_order: inner_spec.child_order, + child_size: inner_spec.child_size, + min_prefix_length: inner_spec.min_prefix_length, + max_prefix_length: inner_spec.max_prefix_length, + empty_child: inner_spec.empty_child, + hash: inner_spec.hash, + }) + } +} + +impl From for Ics23InnerSpec { + fn from(inner_spec: InnerSpec) -> Self { + let inner_spec = inner_spec.0; + Ics23InnerSpec { + child_order: inner_spec.child_order, + child_size: inner_spec.child_size, + min_prefix_length: inner_spec.min_prefix_length, + max_prefix_length: inner_spec.max_prefix_length, + empty_child: inner_spec.empty_child, + hash: inner_spec.hash, } - raw_specs } } diff --git a/proto-compiler/src/cmd/clone.rs b/proto-compiler/src/cmd/clone.rs index 59afa32ad2..76556f5610 100644 --- a/proto-compiler/src/cmd/clone.rs +++ b/proto-compiler/src/cmd/clone.rs @@ -173,4 +173,4 @@ fn checkout_tag(repo: &Repository, tag_name: &str) -> Result<(), git2::Error> { } Ok(()) -} \ No newline at end of file +} diff --git a/proto-compiler/src/cmd/compile.rs b/proto-compiler/src/cmd/compile.rs index 506f13fc81..4d88f0ff41 100644 --- a/proto-compiler/src/cmd/compile.rs +++ b/proto-compiler/src/cmd/compile.rs @@ -100,6 +100,7 @@ impl CompileCmd { // List available paths for dependencies let includes: Vec = proto_includes_paths.iter().map(PathBuf::from).collect(); + let attrs_serde_eq = "#[derive(::serde::Serialize, ::serde::Deserialize, Eq)]"; let compilation = tonic_build::configure() .build_client(true) @@ -107,6 +108,10 @@ impl CompileCmd { .format(true) .out_dir(out_dir) .extern_path(".tendermint", "::tendermint_proto") + .type_attribute(".ics23.LeafOp", attrs_serde_eq) + .type_attribute(".ics23.InnerOp", attrs_serde_eq) + .type_attribute(".ics23.ProofSpec", attrs_serde_eq) + .type_attribute(".ics23.InnerSpec", attrs_serde_eq) .compile(&protos, &includes); match compilation { @@ -139,7 +144,6 @@ impl CompileCmd { format!("{}/proto/cosmos/upgrade", sdk_dir.display()), ]; - let mut proto_includes_paths = vec![ format!("{}/../proto", root), format!("{}/proto", sdk_dir.display()), @@ -148,7 +152,7 @@ impl CompileCmd { if let Some(ibc_dir) = ibc_dep { // Use the IBC proto files from the SDK - proto_includes_paths.push(format!("{}/proto", ibc_dir.display()),); + proto_includes_paths.push(format!("{}/proto", ibc_dir.display())); } // List available proto files diff --git a/proto/Cargo.toml b/proto/Cargo.toml index fdc562b3b2..3b1e19c56b 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -27,6 +27,7 @@ prost-types = "0.9" bytes = "1.1" tonic = "0.6" getrandom = { version = "0.2", features = ["js"] } +serde = "1.0" [dependencies.tendermint-proto] version = "=0.23.1" diff --git a/proto/src/prost/ics23.rs b/proto/src/prost/ics23.rs index d7e2332338..92340fe10d 100644 --- a/proto/src/prost/ics23.rs +++ b/proto/src/prost/ics23.rs @@ -79,7 +79,7 @@ pub mod commitment_proof { /// ///Then combine the bytes, and hash it ///output = hash(prefix || length(hkey) || hkey || length(hvalue) || hvalue) -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(::serde::Serialize, ::serde::Deserialize, Eq, Clone, PartialEq, ::prost::Message)] pub struct LeafOp { #[prost(enumeration = "HashOp", tag = "1")] pub hash: i32, @@ -110,7 +110,7 @@ pub struct LeafOp { ///Any special data, like prepending child with the length, or prepending the entire operation with ///some value to differentiate from leaf nodes, should be included in prefix and suffix. ///If either of prefix or suffix is empty, we just treat it as an empty string -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(::serde::Serialize, ::serde::Deserialize, Eq, Clone, PartialEq, ::prost::Message)] pub struct InnerOp { #[prost(enumeration = "HashOp", tag = "1")] pub hash: i32, @@ -130,7 +130,7 @@ pub struct InnerOp { ///generate a given hash (by interpretting the preimage differently). ///We need this for proper security, requires client knows a priori what ///tree format server uses. But not in code, rather a configuration object. -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(::serde::Serialize, ::serde::Deserialize, Eq, Clone, PartialEq, ::prost::Message)] pub struct ProofSpec { /// any field in the ExistenceProof must be the same as in this spec. /// except Prefix, which is just the first bytes of prefix (spec can be longer) @@ -154,7 +154,7 @@ pub struct ProofSpec { ///isLeftMost(spec: InnerSpec, op: InnerOp) ///isRightMost(spec: InnerSpec, op: InnerOp) ///isLeftNeighbor(spec: InnerSpec, left: InnerOp, right: InnerOp) -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(::serde::Serialize, ::serde::Deserialize, Eq, Clone, PartialEq, ::prost::Message)] pub struct InnerSpec { /// Child order is the ordering of the children node, must count from 0 /// iavl tree is [0, 1] (left then right) diff --git a/relayer-cli/Cargo.toml b/relayer-cli/Cargo.toml index 9dded191d1..37f6405d3d 100644 --- a/relayer-cli/Cargo.toml +++ b/relayer-cli/Cargo.toml @@ -33,7 +33,7 @@ ibc-telemetry = { version = "0.9.0", path = "../telemetry", optional = true } ibc-relayer-rest = { version = "0.9.0", path = "../relayer-rest", optional = true } gumdrop = { version = "0.7", features = ["default_expr"] } -serde = { version = "1", features = ["serde_derive"] } +serde = { version = "1.0", features = ["serde_derive"] } tokio = { version = "1.0", features = ["full"] } tracing = "0.1.29" tracing-subscriber = { version = "0.3.2", features = ["fmt", "env-filter", "json"]} diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index 741a9e3fd2..11ad39bd72 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -1929,6 +1929,7 @@ impl ChainEndpoint for CosmosSdkChain { max_clock_drift, height, ICSHeight::zero(), + self.config.proof_specs.clone(), vec!["upgrade".to_string(), "upgradedIBCState".to_string()], AllowUpdate { after_expiry: true, diff --git a/relayer/src/chain/mock.rs b/relayer/src/chain/mock.rs index 3cb765d02b..21923e7b63 100644 --- a/relayer/src/chain/mock.rs +++ b/relayer/src/chain/mock.rs @@ -18,7 +18,7 @@ use ibc::core::ics03_connection::connection::{ConnectionEnd, IdentifiedConnectio use ibc::core::ics04_channel::channel::{ChannelEnd, IdentifiedChannelEnd}; use ibc::core::ics04_channel::context::ChannelReader; use ibc::core::ics04_channel::packet::{PacketMsgType, Sequence}; -use ibc::core::ics23_commitment::commitment::CommitmentPrefix; +use ibc::core::ics23_commitment::{commitment::CommitmentPrefix, specs::ProofSpecs}; use ibc::core::ics24_host::identifier::{ChainId, ChannelId, ClientId, ConnectionId, PortId}; use ibc::downcast; use ibc::events::IbcEvent; @@ -356,6 +356,7 @@ impl ChainEndpoint for MockChain { self.config.clock_drift + dst_config.clock_drift + dst_config.max_block_time, height, Height::zero(), + ProofSpecs::default(), vec!["upgrade/upgradedClient".to_string()], AllowUpdate { after_expiry: false, @@ -468,6 +469,7 @@ pub mod test_utils { packet_filter: PacketFilter::default(), address_type: AddressType::default(), memo_prefix: Default::default(), + proof_specs: Default::default(), } } } diff --git a/relayer/src/config.rs b/relayer/src/config.rs index 5b9e9f09f6..bdaf137bcf 100644 --- a/relayer/src/config.rs +++ b/relayer/src/config.rs @@ -1,5 +1,6 @@ //! Relayer configuration +mod proof_specs; pub mod reload; pub mod types; @@ -12,6 +13,7 @@ use std::{fs, fs::File, io::Write, path::Path}; use serde_derive::{Deserialize, Serialize}; use tendermint_light_client::types::TrustThreshold; +use ibc::core::ics23_commitment::specs::ProofSpecs; use ibc::core::ics24_host::identifier::{ChainId, ChannelId, PortId}; use ibc::timestamp::ZERO_DURATION; @@ -394,6 +396,8 @@ pub struct ChainConfig { pub trusting_period: Option, #[serde(default)] pub memo_prefix: Memo, + #[serde(default, with = "self::proof_specs")] + pub proof_specs: ProofSpecs, // these two need to be last otherwise we run into `ValueAfterTable` error when serializing to TOML #[serde(default)] diff --git a/relayer/src/config/proof_specs.rs b/relayer/src/config/proof_specs.rs new file mode 100644 index 0000000000..0178ddd4d2 --- /dev/null +++ b/relayer/src/config/proof_specs.rs @@ -0,0 +1,35 @@ +//! Custom `serde` deserializer for `ProofSpecs` + +use core::fmt; +use ibc::core::ics23_commitment::specs::ProofSpecs; +use serde::{de, ser, Deserializer, Serializer}; + +pub fn serialize( + proof_specs: &ProofSpecs, + serializer: S, +) -> Result { + let json_str = serde_json::to_string_pretty(proof_specs).map_err(ser::Error::custom)?; + serializer.serialize_str(&json_str) +} + +struct ProofSpecsVisitor; + +impl<'de> de::Visitor<'de> for ProofSpecsVisitor { + type Value = ProofSpecs; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("ICS23 proof-specs serialized as a JSON array") + } + + fn visit_str(self, v: &str) -> Result { + serde_json::from_str(v).map_err(E::custom) + } + + fn visit_string(self, v: String) -> Result { + self.visit_str(&v) + } +} + +pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + deserializer.deserialize_string(ProofSpecsVisitor) +} diff --git a/tools/integration-test/src/types/single/node.rs b/tools/integration-test/src/types/single/node.rs index 743169c317..c3803939ec 100644 --- a/tools/integration-test/src/types/single/node.rs +++ b/tools/integration-test/src/types/single/node.rs @@ -147,6 +147,7 @@ impl FullNode { packet_filter: Default::default(), address_type: Default::default(), memo_prefix: Default::default(), + proof_specs: Default::default(), }) }