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