From f87d69c4db519a6951179d9379d46cb3ad740fe3 Mon Sep 17 00:00:00 2001 From: Chris Czub Date: Thu, 8 Aug 2024 17:11:55 -0400 Subject: [PATCH] Mock IBC Test: Handshake --- Cargo.lock | 2 +- crates/bin/pd/src/network/config.rs | 44 +- crates/cnidarium/src/store/substore.rs | 7 + crates/core/app/Cargo.toml | 1 + crates/core/app/src/app/mod.rs | 1 + crates/core/app/src/server/consensus.rs | 39 +- crates/core/app/tests/common/ibc_tests/mod.rs | 351 +++++ .../core/app/tests/common/ibc_tests/node.rs | 320 +++++ .../app/tests/common/ibc_tests/relayer.rs | 1179 +++++++++++++++++ crates/core/app/tests/common/mod.rs | 4 + crates/core/app/tests/ibc_handshake.rs | 844 ++++++++++++ .../core/component/sct/src/component/clock.rs | 5 + crates/test/mock-consensus/src/block.rs | 1 - .../mock-consensus/src/block/signature.rs | 1 + .../mock-consensus/src/builder/init_chain.rs | 2 +- crates/test/mock-consensus/src/lib.rs | 13 +- .../test/mock-tendermint-proxy/src/proxy.rs | 8 +- crates/view/Cargo.toml | 1 - 18 files changed, 2806 insertions(+), 17 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..a3a901332b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4568,6 +4568,7 @@ dependencies = [ "tap", "tempfile", "tendermint", + "tendermint-config", "tendermint-light-client-verifier", "tendermint-proto", "tokio", @@ -5719,7 +5720,6 @@ dependencies = [ "tokio-stream", "tonic", "tracing", - "tracing-subscriber 0.3.18", "url", ] 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/cnidarium/src/store/substore.rs b/crates/cnidarium/src/store/substore.rs index 876a94b744..6234780f46 100644 --- a/crates/cnidarium/src/store/substore.rs +++ b/crates/cnidarium/src/store/substore.rs @@ -229,6 +229,13 @@ impl SubstoreSnapshot { ) -> Result<(Option>, ics23::CommitmentProof)> { let version = self.version(); let tree = jmt::Sha256Jmt::new(self); + let rh = tree.get_root_hash(version)?; + println!( + "HERE in SubstoreSnapshot get_with_proof, key: {}, version: {}, root hash: {}", + hex::encode(key.clone()), + version, + hex::encode(rh.0) + ); tree.get_with_ics23_proof(key, version) } 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/app/mod.rs b/crates/core/app/src/app/mod.rs index cec052d4ea..967e949813 100644 --- a/crates/core/app/src/app/mod.rs +++ b/crates/core/app/src/app/mod.rs @@ -628,6 +628,7 @@ impl App { } tracing::debug!(?jmt_root, "finished committing state"); + println!("finished committing state {}", hex::encode(jmt_root)); // Get the latest version of the state, now that we've committed it. self.state = Arc::new(StateDelta::new(storage.latest_snapshot())); diff --git a/crates/core/app/src/server/consensus.rs b/crates/core/app/src/server/consensus.rs index 5f4ecdca7b..aedfd45d8a 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,26 @@ 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 storage_revision_height = self.storage.latest_snapshot().version(); + let storage_root = self.storage.latest_snapshot().root_hash().await?; + println!( + "BEFORE begin_block {} storage height is {} and storage root is {}", + begin_block.header.height, + storage_revision_height, + hex::encode(storage_root.0) + ); let events = self.app.begin_block(&begin_block).await; + + let storage_revision_height = self.storage.latest_snapshot().version(); + let storage_root = self.storage.latest_snapshot().root_hash().await?; + println!( + "AFTER begin_block {} storage height is {} and storage root is {}", + begin_block.header.height, + storage_revision_height, + hex::encode(storage_root.0) + ); + Ok(response::BeginBlock { events }) } @@ -240,9 +259,25 @@ impl Consensus { } async fn commit(&mut self) -> Result { + let storage_revision_height = self.storage.latest_snapshot().version(); + let storage_root = self.storage.latest_snapshot().root_hash().await?; + println!( + "BEFORE commit storage height is {} and storage root is {}", + storage_revision_height, + hex::encode(storage_root.0) + ); + let app_hash = self.app.commit(self.storage.clone()).await; tracing::info!(?app_hash, "committed block"); + let storage_revision_height = self.storage.latest_snapshot().version(); + let storage_root = self.storage.latest_snapshot().root_hash().await?; + println!( + "AFTER commit storage height is {} and storage root is {}", + storage_revision_height, + hex::encode(storage_root.0) + ); + Ok(response::Commit { data: app_hash.0.to_vec().into(), retain_height: 0u32.into(), 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..7f275a659f --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/mod.rs @@ -0,0 +1,351 @@ +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) +} + +// export keys and genesis to files for testing +// TODO: this is kind of useful to have in a test utility crate +// pub fn export_key() { +// // Write this node's priv_validator_key.json +// let priv_validator_key_filepath = "/tmp/priv_validator_key.json"; +// tracing::debug!(priv_validator_key_filepath = %priv_validator_key_filepath, "writing validator private key"); +// let mut priv_validator_key_file = File::create(priv_validator_key_filepath)?; +// // let priv_validator_key: PrivValidatorKey = sk_b.into(); +// let pub_key_a = +// tendermint::PublicKey::from_raw_ed25519(vk_a.as_bytes()).expect("pub key present"); +// let pub_key_b = +// tendermint::PublicKey::from_raw_ed25519(vk_b.as_bytes()).expect("pub key present"); +// let address_a = tendermint::account::Id::new( +// ::digest(vk_a.as_bytes()).as_slice()[0..20] +// .try_into() +// .expect(""), +// ); +// let priv_validator_key = PrivValidatorKey { +// address: address_a, +// pub_key: pub_key_a, +// priv_key: tendermint::PrivateKey::Ed25519( +// tendermint::crypto::ed25519::SigningKey::try_from(sk_a.clone().as_bytes())?, +// ), +// }; +// priv_validator_key_file +// .write_all(serde_json::to_string_pretty(&priv_validator_key)?.as_bytes())?; +// // 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 spend_key_b = SpendKey::from(vkeys_b.validator_spend_key.clone()); +// tendermint::PublicKey::from_raw_ed25519(&vkeys_a.validator_cons_pk.to_bytes()) +// .ok_or_else(|| anyhow::anyhow!("invalid ed25519 consensus pubkey")) +// .unwrap(); +// 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 validator_b = Validator { +// identity_key: Some(IdentityKey(vkeys_b.validator_id_vk.into()).into()), +// governance_key: Some(GovernanceKey(spend_key_b.spend_auth_key().into()).into()), +// consensus_key: vkeys_b.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, +// }; +// genesis_contents +// .stake_content +// .validators +// .push(validator_a.clone()); +// // TODO: actually 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_b.clone()); + +// // the two validators need 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 identity_key_b: IdentityKey = IdentityKey( +// spend_key_b +// .full_viewing_key() +// .spend_verification_key() +// .clone() +// .into(), +// ); +// let delegation_id_b = DelegationToken::from(&identity_key_b).denom(); +// let ivk_b = spend_key_b.incoming_viewing_key(); +// // genesis_contents +// // .shielded_pool_content +// // .allocations +// // .push(Allocation { +// // address: ivk_b.payment_address(0u32.into()).0, +// // raw_amount: (25_000 * 10u128.pow(6)).into(), +// // raw_denom: delegation_id_b.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: penumbra_app::genesis::AppState::Content(genesis_contents), +// // 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![], +// }; +// let genesis_file = "/tmp/genesis.json"; +// tracing::info!(?genesis_file, "writing genesis file to comet config"); +// let mut genesis_file = File::create(genesis_file)?; +// genesis_file.write_all(serde_json::to_string_pretty(&genesis)?.as_bytes())?; +// }; +// } 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..4dc9f34774 --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/node.rs @@ -0,0 +1,320 @@ +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}, + 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())); + // TODO: genesis contents need to contain validator information in the app state + 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"))? + }; + + // TODO: hacky lol + let (_other_suffix, index) = match suffix { + "a" => ("b", 0), + "b" => ("a", 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")); + + println!("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(|_| println!("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(|_| println!("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: 1i64.try_into()?, + 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: 1i64.try_into()?, + 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, + }), + }; + println!( + "Created header: {} with trusted height: {}", + hex::encode(header.signed_header.header.app_hash.as_bytes().to_vec()), + header.trusted_height + ); + 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..5ea47936dc --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/relayer.rs @@ -0,0 +1,1179 @@ +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"); + let validator_set = tendermint::validator::Set::new( + vec![tendermint::validator::Info { + address: proposer_address.try_into()?, + pub_key, + power: 1i64.try_into()?, + 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: 1i64.try_into()?, + name: Some("test validator".to_string()), + proposer_priority: 1i64.try_into()?, + }), + ); + let validators_hash = validator_set.hash(); + // RIGHT HERE everything is correct. + // Creating client on penumbra-test-chain-b with height 2 and merkle root 58055... + // later on, though: proof root for height 3: 58055... + // that is not right + println!( + "Creating client on {} with height {} and merkle root {}", + chain_a_ibc.chain_id, + chain_b_ibc.get_latest_height().await?.revision_height, + hex::encode(chain_b_ibc.node.last_app_hash()) + ); + // 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: validators_hash.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, target_height: Height) -> Result<()> { + tracing::info!( + "send update client for chain {} to chain {} about height {}", + self.chain_a_ibc.chain_id, + self.chain_b_ibc.chain_id, + target_height + ); + // 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, target_height).await + } + + // helper function to build UpdateClient to send to chain A + pub async fn _build_and_send_update_client_a(&mut self, target_height: Height) -> Result<()> { + tracing::info!( + "send update client for chain {} to chain {} about height {}", + self.chain_b_ibc.chain_id, + self.chain_a_ibc.chain_id, + target_height + ); + 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, target_height).await + } + + // Send an ACK message to chain A + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L1126 + async fn _build_and_send_connection_open_ack(&mut self) -> Result<()> { + let chain_b_connection_id = self.chain_b_ibc.connection_id.clone(); + let chain_a_connection_id = self.chain_a_ibc.connection_id.clone(); + 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(); + + // Build message(s) for updating client on source + let src_client_target_height = self.chain_a_ibc.get_latest_height().await?; + self._build_and_send_update_client_a(src_client_target_height) + .await?; + // Build message(s) for updating client on destination + let dst_client_target_height = self.chain_b_ibc.get_latest_height().await?; + self._build_and_send_update_client_b(dst_client_target_height) + .await?; + + 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(); + + 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(connection_of_a_on_b_response.clone().proof_height.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 src_client_target_height = self.chain_a_ibc.get_latest_height().await?; + let client_msgs = self + ._build_and_send_update_client_a(src_client_target_height) + .await?; + + // Make sure chain B has a client state for this height + println!("UPDATE1"); + // self._build_and_send_update_client_b().await?; + // self._sync_chains().await?; + + // the height chain b's client for chain a should have state for + let chain_b_client_a_target_height = self.chain_a_ibc.get_latest_height().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(); + println!( + "fetching consensus state at height: {:?}", + connection_of_b_on_a_response.proof_height.clone().unwrap() + ); + + assert_eq!( + client_state_of_b_on_a_response.proof_height, + connection_of_b_on_a_response.proof_height + ); + + 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: connection_of_b_on_a_response + .proof_height + .clone() + .unwrap() + .revision_number, + // use the same height for the consensus state as the connection + revision_height: connection_of_b_on_a_response + .proof_height + .clone() + .unwrap() + .revision_height, + latest_height: false, + }) + .await? + .into_inner(); + println!( + "consensus_state_of_b_on_a_response: {:?}", + consensus_state_of_b_on_a_response + ); + + // 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())?; + 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 existence_proofs: Vec<(Vec, Vec)> = proof_conn_end_on_a + .proofs + .iter() + .map(|proof| { + let p = proof.proof.clone().unwrap(); + match p { + ics23::commitment_proof::Proof::Exist(p) => (p.key, p.value), + _ => (vec![], vec![]), + } + }) + .inspect(|(k, v)| { + println!( + "proof_conn_end_on_a: k {} v {}", + hex::encode(k), + hex::encode(v) + ); + }) + .collect(); + + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L1010 + // https://github.com/penumbra-zone/hermes/blob/main/crates/relayer/src/foreign_client.rs#L1144 + // Send an update to both sides to ensure they are up to date + // Build message(s) for updating client on source + // chain B needs to know about chain A at the proof height + let proofs_height_on_a: Height = connection_of_b_on_a_response + .proof_height + .clone() + .unwrap() + .try_into()?; + // the proof was included in proof_height, but the root for the proof + // will be available in the next block's header, so we need to increment + let proofs_height_on_a = proofs_height_on_a.increment(); + self._build_and_send_update_client_b(proofs_height_on_a.try_into()?) + .await?; + println!( + "client state target height: {:?}", + chain_b_client_a_target_height + ); + + // 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()); + + // Build message(s) for updating client on destination + println!( + "proof height: {:?}", + connection_of_b_on_a_response.proof_height + ); + + // d01e8818de18fb050c7aff28a59d430db07802f91c14c4370cf8be785381d4c7 is trusted (for height 6) + // but the proof comes in for ae0565596d0d8fb9019815400868ecf002b3aa48f4e0134e116f3184cfa49c4a (height 7) + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + println!( + "chain b latest height: {:?}", + self.chain_b_ibc.get_latest_height().await? + ); + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L943 + + let mut latest_client_state = + ibc_types::lightclients::tendermint::client_state::ClientState::try_from( + self.chain_b_ibc + .ibc_client_query_client + .client_state(QueryClientStateRequest { + client_id: self.chain_a_ibc.client_id.to_string(), + }) + .await? + .into_inner() + .client_state + .unwrap(), + )?; + + self._build_and_send_update_client_b(proofs_height_on_a) + .await?; + self._sync_chains().await?; + + let cs: TendermintClientState = client_state_of_b_on_a_response + .clone() + .client_state + .unwrap() + .try_into()?; + // let cs = ibc_types::lightclients::tendermint::client_state::ClientState::try_from( + // client_state_of_b_on_a_response + // .clone() + // .client_state + // .unwrap(), + // )?; + println!("cs: {:?}", cs); + println!("before send cs latest height: {}", cs.latest_height); + 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: latest_client_state.latest_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?; + // assert_eq!( + // hex::encode(pre_tx_hash), + // "27572242ba7935d5c9acf4ee162fc1418be1f749677aecd0eb11d832bb8d8613".to_string() + // ); + + // Validate the tx hash is what we expect: + let tx_hash = sha2::Sha256::digest(&tx.encode_to_vec()); + // assert_eq!( + // hex::encode(tx_hash), + // "8b741a3cfb2bcd4cf665780d3f9e18a6b954f15912591c1acc6d21de015848dc".to_string() + // ); + + 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?; + 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<()> { + 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(); + + // Build message(s) for updating client on destination + println!("UPDATE4"); + let dst_client_target_height = self.chain_a_ibc.get_latest_height().await?; + self._build_and_send_update_client_b(dst_client_target_height) + .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 +async fn _build_and_send_update_client( + chain_a_ibc: &mut TestNodeWithIBC, + chain_b_ibc: &mut TestNodeWithIBC, + target_height: Height, +) -> Result<()> { + let chain_b_height = chain_b_ibc.get_latest_height().await?; + println!("chain_b latest height: {:?}", chain_b_height); + 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 client_latest_height = + ibc_types::lightclients::tendermint::client_state::ClientState::try_from( + client_state_of_b_on_a_response + .clone() + .client_state + .unwrap(), + )? + .latest_height; + let trusted_height = client_latest_height; + println!( + "Telling chain a about chain b latest block: {} and trusted height: {}", + hex::encode(chain_b_latest_block.clone().block_id.unwrap().hash), + trusted_height + ); + // println!( + // "header: {:?}", + // chain_b_latest_block.block.clone().unwrap().header.unwrap() + // ); + println!( + "chain_id {}, height {}, last_commit_hash: {}", + chain_b_latest_block + .block + .clone() + .unwrap() + .header + .unwrap() + .chain_id, + chain_b_latest_block + .block + .clone() + .unwrap() + .header + .unwrap() + .height, + hex::encode( + chain_b_latest_block + .block + .clone() + .unwrap() + .header + .unwrap() + .last_commit_hash + ) + ); + 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)? + .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?; + // let consensus_state = chain_b_ibc + // .ibc_client_query_client + // .consensus_state(QueryConsensusStateRequest { + // client_id: chain_a_ibc.client_id.to_string(), + // revision_number: target_height.revision_number, + // revision_height: target_height.revision_height, + // latest_height: false, + // }) + // .await? + // .into_inner(); + + // if let Some(consensus_state) = consensus_state.consensus_state { + // tracing::info!("consensus state already exists at height {target_height}, skipping update"); + // tracing::trace!(?consensus_state, "consensus state"); + // return Ok(()); + // } + + // let mut src_application_latest_height = chain_a_ibc.get_latest_height().await?; + // // Wait for the source network to produce block(s) & reach `target_height`. + // while src_application_latest_height < target_height { + // // advance both blocks + // chain_a_ibc.node.block().execute().await?; + // chain_b_ibc.node.block().execute().await?; + // src_application_latest_height = chain_a_ibc.get_latest_height().await?; + // } + + // // Get the latest client state on destination. + // let client_state_of_a_on_b_response = chain_b_ibc + // .ibc_client_query_client + // .client_state(QueryClientStateRequest { + // client_id: chain_a_ibc.client_id.to_string(), + // }) + // .await? + // .into_inner(); + + // let client_latest_height = + // ibc_types::lightclients::tendermint::client_state::ClientState::try_from( + // client_state_of_a_on_b_response + // .clone() + // .client_state + // .unwrap(), + // )? + // .latest_height; + // let trusted_height = if client_latest_height < target_height { + // client_latest_height + // } else { + // panic!("unsupported, no sending updates to the past"); + // }; + + // if trusted_height >= target_height { + // tracing::warn!( + // "skipping update: trusted height ({}) >= chain target height ({})", + // trusted_height, + // target_height + // ); + + // return Ok(()); + // } + + // println!("target chain b height: {:?}", target_height); + // 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: target_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 prev_counterparty_consensus_state = + // // ConsensusState::try_from(consensus_state.consensus_state.unwrap())?; + // println!( + // "Telling chain a about chain b latest block: {}", + // hex::encode(chain_b_latest_block.clone().block_id.unwrap().hash) + // ); + // // println!( + // // "header: {:?}", + // // chain_b_latest_block.block.clone().unwrap().header.unwrap() + // // ); + // println!( + // "chain_id {}, height {}, last_commit_hash: {}", + // chain_b_latest_block + // .block + // .clone() + // .unwrap() + // .header + // .unwrap() + // .chain_id, + // chain_b_latest_block + // .block + // .clone() + // .unwrap() + // .header + // .unwrap() + // .height, + // hex::encode( + // chain_b_latest_block + // .block + // .clone() + // .unwrap() + // .header + // .unwrap() + // .last_commit_hash + // ) + // ); + // 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)? + // .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(()) +} 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..e821957e36 --- /dev/null +++ b/crates/core/app/tests/ibc_handshake.rs @@ -0,0 +1,844 @@ +use { + anyhow::{Context as _, Result}, + cnidarium::{StateRead as _, TempStorage}, + common::{ + ibc_tests::{get_verified_genesis, MockRelayer, TestNodeWithIBC, ValidatorKeys}, + TempStorageExt as _, + }, + ibc_proto::ibc::core::client::v1::{ + query_client::QueryClient as IbcClientQueryClient, QueryClientStateRequest, + }, + ibc_types::{ + core::{ + client::{msgs::MsgCreateClient, ClientId, Height}, + commitment::{MerkleProof, MerkleRoot}, + }, + lightclients::tendermint::{ + client_state::{AllowUpdate, ClientState as TendermintClientState}, + TrustThreshold, + }, + path::ClientStatePath, + DomainType as _, + }, + once_cell::sync::Lazy, + penumbra_app::server::consensus::Consensus, + penumbra_ibc::{IbcRelay, MerklePrefixExt, IBC_COMMITMENT_PREFIX, IBC_PROOF_SPECS}, + penumbra_keys::test_keys, + penumbra_mock_client::MockClient, + penumbra_mock_consensus::TestNode, + penumbra_proto::{ + cnidarium::v1::{ + query_service_client::QueryServiceClient as CnidariumQueryServiceClient, + KeyValueRequest, + }, + util::tendermint_proxy::v1::{ + tendermint_proxy_service_client::TendermintProxyServiceClient, GetBlockByHeightRequest, + }, + DomainType, Message as _, + }, + penumbra_test_subscriber::set_tracing_subscriber_with_env_filter, + penumbra_transaction::{TransactionParameters, TransactionPlan}, + std::{str::FromStr as _, time::Duration}, + tap::{Tap, TapFallible as _}, + tendermint::Hash, + tokio::time, + tonic::transport::Channel, +}; + +/// The proof specs for the main store. +pub static MAIN_STORE_PROOF_SPEC: Lazy> = + Lazy::new(|| vec![cnidarium::ics23_spec()]); + +mod common; + +fn set_tracing_subscriber() -> tracing::subscriber::DefaultGuard { + let filter = "info,penumbra_app=info,penumbra_mock_consensus=trace,jmt=trace"; + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new(filter)) + .expect("should have a valid filter directive"); + set_tracing_subscriber_with_env_filter(filter) +} + +// Snapshot version is used as the revision height in the IBC client_state query. +// Therefore we need to validate that the snapshot revision is the same as the +// Mock Tendermint height. +#[tokio::test] +async fn mocktendermint_snapshot_versions() -> anyhow::Result<()> { + let _guard = set_tracing_subscriber(); + + let storage = TempStorage::new_with_penumbra_prefixes().await?; + + let proxy = penumbra_mock_tendermint_proxy::TestNodeProxy::new::(); + let mut node = { + let genesis = get_verified_genesis()?; + let consensus = Consensus::new(storage.clone()); + // Hardcoded keys for each chain for test reproducibility: + let sk_a = ed25519_consensus::SigningKey::from([0u8; 32]); + let vk_a = sk_a.verification_key(); + let keys = (sk_a, vk_a); + // let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .with_keys(vec![keys]) + .single_validator() + .with_tendermint_genesis(genesis) + .on_block(proxy.on_block_callback()) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + let grpc_url = "http://127.0.0.1:8081" // see #4517 + .parse::()? + .tap(|url| tracing::debug!(%url, "parsed grpc url")); + // Spawn the node's RPC server. + let _rpc_server = { + let make_svc = + penumbra_app::rpc::router(&storage, proxy, false /*enable_expensive_rpc*/)? + .into_router() + .layer(tower_http::cors::CorsLayer::permissive()) + .into_make_service() + .tap(|_| println!("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(|_| println!("grpc server is running")) + }; + time::sleep(time::Duration::from_secs(1)).await; + 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 mut tendermint_proxy_service_client = TendermintProxyServiceClient::new(channel.clone()); + + assert_eq!(u64::from(*node.height()), 0u64); + + // we're still on block 0, execute a block 1 with no transactions. + node.block().execute().await?; + + // block header 1 has now been created. + let block_1: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse = + tendermint_proxy_service_client + .get_block_by_height(GetBlockByHeightRequest { + // get block height 1 + height: 1.into(), + }) + .await? + .into_inner(); + + assert_eq!(u64::from(*node.height()), 1u64); + + // we know the block 1 app_hash should always be 5c94f2eabd29ac36f5be7f812a586b5dd44c10d586d2bb1a18e3679801d1b5dd + // for the test genesis data + println!("block 1: {:?}", block_1); + assert_eq!( + hex::decode("5c94f2eabd29ac36f5be7f812a586b5dd44c10d586d2bb1a18e3679801d1b5dd")?, + block_1.block.unwrap().header.unwrap().app_hash + ); + + let snapshot = storage.latest_snapshot(); + let storage_revision_height = snapshot.version(); + + let saved_height = node.height().clone(); + // JMT storage revision height should always match the mock tendermint height + assert_eq!(u64::from(saved_height), storage_revision_height); + // store the root of storage at this height for later verification + let saved_storage_root = snapshot.root_hash().await?; + println!( + "storage height is {} and storage root is {}", + storage_revision_height, + hex::encode(saved_storage_root.0) + ); + + // execute a few blocks + node.block().execute().await?; + node.block().execute().await?; + node.block().execute().await?; + + let proof_block: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse = + tendermint_proxy_service_client + .get_block_by_height(GetBlockByHeightRequest { + // Use the height from earlier + height: saved_height.into(), + }) + .await? + .into_inner(); + + // We fetched the block associated with the height from earlier + // and can validate that its app hash in the block header + // matches the value we got directly from storage earlier: + assert_eq!( + proof_block.block.clone().unwrap().header.unwrap().app_hash, + saved_storage_root.0, + "block app hash {} should match storage root {}", + hex::encode(proof_block.block.unwrap().header.unwrap().app_hash), + hex::encode(saved_storage_root.0) + ); + + Ok(()) +} + +/// Validates the cometbft mock behavior against real cometbft +/// using the same genesis data. +#[tokio::test] +async fn cometbft_mock_verification() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let _guard = set_tracing_subscriber(); + + let storage = TempStorage::new_with_penumbra_prefixes().await?; + + let mut node = { + let genesis = get_verified_genesis()?; + let consensus = Consensus::new(storage.clone()); + // let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_tendermint_genesis(genesis) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + // This is the app hash cometBFT sees in the InitChain message + assert_eq!( + node.last_app_hash_hex().to_lowercase(), + "5c94f2eabd29ac36f5be7f812a586b5dd44c10d586d2bb1a18e3679801d1b5dd" + ); + + node.block().execute().await?; + node.block().execute().await?; + node.block().execute().await?; + + Ok(()) +} + +#[tokio::test] +async fn verify_storage_proof_simple() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = set_tracing_subscriber(); + + let storage = TempStorage::new_with_penumbra_prefixes().await?; + + let start_time = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?; + + let proxy = penumbra_mock_tendermint_proxy::TestNodeProxy::new::(); + let mut node = { + let genesis = get_verified_genesis()?; + let consensus = Consensus::new(storage.clone()); + // let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_tendermint_genesis(genesis) + .on_block(proxy.on_block_callback()) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + let client = MockClient::new(test_keys::SPEND_KEY.clone()) + .with_sync_to_inner_storage(storage.clone()) + .await? + .tap( + |c| tracing::info!(client.notes = %c.notes.len(), "mock client synced to test storage"), + ); + + node.block().execute().await?; + + // Force the node to write an IBC client status into storage + // so we can retrieve with a proof: + let plan = { + let ibc_msg = IbcRelay::CreateClient(MsgCreateClient { + signer: "test".to_string(), + client_state: ibc_types::lightclients::tendermint::client_state::ClientState { + chain_id: TestNode::<()>::CHAIN_ID.to_string().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), + latest_height: Height { + revision_height: 55, + revision_number: 0, + }, + 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: start_time, + // These values don't matter since we are only checking the proof + // of the client state. + root: MerkleRoot { + hash: vec![0u8; 32], + }, + next_validators_hash: Hash::Sha256([0u8; 32]), + } + .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: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + } + }; + let tx = client.witness_auth_build(&plan).await?; + let client_id = "07-tendermint-0".to_string(); + let key = IBC_COMMITMENT_PREFIX + .apply_string(ClientStatePath(ClientId::from_str(&client_id)?).to_string()) + .as_bytes() + .to_vec(); + + // Create the fake client + node.block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + + // Now retrieving the client state directly from storage should succeed: + let snapshot = storage.latest_snapshot(); + + let unproven = snapshot + .get_raw(&String::from_utf8(key.clone())?) + .await? + .expect("present in storage"); + + // The unproven version should be present + assert!(!unproven.is_empty()); + + let (cnid_client_state, cnid_proof) = snapshot + .get_with_proof(key.clone()) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't get connection: {e}")))?; + + // The proven version should also be present + let cnid_client_state = cnid_client_state.unwrap(); + + // The proven version should be the same as the unproven. + assert_eq!(cnid_client_state, unproven); + + // Common proof parameters: + let proof_specs = IBC_PROOF_SPECS.to_vec(); + // The root will be the latest jmt hash. + let latest_root = storage.latest_snapshot().root_hash().await.unwrap(); + let root = MerkleRoot { + hash: latest_root.0.to_vec(), + }; + // Initial path is the key... + let csp = ClientStatePath(ClientId::from_str(&client_id)?); + let prefix = &IBC_COMMITMENT_PREFIX; + // With the prefix applied: + let merkle_path = prefix.apply(vec![csp.to_string()]); + + // Verify the proof against the results from calling get_with_proof. + cnid_proof.verify_membership( + &proof_specs, + root.clone(), + merkle_path.clone(), + cnid_client_state.clone(), + 0, + )?; + + println!("verified directly from storage"); + + // now verify the proof retrieved via a gRPC call + let grpc_url = "http://127.0.0.1:8081" // see #4517 + .parse::()? + .tap(|url| tracing::debug!(%url, "parsed grpc url")); + // Spawn the node's RPC server. + let _rpc_server = { + let make_svc = + penumbra_app::rpc::router(&storage, proxy, false /*enable_expensive_rpc*/)? + .into_router() + .layer(tower_http::cors::CorsLayer::permissive()) + .into_make_service() + .tap(|_| println!("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(|_| println!("grpc server is running")) + }; + + time::sleep(time::Duration::from_secs(1)).await; + 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 mut cnidarium_client = CnidariumQueryServiceClient::new(channel.clone()); + let mut ibc_client_query_client = IbcClientQueryClient::new(channel.clone()); + let mut tendermint_proxy_service_client = TendermintProxyServiceClient::new(channel.clone()); + let kvr = cnidarium_client + .key_value(tonic::Request::new(KeyValueRequest { + key: String::from_utf8(key.clone()).unwrap(), + proof: true, + })) + .await? + .into_inner(); + + let proof = kvr.proof.unwrap().try_into()?; + let value = kvr.value.unwrap().value; + + // The proof from cnidarium and from the RPC should be the same since nothing has + // happened on-chain since the cnidarium proof was generated. + assert_eq!(cnid_proof, proof); + // Same for the values. + assert_eq!(value, cnid_client_state); + + proof.verify_membership( + &proof_specs, + root.clone(), + merkle_path.clone(), + value.clone(), + 0, + )?; + + let snapshot = storage.latest_snapshot(); + let storage_revision_height = snapshot.version(); + + let latest_height = node.height().clone(); + assert_eq!(u64::from(latest_height), storage_revision_height); + + // Try fetching the client state via the IBC API + // height 2 + // WRONG vvv these don't match what's in the block headers + let node_last_app_hash = node.last_app_hash(); + println!( + "making IBC client state request at height {} and hash {}", + latest_height, + // e0c071d4b2198c7e5f9fdee7d6618bf36ea75fdecd56df315ba2ae87b9a50718 (height 3 header app_hash) + hex::encode(node_last_app_hash) + ); + let ibc_client_state_response = ibc_client_query_client + .client_state(QueryClientStateRequest { + client_id: "07-tendermint-0".to_string(), + }) + .await? + .into_inner(); + + let ibc_proof = MerkleProof::decode(ibc_client_state_response.clone().proof.as_slice())?; + let ibc_value = ibc_client_state_response.client_state.unwrap(); + + // let cs = ibc_types::lightclients::tendermint::client_state::ClientState::try_from( + // ibc_value.clone(), + // )?; + // println!("client state: {:?}", cs); + // // let cs2 = ibc_types::lightclients::tendermint::client_state::ClientState::try_from(Any { + // // type_url: TENDERMINT_CLIENT_STATE_TYPE_URL.to_string(), + // // value: value.clone().into(), + // // })?; + // let client_state = ibc_proto::google::protobuf::Any::decode(value.as_ref())?; + // let cs2 = ibc_proto::ibc::lightclients::tendermint::v1::ClientState::decode( + // &*client_state.value.clone(), + // )?; + // let cs3 = + // ibc_types::lightclients::tendermint::client_state::ClientState::try_from(client_state)?; + // println!("client state2: {:?}", cs2); + // println!("client state3: {:?}", cs3); + + // let client_state = ibc_proto::google::protobuf::Any::decode(value.as_ref())?; + // let cs1 = ibc_proto::ibc::lightclients::tendermint::v1::ClientState::decode(&*client.value)?; + // let client_state1 = TendermintClientState::try_from(cs1.clone())?; + + assert_eq!(ibc_value.encode_to_vec(), value); + + // 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 = + 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 height of the ibc response should be the same as the height of the proof block + assert_eq!( + ibc_client_state_response + .proof_height + .clone() + .unwrap() + .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) + ); + // 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 {}", + // node_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"); + + Ok(()) + .tap(|_| drop(node)) + .tap(|_| drop(storage)) + .tap(|_| drop(guard)) +} + +/// 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(); + + // 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")?; + let start_time_b = 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?; + + chain_a_ibc.node.block().execute().await?; + chain_b_ibc.node.block().execute().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?, + // ); + + // 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 + // The Clients need to be created on each chain prior to the handshake. + relayer._create_clients().await?; + + relayer._sync_chains().await?; + + relayer._build_and_send_connection_open_init().await?; + + relayer._sync_chains().await?; + + relayer._build_and_send_connection_open_try().await?; + + Ok(()).tap(|_| drop(relayer)).tap(|_| drop(guard)) +} + +/// Tests of the mock IBC relayer +#[tokio::test] +async fn ibc_granular_tests() -> anyhow::Result<()> { + // let gk_hex = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29".to_string(); + // let gk = hex::decode(gk_hex).unwrap(); + // assert_eq!(gk.len(), 32); + // let pbgk = ProtoGovernanceKey { gk: gk.clone() }; + + // let governance_key: GovernanceKey = pbgk.try_into().unwrap(); + // gov key needs to be a + // decaf377::Encoding + // aka it needs to actually be a code point + // maybe better to just generate a real key + // or use a preexisting test one + // println!("govvy : {:?}", governance_key); + + // let governance_key: penumbra_stake::GovernanceKey = + // penumbra_proto::core::keys::v1::GovernanceKey { + // gk: [0u8; 32].to_vec(), + // } + // .try_into() + // .unwrap(); + // Install a test logger, and acquire some temporary storage. + let _guard = common::set_tracing_subscriber(); + + let start_time = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?; + + let vkeys_a = ValidatorKeys::from_seed([0u8; 32]); + let sk_a = vkeys_a.validator_cons_sk.ed25519_signing_key().unwrap(); + let vkeys_b = ValidatorKeys::from_seed([1u8; 32]); + let sk_b = vkeys_b.validator_cons_sk.ed25519_signing_key().unwrap(); + + // TODO: make testnodewithibc work with a validatorkeys struct + let ska = ed25519_consensus::SigningKey::try_from(sk_a.as_bytes())?; + let keys_a = (ska.clone(), ska.verification_key()); + let skb = ed25519_consensus::SigningKey::try_from(sk_b.as_bytes())?; + 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.clone(), keys_a).await?; + let mut chain_b_ibc = TestNodeWithIBC::new("b", start_time, keys_b).await?; + + // The two chains can't IBC handshake during the first block, let's fast forward + // them both 1. + chain_a_ibc.node.block().execute().await?; + chain_b_ibc.node.block().execute().await?; + + // The Relayer will handle IBC operations and manage state for the two test chains + let mut relayer = MockRelayer { + chain_a_ibc, + chain_b_ibc, + }; + + relayer._create_clients().await?; + // relayer._sync_chains().await?; + // relayer._build_and_send_connection_open_init().await?; + relayer._sync_chains().await?; + + relayer.chain_a_ibc.node.block().execute().await?; + relayer.chain_b_ibc.node.block().execute().await?; + + // chain A will have created a new client for chain B + // we should be able to retrieve the connection, check its + // details, and verify the proof that was generated for it + let client_state_of_b_on_a_response = relayer + .chain_a_ibc + .ibc_client_query_client + .client_state(QueryClientStateRequest { + client_id: relayer.chain_b_ibc.client_id.to_string(), + }) + .await? + .into_inner(); + + let client_state = TendermintClientState::try_from( + client_state_of_b_on_a_response + .client_state + .clone() + .unwrap(), + )?; + println!( + "FETCHED CLIENT STATE OF B ON A AT B HEIGHT {}, A HEIGHT {}, CLIENT STATE HEIGHT {}", + relayer.chain_a_ibc.get_latest_height().await?, + relayer.chain_b_ibc.get_latest_height().await?, + client_state.latest_height() + ); + + // Decode the proofs... + let proof_client_state_of_b_on_a = + MerkleProof::decode(client_state_of_b_on_a_response.clone().proof.as_slice())?; + + // Validate the proofs + let proof_specs = IBC_PROOF_SPECS.to_vec(); + // Initial path is the key... + let csp = ClientStatePath(ClientId::from_str(&relayer.chain_b_ibc.client_id.as_str())?); + let prefix = &IBC_COMMITMENT_PREFIX; + + // The root is the current root of chain a at the height of the proof + let proof_height = client_state_of_b_on_a_response + .proof_height + .clone() + .unwrap(); + let chain_a_proof_block: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse = + relayer + .chain_a_ibc + .tendermint_proxy_service_client + .get_block_by_height(GetBlockByHeightRequest { + height: proof_height.revision_height.try_into()?, + }) + .await? + .into_inner(); + let proof_root = MerkleRoot { + hash: chain_a_proof_block.block.unwrap().header.unwrap().app_hash, + }; + println!( + "proof root for height {}: {}", + proof_height.revision_height, + hex::encode(proof_root.hash.clone()) + ); + let merkle_path = prefix.apply(vec![csp.to_string()]); + + let unproven_client_state = client_state_of_b_on_a_response + .clone() + .client_state + .unwrap(); + + // Now retrieving the client state directly from storage should succeed: + let snapshot = relayer.chain_a_ibc.storage.clone().latest_snapshot(); + + let client_id = "07-tendermint-0".to_string(); + let key = IBC_COMMITMENT_PREFIX + .apply_string(ClientStatePath(ClientId::from_str(&client_id)?).to_string()) + .as_bytes() + .to_vec(); + let (cs_opt, cnid_proof) = snapshot + .get_with_proof(key.clone()) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't get client: {e}")))?; + + let client_state = cs_opt + .clone() + .map(|cs_opt| ibc_proto::google::protobuf::Any::decode(cs_opt.as_ref())) + .transpose() + .map_err(|e| tonic::Status::aborted(format!("couldn't decode client state: {e}")))?; + + // The proven version should also be present + let client = client_state.unwrap(); + let unproven_client = unproven_client_state.clone(); + + // assert_eq!(client.value, cs_opt.clone().unwrap()); + let cs1 = ibc_proto::ibc::lightclients::tendermint::v1::ClientState::decode(&*client.value)?; + let client_state1 = TendermintClientState::try_from(cs1.clone())?; + let cs2 = + ibc_proto::ibc::lightclients::tendermint::v1::ClientState::decode(&*unproven_client.value)?; + let client_state2 = TendermintClientState::try_from(cs2.clone())?; + println!("client state 1 (from cnidarium): {:?}", client_state1); + println!( + "client state 2 (from the client_state RPC): {:?}", + client_state2 + ); + assert_eq!(cs1, cs2); + + // The response from the RPC should be the same as the client state from storage + assert_eq!( + cs_opt.clone().unwrap(), + unproven_client_state.encode_to_vec() + ); + + // Test the signature directly from cnid against the client state directly + // from cnid + println!("cnid_proof check"); + let cnid_root = snapshot.root_hash().await?; + cnid_proof.verify_membership( + &proof_specs, + MerkleRoot { + hash: cnid_root.0.to_vec(), + }, + merkle_path.clone(), + cs_opt.unwrap(), + 0, + )?; + + println!("rpc proof check"); + // The unproven client state requires a root of the proof height + // HOWEVER right now it seems like the root you get (from the GetBlockByHeight + // app_hash ultimately) is one behind the root it was actually retrieved with. + + // statement of the problem: + // the storage version (height) + // is off by one with the tendermint header + // height. + // the root returned in the getblocksbyheight header + // is for one height too low, i.e. for height 3 it returns the height 2 + // root. + // are we wrong when creating the header? + // when we are creating the client, the node's merkle root is correct. + // then we call commit to commit that block. + proof_client_state_of_b_on_a.verify_membership( + &proof_specs, + proof_root, + merkle_path.clone(), + unproven_client_state.encode_to_vec(), + 0, + )?; + + // _build_and_send_update_client(&mut relayer.chain_a_ibc, &mut relayer.chain_b_ibc).await?; + + Ok(()) +} + +#[tokio::test] +async fn real_cometbft_tests() -> Result<()> { + let grpc_url = "http://127.0.0.1:8080" + .parse::()? + .tap(|url| tracing::debug!(%url, "parsed grpc url")); + + 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 mut tendermint_proxy_service_client = TendermintProxyServiceClient::new(channel.clone()); + + let b = tendermint_proxy_service_client + .get_block_by_height(GetBlockByHeightRequest { height: 333 }) + .await? + .into_inner(); + + println!( + "block 333 app_hash: {}, last_block_id: {:?}, height: {} last_commit_hash: {}", + hex::encode(b.clone().block.unwrap().header.unwrap().app_hash), + b.clone().block.unwrap().header.unwrap().last_block_id, + b.clone().block.unwrap().header.unwrap().height, + hex::encode(b.clone().block.unwrap().header.unwrap().last_commit_hash), + ); + + println!("BLOCK: {:?}", b); + + Ok(()) +} diff --git a/crates/core/component/sct/src/component/clock.rs b/crates/core/component/sct/src/component/clock.rs index e74b1aec84..cafb39be4c 100644 --- a/crates/core/component/sct/src/component/clock.rs +++ b/crates/core/component/sct/src/component/clock.rs @@ -121,6 +121,11 @@ pub trait EpochManager: StateWrite { /// Index the current epoch by height. fn put_epoch_by_height(&mut self, height: u64, epoch: Epoch) { + println!("put_epoch_by_height: {}, epoch: {:?}", height, epoch); + println!( + "state_key::epoch_manager::epoch_by_height(height): {}", + state_key::epoch_manager::epoch_by_height(height) + ); self.put(state_key::epoch_manager::epoch_by_height(height), epoch) } } diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index 2be7700835..0209f6207a 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -185,7 +185,6 @@ where // The first (non-genesis) block has height 1. let height = { let height = test_node.height.increment(); - test_node.height = height; tracing::Span::current().record("height", height.value()); height }; diff --git a/crates/test/mock-consensus/src/block/signature.rs b/crates/test/mock-consensus/src/block/signature.rs index 15ce62589a..53cd544220 100644 --- a/crates/test/mock-consensus/src/block/signature.rs +++ b/crates/test/mock-consensus/src/block/signature.rs @@ -42,6 +42,7 @@ mod sign { CommitSig::BlockIdFlagCommit { validator_address, + // TODO: this time needs to be one block forward timestamp: canonical.timestamp.expect("timestamp should be present"), signature: Some(signature.into()), } diff --git a/crates/test/mock-consensus/src/builder/init_chain.rs b/crates/test/mock-consensus/src/builder/init_chain.rs index 909e61c074..beea8b39a1 100644 --- a/crates/test/mock-consensus/src/builder/init_chain.rs +++ b/crates/test/mock-consensus/src/builder/init_chain.rs @@ -171,9 +171,9 @@ impl Builder { timestamp, ts_callback: ts_callback.unwrap_or(Box::new(default_ts_callback)), chain_id, - consensus_params_hash: sha2::Sha256::digest(hashed_params.encode_to_vec()).to_vec(), // No last commit for the genesis block. last_commit: None, + consensus_params_hash: sha2::Sha256::digest(hashed_params.encode_to_vec()).to_vec(), }) } diff --git a/crates/test/mock-consensus/src/lib.rs b/crates/test/mock-consensus/src/lib.rs index acaa435b80..70828ca4b9 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,11 @@ 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 most recent `timestamp` value. pub fn timestamp(&self) -> &Time { &self.timestamp diff --git a/crates/test/mock-tendermint-proxy/src/proxy.rs b/crates/test/mock-tendermint-proxy/src/proxy.rs index 0a4d9a8a66..3fc52a19f3 100644 --- a/crates/test/mock-tendermint-proxy/src/proxy.rs +++ b/crates/test/mock-tendermint-proxy/src/proxy.rs @@ -52,8 +52,8 @@ impl TestNodeProxy { Box::new(move |block| inner.on_block(block)) } - /// Returns the last committed block height. - fn last_block_height(&self) -> tendermint::block::Height { + /// Returns the latest committed block height. + fn latest_block_height(&self) -> tendermint::block::Height { self.inner .blocks() .last_key_value() @@ -164,7 +164,7 @@ impl TendermintProxyService for TestNodeProxy { req: tonic::Request, ) -> Result, Status> { let GetStatusRequest { .. } = req.into_inner(); - let latest_block_height = self.last_block_height().into(); + let latest_block_height = self.latest_block_height().into(); let block_ts: tendermint_proto::google::protobuf::Timestamp = self.timestamp().into(); let sync_info = SyncInfo { latest_block_hash: self @@ -213,6 +213,7 @@ impl TendermintProxyService for TestNodeProxy { let GetBlockByHeightRequest { height } = req.into_inner(); let height = tendermint::block::Height::try_from(height).expect("height should be less than 2^63"); + println!("get block height: {:?}", height); let block = self.inner.blocks().get(&height).cloned(); // the response uses the penumbra type but internally we use the tendermint type @@ -224,7 +225,6 @@ impl TendermintProxyService for TestNodeProxy { tracing::warn!(?height, error = ?e, "proxy: error fetching blocks"); Err(tonic::Status::internal("error fetching blocks")) })?; - Ok(GetBlockByHeightResponse { block_id: block.map(|b| penumbra_proto::tendermint::types::BlockId { hash: b.header.hash().into(), diff --git a/crates/view/Cargo.toml b/crates/view/Cargo.toml index 4f43124d18..443690c576 100644 --- a/crates/view/Cargo.toml +++ b/crates/view/Cargo.toml @@ -66,6 +66,5 @@ tokio = {workspace = true, features = ["full"]} tokio-stream = {workspace = true, features = ["sync"]} tonic = {workspace = true} tracing = {workspace = true} -tracing-subscriber = {workspace = true} url = {workspace = true} pbjson-types = { workspace = true }