diff --git a/Cargo.lock b/Cargo.lock index 61d7d73116..77a87f1f0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5168,6 +5168,7 @@ dependencies = [ "bytes", "ed25519-consensus", "hex", + "prost", "rand_core", "sha2 0.10.8", "tap", diff --git a/crates/cnidarium/src/snapshot.rs b/crates/cnidarium/src/snapshot.rs index eb3518805a..f4ae267da6 100644 --- a/crates/cnidarium/src/snapshot.rs +++ b/crates/cnidarium/src/snapshot.rs @@ -82,11 +82,13 @@ impl Snapshot { db: db.clone(), }; + println!("HERE_"); let (substore_value, substore_commitment_proof) = tokio::task::spawn_blocking({ let span = span.clone(); move || span.in_scope(|| substore.get_with_proof(substore_key_bytes)) }) .await??; + println!("HERE2"); proofs.push(substore_commitment_proof); @@ -103,15 +105,18 @@ impl Snapshot { db, }; + println!("HERE3"); let (_, main_commitment_proof) = tokio::task::spawn_blocking({ let span = span.clone(); move || span.in_scope(|| mainstore.get_with_proof(key_to_substore_root.into())) }) .await??; + println!("HERE4"); proofs.push(main_commitment_proof); } + println!("HERE5, substore_value: {:?}", substore_value); Ok(( substore_value, MerkleProof { diff --git a/crates/cnidarium/src/store/substore.rs b/crates/cnidarium/src/store/substore.rs index 876a94b744..6234780f46 100644 --- a/crates/cnidarium/src/store/substore.rs +++ b/crates/cnidarium/src/store/substore.rs @@ -229,6 +229,13 @@ impl SubstoreSnapshot { ) -> Result<(Option>, ics23::CommitmentProof)> { let version = self.version(); let tree = jmt::Sha256Jmt::new(self); + let rh = tree.get_root_hash(version)?; + println!( + "HERE in SubstoreSnapshot get_with_proof, key: {}, version: {}, root hash: {}", + hex::encode(key.clone()), + version, + hex::encode(rh.0) + ); tree.get_with_ics23_proof(key, version) } diff --git a/crates/core/app/tests/common/ibc_tests/node.rs b/crates/core/app/tests/common/ibc_tests/node.rs index c15f4235f5..b310815422 100644 --- a/crates/core/app/tests/common/ibc_tests/node.rs +++ b/crates/core/app/tests/common/ibc_tests/node.rs @@ -59,6 +59,8 @@ pub struct TestNodeWithIBC { } #[allow(unused)] +/// This interacts with a node similarly to how a relayer would. We intentionally call +/// against the external gRPC interfaces to get the most comprehensive test coverage. impl TestNodeWithIBC { pub async fn new( suffix: &str, @@ -191,6 +193,7 @@ impl TestNodeWithIBC { client_id: &ClientId, height: &Height, ) -> Result> { + // TODO: this probably shouldn't reach into storage directly huh self.storage .clone() .latest_snapshot() diff --git a/crates/core/app/tests/common/temp_storage_ext.rs b/crates/core/app/tests/common/temp_storage_ext.rs index efff367ca8..f3e24f76d4 100644 --- a/crates/core/app/tests/common/temp_storage_ext.rs +++ b/crates/core/app/tests/common/temp_storage_ext.rs @@ -14,6 +14,7 @@ pub trait TempStorageExt: Sized { #[async_trait] impl TempStorageExt for TempStorage { + // TODO: move this to an Ext trait for Storage instead of TempStorage async fn new_with_penumbra_prefixes() -> anyhow::Result { TempStorage::new_with_prefixes(SUBSTORE_PREFIXES.to_vec()).await } diff --git a/crates/core/app/tests/ibc_handshake.rs b/crates/core/app/tests/ibc_handshake.rs index 6dde51acb7..f43865488a 100644 --- a/crates/core/app/tests/ibc_handshake.rs +++ b/crates/core/app/tests/ibc_handshake.rs @@ -1,12 +1,369 @@ use { - common::ibc_tests::{MockRelayer, TestNodeWithIBC}, - ibc_types::core::client::Height, - std::time::Duration, - tap::Tap, + anyhow::Context as _, + cnidarium::{StateDelta, StateRead as _, StateWrite as _, Storage, TempStorage}, + common::{ + ibc_tests::{MockRelayer, TestNodeWithIBC}, + BuilderExt as _, TempStorageExt as _, + }, + ibc_types::{ + core::{ + client::Height, + commitment::{MerklePath, MerklePrefix, MerkleProof, MerkleRoot}, + }, + path::Path, + DomainType as _, + }, + once_cell::sync::Lazy, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, + penumbra_ibc::{component::proof_verification, IBC_PROOF_SPECS, IBC_SUBSTORE_PREFIX}, + penumbra_keys::test_keys, + penumbra_mock_client::MockClient, + penumbra_mock_consensus::TestNode, + penumbra_proto::{ + cnidarium::v1::{ + query_service_client::QueryServiceClient as CnidariumQueryServiceClient, + KeyValueRequest, + }, + core::component::sct::v1::{ + query_service_client::QueryServiceClient as SctQueryServiceClient, EpochByHeightRequest, + }, + Message as _, StateReadProto as _, StateWriteProto as _, + }, + penumbra_sct::epoch::Epoch, + penumbra_test_subscriber::set_tracing_subscriber_with_env_filter, + sha2::Digest, + std::{sync::Arc, time::Duration}, + tap::{Tap, TapFallible as _}, + tokio::time, + tonic::transport::Channel, }; +/// The proof specs for the main store. +pub static MAIN_STORE_PROOF_SPEC: Lazy> = + Lazy::new(|| vec![cnidarium::ics23_spec()]); + mod common; +fn set_tracing_subscriber() -> tracing::subscriber::DefaultGuard { + let filter = "info,penumbra_app=info,penumbra_mock_consensus=trace,jmt=trace"; + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new(filter)) + .expect("should have a valid filter directive"); + set_tracing_subscriber_with_env_filter(filter) +} + +async fn create_storage_instance() -> Storage { + use tempfile::tempdir; + // create a storage backend for testing + let dir = tempdir().expect("unable to create tempdir"); + let file_path = dir.path().join("snapshot-cache-testing.db"); + + Storage::load(file_path, penumbra_app::SUBSTORE_PREFIXES.to_vec()) + .await + .expect("unable to load storage") +} + +#[tokio::test] +async fn verify_storage_proof_simple() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = set_tracing_subscriber(); + + // let storage = TempStorage::new_with_penumbra_prefixes().await?; + let storage = create_storage_instance().await; + + let mut node = { + let app_state = AppState::Content( + genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()), + ); + let consensus = Consensus::new(storage.clone()); + // let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + node.block().execute().await?; + + let mut delta = StateDelta::new(storage.latest_snapshot()); + let test_key = "banana".to_string(); + let test_value = "a good fruit".as_bytes().to_vec(); + delta.put_raw(test_key.clone(), test_value.clone()); + let post_commit_root = storage.commit_in_place(delta).await?; + + node.block().execute().await?; + + let unproven: Option> = storage.latest_snapshot().get_raw(&test_key).await?; + let unproven = unproven.expect("present in storage"); + assert_eq!(unproven, test_value); + + let (some_value, proof) = storage + .latest_snapshot() + .get_with_proof(test_key.as_bytes().to_vec()) + .await?; + + assert!(some_value.is_some()); + assert_eq!(some_value.unwrap(), test_value); + + Ok(()) +} + +#[tokio::test] +/// Exercises that the mock tendermint creates merkle proofs that are verifiable. +/// adding this test required changing the visibility +/// of `proof_verification` to pub. code should maybe be reorganized but i'm not +/// sure how yet. +async fn verify_storage_proof() -> anyhow::Result<()> { + fn verify_merkle_proof( + proof_specs: &[ics23::ProofSpec], + proof: &MerkleProof, + root: &MerkleRoot, + merkle_path: MerklePath, + value: Vec, + ) -> anyhow::Result<()> { + proof.verify_membership(proof_specs, root.clone(), merkle_path, value, 0)?; + + Ok(()) + } + // Install a test logger, and acquire some temporary storage. + let guard = common::set_tracing_subscriber(); + + // Fixed start times (both chains start at the same time to avoid unintended timeouts): + let start_time = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?; + + // 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 mut node = { + let app_state = AppState::Content( + genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()), + ); + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_initial_timestamp(start_time) + .with_penumbra_auto_app_state(app_state)? + .on_block(proxy.on_block_callback()) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + // 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| tracing::info!(client.notes = %c.notes.len(), "mock client synced to test storage"), + ); + + let grpc_url = "http://127.0.0.1:8081" // 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 mut cnidarium_client = CnidariumQueryServiceClient::new(channel); + + let pk = node + .keyring() + .iter() + .next() + .expect("validator key in keyring") + .0; + + let mut delta = StateDelta::new(storage.latest_snapshot()); + // let mut delta = StateDelta::new(storage); + let test_key = "banana".to_string(); + let test_value = "a good fruit".as_bytes().to_vec(); + delta.put_raw(test_key.clone(), test_value.clone()); + let post_commit_root = storage.commit_in_place(delta).await?; + + // Execute 1 block on the test node, to commit the block. + node.block().execute().await?; + + let unproven: Option> = storage.latest_snapshot().get_raw(&test_key).await?; + let unproven = unproven.expect("present in storage"); + assert_eq!(unproven, test_value); + + let (some_value, proof) = storage + .latest_snapshot() + .get_with_proof(test_key.as_bytes().to_vec()) + .await?; + + let retrieved_value = some_value.expect("migration key is found in the latest snapshot"); + assert_eq!(retrieved_value, test_value); + + let merkle_path = MerklePath { + key_path: vec![test_key], + }; + let merkle_root = MerkleRoot { + hash: post_commit_root.0.to_vec(), + }; + + proof + .verify_membership( + &MAIN_STORE_PROOF_SPEC, + merkle_root, + merkle_path, + retrieved_value, + 0, + ) + .map_err(|e| tracing::error!("proof verification failed: {:?}", e)) + .expect("membership proof verifies"); + + // Only things written to substores can be retrieved with proofs. + // So, let's just store an arbitrary key in the ibc-data substore. + // We'll store a real data type just to make sure everything works. + let prefix = IBC_SUBSTORE_PREFIX; + let key = format!( + "{}/{}", + prefix, + penumbra_sct::state_key::epoch_manager::epoch_by_height(2) + ); + + // Execute 1 block on the test node + node.block().execute().await?; + + // // Put the test epoch data in the storage + // let mut tx = StateDelta::new(a); + // tx.put( + // key.clone(), + // Epoch { + // index: 12345, + // start_height: 6789, + // }, + // ); + // tx.apply().await?; + // let postmigration_root = storage.commit_in_place(delta).await?; + + // Execute 1 block on the test node + node.block().execute().await?; + + // Common proof parameters: + let proof_specs = IBC_PROOF_SPECS.to_vec(); + // The root will be the latest jmt hash. + let latest_root = storage.latest_snapshot().root_hash().await.unwrap(); + let root = MerkleRoot { + hash: latest_root.0.to_vec(), + }; + // no prefix so the path is just the key + let merkle_path = MerklePath { + key_path: vec![key.clone()], + }; + + let snapshot = storage.latest_snapshot(); + + // Retrieve the proof directly from cnidarium. + let (trusted_epoch, trusted_epoch_proof) = snapshot + .get_with_proof(key.clone().into_bytes()) + .await + .expect("epoch should be in state"); + + let trusted_epoch = trusted_epoch.expect("epoch should be in state"); + let e: Epoch = snapshot + .get(&key.clone()) + .await + .expect("epoch should be in state") + .expect("epoch should be in state"); + assert!(e.index == 12345); + assert!(!trusted_epoch.is_empty()); + + // Verify the proof from directly accessing cnidarium. + assert!(!trusted_epoch.is_empty()); + verify_merkle_proof( + &proof_specs, + &trusted_epoch_proof, + &root, + merkle_path.clone(), + trusted_epoch, + )?; + + // Now we should be able to get a key_value response with a proof from the RPC server. + println!("key: {}", key); + let kvr = cnidarium_client + .key_value(tonic::Request::new(KeyValueRequest { + key: key.clone(), + proof: true, + })) + .await? + .into_inner(); + + assert_eq!(*node.height(), 2i64.try_into()?,); + + // Verify the proof from the KV RPC. + let proof = kvr.proof.unwrap().try_into()?; + let value = kvr.value.unwrap().value; + assert!(!value.is_empty()); + verify_merkle_proof(&proof_specs, &proof, &root, merkle_path, value)?; + + // proof + // .verify_membership( + // &MAIN_STORE_PROOF_SPEC, + // merkle_root, + // merkle_path, + // retrieved_value, + // 0, + // ) + // .map_err(|e| tracing::error!("proof verification failed: {:?}", e)) + // .expect("membership proof verifies"); + // proof_verification::verify_connection_state( + // &trusted_client_state, + // self.proofs_height_on_a, + // &self.counterparty.prefix, + // &proof_conn_end_on_a, + // &trusted_consensus_state.root, + // &ConnectionPath::new( + // self.counterparty + // .connection_id + // .as_ref() + // .ok_or_else(|| anyhow::anyhow!("counterparty connection id is not set"))?, + // ), + // &expected_conn, + // ) + // .context("failed to verify connection state")?; + + Ok(()) + .tap(|_| drop(node)) + .tap(|_| drop(storage)) + .tap(|_| drop(guard)) +} + /// Exercises that the IBC handshake succeeds. #[tokio::test] async fn ibc_handshake() -> anyhow::Result<()> { diff --git a/crates/core/component/ibc/src/component.rs b/crates/core/component/ibc/src/component.rs index 556df345cc..344a0ff1c0 100644 --- a/crates/core/component/ibc/src/component.rs +++ b/crates/core/component/ibc/src/component.rs @@ -13,7 +13,7 @@ mod host_interface; mod ibc_component; mod metrics; mod msg_handler; -mod proof_verification; +pub mod proof_verification; mod view; pub mod app_handler; diff --git a/crates/core/component/sct/src/component/clock.rs b/crates/core/component/sct/src/component/clock.rs index e74b1aec84..cafb39be4c 100644 --- a/crates/core/component/sct/src/component/clock.rs +++ b/crates/core/component/sct/src/component/clock.rs @@ -121,6 +121,11 @@ pub trait EpochManager: StateWrite { /// Index the current epoch by height. fn put_epoch_by_height(&mut self, height: u64, epoch: Epoch) { + println!("put_epoch_by_height: {}, epoch: {:?}", height, epoch); + println!( + "state_key::epoch_manager::epoch_by_height(height): {}", + state_key::epoch_manager::epoch_by_height(height) + ); self.put(state_key::epoch_manager::epoch_by_height(height), epoch) } } diff --git a/crates/test/mock-consensus/Cargo.toml b/crates/test/mock-consensus/Cargo.toml index ae02cac011..32f50e4e46 100644 --- a/crates/test/mock-consensus/Cargo.toml +++ b/crates/test/mock-consensus/Cargo.toml @@ -16,6 +16,7 @@ anyhow = { workspace = true } bytes = { workspace = true } ed25519-consensus = { workspace = true } hex = { workspace = true } +prost = { workspace = true } rand_core = { workspace = true } sha2 = { workspace = true } tap = { workspace = true } diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index faacda3d77..a783b27812 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -4,7 +4,7 @@ use { crate::TestNode, - sha2::Sha256, + sha2::{Digest, Sha256}, tap::Tap, tendermint::{ account, @@ -224,21 +224,29 @@ where ); let validators_hash = validator_set.hash(); + // The data hash is the sha256 hash of all the transactions + // I think as long as we are consistent here it's fine. + let data_hash = sha2::Sha256::digest(&data.concat()).to_vec(); + let consensus_hash = + Hash::Sha256(test_node.consensus_params_hash.clone().try_into().unwrap()); + // TODO: would be great to see if we could load a cometBFT node with + // the same configs as here and produce the same values let header = Header { + // Protocol version version: Version { block: 1, app: 1 }, chain_id: tendermint::chain::Id::try_from(test_node.chain_id.clone())?, height, time: timestamp, 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, + data_hash: Some(tendermint::Hash::Sha256(data_hash.try_into().unwrap())), // 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, + consensus_hash, app_hash: tendermint::AppHash::try_from(test_node.last_app_hash().to_vec())?, + // TODO: we should probably have a way to set this last_results_hash: None, evidence_hash: None, proposer_address, diff --git a/crates/test/mock-consensus/src/builder/init_chain.rs b/crates/test/mock-consensus/src/builder/init_chain.rs index 2bb6dcd447..aa06be5118 100644 --- a/crates/test/mock-consensus/src/builder/init_chain.rs +++ b/crates/test/mock-consensus/src/builder/init_chain.rs @@ -2,6 +2,8 @@ use { super::*, anyhow::{anyhow, bail}, bytes::Bytes, + prost::Message, + sha2::Digest as _, std::{collections::BTreeMap, time}, tap::TapFallible, tendermint::{ @@ -61,7 +63,11 @@ impl Builder { .tap_err(|error| error!(?error, "failed waiting for consensus service")) .map_err(|_| anyhow!("failed waiting for consensus service"))?; - let response::InitChain { app_hash, .. } = match service + let response::InitChain { + app_hash, + consensus_params, + .. + } = match service .call(request) .await .tap_ok(|resp| debug!(?resp, "received response from consensus service")) @@ -86,6 +92,13 @@ impl Builder { timestamp, ts_callback: ts_callback.unwrap_or(Box::new(default_ts_callback)), chain_id, + consensus_params_hash: sha2::Sha256::digest( + tendermint_proto::v0_37::types::ConsensusParams::from( + consensus_params.expect("consensus params"), + ) + .encode_to_vec(), + ) + .to_vec(), }) } diff --git a/crates/test/mock-consensus/src/lib.rs b/crates/test/mock-consensus/src/lib.rs index f24439e7f3..59a26d0eef 100644 --- a/crates/test/mock-consensus/src/lib.rs +++ b/crates/test/mock-consensus/src/lib.rs @@ -32,7 +32,7 @@ use { ed25519_consensus::{SigningKey, VerificationKey}, std::collections::BTreeMap, - tendermint::Time, + tendermint::{block::Height, Time}, }; pub mod block; @@ -81,6 +81,8 @@ pub struct TestNode { last_tm_hash: Option, /// The last tendermint block header commit value. last_commit: Option, + /// The consensus params hash. + consensus_params_hash: Vec, /// The current block [`Height`][tendermint::block::Height]. height: tendermint::block::Height, /// Validators' consensus keys. @@ -143,6 +145,10 @@ impl TestNode { pub fn keyring_mut(&mut self) -> &mut Keyring { &mut self.keyring } + + pub fn height(&self) -> &Height { + &self.height + } } /// Fast forward interfaces.