From 8f87b286d0423fcc12b6cb4ec0a63116d32842f5 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 | 5 +- .../src/gen/proto_descriptor.bin.no_lfs | Bin 99489 -> 102002 bytes crates/core/app/Cargo.toml | 1 - ..._can_define_and_delegate_to_a_validator.rs | 3 +- .../app_can_deposit_into_community_pool.rs | 3 +- .../app_can_disable_community_pool_spends.rs | 3 +- .../app_can_propose_community_pool_spends.rs | 3 +- .../app_can_spend_notes_and_detect_outputs.rs | 3 +- ...p_can_sweep_a_collection_of_small_notes.rs | 3 +- .../app_can_undelegate_from_a_validator.rs | 3 +- crates/core/app/tests/app_check_dex_vcb.rs | 5 +- ...ator_definitions_with_invalid_auth_sigs.rs | 3 +- .../app_reproduce_testnet_75_vcb_close.rs | 5 +- ...me_for_genesis_validator_missing_blocks.rs | 4 +- ...me_for_genesis_validator_signing_blocks.rs | 3 +- ..._uptime_for_validators_only_once_active.rs | 3 +- crates/core/app/tests/common/ibc_tests/mod.rs | 136 ++++ .../core/app/tests/common/ibc_tests/node.rs | 190 +++++ .../app/tests/common/ibc_tests/relayer.rs | 765 ++++++++++++++++++ crates/core/app/tests/common/mod.rs | 3 + .../core/app/tests/common/temp_storage_ext.rs | 7 +- .../app/tests/common/test_node_builder_ext.rs | 7 +- crates/core/app/tests/ibc_handshake.rs | 39 + ...onsensus_can_define_a_genesis_validator.rs | 3 +- ...sus_can_send_a_sequence_of_empty_blocks.rs | 3 +- crates/core/app/tests/spend.rs | 11 +- crates/core/app/tests/swap_and_swap_claim.rs | 12 +- .../view_server_can_be_served_on_localhost.rs | 3 +- .../component/ibc/src/component/client.rs | 7 +- .../ibc/src/component/ics02_validation.rs | 6 +- .../msg_handler/connection_open_ack.rs | 6 +- .../msg_handler/connection_open_try.rs | 6 +- crates/core/component/ibc/src/lib.rs | 2 +- crates/crypto/proof-params/Cargo.toml | 1 - .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 547351 -> 549864 bytes .../proto/src/protobuf/tendermint_compat.rs | 163 ++-- crates/test/mock-consensus/Cargo.toml | 2 + crates/test/mock-consensus/src/abci.rs | 4 + crates/test/mock-consensus/src/block.rs | 100 ++- .../mock-consensus/src/block/signature.rs | 68 +- crates/test/mock-consensus/src/builder.rs | 1 + .../mock-consensus/src/builder/init_chain.rs | 38 +- crates/test/mock-consensus/src/lib.rs | 6 + .../test/mock-tendermint-proxy/src/proxy.rs | 6 +- .../tendermint-proxy/src/tendermint_proxy.rs | 10 +- crates/view/Cargo.toml | 1 - 46 files changed, 1518 insertions(+), 138 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 efa355b140..abf228d648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4549,7 +4549,6 @@ dependencies = [ "penumbra-shielded-pool", "penumbra-stake", "penumbra-tct", - "penumbra-tendermint-proxy", "penumbra-test-subscriber", "penumbra-tower-trace", "penumbra-transaction", @@ -5168,10 +5167,12 @@ dependencies = [ "anyhow", "bytes", "ed25519-consensus", + "hex", "rand_core", "sha2 0.10.8", "tap", "tendermint", + "tendermint-proto", "tower", "tracing", ] @@ -5241,7 +5242,6 @@ dependencies = [ "bech32", "decaf377", "hex", - "lazy_static", "num-bigint", "once_cell", "rand", @@ -5713,7 +5713,6 @@ dependencies = [ "tokio-stream", "tonic", "tracing", - "tracing-subscriber 0.3.18", "url", ] diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index 6e23fc5c3fa90b2ae6af49ab1a84a133ac4ac9bb..f41da7426e0409c58117d4e9d6a186a1632aff36 100644 GIT binary patch delta 23732 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 delta 21358 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 diff --git a/crates/core/app/Cargo.toml b/crates/core/app/Cargo.toml index 0815bd9191..6fc5a99c54 100644 --- a/crates/core/app/Cargo.toml +++ b/crates/core/app/Cargo.toml @@ -53,7 +53,6 @@ penumbra-sct = { workspace = true, default-features = true } penumbra-shielded-pool = { workspace = true, features = ["component"], default-features = true } penumbra-stake = { workspace = true, default-features = true } penumbra-tct = { workspace = true, default-features = true } -penumbra-tendermint-proxy = { path = "../../util/tendermint-proxy" } penumbra-test-subscriber = { workspace = true } penumbra-tower-trace = { path = "../../util/tower-trace" } penumbra-transaction = { workspace = true, features = ["parallel"], default-features = true } diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index fe1cf55adc..0027ee1408 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -2,6 +2,7 @@ use { self::common::{BuilderExt, TestNodeExt, ValidatorDataReadExt}, anyhow::anyhow, cnidarium::TempStorage, + common::TempStorageExt as _, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{ genesis::{self, AppState}, @@ -32,7 +33,7 @@ const EPOCH_DURATION: u64 = 8; async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Configure an AppState with slightly shorter epochs than usual. let app_state = AppState::Content( diff --git a/crates/core/app/tests/app_can_deposit_into_community_pool.rs b/crates/core/app/tests/app_can_deposit_into_community_pool.rs index c2b69a7d04..895e61014e 100644 --- a/crates/core/app/tests/app_can_deposit_into_community_pool.rs +++ b/crates/core/app/tests/app_can_deposit_into_community_pool.rs @@ -2,6 +2,7 @@ use { self::common::BuilderExt, anyhow::anyhow, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -28,7 +29,7 @@ mod common; async fn app_can_deposit_into_community_pool() -> anyhow::Result<()> { // Install a test logger, and acquire some temporary storage. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Define our application state, and start the test node. let mut test_node = { diff --git a/crates/core/app/tests/app_can_disable_community_pool_spends.rs b/crates/core/app/tests/app_can_disable_community_pool_spends.rs index 4cfb3c6b3a..0854e3633f 100644 --- a/crates/core/app/tests/app_can_disable_community_pool_spends.rs +++ b/crates/core/app/tests/app_can_disable_community_pool_spends.rs @@ -2,6 +2,7 @@ use { self::common::ValidatorDataReadExt, anyhow::anyhow, cnidarium::TempStorage, + common::TempStorageExt as _, decaf377_rdsa::VerificationKey, penumbra_app::{ genesis::{AppState, Content}, @@ -49,7 +50,7 @@ const PROPOSAL_VOTING_BLOCKS: u64 = 3; async fn app_can_disable_community_pool_spends() -> anyhow::Result<()> { // Install a test logger, and acquire some temporary storage. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Define a helper to get the current community pool balance. let pool_balance = || async { storage.latest_snapshot().community_pool_balance().await }; diff --git a/crates/core/app/tests/app_can_propose_community_pool_spends.rs b/crates/core/app/tests/app_can_propose_community_pool_spends.rs index 2b7d31be0f..d8af8d1b07 100644 --- a/crates/core/app/tests/app_can_propose_community_pool_spends.rs +++ b/crates/core/app/tests/app_can_propose_community_pool_spends.rs @@ -2,6 +2,7 @@ use { self::common::ValidatorDataReadExt, anyhow::anyhow, cnidarium::TempStorage, + common::TempStorageExt as _, decaf377_rdsa::VerificationKey, penumbra_app::{ genesis::{AppState, Content}, @@ -49,7 +50,7 @@ const PROPOSAL_VOTING_BLOCKS: u64 = 3; async fn app_can_propose_community_pool_spends() -> anyhow::Result<()> { // Install a test logger, and acquire some temporary storage. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Define a helper to get the current community pool balance. let pool_balance = || async { storage.latest_snapshot().community_pool_balance().await }; diff --git a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs index 21b40406ba..49d7b93018 100644 --- a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs +++ b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs @@ -2,6 +2,7 @@ use { self::common::BuilderExt, anyhow::anyhow, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -27,7 +28,7 @@ mod common; async fn app_can_spend_notes_and_detect_outputs() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; let mut test_node = { let app_state = AppState::Content( genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()), diff --git a/crates/core/app/tests/app_can_sweep_a_collection_of_small_notes.rs b/crates/core/app/tests/app_can_sweep_a_collection_of_small_notes.rs index c182a4cca5..dfe096bdde 100644 --- a/crates/core/app/tests/app_can_sweep_a_collection_of_small_notes.rs +++ b/crates/core/app/tests/app_can_sweep_a_collection_of_small_notes.rs @@ -1,6 +1,7 @@ use { anyhow::Context, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{AppState, Content}, server::consensus::Consensus, @@ -37,7 +38,7 @@ const COUNT: usize = SWEEP_COUNT + 1; async fn app_can_sweep_a_collection_of_small_notes() -> anyhow::Result<()> { // Install a test logger, and acquire some temporary storage. let guard = common::set_tracing_subscriber_with_env_filter("info".into()); - let storage = TempStorage::new().await?; + 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::(); diff --git a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs index d927fd823f..82ec8b14b6 100644 --- a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs +++ b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs @@ -2,6 +2,7 @@ use { self::common::{BuilderExt, TestNodeExt, ValidatorDataReadExt}, anyhow::anyhow, cnidarium::TempStorage, + common::TempStorageExt as _, decaf377_fmd::Precision, penumbra_app::{ genesis::{self, AppState}, @@ -37,7 +38,7 @@ const UNBONDING_DELAY: u64 = 4; async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Helper function to get the latest block height. let get_latest_height = || async { diff --git a/crates/core/app/tests/app_check_dex_vcb.rs b/crates/core/app/tests/app_check_dex_vcb.rs index 266e15f4a7..85d6a87f5a 100644 --- a/crates/core/app/tests/app_check_dex_vcb.rs +++ b/crates/core/app/tests/app_check_dex_vcb.rs @@ -25,7 +25,10 @@ use std::{ops::Deref, sync::Arc}; /// This bug was fixed in #4643. async fn dex_vcb_tracks_multiswap() -> anyhow::Result<()> { let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1776); - let storage = TempStorage::new().await?.apply_default_genesis().await?; + let storage = TempStorage::new_with_penumbra_prefixes() + .await? + .apply_default_genesis() + .await?; let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); // Create the first swap: diff --git a/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs b/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs index 8242d817fa..5bc71e50ed 100644 --- a/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs +++ b/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs @@ -1,6 +1,7 @@ use { self::common::{BuilderExt, ValidatorDataReadExt}, cnidarium::TempStorage, + common::TempStorageExt as _, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{ genesis::{self, AppState}, @@ -23,7 +24,7 @@ mod common; async fn app_rejects_validator_definitions_with_invalid_auth_sigs() -> anyhow::Result<()> { // Install a test logger, and acquire some temporary storage. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Start the test node. let mut node = { diff --git a/crates/core/app/tests/app_reproduce_testnet_75_vcb_close.rs b/crates/core/app/tests/app_reproduce_testnet_75_vcb_close.rs index 17c3316a74..fcf7a4e21e 100644 --- a/crates/core/app/tests/app_reproduce_testnet_75_vcb_close.rs +++ b/crates/core/app/tests/app_reproduce_testnet_75_vcb_close.rs @@ -2,18 +2,19 @@ use { self::common::BuilderExt, anyhow::anyhow, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, }, penumbra_asset::{Value, STAKING_TOKEN_ASSET_ID}, - penumbra_auction::StateReadExt as _, penumbra_auction::{ auction::{ dutch::{ActionDutchAuctionEnd, ActionDutchAuctionSchedule, DutchAuctionDescription}, AuctionNft, }, component::AuctionStoreRead, + StateReadExt as _, }, penumbra_keys::test_keys, penumbra_mock_client::MockClient, @@ -65,7 +66,7 @@ async fn app_can_reproduce_tesnet_75_vcb_close() -> anyhow::Result<()> { common::set_tracing_subscriber_with_env_filter(filter) }; - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; let app_state = AppState::Content( genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()), ); diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs index 6d55488163..1719a0e657 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs @@ -2,6 +2,7 @@ use { self::common::{BuilderExt, ValidatorDataReadExt}, anyhow::Context, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -18,7 +19,7 @@ mod common; async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Start the test node. let mut node = { @@ -60,7 +61,6 @@ async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Res let height = 4; for i in 1..=height { node.block() - .with_signatures(Default::default()) .execute() .tap(|_| trace!(%i, "executing block with no signatures")) .instrument(error_span!("executing block with no signatures", %i)) diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs index 9685a37395..309afa4256 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs @@ -2,6 +2,7 @@ use { self::common::{BuilderExt, ValidatorDataReadExt}, anyhow::Context, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -18,7 +19,7 @@ mod common; async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Start the test node. let mut node = { diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index 19674ae33e..de687b1356 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -1,6 +1,7 @@ use { self::common::{BuilderExt, TestNodeExt, ValidatorDataReadExt}, cnidarium::TempStorage, + common::TempStorageExt as _, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{ genesis::{self, AppState}, @@ -32,7 +33,7 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; // Configure an AppState with slightly shorter epochs than usual. let app_state = AppState::Content( 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..3fbc110b57 --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/mod.rs @@ -0,0 +1,136 @@ +use { + anyhow::{anyhow, Result}, + ibc_types::{ + core::client::Height, + lightclients::tendermint::{ + consensus_state::ConsensusState, header::Header as TendermintHeader, + }, + }, +}; + +mod relayer; +#[allow(unused_imports)] +pub use relayer::MockRelayer; + +mod node; +pub use node::TestNodeWithIBC; + +// TODO: this needs to move somewhere else +#[allow(dead_code)] +pub fn create_tendermint_header( + pk: &ed25519_consensus::VerificationKey, + prev_counterparty_consensus_state: Option<(Height, ConsensusState)>, + penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse{block_id: _, block}: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse, +) -> Result { + 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; + 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: block + .last_commit + .map(|lc| { + lc.signatures + .iter() + .map(|a| tendermint_proto::v0_37::types::CommitSig { + block_id_flag: a.block_id_flag, + validator_address: a.validator_address.clone(), + timestamp: Some(tendermint_proto::google::protobuf::Timestamp { + seconds: a.timestamp.as_ref().expect("time").seconds, + nanos: a.timestamp.clone().expect("time").nanos, + }), + signature: a.signature.clone(), + }) + .collect() + }) + .unwrap(), + }), + }; + + 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(""), + ); + 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 + Ok(TendermintHeader { + signed_header, + validator_set: validator_set.clone(), + trusted_validator_set: validator_set.clone(), + trusted_height: prev_counterparty_consensus_state + .map(|cs| cs.0) + .unwrap_or_else(|| ibc_types::core::client::Height { + revision_number: 0, + revision_height: 1, + }), + }) +} 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..cb001cad59 --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/node.rs @@ -0,0 +1,190 @@ +use { + crate::common::{BuilderExt as _, TempStorageExt as _}, + anyhow::{anyhow, Context as _, Result}, + cnidarium::TempStorage, + 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, + }, + 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}, + 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 client: MockClient, + 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)] +impl TestNodeWithIBC { + pub async fn new(suffix: &str) -> 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() + .single_validator() + .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"))? + }; + + // Sync the mock client, using the test wallet's spend key, to the latest snapshot. + let client = MockClient::new(test_keys::SPEND_KEY.clone()) + .with_sync_to_storage(&storage) + .await? + .tap(|c| info!(client.notes = %c.notes.len(), "mock client synced to test storage")); + + // 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, + client, + 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 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, + )?) + } + + pub async fn get_prev_counterparty_consensus_state( + &self, + client_id: &ClientId, + height: &Height, + ) -> Result> { + self.storage + .clone() + .latest_snapshot() + .prev_verified_consensus_state(client_id, height) + .await + } +} 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..a9aa2ac44d --- /dev/null +++ b/crates/core/app/tests/common/ibc_tests/relayer.rs @@ -0,0 +1,765 @@ +use { + super::TestNodeWithIBC, + crate::common::ibc_tests::create_tendermint_header, + anyhow::Result, + ibc_proto::ibc::core::{ + client::v1::{QueryClientStateRequest, QueryConsensusStateRequest}, + connection::v1::QueryConnectionRequest, + }, + ibc_types::{ + core::{ + client::msgs::{MsgCreateClient, MsgUpdateClient}, + commitment::{MerkleProof, MerkleRoot}, + connection::{ + msgs::{ + MsgConnectionOpenAck, MsgConnectionOpenConfirm, MsgConnectionOpenInit, + MsgConnectionOpenTry, + }, + ConnectionEnd, Counterparty, State as ConnectionState, Version, + }, + }, + lightclients::tendermint::{client_state::AllowUpdate, TrustThreshold}, + DomainType as _, + }, + penumbra_ibc::{ + component::ConnectionStateReadExt as _, IbcRelay, IBC_COMMITMENT_PREFIX, IBC_PROOF_SPECS, + }, + penumbra_proto::{util::tendermint_proxy::v1::GetBlockByHeightRequest, DomainType}, + penumbra_transaction::{TransactionParameters, TransactionPlan}, + 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 + + 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 + { + println!("Send Init to chain A"); + _build_and_send_connection_open_init(&mut self.chain_a_ibc, &mut self.chain_b_ibc) + .await?; + } + + let (a_state, b_state) = self.get_connection_states().await?; + assert!(a_state == ConnectionState::Init && b_state == ConnectionState::Uninitialized); + + // 2. send the Try message to chain B + { + println!("Send Try to chain B"); + _build_and_send_connection_open_try(&mut self.chain_a_ibc, &mut self.chain_b_ibc) + .await?; + } + + let (a_state, b_state) = self.get_connection_states().await?; + assert!(a_state == ConnectionState::Init && b_state == ConnectionState::TryOpen); + + // 3. Send the Ack message to chain A + { + println!("Send Ack to chain A"); + _build_and_send_connection_open_ack(&mut self.chain_a_ibc, &mut self.chain_b_ibc) + .await?; + } + + let (a_state, b_state) = self.get_connection_states().await?; + assert!(a_state == ConnectionState::Open && b_state == ConnectionState::TryOpen); + + // 4. Send the Confirm message to chain B + { + println!("Send Try to chain B"); + _build_and_send_connection_open_confirm(&mut self.chain_a_ibc, &mut self.chain_b_ibc) + .await?; + } + + let (a_state, b_state) = self.get_connection_states().await?; + assert!(a_state == ConnectionState::Open && b_state == ConnectionState::Open); + + Ok(()) + } + + pub async fn _create_clients(&mut self) -> Result<(), anyhow::Error> { + // 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(); + // 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: Time::now(), + 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.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(()) + } + + 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 build UpdateClient to send to chain A +async fn _build_and_send_update_client( + chain_a_ibc: &mut TestNodeWithIBC, + chain_b_ibc: &mut TestNodeWithIBC, +) -> Result<()> { + println!( + "send update client for chain {} to chain {}", + chain_b_ibc.chain_id, chain_a_ibc.chain_id + ); + + // Fetch validators from chain B + // Note: since there's no real tendermint running + // and this isn't implemented in the TendermintProxyServiceClient, + // we just fake it for the test + println!("get latest height for chain B"); + let chain_b_height = chain_b_ibc.get_latest_height().await?; + println!("get block b 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(); + println!("Chain B latest block: {:?}", chain_b_latest_block); + + // Look up the last recorded consensus state for the counterparty client on chain A + let prev_counterparty_consensus_state = chain_a_ibc + .get_prev_counterparty_consensus_state(&chain_a_ibc.client_id, &chain_b_height) + .await?; + println!( + "prev counterparty consensus state: {:?}", + prev_counterparty_consensus_state + ); + let plan = { + let ibc_msg = IbcRelay::UpdateClient(MsgUpdateClient { + signer: chain_b_ibc.signer.clone(), + client_id: chain_a_ibc.client_id.clone(), + client_message: create_tendermint_header( + chain_b_ibc + .node + .keyring() + .iter() + .next() + .expect("validator key in keyring") + .0, + prev_counterparty_consensus_state, + 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.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(()) +} + +// 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( + chain_a_ibc: &mut TestNodeWithIBC, + chain_b_ibc: &mut TestNodeWithIBC, +) -> Result<()> { + let chain_b_connection_id = chain_b_ibc.connection_id.clone(); + let chain_a_connection_id = chain_a_ibc.connection_id.clone(); + let connection_of_a_on_b_response = 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 + _build_and_send_update_client(chain_a_ibc, chain_b_ibc).await?; + + 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 consensus_state_of_a_on_b_response = chain_b_ibc + .ibc_client_query_client + .consensus_state(QueryConsensusStateRequest { + client_id: chain_a_ibc.client_id.to_string(), + revision_number: 0, + revision_height: 0, + latest_height: true, + }) + .await? + .into_inner(); + + // Build message(s) for updating client on destination + _build_and_send_update_client(chain_b_ibc, chain_a_ibc).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: 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: 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: chain_a_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = chain_a_ibc.client.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 "OPEN" + { + // Connection should be in INIT pre-commit + let connection = pre_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, ConnectionState::Init); + + // Post-commit, the connection should be in the "OPEN" 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, ConnectionState::Open); + + chain_a_ibc.connection = Some(connection); + } + + 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. +async fn _build_and_send_connection_open_confirm( + chain_a_ibc: &mut TestNodeWithIBC, + chain_b_ibc: &mut TestNodeWithIBC, +) -> Result<()> { + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L1296 + let chain_b_connection_id = chain_b_ibc.connection_id.clone(); + let connection_of_b_on_a_response = 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 + _build_and_send_update_client(chain_b_ibc, chain_a_ibc).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: 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: 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: chain_b_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = chain_b_ibc.client.witness_auth_build(&plan).await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = chain_b_ibc.storage.latest_snapshot(); + chain_b_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + let post_tx_snapshot = 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(&chain_b_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &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(&chain_b_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &chain_b_ibc.connection_id + ) + })?; + + assert_eq!(connection.state, ConnectionState::Open); + + chain_b_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. +async fn _build_and_send_connection_open_try( + chain_a_ibc: &mut TestNodeWithIBC, + chain_b_ibc: &mut TestNodeWithIBC, +) -> Result<()> { + // 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 + _build_and_send_update_client(chain_a_ibc, chain_b_ibc).await?; + + // all proofs for a chain need to be at the same height + let chain_a_connection_id = chain_a_ibc.connection_id.clone(); + let chain_b_connection_id = chain_b_ibc.connection_id.clone(); + 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 connection_of_b_on_a_response = chain_a_ibc + .ibc_connection_query_client + .connection(QueryConnectionRequest { + connection_id: chain_b_connection_id.to_string(), + }) + .await? + .into_inner(); + let consensus_state_of_b_on_a_response = chain_a_ibc + .ibc_client_query_client + .consensus_state(QueryConsensusStateRequest { + client_id: 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())?; + 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())?; + // TODO: too side-effecty? + chain_b_ibc.counterparty.connection_id = Some(chain_a_ibc.connection_id.clone()); + chain_a_ibc.counterparty.connection_id = Some(chain_b_ibc.connection_id.clone()); + + // Build message(s) for updating client on destination + _build_and_send_update_client(chain_b_ibc, chain_a_ibc).await?; + + let client_state_b_on_a = + ibc_types::lightclients::tendermint::client_state::ClientState::try_from( + client_state_of_b_on_a_response + .clone() + .client_state + .unwrap(), + )?; + 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: chain_a_ibc.client_id.clone(), + connection_id: Some(chain_a_connection_id.clone()), + prefix: IBC_COMMITMENT_PREFIX.to_owned(), + }, + delay_period: Duration::from_secs(1), + signer: chain_a_ibc.signer.clone(), + client_id_on_b: 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: client_state_of_b_on_a_response + .proof_height + .expect("proof height") + .try_into()?, + consensus_height_of_b_on_a: client_state_b_on_a.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: chain_b_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = chain_b_ibc.client.witness_auth_build(&plan).await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = chain_b_ibc.storage.latest_snapshot(); + chain_b_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + let post_tx_snapshot = 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(&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(&chain_b_ibc.connection_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no connection with the specified ID {} exists", + &chain_b_ibc.connection_id + ) + })?; + + assert_eq!(connection.state, ConnectionState::TryOpen); + + chain_b_ibc.connection = Some(connection); + } + + Ok(()) +} + +// helper function to build ConnectionOpenInit to chain A +async fn _build_and_send_connection_open_init( + chain_a_ibc: &mut TestNodeWithIBC, + chain_b_ibc: &mut TestNodeWithIBC, +) -> Result<()> { + 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.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(()) +} diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index 6774c878f5..fb9162e13c 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -28,3 +28,6 @@ mod test_node_ext; /// See [`ValidatorDataRead`][penumbra_stake::component::validator_handler::ValidatorDataRead], /// and [`ValidatorDataReadExt`]. mod validator_read_ext; + +/// Methods for testing IBC functionality. +pub mod ibc_tests; diff --git a/crates/core/app/tests/common/temp_storage_ext.rs b/crates/core/app/tests/common/temp_storage_ext.rs index 8ef1233b9d..efff367ca8 100644 --- a/crates/core/app/tests/common/temp_storage_ext.rs +++ b/crates/core/app/tests/common/temp_storage_ext.rs @@ -1,7 +1,7 @@ use { async_trait::async_trait, cnidarium::TempStorage, - penumbra_app::{app::App, genesis::AppState}, + penumbra_app::{app::App, genesis::AppState, SUBSTORE_PREFIXES}, std::ops::Deref, }; @@ -9,10 +9,15 @@ use { pub trait TempStorageExt: Sized { async fn apply_genesis(self, genesis: AppState) -> anyhow::Result; async fn apply_default_genesis(self) -> anyhow::Result; + async fn new_with_penumbra_prefixes() -> anyhow::Result; } #[async_trait] impl TempStorageExt for TempStorage { + async fn new_with_penumbra_prefixes() -> anyhow::Result { + TempStorage::new_with_prefixes(SUBSTORE_PREFIXES.to_vec()).await + } + async fn apply_genesis(self, genesis: AppState) -> anyhow::Result { // Check that we haven't already applied a genesis state: if self.latest_version() != u64::MAX { diff --git a/crates/core/app/tests/common/test_node_builder_ext.rs b/crates/core/app/tests/common/test_node_builder_ext.rs index b074232cb5..ed4357851c 100644 --- a/crates/core/app/tests/common/test_node_builder_ext.rs +++ b/crates/core/app/tests/common/test_node_builder_ext.rs @@ -26,7 +26,7 @@ pub trait BuilderExt: Sized { impl BuilderExt for Builder { type Error = anyhow::Error; - fn with_penumbra_auto_app_state(self, app_state: AppState) -> Result { + fn with_penumbra_auto_app_state(mut self, app_state: AppState) -> Result { let Self { keyring, .. } = &self; let mut content = match app_state { AppState::Content(c) => c, @@ -50,6 +50,11 @@ impl BuilderExt for Builder { content.shielded_pool_content.allocations.push(allocation); } + // Set the chain ID from the content + if !content.chain_id.is_empty() { + self.chain_id = Some(content.chain_id.clone()); + } + // Serialize the app state into bytes, and add it to the builder. let app_state = AppState::Content(content); serde_json::to_vec(&app_state) diff --git a/crates/core/app/tests/ibc_handshake.rs b/crates/core/app/tests/ibc_handshake.rs new file mode 100644 index 0000000000..1d576f7e11 --- /dev/null +++ b/crates/core/app/tests/ibc_handshake.rs @@ -0,0 +1,39 @@ +use { + common::ibc_tests::{MockRelayer, TestNodeWithIBC}, + tap::Tap, +}; + +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(); + + // Set up some configuration for the two different chains we'll need to keep around. + let mut chain_a_ibc = TestNodeWithIBC::new("a").await?; + let mut chain_b_ibc = TestNodeWithIBC::new("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 mixed up. + for _ in 0..42 { + 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, + }; + + // 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_can_define_a_genesis_validator.rs b/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs index da66bd8575..a0077ae8f8 100644 --- a/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs +++ b/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs @@ -2,6 +2,7 @@ use { self::common::{BuilderExt, ValidatorDataReadExt}, anyhow::anyhow, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -19,7 +20,7 @@ mod common; async fn mock_consensus_can_define_a_genesis_validator() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; let test_node = { let app_state = AppState::Content( genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()), diff --git a/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs b/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs index cf66c15fdc..b0461b3d69 100644 --- a/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs +++ b/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs @@ -1,6 +1,7 @@ use { self::common::BuilderExt, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -18,7 +19,7 @@ mod common; async fn mock_consensus_can_send_a_sequence_of_empty_blocks() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + let storage = TempStorage::new_with_penumbra_prefixes().await?; let mut test_node = { let app_state = AppState::Content( genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()), diff --git a/crates/core/app/tests/spend.rs b/crates/core/app/tests/spend.rs index 703c41097c..94c9f63caf 100644 --- a/crates/core/app/tests/spend.rs +++ b/crates/core/app/tests/spend.rs @@ -26,7 +26,10 @@ use tendermint::abci; async fn spend_happy_path() -> anyhow::Result<()> { let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1312); - let storage = TempStorage::new().await?.apply_default_genesis().await?; + let storage = TempStorage::new_with_penumbra_prefixes() + .await? + .apply_default_genesis() + .await?; let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); let height = 1; @@ -102,7 +105,7 @@ async fn spend_happy_path() -> anyhow::Result<()> { async fn invalid_dummy_spend() { let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1312); - let storage = TempStorage::new() + let storage = TempStorage::new_with_penumbra_prefixes() .await .unwrap() .apply_default_genesis() @@ -203,7 +206,7 @@ async fn invalid_dummy_spend() { async fn spend_duplicate_nullifier_previous_transaction() { let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1312); - let storage = TempStorage::new() + let storage = TempStorage::new_with_penumbra_prefixes() .await .expect("can start new temp storage") .apply_default_genesis() @@ -294,7 +297,7 @@ async fn spend_duplicate_nullifier_previous_transaction() { async fn spend_duplicate_nullifier_same_transaction() { let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1312); - let storage = TempStorage::new() + let storage = TempStorage::new_with_penumbra_prefixes() .await .expect("can start new temp storage") .apply_default_genesis() diff --git a/crates/core/app/tests/swap_and_swap_claim.rs b/crates/core/app/tests/swap_and_swap_claim.rs index 717f51bc4e..6cf1d8de45 100644 --- a/crates/core/app/tests/swap_and_swap_claim.rs +++ b/crates/core/app/tests/swap_and_swap_claim.rs @@ -30,7 +30,10 @@ use tendermint::abci; async fn swap_and_swap_claim() -> anyhow::Result<()> { let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1312); - let storage = TempStorage::new().await?.apply_default_genesis().await?; + let storage = TempStorage::new_with_penumbra_prefixes() + .await? + .apply_default_genesis() + .await?; let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); let height = 1; @@ -141,7 +144,7 @@ async fn swap_and_swap_claim() -> anyhow::Result<()> { async fn swap_claim_duplicate_nullifier_previous_transaction() { let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1312); - let storage = TempStorage::new() + let storage = TempStorage::new_with_penumbra_prefixes() .await .unwrap() .apply_default_genesis() @@ -267,7 +270,10 @@ async fn swap_claim_duplicate_nullifier_previous_transaction() { async fn swap_with_nonzero_fee() -> anyhow::Result<()> { let mut rng = rand_chacha::ChaChaRng::seed_from_u64(1312); - let storage = TempStorage::new().await?.apply_default_genesis().await?; + let storage = TempStorage::new_with_penumbra_prefixes() + .await? + .apply_default_genesis() + .await?; let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); let height = 1; diff --git a/crates/core/app/tests/view_server_can_be_served_on_localhost.rs b/crates/core/app/tests/view_server_can_be_served_on_localhost.rs index 1ba83aabc7..6ef2571ac3 100644 --- a/crates/core/app/tests/view_server_can_be_served_on_localhost.rs +++ b/crates/core/app/tests/view_server_can_be_served_on_localhost.rs @@ -2,6 +2,7 @@ use { self::common::BuilderExt, anyhow::Context, cnidarium::TempStorage, + common::TempStorageExt as _, penumbra_app::{ genesis::{self, AppState}, server::consensus::Consensus, @@ -30,7 +31,7 @@ mod common; async fn view_server_can_be_served_on_localhost() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; + 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::(); diff --git a/crates/core/component/ibc/src/component/client.rs b/crates/core/component/ibc/src/component/client.rs index b6f6ffd327..dd6bde1aba 100644 --- a/crates/core/component/ibc/src/component/client.rs +++ b/crates/core/component/ibc/src/component/client.rs @@ -103,13 +103,14 @@ pub(crate) trait Ics2ClientExt: StateWrite { // case 1: if we have a verified consensus state previous to this header, verify that this // header's timestamp is greater than or equal to the stored consensus state's timestamp - if let Some(prev_state) = prev_consensus_state { + if let Some((_, prev_state)) = prev_consensus_state { if verified_header.signed_header.header().time < prev_state.timestamp { return ( trusted_client_state .with_header(verified_header.clone()) .expect("able to add header to client state") .with_frozen_height(ibc_types::core::client::Height { + // TODO: is this right? revision_number: 0, revision_height: 1, }), @@ -432,7 +433,7 @@ pub trait StateReadExt: StateRead + penumbra_sct::component::clock::EpochRead { &self, client_id: &ClientId, height: &Height, - ) -> Result> { + ) -> Result> { let mut verified_heights = self.get_verified_heights(client_id) .await? @@ -451,7 +452,7 @@ pub trait StateReadExt: StateRead + penumbra_sct::component::clock::EpochRead { let prev_cons_state = self .get_verified_consensus_state(prev_height, client_id) .await?; - return Ok(Some(prev_cons_state)); + return Ok(Some((*prev_height, prev_cons_state))); } else { return Ok(None); } diff --git a/crates/core/component/ibc/src/component/ics02_validation.rs b/crates/core/component/ibc/src/component/ics02_validation.rs index c117f50517..734e404d16 100644 --- a/crates/core/component/ibc/src/component/ics02_validation.rs +++ b/crates/core/component/ibc/src/component/ics02_validation.rs @@ -97,7 +97,11 @@ pub fn validate_penumbra_client_state( // https://github.com/informalsystems/ibc-rs/pull/304#discussion_r503917283 let chain_id = ChainId::from_string(chain_id); if chain_id != tm_client_state.chain_id { - anyhow::bail!("invalid client state: chain id does not match"); + anyhow::bail!( + "invalid client state: client state's chain id {} does not match current chain {}", + tm_client_state.chain_id, + chain_id + ); } // check that the revision number is the same as our chain ID's version diff --git a/crates/core/component/ibc/src/component/msg_handler/connection_open_ack.rs b/crates/core/component/ibc/src/component/msg_handler/connection_open_ack.rs index 661d9feb6c..a4ac0b095f 100644 --- a/crates/core/component/ibc/src/component/msg_handler/connection_open_ack.rs +++ b/crates/core/component/ibc/src/component/msg_handler/connection_open_ack.rs @@ -198,7 +198,11 @@ async fn consensus_height_is_correct( HI::get_block_height(&state).await?, )?; if msg.consensus_height_of_a_on_b >= current_height { - anyhow::bail!("consensus height is greater than the current block height",); + anyhow::bail!( + "consensus height {} is greater than the current block height {}", + msg.consensus_height_of_a_on_b, + current_height + ); } Ok(()) diff --git a/crates/core/component/ibc/src/component/msg_handler/connection_open_try.rs b/crates/core/component/ibc/src/component/msg_handler/connection_open_try.rs index f9b85c836e..4e85982b8d 100644 --- a/crates/core/component/ibc/src/component/msg_handler/connection_open_try.rs +++ b/crates/core/component/ibc/src/component/msg_handler/connection_open_try.rs @@ -205,7 +205,11 @@ async fn consensus_height_is_correct( HI::get_block_height(&state).await?, )?; if msg.consensus_height_of_b_on_a >= current_height { - anyhow::bail!("consensus height is greater than the current block height",); + anyhow::bail!( + "consensus height {} is greater than the current block height {}", + msg.consensus_height_of_b_on_a, + current_height + ); } Ok(()) diff --git a/crates/core/component/ibc/src/lib.rs b/crates/core/component/ibc/src/lib.rs index b45d9b64fd..172d74d2e2 100644 --- a/crates/core/component/ibc/src/lib.rs +++ b/crates/core/component/ibc/src/lib.rs @@ -18,7 +18,7 @@ pub mod params; mod version; mod prefix; -pub use prefix::{IBC_COMMITMENT_PREFIX, IBC_PROOF_SPECS, IBC_SUBSTORE_PREFIX}; +pub use prefix::{MerklePrefixExt, IBC_COMMITMENT_PREFIX, IBC_PROOF_SPECS, IBC_SUBSTORE_PREFIX}; pub use ibc_action::IbcRelay; pub use ibc_token::IbcToken; diff --git a/crates/crypto/proof-params/Cargo.toml b/crates/crypto/proof-params/Cargo.toml index 93697546be..af3bdaf815 100644 --- a/crates/crypto/proof-params/Cargo.toml +++ b/crates/crypto/proof-params/Cargo.toml @@ -44,7 +44,6 @@ ark-snark = {workspace = true} ark-std = {workspace = true, default-features = false} bech32 = {workspace = true} decaf377 = {workspace = true, features = ["r1cs"], default-features = true} -lazy_static = "1.4.0" num-bigint = {workspace = true} once_cell = {workspace = true} rand = {workspace = true} diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 294b190c53a6d5dce0b8c73d3c0450f223e28822..049d2776782e15931731ee25bc163b11d42ee9ca 100644 GIT binary patch delta 24043 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 Is2i#N-ymTYbpQYW delta 21592 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 diff --git a/crates/proto/src/protobuf/tendermint_compat.rs b/crates/proto/src/protobuf/tendermint_compat.rs index 5a555d6980..661b3012ef 100644 --- a/crates/proto/src/protobuf/tendermint_compat.rs +++ b/crates/proto/src/protobuf/tendermint_compat.rs @@ -5,6 +5,7 @@ // library. accordingly, it is grouped by conversions needed for each RPC endpoint. use crate::util::tendermint_proxy::v1 as penumbra_pb; +use anyhow::anyhow; // === get_tx === @@ -323,9 +324,7 @@ impl From for crate::tendermint::crypto::Pro // === get_block_by_height === impl TryFrom for penumbra_pb::GetBlockByHeightResponse { - // TODO(kate): ideally this would not return a tonic status object, but we'll use this for - // now to avoid invasively refactoring this code. - type Error = tonic::Status; + type Error = anyhow::Error; fn try_from( tendermint_rpc::endpoint::block::Response { block, @@ -338,11 +337,8 @@ impl TryFrom for penumbra_pb::GetBloc }) } } - impl TryFrom for crate::tendermint::types::Block { - // TODO(kate): ideally this would not return a tonic status object, but we'll use this for - // now to avoid invasively refactoring this code. - type Error = tonic::Status; + type Error = anyhow::Error; fn try_from( tendermint::Block { header, @@ -356,14 +352,76 @@ impl TryFrom for crate::tendermint::types::Block { header: header.try_into().map(Some)?, data: Some(crate::tendermint::types::Data { txs: data }), evidence: evidence.try_into().map(Some)?, - last_commit: Some( - last_commit - .map(crate::tendermint::types::Commit::try_from) - .transpose()? - // TODO(kate): this probably should not panic, but this is here to preserve - // existing behavior. panic if no last commit is set. - .expect("last_commit"), - ), + last_commit: last_commit + .map(crate::tendermint::types::Commit::try_from) + .transpose()?, + }) + } +} + +impl TryFrom for tendermint::block::parts::Header { + type Error = anyhow::Error; + fn try_from( + crate::tendermint::types::PartSetHeader { total, hash }: crate::tendermint::types::PartSetHeader, + ) -> Result { + Ok(Self::new(total, hash.try_into()?)?) + } +} + +impl TryFrom for tendermint::block::Header { + type Error = anyhow::Error; + fn try_from( + crate::tendermint::types::Header { + version, + chain_id, + height, + time, + last_block_id, + last_commit_hash, + data_hash, + validators_hash, + next_validators_hash, + consensus_hash, + app_hash, + last_results_hash, + evidence_hash, + proposer_address, + }: crate::tendermint::types::Header, + ) -> Result { + Ok(Self { + version: tendermint::block::header::Version { + block: version.clone().ok_or(anyhow!("version"))?.block, + app: version.ok_or(anyhow!("version"))?.app, + }, + chain_id: tendermint::chain::Id::try_from(chain_id)?, + height: tendermint::block::Height::try_from(height)?, + time: tendermint::Time::from_unix_timestamp( + time.clone().ok_or(anyhow!("time"))?.seconds, + time.clone() + .ok_or(anyhow!("missing time"))? + .nanos + .try_into()?, + )?, + last_block_id: match last_block_id { + Some(last_block_id) => Some(tendermint::block::Id { + hash: tendermint::Hash::try_from(last_block_id.hash)?, + part_set_header: tendermint::block::parts::Header::try_from( + last_block_id + .part_set_header + .ok_or(anyhow::anyhow!("bad part set header"))?, + )?, + }), + None => None, + }, + last_commit_hash: Some(last_commit_hash.try_into()?), + data_hash: Some(data_hash.try_into()?), + validators_hash: validators_hash.try_into()?, + next_validators_hash: next_validators_hash.try_into()?, + consensus_hash: consensus_hash.try_into()?, + app_hash: app_hash.try_into()?, + last_results_hash: Some(last_results_hash.try_into()?), + evidence_hash: Some(evidence_hash.try_into()?), + proposer_address: proposer_address.try_into()?, }) } } @@ -394,7 +452,11 @@ impl TryFrom for crate::tendermint::types::Header { // around a `time::PrimitiveDateTime` however it's private so we // have to use string parsing to get to the prost type we want :( let header_time = chrono::DateTime::parse_from_rfc3339(time.to_rfc3339().as_str()) - .expect("timestamp should roundtrip to string"); + .or_else(|_| { + Err(tonic::Status::invalid_argument( + "timestamp should roundtrip to string", + )) + })?; Ok(Self { version: Some(crate::tendermint::version::Consensus { block: version.block, @@ -404,10 +466,7 @@ impl TryFrom for crate::tendermint::types::Header { height: height.into(), time: Some(pbjson_types::Timestamp { seconds: header_time.timestamp(), - nanos: header_time - .timestamp_nanos_opt() - .ok_or_else(|| tonic::Status::invalid_argument("missing header_time nanos"))? - as i32, + nanos: header_time.timestamp_subsec_nanos() as i32, }), last_block_id: last_block_id.map(|id| crate::tendermint::types::BlockId { hash: id.hash.into(), @@ -430,9 +489,7 @@ impl TryFrom for crate::tendermint::types::Header { } impl TryFrom for crate::tendermint::types::EvidenceList { - // TODO(kate): ideally this would not return a tonic status object, but we'll use this for - // now to avoid invasively refactoring this code. - type Error = tonic::Status; + type Error = anyhow::Error; fn try_from(list: tendermint::evidence::List) -> Result { list.into_vec() .into_iter() @@ -443,11 +500,9 @@ impl TryFrom for crate::tendermint::types::EvidenceL } // TODO(kate): this should be decomposed further at a later point, i am refraining from doing -// so right now. there are `Option::expect()` calls below that should be considered. +// so right now. impl TryFrom for crate::tendermint::types::Evidence { - // TODO(kate): ideally this would not return a tonic status object, but we'll use this for - // now to avoid invasively refactoring this code. - type Error = tonic::Status; + type Error = anyhow::Error; fn try_from(evidence: tendermint::evidence::Evidence) -> Result { use {chrono::DateTime, std::ops::Deref}; Ok(Self { @@ -469,21 +524,27 @@ impl TryFrom for crate::tendermint::types::Evide height: e.votes().0.height.into(), round: e.votes().0.round.into(), block_id: Some(crate::tendermint::types::BlockId { - hash: e.votes().0.block_id.expect("block id").hash.into(), + hash: e + .votes() + .0 + .block_id + .ok_or(anyhow!("block id"))? + .hash + .into(), part_set_header: Some( crate::tendermint::types::PartSetHeader { total: e .votes() .0 .block_id - .expect("block id") + .ok_or(anyhow!("block id"))? .part_set_header .total, hash: e .votes() .0 .block_id - .expect("block id") + .ok_or(anyhow!("block id"))? .part_set_header .hash .into(), @@ -492,18 +553,25 @@ impl TryFrom for crate::tendermint::types::Evide }), timestamp: Some(pbjson_types::Timestamp { seconds: DateTime::parse_from_rfc3339( - &e.votes().0.timestamp.expect("timestamp").to_rfc3339(), + &e.votes() + .0 + .timestamp + .ok_or(tonic::Status::invalid_argument( + "bad timestamp", + ))? + .to_rfc3339(), ) - .expect("timestamp should roundtrip to string") + .or_else(|_| { + Err(tonic::Status::invalid_argument("bad timestamp")) + })? .timestamp(), nanos: DateTime::parse_from_rfc3339( &e.votes().0.timestamp.expect("timestamp").to_rfc3339(), ) .expect("timestamp should roundtrip to string") - .timestamp_nanos_opt() - .ok_or_else(|| { - tonic::Status::invalid_argument("missing timestamp nanos") - })? as i32, + .timestamp_subsec_nanos() + .try_into() + .expect("good round trip timestamps"), }), validator_address: e.votes().0.validator_address.into(), validator_index: e.votes().0.validator_index.into(), @@ -558,10 +626,9 @@ impl TryFrom for crate::tendermint::types::Evide &e.votes().1.timestamp.expect("timestamp").to_rfc3339(), ) .expect("timestamp should roundtrip to string") - .timestamp_nanos_opt() - .ok_or_else(|| { - tonic::Status::invalid_argument("missing timestamp nanos") - })? as i32, + .timestamp_subsec_nanos() + .try_into() + .expect("good round trip timestamps"), }), validator_address: e.votes().1.validator_address.into(), validator_index: e.votes().1.validator_index.into(), @@ -652,12 +719,11 @@ impl TryFrom for crate::tendermint::types::CommitS .timestamp(), nanos: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()) .expect("timestamp should roundtrip to string") - .timestamp_nanos_opt() - .ok_or_else(|| { - tonic::Status::invalid_argument("missing timestamp nanos") - })? as i32, + .timestamp_subsec_nanos() + .try_into() + .expect("good round trip timestamps"), }), - signature: signature.expect("signature").into(), + signature: signature.map(Into::into).unwrap_or_default(), }, tendermint::block::CommitSig::BlockIdFlagNil { validator_address, @@ -672,10 +738,9 @@ impl TryFrom for crate::tendermint::types::CommitS .timestamp(), nanos: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()) .expect("timestamp should roundtrip to string") - .timestamp_nanos_opt() - .ok_or_else(|| { - tonic::Status::invalid_argument("missing timestamp nanos") - })? as i32, + .timestamp_subsec_nanos() + .try_into() + .expect("good round trip timestamps"), }), signature: signature.expect("signature").into(), }, diff --git a/crates/test/mock-consensus/Cargo.toml b/crates/test/mock-consensus/Cargo.toml index 8c75ced395..ae02cac011 100644 --- a/crates/test/mock-consensus/Cargo.toml +++ b/crates/test/mock-consensus/Cargo.toml @@ -15,9 +15,11 @@ license.workspace = true anyhow = { workspace = true } bytes = { workspace = true } ed25519-consensus = { workspace = true } +hex = { workspace = true } rand_core = { workspace = true } sha2 = { workspace = true } tap = { workspace = true } tendermint = { workspace = true, default-features = true } +tendermint-proto = { workspace = true } tower = { workspace = true, features = ["full"] } tracing = { workspace = true } diff --git a/crates/test/mock-consensus/src/abci.rs b/crates/test/mock-consensus/src/abci.rs index fd5c67f6bb..7d584115e9 100644 --- a/crates/test/mock-consensus/src/abci.rs +++ b/crates/test/mock-consensus/src/abci.rs @@ -160,6 +160,10 @@ where retain_height, } = &response; trace!(?data, ?retain_height, "received Commit response"); + + // Set the last app hash to the new block's app hash. + assert!(response.data.to_vec().len() > 0); + self.last_app_hash = response.data.to_vec(); Ok(response) } response => { diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index b9a116d23e..b9476aafd4 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -4,13 +4,14 @@ use { crate::TestNode, + sha2::Sha256, tap::Tap, tendermint::{ account, block::{self, header::Version, Block, Commit, Header, Round}, - chain, evidence, + evidence, v0_37::abci::{ConsensusRequest, ConsensusResponse}, - AppHash, Hash, + Hash, }, tower::{BoxError, Service}, tracing::{instrument, trace}, @@ -34,8 +35,6 @@ pub struct Builder<'e, C> { data: Vec>, /// Evidence of malfeasance. evidence: evidence::List, - /// The list of signatures. - signatures: Vec, } // === impl TestNode === @@ -43,16 +42,11 @@ pub struct Builder<'e, C> { impl TestNode { /// Returns a new [`Builder`]. /// - /// By default, signatures for all of the validators currently within the keyring will be - /// included in the block. Use [`Builder::with_signatures()`] to set a different set of - /// validator signatures. pub fn block(&mut self) -> Builder<'_, C> { - let signatures = self.generate_signatures().collect(); Builder { test_node: self, data: Default::default(), evidence: Default::default(), - signatures, } } } @@ -84,11 +78,6 @@ impl<'e, C> Builder<'e, C> { pub fn with_evidence(self, evidence: evidence::List) -> Self { Self { evidence, ..self } } - - /// Sets the [`CommitSig`][block::CommitSig] commit signatures for this block. - pub fn with_signatures(self, signatures: Vec) -> Self { - Self { signatures, ..self } - } } impl<'e, C> Builder<'e, C> @@ -103,6 +92,10 @@ where /// Consumes this builder, executing the [`Block`] using the consensus service. /// /// Use [`TestNode::block()`] to build a new block. + /// + /// By default, signatures for all of the validators currently within the keyring will be + /// included in the block. Use [`Builder::with_signatures()`] to set a different set of + /// validator signatures. #[instrument(level = "info", skip_all, fields(height, time))] pub async fn execute(self) -> Result<(), anyhow::Error> { let (test_node, block) = self.finish()?; @@ -148,9 +141,10 @@ where data, evidence, test_node, - signatures, } = self; + let time = tendermint::Time::now(); + let height = { let height = test_node.height.increment(); test_node.height = height; @@ -158,39 +152,89 @@ where height }; - let last_commit = if height.value() != 1 { + let mut last_commit = if height.value() != 1 { + // This needs to be the header hash of the last block let block_id = block::Id { - hash: Hash::None, + hash: test_node + .last_tm_hash + .expect("last tm hash should be available on subsequent blocks"), part_set_header: block::parts::Header::new(0, Hash::None)?, }; Some(Commit { height, round: Round::default(), block_id, - signatures, + // Note: the signatures are blank here, and then the signing happens below + signatures: vec![], }) } else { None // The first block has no previous commit to speak of. }; + // Set the validator set based on the current configuration. + let pk = test_node + .keyring + .iter() + .next() + .expect("validator key in keyring") + .0; + let proposer_address = 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(); + let header = Header { version: Version { block: 1, app: 1 }, - chain_id: chain::Id::try_from("test".to_owned())?, + chain_id: tendermint::chain::Id::try_from(test_node.chain_id.clone())?, height, - time: tendermint::Time::now(), - last_block_id: None, - last_commit_hash: None, + time, + last_block_id: test_node.last_commit.as_ref().map(|c| c.block_id.clone()), + last_commit_hash: test_node.last_tm_hash, data_hash: None, - validators_hash: Hash::None, - next_validators_hash: Hash::None, + // force the header to have the hash of the validator set to pass + // the validation + validators_hash: validators_hash.into(), + next_validators_hash: validators_hash.into(), + // TODO: how to set? consensus_hash: Hash::None, - app_hash: AppHash::try_from(Vec::default())?, + app_hash: tendermint::AppHash::try_from(test_node.last_app_hash().to_vec())?, last_results_hash: None, evidence_hash: None, - proposer_address: account::Id::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]), + proposer_address, }; + + // Immediately before committing, sign the block + let signatures: Vec = test_node.generate_signatures(&header).collect(); + last_commit + .as_mut() + .map(|commit| commit.signatures = signatures); + + // Store the header hash in a cache for later reference + test_node.last_tm_hash = Some(header.hash()); + let block = Block::new(header, data, evidence, last_commit)?; Ok((test_node, block)) diff --git a/crates/test/mock-consensus/src/block/signature.rs b/crates/test/mock-consensus/src/block/signature.rs index 249e91e629..d2e692594b 100644 --- a/crates/test/mock-consensus/src/block/signature.rs +++ b/crates/test/mock-consensus/src/block/signature.rs @@ -17,11 +17,28 @@ mod sign { /// Returns a [commit signature] saying this validator voted for the block. /// /// [commit signature]: CommitSig - pub(super) fn commit(validator_address: Id) -> CommitSig { + pub(super) fn commit( + validator_address: Id, + validator_key: &ed25519_consensus::SigningKey, + canonical: &tendermint::vote::CanonicalVote, + ) -> CommitSig { + // Create a vote to be signed + // https://github.com/informalsystems/tendermint-rs/blob/14fd628e82ae51b9f15c135a6db8870219fe3c33/testgen/src/commit.rs#L214 + // https://github.com/informalsystems/tendermint-rs/blob/14fd628e82ae51b9f15c135a6db8870219fe3c33/testgen/src/commit.rs#L104 + + use tendermint_proto::v0_37::types::CanonicalVote as RawCanonicalVote; + let sign_bytes = + tendermint_proto::Protobuf::::encode_length_delimited_vec( + canonical.clone(), + ); + + // encode to stable-json deterministic JSON wire encoding, + // https://github.com/informalsystems/tendermint-rs/blob/14fd628e82ae51b9f15c135a6db8870219fe3c33/testgen/src/helpers.rs#L43C1-L44C1 + CommitSig::BlockIdFlagCommit { validator_address, - timestamp: timestamp(), - signature: None, + timestamp: canonical.timestamp.expect("timestamp should be present"), + signature: Some(validator_key.sign(sign_bytes.as_slice()).into()), } } @@ -33,6 +50,7 @@ mod sign { CommitSig::BlockIdFlagNil { validator_address, timestamp: timestamp(), + // TODO: we need a valid signature here signature: None, } } @@ -42,7 +60,7 @@ mod sign { // TODO(kate): see https://github.com/penumbra-zone/penumbra/issues/3759, re: timestamps. // eventually, we will add hooks so that we can control these timestamps. fn timestamp() -> Time { - Time::now() + tendermint::Time::now() } } @@ -54,16 +72,40 @@ impl TestNode { // commit signatures from all of the validators. /// Returns an [`Iterator`] of signatures for validators in the keyring. - pub(super) fn generate_signatures(&self) -> impl Iterator + '_ { - self.keyring - .keys() - .map(|vk| { - ::digest(vk).as_slice()[0..20] - .try_into() - .expect("") + pub(super) fn generate_signatures( + &self, + header: &tendermint::block::Header, + ) -> impl Iterator + '_ { + let block_id = tendermint::block::Id { + hash: header.hash(), + part_set_header: tendermint::block::parts::Header::new(0, tendermint::Hash::None) + .unwrap(), + }; + let canonical = tendermint::vote::CanonicalVote { + // The mock consensus engine ONLY has precommit votes right now + vote_type: tendermint::vote::Type::Precommit, + height: tendermint::block::Height::from(self.height), + // round is always 0 + round: 0u8.into(), + block_id: Some(block_id), + // Block header time is used throughout + timestamp: Some(header.time.clone()), + // timestamp: Some(last_commit_info.timestamp), + chain_id: self.chain_id.clone(), + }; + + return self + .keyring + .iter() + .map(|(vk, sk)| { + ( + ::digest(vk).as_slice()[0..20] + .try_into() + .expect(""), + sk, + ) }) - .map(account::Id::new) - .map(self::sign::commit) + .map(move |(id, sk)| self::sign::commit(account::Id::new(id), sk, &canonical)); } } diff --git a/crates/test/mock-consensus/src/builder.rs b/crates/test/mock-consensus/src/builder.rs index dd4b19e5cb..e312753d1c 100644 --- a/crates/test/mock-consensus/src/builder.rs +++ b/crates/test/mock-consensus/src/builder.rs @@ -16,6 +16,7 @@ pub struct Builder { pub app_state: Option, pub keyring: Keyring, pub on_block: Option, + pub chain_id: Option, } impl TestNode<()> { diff --git a/crates/test/mock-consensus/src/builder/init_chain.rs b/crates/test/mock-consensus/src/builder/init_chain.rs index e6852e68b8..4ab8b01075 100644 --- a/crates/test/mock-consensus/src/builder/init_chain.rs +++ b/crates/test/mock-consensus/src/builder/init_chain.rs @@ -2,7 +2,7 @@ use { super::*, anyhow::{anyhow, bail}, bytes::Bytes, - std::time, + std::{collections::BTreeMap, time}, tap::TapFallible, tendermint::{ block, @@ -39,12 +39,17 @@ impl Builder { app_state: Some(app_state), keyring, on_block, + chain_id, } = self else { bail!("builder was not fully initialized") }; - let request = Self::init_chain_request(app_state)?; + let chain_id = tendermint::chain::Id::try_from( + chain_id.unwrap_or(TestNode::<()>::CHAIN_ID.to_string()), + )?; + + let request = Self::init_chain_request(app_state, &keyring, chain_id.clone())?; let service = consensus .ready() .await @@ -69,20 +74,41 @@ impl Builder { consensus, height: block::Height::from(0_u8), last_app_hash: app_hash.as_bytes().to_owned(), + last_tm_hash: None, + last_commit: None, keyring, on_block, + chain_id, }) } - fn init_chain_request(app_state_bytes: Bytes) -> Result { + fn init_chain_request( + app_state_bytes: Bytes, + keyring: &BTreeMap, + chain_id: tendermint::chain::Id, + ) -> Result { use tendermint::v0_37::abci::request::InitChain; - let chain_id = TestNode::<()>::CHAIN_ID.to_string(); let consensus_params = Self::consensus_params(); + + let pub_keys = keyring + .iter() + .map(|(pk, _)| pk) + .map(|pk| { + tendermint::PublicKey::from_raw_ed25519(pk.as_bytes()).expect("pub key present") + }) + .collect::>(); + Ok(ConsensusRequest::InitChain(InitChain { time: tendermint::Time::now(), - chain_id, + chain_id: chain_id.into(), consensus_params, - validators: vec![], + validators: pub_keys + .into_iter() + .map(|pub_key| tendermint::validator::Update { + pub_key, + power: 1u64.try_into().unwrap(), + }) + .collect::>(), app_state_bytes, initial_height: 1_u32.into(), })) diff --git a/crates/test/mock-consensus/src/lib.rs b/crates/test/mock-consensus/src/lib.rs index b2a711a782..ce4a93dbea 100644 --- a/crates/test/mock-consensus/src/lib.rs +++ b/crates/test/mock-consensus/src/lib.rs @@ -76,6 +76,10 @@ pub struct TestNode { consensus: C, /// The last `app_hash` value. last_app_hash: Vec, + /// The last tendermint header hash value. + last_tm_hash: Option, + /// The last tendermint block header commit value. + last_commit: Option, /// The current block [`Height`][tendermint::block::Height]. height: tendermint::block::Height, /// Validators' consensus keys. @@ -84,6 +88,8 @@ pub struct TestNode { keyring: Keyring, /// A callback that will be invoked when a new block is constructed. on_block: Option, + /// The chain ID. + chain_id: tendermint::chain::Id, } /// A type alias for the `TestNode::on_block` callback. diff --git a/crates/test/mock-tendermint-proxy/src/proxy.rs b/crates/test/mock-tendermint-proxy/src/proxy.rs index f46f0ecb7b..12b9b77466 100644 --- a/crates/test/mock-tendermint-proxy/src/proxy.rs +++ b/crates/test/mock-tendermint-proxy/src/proxy.rs @@ -193,7 +193,11 @@ impl TendermintProxyService for TestNodeProxy { .get(&height) .cloned() .map(penumbra_proto::tendermint::types::Block::try_from) - .transpose()?; + .transpose() + .or_else(|e| { + tracing::warn!(?height, error = ?e, "proxy: error fetching blocks"); + Err(tonic::Status::internal("error fetching blocks")) + })?; let block_id = block .as_ref() // is this off-by-one? should we be getting the id of the last commit? .and_then(|b| b.last_commit.as_ref()) diff --git a/crates/util/tendermint-proxy/src/tendermint_proxy.rs b/crates/util/tendermint-proxy/src/tendermint_proxy.rs index fff613c409..7c902f9314 100644 --- a/crates/util/tendermint-proxy/src/tendermint_proxy.rs +++ b/crates/util/tendermint-proxy/src/tendermint_proxy.rs @@ -197,7 +197,15 @@ impl TendermintProxyService for TendermintProxy { .block(height) .await .map_err(|e| tonic::Status::unavailable(format!("error querying abci: {e}"))) - .and_then(GetBlockByHeightResponse::try_from) + .and_then(|b| { + match GetBlockByHeightResponse::try_from(b) { + Ok(b) => Ok(b), + Err(e) => { + tracing::warn!(?height, error = ?e, "proxy: error deserializing GetBlockByHeightResponse"); + Err(tonic::Status::internal("error deserializing GetBlockByHeightResponse")) + } + } + }) .map(tonic::Response::new) } } 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 }