From 8ee733c3b39779aaca2d9032dc11b37c81e89353 Mon Sep 17 00:00:00 2001 From: Chris Czub Date: Thu, 22 Aug 2024 12:17:27 -0400 Subject: [PATCH] Basic IBC Handshake Test (#4797) ## Describe your changes This adds a basic IBC handshake test, using the existing mock client. Some opportunistic refactoring is also included. This PR adds a new `tests::common::ibc_tests` module, which contains a `MockRelayer` that can be extended later on. Follow-up tasks should be basic transfer testing, transfer timeout testing, and testing with malformed requests. While debugging this test, bugs were found in the various IBC query APIs, specifically that the `proof_height` was consistently being returned one lower than the height whose header would contain the app_hash necessary for validating the proof. The Go relayer is unaffected because it uses the ABCI RPC query interface instead, and Hermes uses the affected APIs but discards the affected `proof_height` fields and uses its own internal mechanisms for height tracking instead. Fixes were included for the affected APIs. ## Issue Closes #3758 ## Checklist before requesting a review - [ ] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > There are changes to TendermintProxy and IBC RPC responses however anyone using the affected RPCs would quickly run into the issues we saw in testing that revealed the bugs in the RPC responses, and the chain would properly block any requests made based on the incorrect response values. The changeset also affects the test code heavily however there should be nothing that affects consensus or state. --------- Co-authored-by: Ava Howell --- Cargo.lock | 1 + crates/bin/pd/src/network/config.rs | 44 +- crates/core/app/Cargo.toml | 1 + crates/core/app/src/server/consensus.rs | 6 +- crates/core/app/tests/common/ibc_tests/mod.rs | 193 ++++ .../core/app/tests/common/ibc_tests/node.rs | 315 ++++++ .../app/tests/common/ibc_tests/relayer.rs | 907 ++++++++++++++++++ crates/core/app/tests/common/mod.rs | 4 + crates/core/app/tests/ibc_handshake.rs | 77 ++ .../app/tests/mock_consensus_block_proving.rs | 98 +- .../ibc/src/component/rpc/client_query.rs | 43 +- .../ibc/src/component/rpc/connection_query.rs | 60 +- .../ibc/src/component/rpc/consensus_query.rs | 106 +- crates/test/mock-consensus/src/block.rs | 11 +- crates/test/mock-consensus/src/lib.rs | 18 +- 15 files changed, 1762 insertions(+), 122 deletions(-) create mode 100644 crates/core/app/tests/common/ibc_tests/mod.rs create mode 100644 crates/core/app/tests/common/ibc_tests/node.rs create mode 100644 crates/core/app/tests/common/ibc_tests/relayer.rs create mode 100644 crates/core/app/tests/ibc_handshake.rs diff --git a/Cargo.lock b/Cargo.lock index 50dc26d63a..e0ab44877a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4568,6 +4568,7 @@ dependencies = [ "tap", "tempfile", "tendermint", + "tendermint-config", "tendermint-light-client-verifier", "tendermint-proto", "tokio", diff --git a/crates/bin/pd/src/network/config.rs b/crates/bin/pd/src/network/config.rs index 5334eadbc6..1cc87b6c1f 100644 --- a/crates/bin/pd/src/network/config.rs +++ b/crates/bin/pd/src/network/config.rs @@ -175,6 +175,48 @@ pub struct ValidatorKeys { } impl ValidatorKeys { + /// Use a hard-coded seed to generate a new set of validator keys. + pub fn from_seed(seed: [u8; 32]) -> Self { + // Create the spend key for this node. + let seed = SpendKeyBytes(seed); + let spend_key = SpendKey::from(seed.clone()); + + // Create signing key and verification key for this node. + let validator_id_sk = spend_key.spend_auth_key(); + let validator_id_vk = VerificationKey::from(validator_id_sk); + + let validator_cons_sk = ed25519_consensus::SigningKey::new(OsRng); + + // generate consensus key for tendermint. + let validator_cons_sk = tendermint::PrivateKey::Ed25519( + validator_cons_sk + .as_bytes() + .as_slice() + .try_into() + .expect("32 bytes"), + ); + let validator_cons_pk = validator_cons_sk.public_key(); + + // generate P2P auth key for tendermint. + let node_key_sk = ed25519_consensus::SigningKey::from(seed.0); + let signing_key_bytes = node_key_sk.as_bytes().as_slice(); + + // generate consensus key for tendermint. + let node_key_sk = + tendermint::PrivateKey::Ed25519(signing_key_bytes.try_into().expect("32 bytes")); + let node_key_pk = node_key_sk.public_key(); + + ValidatorKeys { + validator_id_sk: validator_id_sk.clone(), + validator_id_vk, + validator_cons_sk, + validator_cons_pk, + node_key_sk, + node_key_pk, + validator_spend_key: seed, + } + } + pub fn generate() -> Self { // Create the spend key for this node. // TODO: change to use seed phrase @@ -198,7 +240,7 @@ impl ValidatorKeys { let validator_cons_pk = validator_cons_sk.public_key(); // generate P2P auth key for tendermint. - let node_key_sk = ed25519_consensus::SigningKey::new(OsRng); + let node_key_sk = ed25519_consensus::SigningKey::from(seed.0); let signing_key_bytes = node_key_sk.as_bytes().as_slice(); // generate consensus key for tendermint. diff --git a/crates/core/app/Cargo.toml b/crates/core/app/Cargo.toml index 6fc5a99c54..f16d81a4e1 100644 --- a/crates/core/app/Cargo.toml +++ b/crates/core/app/Cargo.toml @@ -98,6 +98,7 @@ rand_chacha = { workspace = true } rand_core = { workspace = true } tap = { workspace = true } tempfile = { workspace = true } +tendermint-config = { workspace = true } tower-http = { workspace = true } tracing-subscriber = { workspace = true } url = { workspace = true } diff --git a/crates/core/app/src/server/consensus.rs b/crates/core/app/src/server/consensus.rs index 5f4ecdca7b..b60e1aaffe 100644 --- a/crates/core/app/src/server/consensus.rs +++ b/crates/core/app/src/server/consensus.rs @@ -173,8 +173,8 @@ impl Consensus { &mut self, proposal: request::ProcessProposal, ) -> Result { - tracing::info!(height = ?proposal.height, proposer = ?proposal.proposer_address, hash = %proposal.hash, "processing proposal"); - // We process the propopsal in an isolated state fork. Eventually, we should cache this work and + tracing::info!(height = ?proposal.height, proposer = ?proposal.proposer_address, proposal_hash = %proposal.hash, "processing proposal"); + // We process the proposal in an isolated state fork. Eventually, we should cache this work and // re-use it when processing a `FinalizeBlock` message (starting in `0.38.x`). let mut tmp_app = App::new(self.storage.latest_snapshot()); Ok(tmp_app.process_proposal(proposal).await) @@ -187,7 +187,9 @@ impl Consensus { // We don't need to print the block height, because it will already be // included in the span modeling the abci request handling. tracing::info!(time = ?begin_block.header.time, "beginning block"); + let events = self.app.begin_block(&begin_block).await; + Ok(response::BeginBlock { events }) } diff --git a/crates/core/app/tests/common/ibc_tests/mod.rs b/crates/core/app/tests/common/ibc_tests/mod.rs new file mode 100644 index 0000000000..4c03e8a44c --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/mod.rs @@ -0,0 +1,193 @@ +use {anyhow::Result, std::time::Duration}; + +mod relayer; +use anyhow::Context as _; +use decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}; +use penumbra_app::{ + app::{MAX_BLOCK_TXS_PAYLOAD_BYTES, MAX_EVIDENCE_SIZE_BYTES}, + genesis, +}; +use penumbra_keys::keys::{SpendKey, SpendKeyBytes}; +use penumbra_mock_consensus::TestNode; +use penumbra_proto::core::component::stake::v1::Validator; +use penumbra_shielded_pool::genesis::Allocation; +use penumbra_stake::{DelegationToken, GovernanceKey, IdentityKey}; +#[allow(unused_imports)] +pub use relayer::MockRelayer; + +mod node; +pub use node::TestNodeWithIBC; +use serde::Deserialize; +use tendermint::{consensus::params::AbciParams, public_key::Algorithm, Genesis}; + +/// Collection of all keypairs required for a Penumbra validator. +/// Used to generate a stable identity for a [`NetworkValidator`]. +/// TODO: copied this from pd crate +#[derive(Deserialize)] +pub struct ValidatorKeys { + /// Penumbra spending key and viewing key for this node. + /// These need to be real curve points. + pub validator_id_sk: SigningKey, + pub validator_id_vk: VerificationKey, + pub validator_spend_key: SpendKeyBytes, + /// Consensus key for tendermint. + pub validator_cons_sk: tendermint::PrivateKey, + pub validator_cons_pk: tendermint::PublicKey, + /// P2P auth key for tendermint. + pub node_key_sk: tendermint::PrivateKey, + /// The identity key for the validator. + pub identity_key: IdentityKey, + #[allow(unused_variables, dead_code)] + pub node_key_pk: tendermint::PublicKey, +} + +impl ValidatorKeys { + /// Use a hard-coded seed to generate a new set of validator keys. + pub fn from_seed(seed: [u8; 32]) -> Self { + // Create the spend key for this node. + let seed = SpendKeyBytes(seed); + let spend_key = SpendKey::from(seed.clone()); + + // Create signing key and verification key for this node. + let validator_id_sk = spend_key.spend_auth_key(); + let validator_id_vk = VerificationKey::from(validator_id_sk); + + let validator_cons_sk = ed25519_consensus::SigningKey::from(seed.0); + + // generate consensus key for tendermint. + let validator_cons_sk = tendermint::PrivateKey::Ed25519( + validator_cons_sk + .as_bytes() + .as_slice() + .try_into() + .expect("32 bytes"), + ); + let validator_cons_pk = validator_cons_sk.public_key(); + + // generate P2P auth key for tendermint. + let node_key_sk = ed25519_consensus::SigningKey::from(seed.0); + let signing_key_bytes = node_key_sk.as_bytes().as_slice(); + + // generate consensus key for tendermint. + let node_key_sk = + tendermint::PrivateKey::Ed25519(signing_key_bytes.try_into().expect("32 bytes")); + let node_key_pk = node_key_sk.public_key(); + + let identity_key: IdentityKey = IdentityKey( + spend_key + .full_viewing_key() + .spend_verification_key() + .clone() + .into(), + ); + ValidatorKeys { + validator_id_sk: validator_id_sk.clone(), + validator_id_vk, + validator_cons_sk, + validator_cons_pk, + node_key_sk, + node_key_pk, + validator_spend_key: seed, + identity_key, + } + } +} + +/// A genesis state that can be fed into CometBFT as well, +/// for verifying compliance of the mock tendermint implementation. +pub fn get_verified_genesis() -> Result { + let start_time = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?; + let vkeys_a = ValidatorKeys::from_seed([0u8; 32]); + + // TODO: make it possible to flag exporting the app state, keys, etc. + // to files possible on the builder + // genesis contents need to contain validator information in the app state + let mut genesis_contents = + genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()); + + let spend_key_a = SpendKey::from(vkeys_a.validator_spend_key.clone()); + let validator_a = Validator { + identity_key: Some(IdentityKey(vkeys_a.validator_id_vk.into()).into()), + governance_key: Some(GovernanceKey(spend_key_a.spend_auth_key().into()).into()), + consensus_key: vkeys_a.validator_cons_pk.to_bytes(), + name: "test".to_string(), + website: "https://example.com".to_string(), + description: "test".to_string(), + enabled: true, + funding_streams: vec![], + sequence_number: 0, + }; + + // let's only do one validator per chain for now + // since it's easier to validate against cometbft + genesis_contents + .stake_content + .validators + .push(validator_a.clone()); + + // the validator needs some initial delegations + let identity_key_a: IdentityKey = IdentityKey( + spend_key_a + .full_viewing_key() + .spend_verification_key() + .clone() + .into(), + ); + let delegation_id_a = DelegationToken::from(&identity_key_a).denom(); + let ivk_a = spend_key_a.incoming_viewing_key(); + genesis_contents + .shielded_pool_content + .allocations + .push(Allocation { + address: ivk_a.payment_address(0u32.into()).0, + raw_amount: (25_000 * 10u128.pow(6)).into(), + raw_denom: delegation_id_a.to_string(), + }); + + let genesis = Genesis { + genesis_time: start_time.clone(), + chain_id: genesis_contents + .chain_id + .parse::() + .context("failed to parse chain ID")?, + initial_height: 0, + consensus_params: tendermint::consensus::Params { + abci: AbciParams::default(), + block: tendermint::block::Size { + // 1MB + max_bytes: MAX_BLOCK_TXS_PAYLOAD_BYTES as u64, + // Set to infinity since a chain running Penumbra won't use + // cometbft's notion of gas. + max_gas: -1, + // Minimum time increment between consecutive blocks. + time_iota_ms: 500, + }, + evidence: tendermint::evidence::Params { + // We should keep this in approximate sync with the recommended default for + // `StakeParameters::unbonding_delay`, this is roughly a week. + max_age_num_blocks: 130000, + // Similarly, we set the max age duration for evidence to be a little over a week. + max_age_duration: tendermint::evidence::Duration(Duration::from_secs(650000)), + // 30KB + max_bytes: MAX_EVIDENCE_SIZE_BYTES as i64, + }, + validator: tendermint::consensus::params::ValidatorParams { + pub_key_types: vec![Algorithm::Ed25519], + }, + version: Some(tendermint::consensus::params::VersionParams { app: 0 }), + }, + // always empty in genesis json + app_hash: tendermint::AppHash::default(), + // app_state: genesis_contents.into(), + app_state: serde_json::value::to_value(penumbra_app::genesis::AppState::Content( + genesis_contents, + )) + .unwrap(), + // Set empty validator set for Tendermint config, which falls back to reading + // validators from the AppState, via ResponseInitChain: + // https://docs.tendermint.com/v0.32/tendermint-core/using-tendermint.html + validators: vec![], + }; + + Ok(genesis) +} diff --git a/crates/core/app/tests/common/ibc_tests/node.rs b/crates/core/app/tests/common/ibc_tests/node.rs new file mode 100644 index 0000000000..bbe4edb122 --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/node.rs @@ -0,0 +1,315 @@ +use { + crate::common::{BuilderExt as _, TempStorageExt as _}, + anyhow::{anyhow, Context as _, Result}, + cnidarium::TempStorage, + ed25519_consensus::{SigningKey, VerificationKey}, + ibc_proto::ibc::core::{ + channel::v1::query_client::QueryClient as IbcChannelQueryClient, + client::v1::query_client::QueryClient as IbcClientQueryClient, + connection::v1::query_client::QueryClient as IbcConnectionQueryClient, + }, + ibc_types::{ + core::{ + client::{ClientId, ClientType, Height}, + connection::{ChainId, ConnectionEnd, ConnectionId, Counterparty, Version}, + }, + lightclients::tendermint::{ + consensus_state::ConsensusState, header::Header as TendermintHeader, + }, + }, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, + penumbra_ibc::{component::ClientStateReadExt as _, IBC_COMMITMENT_PREFIX}, + penumbra_keys::test_keys, + penumbra_mock_client::MockClient, + penumbra_mock_consensus::TestNode, + penumbra_proto::util::tendermint_proxy::v1::{ + tendermint_proxy_service_client::TendermintProxyServiceClient, GetStatusRequest, + }, + std::error::Error, + tap::{Tap, TapFallible}, + tendermint::{ + v0_37::abci::{ConsensusRequest, ConsensusResponse}, + vote::Power, + Time, + }, + tokio::time, + tonic::transport::Channel, + tower_actor::Actor, + tracing::info, +}; + +// Contains some data from a single IBC connection + client for test usage. +// This might be better off as an extension trait or additional impl on the TestNode struct. +#[allow(unused)] +pub struct TestNodeWithIBC { + pub connection_id: ConnectionId, + pub client_id: ClientId, + pub chain_id: String, + pub counterparty: Counterparty, + pub version: Version, + pub signer: String, + pub connection: Option, + pub node: TestNode>>, + pub storage: TempStorage, + pub ibc_client_query_client: IbcClientQueryClient, + pub ibc_connection_query_client: IbcConnectionQueryClient, + pub _ibc_channel_query_client: IbcChannelQueryClient, + pub tendermint_proxy_service_client: TendermintProxyServiceClient, +} + +#[allow(unused)] +/// This interacts with a node similarly to how a relayer would. We intentionally call +/// against the external gRPC interfaces to get the most comprehensive test coverage. +impl TestNodeWithIBC { + pub async fn new( + suffix: &str, + start_time: Time, + keys: (SigningKey, VerificationKey), + ) -> Result { + let chain_id = format!("{}-{}", TestNode::<()>::CHAIN_ID, suffix); + // Use the correct substores + let storage = TempStorage::new_with_penumbra_prefixes().await?; + // Instantiate a mock tendermint proxy, which we will connect to the test node. + let proxy = penumbra_mock_tendermint_proxy::TestNodeProxy::new::(); + + let node = { + let app_state = + AppState::Content(genesis::Content::default().with_chain_id(chain_id.clone())); + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .with_keys(vec![keys]) + .single_validator() + .with_initial_timestamp(start_time) + .with_penumbra_auto_app_state(app_state)? + .on_block(proxy.on_block_callback()) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + // to select a port number just index on the suffix for now + let index = match suffix { + "a" => 0, + "b" => 1, + _ => unreachable!("update this hack"), + }; + let grpc_url = format!("http://127.0.0.1:808{}", index) // see #4517 + .parse::()? + .tap(|url| tracing::debug!(%url, "parsed grpc url")); + + tracing::info!("spawning gRPC..."); + // Spawn the node's RPC server. + let _rpc_server = { + let make_svc = penumbra_app::rpc::router( + storage.as_ref(), + proxy, + false, /*enable_expensive_rpc*/ + )? + .into_router() + .layer(tower_http::cors::CorsLayer::permissive()) + .into_make_service() + .tap(|_| tracing::info!("initialized rpc service")); + let [addr] = grpc_url + .socket_addrs(|| None)? + .try_into() + .expect("grpc url can be turned into a socket address"); + let server = axum_server::bind(addr).serve(make_svc); + tokio::spawn(async { server.await.expect("grpc server returned an error") }) + .tap(|_| tracing::info!("grpc server is running")) + }; + + time::sleep(time::Duration::from_secs(1)).await; + // Create an RPC server for each chain to respond to IBC-related queries. + let channel = Channel::from_shared(grpc_url.to_string()) + .with_context(|| "could not parse node URI")? + .connect() + .await + .with_context(|| "could not connect to grpc server") + .tap_err(|error| tracing::error!(?error, "could not connect to grpc server"))?; + + let ibc_connection_query_client = IbcConnectionQueryClient::new(channel.clone()); + let ibc_channel_query_client = IbcChannelQueryClient::new(channel.clone()); + let ibc_client_query_client = IbcClientQueryClient::new(channel.clone()); + let tendermint_proxy_service_client = TendermintProxyServiceClient::new(channel.clone()); + + let pk = node + .keyring() + .iter() + .next() + .expect("validator key in keyring") + .0; + let proposer_address = tendermint::account::Id::new( + ::digest(pk).as_slice()[0..20] + .try_into() + .expect(""), + ); + Ok(Self { + // the test relayer supports only a single connection on each chain as of now + connection_id: ConnectionId::new(0), + node, + storage, + client_id: ClientId::new(ClientType::new("07-tendermint".to_string()), 0)?, + chain_id: chain_id.clone(), + counterparty: Counterparty { + client_id: ClientId::new(ClientType::new("07-tendermint".to_string()), 0)?, + connection_id: None, + prefix: IBC_COMMITMENT_PREFIX.to_owned(), + }, + version: Version::default(), + signer: hex::encode_upper(proposer_address), + connection: None, + ibc_connection_query_client, + _ibc_channel_query_client: ibc_channel_query_client, + ibc_client_query_client, + tendermint_proxy_service_client, + }) + } + + pub async fn client(&mut self) -> Result { + // Sync the mock client, using the test wallet's spend key, to the latest snapshot. + Ok(MockClient::new(test_keys::SPEND_KEY.clone()) + .with_sync_to_storage(&self.storage) + .await? + .tap(|c| info!(client.notes = %c.notes.len(), "mock client synced to test storage"))) + } + + pub async fn get_latest_height(&mut self) -> Result { + let status: penumbra_proto::util::tendermint_proxy::v1::GetStatusResponse = self + .tendermint_proxy_service_client + .get_status(GetStatusRequest {}) + .await? + .into_inner(); + Ok(Height::new( + ChainId::chain_version(&self.chain_id), + status + .sync_info + .ok_or(anyhow!("no sync info"))? + .latest_block_height, + )?) + } + + // TODO: maybe move to an IBC extension trait for TestNode? + // or maybe the Block has everything it needs to produce this? + pub fn create_tendermint_header( + &self, + trusted_height: Option, + penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse{block_id: _, block}: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse, + ) -> Result { + let pk = self + .node + .keyring() + .iter() + .next() + .expect("validator key in keyring") + .0; + let block = block.ok_or(anyhow!("no block"))?; + let header = block.header.ok_or(anyhow!("no header"))?; + + // the tendermint SignedHeader is non_exhaustive so we + // can't use struct syntax to instantiate it and have to do + // some annoying manual construction of the pb type instead. + let h: tendermint::block::Header = header.clone().try_into().expect("bad header"); + use tendermint_proto::v0_37::types::SignedHeader as RawSignedHeader; + // The SignedHeader is the header accompanied by the commit to prove it. + let rsh: RawSignedHeader = RawSignedHeader { + header: Some(tendermint_proto::v0_37::types::Header { + version: Some(tendermint_proto::v0_37::version::Consensus { + block: header.version.as_ref().expect("version").block, + app: header.version.expect("version").app, + }), + chain_id: header.chain_id, + height: header.height.into(), + time: Some(tendermint_proto::google::protobuf::Timestamp { + seconds: header.time.as_ref().expect("time").seconds, + nanos: header.time.expect("time").nanos, + }), + last_block_id: header.last_block_id.clone().map(|a| { + tendermint_proto::v0_37::types::BlockId { + hash: a.hash, + part_set_header: a.part_set_header.map(|b| { + tendermint_proto::v0_37::types::PartSetHeader { + total: b.total, + hash: b.hash, + } + }), + } + }), + last_commit_hash: header.last_commit_hash.into(), + data_hash: header.data_hash.into(), + validators_hash: header.validators_hash.into(), + next_validators_hash: header.next_validators_hash.into(), + consensus_hash: header.consensus_hash.into(), + app_hash: header.app_hash.into(), + last_results_hash: header.last_results_hash.into(), + evidence_hash: header.evidence_hash.into(), + proposer_address: header.proposer_address.into(), + }), + commit: Some(tendermint_proto::v0_37::types::Commit { + // The commit is for the current height + height: header.height.into(), + round: 0.into(), + block_id: Some(tendermint_proto::v0_37::types::BlockId { + hash: h.hash().into(), + part_set_header: Some(tendermint_proto::v0_37::types::PartSetHeader { + total: 0, + hash: vec![], + }), + }), + // signatures for this block + signatures: self + .node + .last_commit() + .unwrap() + .signatures + .clone() + .into_iter() + .map(Into::into) + .collect::>(), + }), + }; + + let signed_header = rsh.clone().try_into()?; + + // now get a SignedHeader + let pub_key = + tendermint::PublicKey::from_raw_ed25519(pk.as_bytes()).expect("pub key present"); + let proposer_address = tendermint::account::Id::new( + ::digest(pk).as_slice()[0..20] + .try_into() + .expect(""), + ); + // TODO: don't hardcode these + let validator_set = tendermint::validator::Set::new( + vec![tendermint::validator::Info { + address: proposer_address.try_into()?, + pub_key, + power: Power::try_from(25_000 * 10i64.pow(6))?, + name: Some("test validator".to_string()), + proposer_priority: 1i64.try_into()?, + }], + // Same validator as proposer? + Some(tendermint::validator::Info { + address: proposer_address.try_into()?, + pub_key, + power: Power::try_from(25_000 * 10i64.pow(6))?, + name: Some("test validator".to_string()), + proposer_priority: 1i64.try_into()?, + }), + ); + + // now we can make the Header + let header = TendermintHeader { + signed_header, + validator_set: validator_set.clone(), + trusted_validator_set: validator_set.clone(), + trusted_height: trusted_height.unwrap_or_else(|| ibc_types::core::client::Height { + revision_number: 0, + revision_height: 0, + }), + }; + Ok(header) + } +} diff --git a/crates/core/app/tests/common/ibc_tests/relayer.rs b/crates/core/app/tests/common/ibc_tests/relayer.rs new file mode 100644 index 0000000000..86705d770d --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/relayer.rs @@ -0,0 +1,907 @@ +use { + super::TestNodeWithIBC, + anyhow::{anyhow, Result}, + ibc_proto::ibc::core::{ + client::v1::{QueryClientStateRequest, QueryConsensusStateRequest}, + connection::v1::QueryConnectionRequest, + }, + ibc_types::lightclients::tendermint::client_state::ClientState as TendermintClientState, + ibc_types::{ + core::{ + client::{ + msgs::{MsgCreateClient, MsgUpdateClient}, + Height, + }, + commitment::{MerkleProof, MerkleRoot}, + connection::{ + msgs::{ + MsgConnectionOpenAck, MsgConnectionOpenConfirm, MsgConnectionOpenInit, + MsgConnectionOpenTry, + }, + ConnectionEnd, Counterparty, State as ConnectionState, Version, + }, + }, + lightclients::tendermint::{ + client_state::AllowUpdate, consensus_state::ConsensusState, + header::Header as TendermintHeader, TrustThreshold, + }, + DomainType as _, + }, + penumbra_ibc::{ + component::ConnectionStateReadExt as _, IbcRelay, IBC_COMMITMENT_PREFIX, IBC_PROOF_SPECS, + }, + penumbra_proto::{util::tendermint_proxy::v1::GetBlockByHeightRequest, DomainType}, + penumbra_stake::state_key::chain, + penumbra_transaction::{TransactionParameters, TransactionPlan}, + prost::Message as _, + sha2::Digest, + std::time::Duration, + tendermint::Time, +}; +#[allow(unused)] +pub struct MockRelayer { + pub chain_a_ibc: TestNodeWithIBC, + pub chain_b_ibc: TestNodeWithIBC, +} + +#[allow(unused)] +impl MockRelayer { + pub async fn get_connection_states(&mut self) -> Result<(ConnectionState, ConnectionState)> { + let connection_on_a_response = self + .chain_a_ibc + .ibc_connection_query_client + .connection(QueryConnectionRequest { + connection_id: self.chain_a_ibc.connection_id.to_string(), + }) + .await? + .into_inner(); + let connection_on_b_response = self + .chain_b_ibc + .ibc_connection_query_client + .connection(QueryConnectionRequest { + connection_id: self.chain_b_ibc.connection_id.to_string(), + }) + .await? + .into_inner(); + + Ok( + match ( + connection_on_a_response.connection, + connection_on_b_response.connection, + ) { + (Some(connection_a), Some(connection_b)) => { + let connection_a: ConnectionEnd = connection_a.try_into().unwrap(); + let connection_b: ConnectionEnd = connection_b.try_into().unwrap(); + (connection_a.state, connection_b.state) + } + (None, None) => ( + ConnectionState::Uninitialized, + ConnectionState::Uninitialized, + ), + (None, Some(connection_b)) => { + let connection_b: ConnectionEnd = connection_b.try_into().unwrap(); + (ConnectionState::Uninitialized, connection_b.state) + } + (Some(connection_a), None) => { + let connection_a: ConnectionEnd = connection_a.try_into().unwrap(); + (connection_a.state, ConnectionState::Uninitialized) + } + }, + ) + } + + pub async fn _handshake(&mut self) -> Result<(), anyhow::Error> { + // The IBC connection handshake has four steps (Init, Try, Ack, Confirm). + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L672 + // https://github.com/cosmos/ibc/blob/main/spec/core/ics-003-connection-semantics/README.md#opening-handshake + + self._sync_chains().await?; + + let (a_state, b_state) = self.get_connection_states().await?; + assert!( + a_state == ConnectionState::Uninitialized && b_state == ConnectionState::Uninitialized + ); + + // 1: send the Init message to chain A + { + tracing::info!("Send Init to chain A"); + self._build_and_send_connection_open_init().await?; + } + + let (a_state, b_state) = self.get_connection_states().await?; + assert!(a_state == ConnectionState::Init && b_state == ConnectionState::Uninitialized); + + self._sync_chains().await?; + + // 2. send the OpenTry message to chain B + { + tracing::info!("send OpenTry to chain B"); + self._build_and_send_connection_open_try().await?; + } + + let (a_state, b_state) = self.get_connection_states().await?; + assert!(a_state == ConnectionState::Init && b_state == ConnectionState::TryOpen); + + self._sync_chains().await?; + + // 3. Send the OpenAck message to chain A + { + tracing::info!("send OpenAck to chain A"); + self._build_and_send_connection_open_ack().await?; + } + + let (a_state, b_state) = self.get_connection_states().await?; + assert!(a_state == ConnectionState::Open && b_state == ConnectionState::TryOpen); + + self._sync_chains().await?; + + // 4. Send the OpenConfirm message to chain B + { + tracing::info!("send OpenConfirm to chain B"); + self._build_and_send_connection_open_confirm().await?; + } + + let (a_state, b_state) = self.get_connection_states().await?; + assert!(a_state == ConnectionState::Open && b_state == ConnectionState::Open); + + // Ensure the chain timestamps remain in sync + self._sync_chains().await?; + + Ok(()) + } + + pub async fn _create_clients(&mut self) -> Result<(), anyhow::Error> { + self._sync_chains().await?; + // helper function to create client for chain B on chain A + async fn _create_client_inner( + chain_a_ibc: &mut TestNodeWithIBC, + chain_b_ibc: &mut TestNodeWithIBC, + ) -> Result<()> { + let pk = chain_b_ibc + .node + .keyring() + .iter() + .next() + .expect("validator key in keyring") + .0; + let proposer_address = tendermint::account::Id::new( + ::digest(pk).as_slice()[0..20] + .try_into() + .expect(""), + ); + let pub_key = + tendermint::PublicKey::from_raw_ed25519(pk.as_bytes()).expect("pub key present"); + + // Create the client for chain B on chain A. + let plan = { + let ibc_msg = IbcRelay::CreateClient(MsgCreateClient { + // Chain B will be signing messages to chain A + signer: chain_b_ibc.signer.clone(), + client_state: ibc_types::lightclients::tendermint::client_state::ClientState { + // Chain ID of the client state is for the counterparty + chain_id: chain_b_ibc.chain_id.clone().into(), + trust_level: TrustThreshold { + numerator: 1, + denominator: 3, + }, + trusting_period: Duration::from_secs(120_000), + unbonding_period: Duration::from_secs(240_000), + max_clock_drift: Duration::from_secs(5), + // The latest_height is for chain B + latest_height: chain_b_ibc.get_latest_height().await?, + // The ICS02 validation is hardcoded to expect 2 proof specs + // (root and substore, see [`penumbra_ibc::component::ics02_validation`]). + proof_specs: IBC_PROOF_SPECS.to_vec(), + upgrade_path: vec!["upgrade".to_string(), "upgradedIBCState".to_string()], + allow_update: AllowUpdate { + after_expiry: false, + after_misbehaviour: false, + }, + frozen_height: None, + } + .into(), + consensus_state: + ibc_types::lightclients::tendermint::consensus_state::ConsensusState { + timestamp: *chain_b_ibc.node.timestamp(), + root: MerkleRoot { + hash: chain_b_ibc.node.last_app_hash().to_vec(), + }, + next_validators_hash: (*chain_b_ibc + .node + .last_validator_set_hash() + .unwrap()) + .into(), + } + .into(), + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: chain_a_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = chain_a_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Create the client for chain B on chain A. + chain_a_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + + Ok(()) + } + + // Each chain will need a client created corresponding to its IBC connection with the other chain: + _create_client_inner(&mut self.chain_a_ibc, &mut self.chain_b_ibc).await?; + _create_client_inner(&mut self.chain_b_ibc, &mut self.chain_a_ibc).await?; + + Ok(()) + } + + // helper function to build ConnectionOpenInit to chain A + pub async fn _build_and_send_connection_open_init(&mut self) -> Result<()> { + self._sync_chains().await?; + let chain_a_ibc = &mut self.chain_a_ibc; + let chain_b_ibc = &mut self.chain_b_ibc; + let plan = { + let ibc_msg = IbcRelay::ConnectionOpenInit(MsgConnectionOpenInit { + client_id_on_a: chain_a_ibc.client_id.clone(), + counterparty: chain_a_ibc.counterparty.clone(), + version: Some(chain_a_ibc.version.clone()), + delay_period: Duration::from_secs(1), + signer: chain_b_ibc.signer.clone(), + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: chain_a_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = chain_a_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = chain_a_ibc.storage.latest_snapshot(); + chain_a_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + let post_tx_snapshot = chain_a_ibc.storage.latest_snapshot(); + + // validate the connection state is now "init" + { + // Connection should not exist pre-commit + assert!(pre_tx_snapshot + .get_connection(&chain_a_ibc.connection_id) + .await? + .is_none(),); + + // Post-commit, the connection should be in the "init" state. + let connection = post_tx_snapshot + .get_connection(&chain_a_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &chain_a_ibc.connection_id + ) + })?; + + assert_eq!(connection.state.clone(), ConnectionState::Init); + + chain_a_ibc.connection = Some(connection.clone()); + } + + Ok(()) + } + + pub async fn handshake(&mut self) -> Result<(), anyhow::Error> { + // Open a connection on each chain to the other chain. + // This is accomplished by following the ICS-003 spec for connection handshakes. + + // The Clients need to be created on each chain prior to the handshake. + self._create_clients().await?; + // The handshake is a multi-step process, this call will ratchet through the steps. + self._handshake().await?; + + Ok(()) + } + + // helper function to sync the chain times + pub async fn _sync_chains(&mut self) -> Result<()> { + let mut chain_a_time = self.chain_a_ibc.node.timestamp(); + let mut chain_b_time = self.chain_b_ibc.node.timestamp(); + + while chain_a_time < chain_b_time { + self.chain_a_ibc.node.block().execute().await?; + chain_a_time = self.chain_a_ibc.node.timestamp(); + } + while chain_b_time < chain_a_time { + self.chain_b_ibc.node.block().execute().await?; + chain_b_time = self.chain_b_ibc.node.timestamp(); + } + + chain_a_time = self.chain_a_ibc.node.timestamp(); + chain_b_time = self.chain_b_ibc.node.timestamp(); + assert_eq!(chain_a_time, chain_b_time); + + Ok(()) + } + + // tell chain b about chain a + pub async fn _build_and_send_update_client_b(&mut self) -> Result { + tracing::info!( + "send update client for chain {} to chain {}", + self.chain_a_ibc.chain_id, + self.chain_b_ibc.chain_id, + ); + // reverse these because we're sending to chain B + let chain_a_ibc = &mut self.chain_b_ibc; + let chain_b_ibc = &mut self.chain_a_ibc; + + _build_and_send_update_client(chain_a_ibc, chain_b_ibc).await + } + + // helper function to build UpdateClient to send to chain A + pub async fn _build_and_send_update_client_a(&mut self) -> Result { + tracing::info!( + "send update client for chain {} to chain {}", + self.chain_b_ibc.chain_id, + self.chain_a_ibc.chain_id, + ); + let chain_a_ibc = &mut self.chain_a_ibc; + let chain_b_ibc = &mut self.chain_b_ibc; + + _build_and_send_update_client(chain_a_ibc, chain_b_ibc).await + } + + // Send an ACK message to chain A + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L1126 + pub async fn _build_and_send_connection_open_ack(&mut self) -> Result<()> { + // This is a load-bearing block execution that should be removed + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._sync_chains().await?; + + let chain_b_connection_id = self.chain_b_ibc.connection_id.clone(); + let chain_a_connection_id = self.chain_a_ibc.connection_id.clone(); + + // Build message(s) for updating client on source + let src_client_height = self._build_and_send_update_client_a().await?; + // Build message(s) for updating client on destination + let dst_client_height = self._build_and_send_update_client_b().await?; + + let connection_of_a_on_b_response = self + .chain_b_ibc + .ibc_connection_query_client + .connection(QueryConnectionRequest { + connection_id: chain_a_connection_id.to_string(), + }) + .await? + .into_inner(); + let client_state_of_a_on_b_response = self + .chain_b_ibc + .ibc_client_query_client + .client_state(QueryClientStateRequest { + client_id: self.chain_a_ibc.client_id.to_string(), + }) + .await? + .into_inner(); + let consensus_state_of_a_on_b_response = self + .chain_b_ibc + .ibc_client_query_client + .consensus_state(QueryConsensusStateRequest { + client_id: self.chain_a_ibc.client_id.to_string(), + revision_number: 0, + revision_height: 0, + latest_height: true, + }) + .await? + .into_inner(); + assert_eq!( + connection_of_a_on_b_response.clone().proof_height, + consensus_state_of_a_on_b_response.clone().proof_height + ); + assert_eq!( + client_state_of_a_on_b_response.clone().proof_height, + consensus_state_of_a_on_b_response.clone().proof_height + ); + + let proof_height_on_b = client_state_of_a_on_b_response.clone().proof_height; + + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._build_and_send_update_client_a().await?; + self._sync_chains().await?; + + let plan = { + // This mocks the relayer constructing a connection open try message on behalf + // of the counterparty chain. + // we can't directly construct this because one of the struct fields is private + // and it's not from this crate, but we _can_ create the proto type and then convert it! + let proto_ack = ibc_proto::ibc::core::connection::v1::MsgConnectionOpenAck { + connection_id: self.chain_a_ibc.connection_id.to_string(), + counterparty_connection_id: chain_b_connection_id.to_string(), + version: Some(Version::default().into()), + client_state: Some( + client_state_of_a_on_b_response + .clone() + .client_state + .unwrap(), + ), + proof_height: Some(proof_height_on_b.unwrap()), + proof_try: connection_of_a_on_b_response.proof, + proof_client: client_state_of_a_on_b_response.clone().proof, + proof_consensus: consensus_state_of_a_on_b_response.proof, + // consensus height of a on b (the height chain b's ibc client trusts chain a at) + consensus_height: Some( + ibc_types::lightclients::tendermint::client_state::ClientState::try_from( + client_state_of_a_on_b_response + .clone() + .client_state + .unwrap(), + )? + .latest_height + .into(), + ), + signer: self.chain_b_ibc.signer.clone(), + // optional field, don't include + host_consensus_state_proof: vec![], + }; + let ibc_msg = + IbcRelay::ConnectionOpenAck(MsgConnectionOpenAck::try_from(proto_ack)?).into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: self.chain_a_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = self + .chain_a_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = self.chain_a_ibc.storage.latest_snapshot(); + self.chain_a_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + let post_tx_snapshot = self.chain_a_ibc.storage.latest_snapshot(); + + // validate the connection state is now "OPEN" + { + // Connection should be in INIT pre-commit + let connection = pre_tx_snapshot + .get_connection(&self.chain_a_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &self.chain_a_ibc.connection_id + ) + })?; + + assert_eq!(connection.state, ConnectionState::Init); + + // Post-commit, the connection should be in the "OPEN" state. + let connection = post_tx_snapshot + .get_connection(&self.chain_a_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &self.chain_a_ibc.connection_id + ) + })?; + + assert_eq!(connection.state, ConnectionState::Open); + + self.chain_a_ibc.connection = Some(connection); + } + + Ok(()) + } + + // helper function to build ConnectionOpenTry to send to chain B + // at this point chain A is in INIT state and chain B has no state + // after this, chain A will be in INIT and chain B will be in TRYOPEN state. + pub async fn _build_and_send_connection_open_try(&mut self) -> Result<()> { + // This is a load-bearing block execution that should be removed + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._sync_chains().await?; + + let src_connection = self + .chain_a_ibc + .ibc_connection_query_client + .connection(QueryConnectionRequest { + connection_id: self.chain_a_ibc.connection_id.to_string(), + }) + .await? + .into_inner(); + + let chain_b_height = self._build_and_send_update_client_a().await?; + let chain_a_height = self._build_and_send_update_client_b().await?; + + let client_state_of_b_on_a_response = self + .chain_a_ibc + .ibc_client_query_client + .client_state(QueryClientStateRequest { + client_id: self.chain_b_ibc.client_id.to_string(), + }) + .await? + .into_inner(); + let connection_of_b_on_a_response = self + .chain_a_ibc + .ibc_connection_query_client + .connection(QueryConnectionRequest { + connection_id: self.chain_b_ibc.connection_id.to_string(), + }) + .await? + .into_inner(); + let consensus_state_of_b_on_a_response = self + .chain_a_ibc + .ibc_client_query_client + .consensus_state(QueryConsensusStateRequest { + client_id: self.chain_b_ibc.client_id.to_string(), + revision_number: 0, + revision_height: 0, + latest_height: true, + }) + .await? + .into_inner(); + + // Then construct the ConnectionOpenTry message + let proof_consensus_state_of_b_on_a = + MerkleProof::decode(consensus_state_of_b_on_a_response.clone().proof.as_slice())?; + + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._sync_chains().await?; + + assert_eq!( + consensus_state_of_b_on_a_response.proof_height, + client_state_of_b_on_a_response.proof_height + ); + assert_eq!( + connection_of_b_on_a_response.proof_height, + client_state_of_b_on_a_response.proof_height + ); + + let proofs_height_on_a: Height = connection_of_b_on_a_response + .proof_height + .clone() + .unwrap() + .try_into()?; + + let proof_client_state_of_b_on_a = + MerkleProof::decode(client_state_of_b_on_a_response.clone().proof.as_slice())?; + let proof_conn_end_on_a = + MerkleProof::decode(connection_of_b_on_a_response.clone().proof.as_slice())?; + let proof_consensus_state_of_b_on_a = + MerkleProof::decode(consensus_state_of_b_on_a_response.clone().proof.as_slice())?; + + // TODO: too side-effecty? + self.chain_b_ibc.counterparty.connection_id = Some(self.chain_a_ibc.connection_id.clone()); + self.chain_a_ibc.counterparty.connection_id = Some(self.chain_b_ibc.connection_id.clone()); + + self._build_and_send_update_client_b().await?; + self._sync_chains().await?; + + let cs: TendermintClientState = client_state_of_b_on_a_response + .clone() + .client_state + .unwrap() + .try_into()?; + let plan = { + // This mocks the relayer constructing a connection open try message on behalf + // of the counterparty chain. + #[allow(deprecated)] + let ibc_msg = IbcRelay::ConnectionOpenTry(MsgConnectionOpenTry { + // Counterparty is chain A. + counterparty: Counterparty { + client_id: self.chain_a_ibc.client_id.clone(), + connection_id: Some(self.chain_a_ibc.connection_id.clone()), + prefix: IBC_COMMITMENT_PREFIX.to_owned(), + }, + delay_period: Duration::from_secs(1), + signer: self.chain_a_ibc.signer.clone(), + client_id_on_b: self.chain_b_ibc.client_id.clone(), + client_state_of_b_on_a: client_state_of_b_on_a_response + .client_state + .expect("client state present"), + versions_on_a: vec![Version::default()], + proof_conn_end_on_a, + proof_client_state_of_b_on_a, + proof_consensus_state_of_b_on_a, + proofs_height_on_a, + consensus_height_of_b_on_a: chain_b_height, + // this seems to be an optional proof + proof_consensus_state_of_b: None, + // deprecated + previous_connection_id: "".to_string(), + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: self.chain_b_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = self + .chain_b_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = self.chain_b_ibc.storage.latest_snapshot(); + + // validate the chain b pre-tx storage root hash is what we expect: + let pre_tx_hash = pre_tx_snapshot.root_hash().await?; + + // Validate the tx hash is what we expect: + let tx_hash = sha2::Sha256::digest(&tx.encode_to_vec()); + + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + + // execute the transaction containing the opentry message + self.chain_b_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + self.chain_b_ibc.node.block().execute().await?; + let post_tx_snapshot = self.chain_b_ibc.storage.latest_snapshot(); + + // validate the connection state is now "tryopen" + { + // Connection should not exist pre-commit + assert!(pre_tx_snapshot + .get_connection(&self.chain_b_ibc.connection_id) + .await? + .is_none(),); + + // Post-commit, the connection should be in the "tryopen" state. + let connection = post_tx_snapshot + .get_connection(&self.chain_b_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &self.chain_b_ibc.connection_id + ) + })?; + + assert_eq!(connection.state, ConnectionState::TryOpen); + + self.chain_b_ibc.connection = Some(connection); + } + + self._sync_chains().await?; + + Ok(()) + } + + // sends a ConnectionOpenConfirm message to chain B + // at this point, chain A is in OPEN and B is in TRYOPEN. + // afterwards, chain A will be in OPEN and chain B will be in OPEN. + pub async fn _build_and_send_connection_open_confirm(&mut self) -> Result<()> { + // This is a load-bearing block execution that should be removed + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._sync_chains().await?; + + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L1296 + let chain_b_connection_id = self.chain_b_ibc.connection_id.clone(); + let connection_of_b_on_a_response = self + .chain_a_ibc + .ibc_connection_query_client + .connection(QueryConnectionRequest { + connection_id: chain_b_connection_id.to_string(), + }) + .await? + .into_inner(); + + let dst_client_target_height = self._build_and_send_update_client_b().await?; + + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._build_and_send_update_client_b().await?; + self._sync_chains().await?; + + let plan = { + // This mocks the relayer constructing a connection open try message on behalf + // of the counterparty chain. + let ibc_msg = IbcRelay::ConnectionOpenConfirm(MsgConnectionOpenConfirm { + conn_id_on_b: self.chain_b_ibc.connection_id.clone(), + proof_conn_end_on_a: MerkleProof::decode( + connection_of_b_on_a_response.clone().proof.as_slice(), + )?, + proof_height_on_a: connection_of_b_on_a_response + .proof_height + .unwrap() + .try_into()?, + signer: self.chain_a_ibc.signer.clone(), + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: self.chain_b_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = self + .chain_b_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = self.chain_b_ibc.storage.latest_snapshot(); + self.chain_b_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + let post_tx_snapshot = self.chain_b_ibc.storage.latest_snapshot(); + + // validate the connection state is now "open" + { + // Connection should be in TRYOPEN pre-commit + let connection = pre_tx_snapshot + .get_connection(&self.chain_b_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &self.chain_b_ibc.connection_id + ) + })?; + + assert_eq!(connection.state, ConnectionState::TryOpen); + + // Post-commit, the connection should be in the "OPEN" state. + let connection = post_tx_snapshot + .get_connection(&self.chain_b_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &self.chain_b_ibc.connection_id + ) + })?; + + assert_eq!(connection.state, ConnectionState::Open); + + self.chain_b_ibc.connection = Some(connection); + } + + Ok(()) + } +} + +// tell chain A about chain B. returns the height of chain b on chain a after update. +async fn _build_and_send_update_client( + chain_a_ibc: &mut TestNodeWithIBC, + chain_b_ibc: &mut TestNodeWithIBC, +) -> Result { + let chain_b_height = chain_b_ibc.get_latest_height().await?; + let chain_b_latest_block: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse = + chain_b_ibc + .tendermint_proxy_service_client + .get_block_by_height(GetBlockByHeightRequest { + height: chain_b_height.revision_height.try_into()?, + }) + .await? + .into_inner(); + + // Look up the last recorded consensus state for the counterparty client on chain A + // to determine the last trusted height. + let client_state_of_b_on_a_response = chain_a_ibc + .ibc_client_query_client + .client_state(QueryClientStateRequest { + client_id: chain_b_ibc.client_id.to_string(), + }) + .await? + .into_inner(); + let trusted_height = ibc_types::lightclients::tendermint::client_state::ClientState::try_from( + client_state_of_b_on_a_response + .clone() + .client_state + .unwrap(), + )? + .latest_height; + let chain_b_new_height = chain_b_latest_block + .block + .clone() + .unwrap() + .header + .unwrap() + .height; + let plan = { + let ibc_msg = IbcRelay::UpdateClient(MsgUpdateClient { + signer: chain_b_ibc.signer.clone(), + client_id: chain_a_ibc.client_id.clone(), + client_message: chain_b_ibc + // The TendermintHeader is derived from the Block + // and represents chain B's claims about its current state. + .create_tendermint_header(Some(trusted_height), chain_b_latest_block.clone())? + .into(), + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: chain_a_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = chain_a_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + chain_a_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + + Ok(chain_b_height) +} diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index 6774c878f5..36179b5cd7 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -28,3 +28,7 @@ mod test_node_ext; /// See [`ValidatorDataRead`][penumbra_stake::component::validator_handler::ValidatorDataRead], /// and [`ValidatorDataReadExt`]. mod validator_read_ext; + +/// Methods for testing IBC functionality. +#[allow(unused)] +pub mod ibc_tests; diff --git a/crates/core/app/tests/ibc_handshake.rs b/crates/core/app/tests/ibc_handshake.rs new file mode 100644 index 0000000000..e000a824eb --- /dev/null +++ b/crates/core/app/tests/ibc_handshake.rs @@ -0,0 +1,77 @@ +use { + common::ibc_tests::{MockRelayer, TestNodeWithIBC, ValidatorKeys}, + once_cell::sync::Lazy, + std::time::Duration, + tap::Tap as _, +}; + +/// The proof specs for the main store. +pub static MAIN_STORE_PROOF_SPEC: Lazy> = + Lazy::new(|| vec![cnidarium::ics23_spec()]); + +mod common; + +/// Exercises that the IBC handshake succeeds. +#[tokio::test] +async fn ibc_handshake() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = common::set_tracing_subscriber(); + + let block_duration = Duration::from_secs(5); + // Fixed start times (both chains start at the same time to avoid unintended timeouts): + let start_time_a = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?; + + // But chain B will be 39 blocks ahead of chain A, so offset chain A's + // start time so they match: + let start_time_b = start_time_a.checked_sub(39 * block_duration).unwrap(); + + // Hardcoded keys for each chain for test reproducibility: + let vkeys_a = ValidatorKeys::from_seed([0u8; 32]); + let vkeys_b = ValidatorKeys::from_seed([1u8; 32]); + let sk_a = vkeys_a.validator_cons_sk.ed25519_signing_key().unwrap(); + let sk_b = vkeys_b.validator_cons_sk.ed25519_signing_key().unwrap(); + + let ska = ed25519_consensus::SigningKey::try_from(sk_a.as_bytes())?; + let skb = ed25519_consensus::SigningKey::try_from(sk_b.as_bytes())?; + let keys_a = (ska.clone(), ska.verification_key()); + let keys_b = (skb.clone(), skb.verification_key()); + + // Set up some configuration for the two different chains we'll need to keep around. + let mut chain_a_ibc = TestNodeWithIBC::new("a", start_time_a, keys_a).await?; + let mut chain_b_ibc = TestNodeWithIBC::new("b", start_time_b, keys_b).await?; + + // The two chains can't IBC handshake during the first block, let's fast forward + // them both a few. + for _ in 0..3 { + chain_a_ibc.node.block().execute().await?; + } + // Do them each a different # of blocks to make sure the heights don't get confused. + for _ in 0..42 { + chain_b_ibc.node.block().execute().await?; + } + + // The chains should be at the same time: + assert_eq!(chain_a_ibc.node.timestamp(), chain_b_ibc.node.timestamp()); + // But their block heights should be different: + assert_ne!( + chain_a_ibc.get_latest_height().await?, + chain_b_ibc.get_latest_height().await?, + ); + + assert_eq!( + chain_a_ibc.get_latest_height().await?.revision_height, + chain_a_ibc.storage.latest_snapshot().version() + ); + + // The Relayer will handle IBC operations and manage state for the two test chains + let mut relayer = MockRelayer { + chain_a_ibc, + chain_b_ibc, + }; + + // Perform the IBC connection handshake between the two chains. + // TODO: some testing of failure cases of the handshake process would be good + relayer.handshake().await?; + + Ok(()).tap(|_| drop(relayer)).tap(|_| drop(guard)) +} diff --git a/crates/core/app/tests/mock_consensus_block_proving.rs b/crates/core/app/tests/mock_consensus_block_proving.rs index f4be2d71d6..f522020769 100644 --- a/crates/core/app/tests/mock_consensus_block_proving.rs +++ b/crates/core/app/tests/mock_consensus_block_proving.rs @@ -247,11 +247,11 @@ async fn verify_storage_proof_simple() -> anyhow::Result<()> { assert_eq!(u64::from(latest_height), storage_revision_height); // Try fetching the client state via the IBC API - let node_last_app_hash = node.last_app_hash(); + let node_last_app_hash = node.last_app_hash().to_vec(); tracing::debug!( "making IBC client state request at height {} and hash {}", latest_height, - hex::encode(node_last_app_hash) + hex::encode(&node_last_app_hash) ); let ibc_client_state_response = ibc_client_query_client .client_state(QueryClientStateRequest { @@ -260,11 +260,51 @@ async fn verify_storage_proof_simple() -> anyhow::Result<()> { .await? .into_inner(); + assert!( + ibc_client_state_response.client_state.as_ref().is_some() + && !ibc_client_state_response + .client_state + .as_ref() + .unwrap() + .value + .is_empty() + ); + let ibc_proof = MerkleProof::decode(ibc_client_state_response.clone().proof.as_slice())?; let ibc_value = ibc_client_state_response.client_state.unwrap(); assert_eq!(ibc_value.encode_to_vec(), value); + // The current height of the node should be one behind the proof height. + assert_eq!( + u64::from(latest_height) + 1, + ibc_client_state_response + .proof_height + .clone() + .unwrap() + .revision_height + ); + + let proof_block: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse = + tendermint_proxy_service_client + .get_block_by_height(GetBlockByHeightRequest { + height: ibc_client_state_response + .proof_height + .clone() + .unwrap() + .revision_height + .try_into()?, + }) + .await? + .into_inner(); + + // The proof block should be nonexistent because we haven't finalized the in-progress + // block yet. + assert!(proof_block.block.is_none()); + + // Execute a block to finalize the proof block. + node.block().execute().await?; + // We should be able to get the block from the proof_height associated with // the proof and use the app_hash as the jmt root and succeed in proving: let proof_block: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse = @@ -289,49 +329,19 @@ async fn verify_storage_proof_simple() -> anyhow::Result<()> { .revision_height, proof_block.block.clone().unwrap().header.unwrap().height as u64 ); - // The node height when we directly retrieved the last app hash - // should match the proof height - assert_eq!( - ibc_client_state_response - .proof_height - .clone() - .unwrap() - .revision_height, - u64::from(latest_height) - ); - // TODO: these tests fail - if false { - // the proof block's app hash should match - assert_eq!( - node_last_app_hash, - proof_block.block.clone().unwrap().header.unwrap().app_hash, - "node claimed app hash for height {} was {}, however block header contained {}", - latest_height, - hex::encode(node_last_app_hash), - hex::encode(proof_block.block.clone().unwrap().header.unwrap().app_hash) - ); - println!( - "proof height: {} proof_block_root: {:?}", - ibc_client_state_response - .proof_height - .unwrap() - .revision_height, - hex::encode(proof_block.block.clone().unwrap().header.unwrap().app_hash) - ); - let proof_block_root = MerkleRoot { - hash: proof_block.block.unwrap().header.unwrap().app_hash, - }; - ibc_proof - .verify_membership( - &proof_specs, - proof_block_root, - merkle_path, - ibc_value.encode_to_vec(), - 0, - ) - .expect("the ibc proof should validate against the root of the proof_height's block"); - } + let proof_block_root = MerkleRoot { + hash: proof_block.block.unwrap().header.unwrap().app_hash, + }; + ibc_proof + .verify_membership( + &proof_specs, + proof_block_root, + merkle_path, + ibc_value.encode_to_vec(), + 0, + ) + .expect("the ibc proof should validate against the root of the proof_height's block"); Ok(()) .tap(|_| drop(node)) diff --git a/crates/core/component/ibc/src/component/rpc/client_query.rs b/crates/core/component/ibc/src/component/rpc/client_query.rs index 9b52f35097..2c4d66ec0a 100644 --- a/crates/core/component/ibc/src/component/rpc/client_query.rs +++ b/crates/core/component/ibc/src/component/rpc/client_query.rs @@ -11,6 +11,7 @@ use ibc_proto::ibc::core::client::v1::{ QueryUpgradedClientStateResponse, QueryUpgradedConsensusStateRequest, QueryUpgradedConsensusStateResponse, }; +use penumbra_sct::component::clock::EpochRead; use prost::Message; use ibc_types::core::client::ClientId; @@ -37,12 +38,6 @@ impl ClientQuery for IbcQuery { let snapshot = self.storage.latest_snapshot(); let client_id = ClientId::from_str(&request.get_ref().client_id) .map_err(|e| tonic::Status::invalid_argument(format!("invalid client id: {e}")))?; - let height = Height { - revision_number: HI::get_revision_number(&snapshot) - .await - .map_err(|e| tonic::Status::aborted(e.to_string()))?, - revision_height: snapshot.version(), - }; // Query for client_state and associated proof. let (cs_opt, proof) = snapshot @@ -60,11 +55,19 @@ impl ClientQuery for IbcQuery { .transpose() .map_err(|e| tonic::Status::aborted(format!("couldn't decode client state: {e}")))?; - let res = QueryClientStateResponse { - client_state, - proof: proof.encode_to_vec(), - proof_height: Some(height.into()), - }; + let res = + QueryClientStateResponse { + client_state, + proof: proof.encode_to_vec(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot.get_block_height().await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })? + 1, + revision_number: HI::get_revision_number(&snapshot).await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })?, + }), + }; Ok(tonic::Response::new(res)) } @@ -136,11 +139,19 @@ impl ClientQuery for IbcQuery { .transpose() .map_err(|e| tonic::Status::aborted(format!("couldn't decode consensus state: {e}")))?; - let res = QueryConsensusStateResponse { - consensus_state, - proof: proof.encode_to_vec(), - proof_height: Some(height.into()), - }; + let res = + QueryConsensusStateResponse { + consensus_state, + proof: proof.encode_to_vec(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot.get_block_height().await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })? + 1, + revision_number: HI::get_revision_number(&snapshot).await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })?, + }), + }; Ok(tonic::Response::new(res)) } diff --git a/crates/core/component/ibc/src/component/rpc/connection_query.rs b/crates/core/component/ibc/src/component/rpc/connection_query.rs index ae98d9f381..4f653fc755 100644 --- a/crates/core/component/ibc/src/component/rpc/connection_query.rs +++ b/crates/core/component/ibc/src/component/rpc/connection_query.rs @@ -16,6 +16,7 @@ use ibc_types::path::{ ClientConnectionPath, ClientConsensusStatePath, ClientStatePath, ConnectionPath, }; use ibc_types::DomainType; +use penumbra_sct::component::clock::EpochRead as _; use prost::Message; use std::str::FromStr; @@ -58,16 +59,19 @@ impl ConnectionQuery for IbcQuery let conn = conn.map_err(|e| tonic::Status::aborted(format!("couldn't decode connection: {e}")))?; - let height = Height { - revision_number: 0, - revision_height: snapshot.version(), - }; - - let res = QueryConnectionResponse { - connection: conn, - proof: proof.encode_to_vec(), - proof_height: Some(height), - }; + let res = + QueryConnectionResponse { + connection: conn, + proof: proof.encode_to_vec(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot.get_block_height().await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })? + 1, + revision_number: HI::get_revision_number(&snapshot).await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })?, + }), + }; Ok(tonic::Response::new(res)) } @@ -90,7 +94,7 @@ impl ConnectionQuery for IbcQuery _request: tonic::Request, ) -> std::result::Result, tonic::Status> { let snapshot = self.storage.latest_snapshot(); - let height = snapshot.version(); + let height = snapshot.version() + 1; let connection_counter = snapshot .get_connection_counter() @@ -163,9 +167,15 @@ impl ConnectionQuery for IbcQuery Ok(tonic::Response::new(QueryClientConnectionsResponse { connection_paths, proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } @@ -211,9 +221,15 @@ impl ConnectionQuery for IbcQuery Ok(tonic::Response::new(QueryConnectionClientStateResponse { identified_client_state: Some(identified_client_state), proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } @@ -263,9 +279,13 @@ impl ConnectionQuery for IbcQuery consensus_state: consensus_state_any, client_id: client_id.to_string(), proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot.get_block_height().await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })? + 1, + revision_number: HI::get_revision_number(&snapshot).await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })?, }), }, )) diff --git a/crates/core/component/ibc/src/component/rpc/consensus_query.rs b/crates/core/component/ibc/src/component/rpc/consensus_query.rs index 39221e2384..c37d654de4 100644 --- a/crates/core/component/ibc/src/component/rpc/consensus_query.rs +++ b/crates/core/component/ibc/src/component/rpc/consensus_query.rs @@ -25,6 +25,7 @@ use ibc_types::DomainType; use ibc_types::core::channel::{ChannelId, IdentifiedChannelEnd, PortId}; use ibc_types::core::connection::ConnectionId; +use penumbra_sct::component::clock::EpochRead as _; use prost::Message; use std::str::FromStr; @@ -67,14 +68,19 @@ impl ConsensusQuery for IbcQuery let channel = channel.map_err(|e| tonic::Status::aborted(format!("couldn't decode channel: {e}")))?; - let res = QueryChannelResponse { - channel, - proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), - }), - }; + let res = + QueryChannelResponse { + channel, + proof: proof.encode_to_vec(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot.get_block_height().await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })? + 1, + revision_number: HI::get_revision_number(&snapshot).await.map_err(|e| { + tonic::Status::aborted(format!("couldn't decode height: {e}")) + })?, + }), + }; Ok(tonic::Response::new(res)) } @@ -251,9 +257,15 @@ impl ConsensusQuery for IbcQuery Ok(tonic::Response::new(QueryChannelClientStateResponse { identified_client_state: Some(identified_client_state), proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } @@ -338,9 +350,15 @@ impl ConsensusQuery for IbcQuery consensus_state: consensus_state_any, client_id: connection.client_id.clone().to_string(), proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } @@ -379,9 +397,15 @@ impl ConsensusQuery for IbcQuery Ok(tonic::Response::new(QueryPacketCommitmentResponse { commitment, proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } @@ -476,9 +500,15 @@ impl ConsensusQuery for IbcQuery Ok(tonic::Response::new(QueryPacketReceiptResponse { received: receipt.is_some(), proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } @@ -515,9 +545,15 @@ impl ConsensusQuery for IbcQuery Ok(tonic::Response::new(QueryPacketAcknowledgementResponse { acknowledgement, proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } @@ -708,9 +744,15 @@ impl ConsensusQuery for IbcQuery Ok(tonic::Response::new(QueryNextSequenceReceiveResponse { next_sequence_receive: next_recv_sequence, proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } @@ -744,9 +786,15 @@ impl ConsensusQuery for IbcQuery Ok(tonic::Response::new(QueryNextSequenceSendResponse { next_sequence_send: next_send_sequence, proof: proof.encode_to_vec(), - proof_height: Some(Height { - revision_number: 0, - revision_height: snapshot.version(), + proof_height: Some(ibc_proto::ibc::core::client::v1::Height { + revision_height: snapshot + .get_block_height() + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))? + + 1, + revision_number: HI::get_revision_number(&snapshot) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?, }), })) } diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index 2be7700835..76a8a77c93 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -190,7 +190,9 @@ where height }; - let last_commit = &test_node.last_commit; + // Pull the current last_commit out of the node, since it will + // be discarded after we build the block. + let last_commit = test_node.last_commit.clone(); // Set the validator set based on the current configuration. let pk = test_node @@ -219,8 +221,7 @@ where height, time: timestamp, // MerkleRoot of the lastCommit’s signatures. The signatures represent the validators that committed to the last block. The first block has an empty slices of bytes for the hash. - last_commit_hash: test_node - .last_commit + last_commit_hash: last_commit .as_ref() .map(|c| c.hash().unwrap()) .unwrap_or(Some( @@ -274,11 +275,13 @@ where last_commit_height=?last_commit.as_ref().map(|c| c.height.value()), "made block" ); - let block = Block::new(header.clone(), data, evidence, last_commit.clone())?; + // pass the current value of last_commit with this header + let block = Block::new(header.clone(), data, evidence, last_commit)?; // Now that the block is finalized, we can transition to the next block. // Generate a commit for the header we just made, that will be // included in the next header. + // Update the last_commit. test_node.last_commit = Some(Commit { height: block.header.height, round: Round::default(), diff --git a/crates/test/mock-consensus/src/lib.rs b/crates/test/mock-consensus/src/lib.rs index acaa435b80..79df9b4948 100644 --- a/crates/test/mock-consensus/src/lib.rs +++ b/crates/test/mock-consensus/src/lib.rs @@ -82,12 +82,8 @@ pub struct TestNode { last_app_hash: Vec, /// The last validator set hash value. last_validator_set_hash: Option, - /// The tendermint validators associated with the node. - /// Updated via processing updates within `BeginBlock` requests. - // pub validators: ValidatorSet, - /// The last tendermint commit. - // TODO: move the create_tendermint_header into TestNode and change vis on this - pub last_commit: Option, + /// The last tendermint block header commit value. + last_commit: Option, /// The consensus params hash. consensus_params_hash: Vec, /// The current block [`Height`][tendermint::block::Height]. @@ -127,6 +123,16 @@ impl TestNode { &self.last_app_hash } + /// Returns the last `commit` value. + pub fn last_commit(&self) -> Option<&Commit> { + self.last_commit.as_ref() + } + + /// Returns the last `validator_set_hash` value. + pub fn last_validator_set_hash(&self) -> Option<&tendermint::Hash> { + self.last_validator_set_hash.as_ref() + } + /// Returns the most recent `timestamp` value. pub fn timestamp(&self) -> &Time { &self.timestamp