From 0c5afd9fc3d71103febac74feb580d136b87a457 Mon Sep 17 00:00:00 2001 From: Chris Czub Date: Thu, 8 Aug 2024 17:11:55 -0400 Subject: [PATCH] Implement block header generation with signing --- Cargo.lock | 6 + ...me_for_genesis_validator_missing_blocks.rs | 2 +- .../app/tests/common/test_node_builder_ext.rs | 16 +- .../app/tests/mock_consensus_block_proving.rs | 340 ++++++++++++++++++ crates/core/component/ibc/src/lib.rs | 2 +- crates/test/mock-client/src/lib.rs | 10 + crates/test/mock-consensus/Cargo.toml | 5 + crates/test/mock-consensus/src/abci.rs | 1 + crates/test/mock-consensus/src/block.rs | 225 +++++++++--- .../mock-consensus/src/block/signature.rs | 64 +++- crates/test/mock-consensus/src/builder.rs | 71 +++- .../mock-consensus/src/builder/init_chain.rs | 142 +++++++- crates/test/mock-consensus/src/lib.rs | 29 +- crates/test/mock-tendermint-proxy/Cargo.toml | 1 + .../test/mock-tendermint-proxy/src/proxy.rs | 47 ++- 15 files changed, 861 insertions(+), 100 deletions(-) create mode 100644 crates/core/app/tests/mock_consensus_block_proving.rs diff --git a/Cargo.lock b/Cargo.lock index b16187d4ac..50dc26d63a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5167,10 +5167,15 @@ dependencies = [ "anyhow", "bytes", "ed25519-consensus", + "hex", + "prost", "rand_core", + "serde", + "serde_json", "sha2 0.10.8", "tap", "tendermint", + "tendermint-proto", "tower", "tracing", ] @@ -5179,6 +5184,7 @@ dependencies = [ name = "penumbra-mock-tendermint-proxy" version = "0.80.2" dependencies = [ + "hex", "pbjson-types", "penumbra-mock-consensus", "penumbra-proto", 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 b242c9c8eb..4678141941 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 @@ -61,7 +61,7 @@ async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Res let height = 4; for i in 1..=height { node.block() - .with_signatures(vec![]) + .without_signatures() .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/common/test_node_builder_ext.rs b/crates/core/app/tests/common/test_node_builder_ext.rs index b074232cb5..93bf7f7fee 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, @@ -34,9 +34,13 @@ impl BuilderExt for Builder { }; for (consensus_vk, _) in keyring { + // Let the seed for the penumbra validator be derived from the verification key, + // that way tests can operate with no rng. + let seed = Some(SpendKeyBytes(consensus_vk.to_bytes())); + // Generate a penumbra validator with this consensus key, and a corresponding // allocation of delegation tokens. - let (validator, allocation) = generate_penumbra_validator(consensus_vk); + let (validator, allocation) = generate_penumbra_validator(consensus_vk, seed); // Add the validator to the staking component's genesis content. trace!(?validator, "adding validator to staking genesis content"); @@ -50,6 +54,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) @@ -61,8 +70,9 @@ impl BuilderExt for Builder { /// Generates a [`Validator`][PenumbraValidator] given a consensus verification key. fn generate_penumbra_validator( consensus_key: &ed25519_consensus::VerificationKey, + seed: Option, ) -> (PenumbraValidator, Allocation) { - let seed = SpendKeyBytes(OsRng.gen()); + let seed = seed.unwrap_or(SpendKeyBytes(OsRng.gen())); let spend_key = SpendKey::from(seed.clone()); let validator_id_sk = spend_key.spend_auth_key(); let validator_id_vk = VerificationKey::from(validator_id_sk); diff --git a/crates/core/app/tests/mock_consensus_block_proving.rs b/crates/core/app/tests/mock_consensus_block_proving.rs new file mode 100644 index 0000000000..f4be2d71d6 --- /dev/null +++ b/crates/core/app/tests/mock_consensus_block_proving.rs @@ -0,0 +1,340 @@ +use { + anyhow::Context as _, + cnidarium::{StateRead as _, TempStorage}, + common::{BuilderExt as _, TempStorageExt as _}, + ibc_proto::ibc::core::client::v1::{ + query_client::QueryClient as IbcClientQueryClient, QueryClientStateRequest, + }, + ibc_types::{ + core::{ + client::{msgs::MsgCreateClient, ClientId, Height}, + commitment::{MerkleProof, MerkleRoot}, + }, + lightclients::tendermint::{client_state::AllowUpdate, TrustThreshold}, + path::ClientStatePath, + DomainType as _, + }, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, + penumbra_ibc::{IbcRelay, MerklePrefixExt as _, IBC_COMMITMENT_PREFIX, IBC_PROOF_SPECS}, + penumbra_keys::test_keys, + penumbra_mock_client::MockClient, + penumbra_mock_consensus::TestNode, + penumbra_proto::{ + cnidarium::v1::{ + query_service_client::QueryServiceClient as CnidariumQueryServiceClient, + KeyValueRequest, + }, + util::tendermint_proxy::v1::{ + tendermint_proxy_service_client::TendermintProxyServiceClient, GetBlockByHeightRequest, + }, + DomainType, Message as _, + }, + penumbra_test_subscriber::set_tracing_subscriber, + penumbra_transaction::{TransactionParameters, TransactionPlan}, + std::{str::FromStr as _, time::Duration}, + tap::{Tap as _, TapFallible as _}, + tendermint::Hash, + tokio::time::{self}, + tonic::transport::Channel, +}; + +mod common; + +#[tokio::test] +async fn verify_storage_proof_simple() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = set_tracing_subscriber(); + + let storage = TempStorage::new_with_penumbra_prefixes().await?; + + let start_time = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?; + + let proxy = penumbra_mock_tendermint_proxy::TestNodeProxy::new::(); + let mut node = { + let 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)? + .on_block(proxy.on_block_callback()) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + let client = MockClient::new(test_keys::SPEND_KEY.clone()) + .with_sync_to_inner_storage(storage.clone()) + .await? + .tap( + |c| tracing::info!(client.notes = %c.notes.len(), "mock client synced to test storage"), + ); + + node.block().execute().await?; + + // Force the node to write an IBC client status into storage + // so we can retrieve with a proof: + let plan = { + let ibc_msg = IbcRelay::CreateClient(MsgCreateClient { + signer: "test".to_string(), + client_state: ibc_types::lightclients::tendermint::client_state::ClientState { + chain_id: TestNode::<()>::CHAIN_ID.to_string().into(), + trust_level: TrustThreshold { + numerator: 1, + denominator: 3, + }, + trusting_period: Duration::from_secs(120_000), + unbonding_period: Duration::from_secs(240_000), + max_clock_drift: Duration::from_secs(5), + latest_height: Height { + revision_height: 55, + revision_number: 0, + }, + proof_specs: IBC_PROOF_SPECS.to_vec(), + upgrade_path: vec!["upgrade".to_string(), "upgradedIBCState".to_string()], + allow_update: AllowUpdate { + after_expiry: false, + after_misbehaviour: false, + }, + frozen_height: None, + } + .into(), + consensus_state: ibc_types::lightclients::tendermint::consensus_state::ConsensusState { + timestamp: start_time, + // These values don't matter since we are only checking the proof + // of the client state. + root: MerkleRoot { + hash: vec![0u8; 32], + }, + next_validators_hash: Hash::Sha256([0u8; 32]), + } + .into(), + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + } + }; + let tx = client.witness_auth_build(&plan).await?; + let client_id = "07-tendermint-0".to_string(); + let key = IBC_COMMITMENT_PREFIX + .apply_string(ClientStatePath(ClientId::from_str(&client_id)?).to_string()) + .as_bytes() + .to_vec(); + + // Create the fake client + node.block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + + // Now retrieving the client state directly from storage should succeed: + let snapshot = storage.latest_snapshot(); + + let unproven = snapshot + .get_raw(&String::from_utf8(key.clone())?) + .await? + .expect("present in storage"); + + // The unproven version should be present + assert!(!unproven.is_empty()); + + let (cnid_client_state, cnid_proof) = snapshot + .get_with_proof(key.clone()) + .await + .map_err(|e| tonic::Status::aborted(format!("couldn't get connection: {e}")))?; + + // The proven version should also be present + let cnid_client_state = cnid_client_state.unwrap(); + + // The proven version should be the same as the unproven. + assert_eq!(cnid_client_state, unproven); + + // Common proof parameters: + let proof_specs = IBC_PROOF_SPECS.to_vec(); + // The root will be the latest jmt hash. + let latest_root = storage.latest_snapshot().root_hash().await.unwrap(); + let root = MerkleRoot { + hash: latest_root.0.to_vec(), + }; + // Initial path is the key... + let csp = ClientStatePath(ClientId::from_str(&client_id)?); + let prefix = &IBC_COMMITMENT_PREFIX; + // With the prefix applied: + let merkle_path = prefix.apply(vec![csp.to_string()]); + + // Verify the proof against the results from calling get_with_proof. + cnid_proof.verify_membership( + &proof_specs, + root.clone(), + merkle_path.clone(), + cnid_client_state.clone(), + 0, + )?; + + // now verify the proof retrieved via a gRPC call + let grpc_url = "http://127.0.0.1:8081" // see #4517 + .parse::()? + .tap(|url| tracing::debug!(%url, "parsed grpc url")); + // Spawn the node's RPC server. + let _rpc_server = { + let make_svc = + penumbra_app::rpc::router(&storage, proxy, false /*enable_expensive_rpc*/)? + .into_router() + .layer(tower_http::cors::CorsLayer::permissive()) + .into_make_service() + .tap(|_| println!("initialized rpc service")); + let [addr] = grpc_url + .socket_addrs(|| None)? + .try_into() + .expect("grpc url can be turned into a socket address"); + let server = axum_server::bind(addr).serve(make_svc); + tokio::spawn(async { server.await.expect("grpc server returned an error") }) + .tap(|_| println!("grpc server is running")) + }; + + time::sleep(time::Duration::from_secs(1)).await; + let channel = Channel::from_shared(grpc_url.to_string()) + .with_context(|| "could not parse node URI")? + .connect() + .await + .with_context(|| "could not connect to grpc server") + .tap_err(|error| tracing::error!(?error, "could not connect to grpc server"))?; + let mut cnidarium_client = CnidariumQueryServiceClient::new(channel.clone()); + let mut ibc_client_query_client = IbcClientQueryClient::new(channel.clone()); + let mut tendermint_proxy_service_client = TendermintProxyServiceClient::new(channel.clone()); + let kvr = cnidarium_client + .key_value(tonic::Request::new(KeyValueRequest { + key: String::from_utf8(key.clone()).unwrap(), + proof: true, + })) + .await? + .into_inner(); + + let proof = kvr.proof.unwrap().try_into()?; + let value = kvr.value.unwrap().value; + + // The proof from cnidarium and from the RPC should be the same since nothing has + // happened on-chain since the cnidarium proof was generated. + assert_eq!(cnid_proof, proof); + // Same for the values. + assert_eq!(value, cnid_client_state); + + proof.verify_membership( + &proof_specs, + root.clone(), + merkle_path.clone(), + value.clone(), + 0, + )?; + + let snapshot = storage.latest_snapshot(); + let storage_revision_height = snapshot.version(); + + let latest_height = node.height().clone(); + assert_eq!(u64::from(latest_height), storage_revision_height); + + // Try fetching the client state via the IBC API + let node_last_app_hash = node.last_app_hash(); + tracing::debug!( + "making IBC client state request at height {} and hash {}", + latest_height, + hex::encode(node_last_app_hash) + ); + let ibc_client_state_response = ibc_client_query_client + .client_state(QueryClientStateRequest { + client_id: "07-tendermint-0".to_string(), + }) + .await? + .into_inner(); + + let ibc_proof = MerkleProof::decode(ibc_client_state_response.clone().proof.as_slice())?; + let ibc_value = ibc_client_state_response.client_state.unwrap(); + + assert_eq!(ibc_value.encode_to_vec(), value); + + // We should be able to get the block from the proof_height associated with + // the proof and use the app_hash as the jmt root and succeed in proving: + let proof_block: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse = + tendermint_proxy_service_client + .get_block_by_height(GetBlockByHeightRequest { + height: ibc_client_state_response + .proof_height + .clone() + .unwrap() + .revision_height + .try_into()?, + }) + .await? + .into_inner(); + + // The proof height of the ibc response should be the same as the height of the proof block + assert_eq!( + ibc_client_state_response + .proof_height + .clone() + .unwrap() + .revision_height, + proof_block.block.clone().unwrap().header.unwrap().height as u64 + ); + // The node height when we directly retrieved the last app hash + // should match the proof height + assert_eq!( + ibc_client_state_response + .proof_height + .clone() + .unwrap() + .revision_height, + u64::from(latest_height) + ); + + // TODO: these tests fail + if false { + // the proof block's app hash should match + assert_eq!( + node_last_app_hash, + proof_block.block.clone().unwrap().header.unwrap().app_hash, + "node claimed app hash for height {} was {}, however block header contained {}", + latest_height, + hex::encode(node_last_app_hash), + hex::encode(proof_block.block.clone().unwrap().header.unwrap().app_hash) + ); + println!( + "proof height: {} proof_block_root: {:?}", + ibc_client_state_response + .proof_height + .unwrap() + .revision_height, + hex::encode(proof_block.block.clone().unwrap().header.unwrap().app_hash) + ); + let proof_block_root = MerkleRoot { + hash: proof_block.block.unwrap().header.unwrap().app_hash, + }; + ibc_proof + .verify_membership( + &proof_specs, + proof_block_root, + merkle_path, + ibc_value.encode_to_vec(), + 0, + ) + .expect("the ibc proof should validate against the root of the proof_height's block"); + } + + Ok(()) + .tap(|_| drop(node)) + .tap(|_| drop(storage)) + .tap(|_| drop(guard)) +} 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/test/mock-client/src/lib.rs b/crates/test/mock-client/src/lib.rs index ea7ecbe960..a46a29e86c 100644 --- a/crates/test/mock-client/src/lib.rs +++ b/crates/test/mock-client/src/lib.rs @@ -42,6 +42,16 @@ impl MockClient { Ok(self) } + pub async fn with_sync_to_inner_storage( + mut self, + storage: cnidarium::Storage, + ) -> anyhow::Result { + let latest = storage.latest_snapshot(); + self.sync_to_latest(latest).await?; + + Ok(self) + } + pub async fn sync_to_latest(&mut self, state: R) -> anyhow::Result<()> { let height = state.get_block_height().await?; self.sync_to(height, state).await?; diff --git a/crates/test/mock-consensus/Cargo.toml b/crates/test/mock-consensus/Cargo.toml index 8c75ced395..9b1ba0b45c 100644 --- a/crates/test/mock-consensus/Cargo.toml +++ b/crates/test/mock-consensus/Cargo.toml @@ -15,9 +15,14 @@ license.workspace = true anyhow = { workspace = true } bytes = { workspace = true } ed25519-consensus = { workspace = true } +hex = { workspace = true } +prost = { workspace = true } rand_core = { workspace = true } +serde = { workspace = true } +serde_json = { 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..dbea3c1a20 100644 --- a/crates/test/mock-consensus/src/abci.rs +++ b/crates/test/mock-consensus/src/abci.rs @@ -160,6 +160,7 @@ where retain_height, } = &response; trace!(?data, ?retain_height, "received Commit response"); + Ok(response) } response => { diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index af75efeda0..2be7700835 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -4,13 +4,16 @@ use { crate::TestNode, + prost::Message, + sha2::{Digest, Sha256}, tap::Tap, tendermint::{ account, block::{self, header::Version, Block, Commit, Header, Round}, - chain, evidence, + evidence, + merkle::simple_hash_from_byte_vectors, v0_37::abci::{ConsensusRequest, ConsensusResponse}, - AppHash, Hash, Time, + Hash, Time, }, tower::{BoxError, Service}, tracing::{instrument, trace}, @@ -34,10 +37,10 @@ pub struct Builder<'e, C> { data: Vec>, /// Evidence of malfeasance. evidence: evidence::List, - /// The list of signatures. - signatures: Vec, /// The timestamp of the block. timestamp: Time, + /// Disable producing signatures. Defaults to produce signatures. + disable_signatures: bool, } // === impl TestNode === @@ -45,19 +48,14 @@ 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 ts = self.timestamp.clone(); - let signatures = self.generate_signatures().collect(); - // set default TS hook Builder { test_node: self, data: Default::default(), evidence: Default::default(), - signatures, timestamp: ts, + disable_signatures: false, } } } @@ -90,9 +88,12 @@ impl<'e, C> Builder<'e, C> { Self { evidence, ..self } } - /// Sets the [`CommitSig`][block::CommitSig] commit signatures for this block. - pub fn with_signatures(self, signatures: Vec) -> Self { - Self { signatures, ..self } + /// Disables producing commit signatures for this block. + pub fn without_signatures(self) -> Self { + Self { + disable_signatures: true, + ..self + } } } @@ -108,14 +109,22 @@ 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::without_signatures()`] to disable producing + /// validator signatures. #[instrument(level = "info", skip_all, fields(height, time))] pub async fn execute(self) -> Result<(), anyhow::Error> { + // Calling `finish` finishes the previous block + // and prepares the current block. let (test_node, block) = self.finish()?; let Block { + // The header for the current block header, data, evidence: _, + // Votes for the previous block last_commit, .. } = block.clone().tap(|block| { @@ -132,15 +141,25 @@ where test_node.deliver_tx(tx).await?; } test_node.end_block().await?; - test_node.commit().await?; - trace!("finished sending block"); + + // the commit call will set test_node.last_app_hash, preparing + // for the next block to begin execution + let commit_response = test_node.commit().await?; + + // NOTE: after calling .commit(), the internal status of the pd node's storage is going to be updated + // to the next block + // therefore we need to update the height within our mock now now + + // Set the last app hash to the new block's app hash. + test_node.last_app_hash = commit_response.data.to_vec(); + trace!( + last_app_hash = ?hex::encode(commit_response.data.to_vec()), + "test node has committed block, setting last_app_hash" + ); // If an `on_block` callback was set, call it now. test_node.on_block.as_mut().map(move |f| f(block)); - // Call the timestamp callback to increment the node's current timestamp. - test_node.timestamp = (test_node.ts_callback)(test_node.timestamp.clone()); - Ok(()) } @@ -156,10 +175,14 @@ where data, evidence, test_node, - signatures, timestamp, + disable_signatures, } = self; + // Call the timestamp callback to increment the node's current timestamp. + test_node.timestamp = (test_node.ts_callback)(test_node.timestamp.clone()); + + // The first (non-genesis) block has height 1. let height = { let height = test_node.height.increment(); test_node.height = height; @@ -167,41 +190,147 @@ where height }; - let last_commit = if height.value() != 1 { - let block_id = block::Id { - hash: Hash::None, - part_set_header: block::parts::Header::new(0, Hash::None)?, - }; - Some(Commit { - height, - round: Round::default(), - block_id, - signatures, - }) - } else { - None // The first block has no previous commit to speak of. - }; + let last_commit = &test_node.last_commit; + + // 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 validators_hash = test_node.last_validator_set_hash.clone().unwrap(); + // 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 = test_node.consensus_params_hash.clone().try_into().unwrap(); let header = Header { - version: Version { block: 1, app: 1 }, - chain_id: chain::Id::try_from("test".to_owned())?, + // Protocol version. Block version 11 matches cometbft when tests were written. + version: Version { block: 11, app: 0 }, + chain_id: tendermint::chain::Id::try_from(test_node.chain_id.clone())?, + // Height is the height for this header. height, time: timestamp, - last_block_id: None, - last_commit_hash: None, - data_hash: None, - validators_hash: Hash::None, - next_validators_hash: Hash::None, - consensus_hash: Hash::None, - app_hash: AppHash::try_from(Vec::default())?, - 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, - ]), + // MerkleRoot of the lastCommit’s signatures. The signatures represent the validators that committed to the last block. The first block has an empty slices of bytes for the hash. + last_commit_hash: test_node + .last_commit + .as_ref() + .map(|c| c.hash().unwrap()) + .unwrap_or(Some( + // empty hash value + hex::decode( + "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + )? + .try_into()?, + )), + last_block_id: test_node.last_commit.as_ref().map(|c| c.block_id.clone()), + // MerkleRoot of the hash of transactions. Note: The transactions are hashed before being included in the merkle tree, the leaves of the Merkle tree are the hashes, not the transactions themselves. + 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 + // MerkleRoot of the current validator set + validators_hash: validators_hash.into(), + // MerkleRoot of the next validator set + next_validators_hash: validators_hash.into(), + // Hash of the protobuf encoded consensus parameters. + consensus_hash, + // Arbitrary byte array returned by the application after executing and committing the previous block. + app_hash: tendermint::AppHash::try_from(test_node.last_app_hash().to_vec())?, + // TODO: we should probably have a way to set this + // root hash of a Merkle tree built from DeliverTxResponse responses(Log,Info, Codespace and Events fields are ignored).The first block has block.Header.ResultsHash == MerkleRoot(nil), i.e. the hash of an empty input, for RFC-6962 conformance. + // the go version will shasum empty bytes and produce "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" + last_results_hash: Some( + hex::decode("E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855")? + .try_into()?, + ), + // MerkleRoot of the evidence of Byzantine behavior included in this block. + evidence_hash: Some( + hex::decode("E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855")? + .try_into()?, + ), + // Address of the original proposer of the block. Validator must be in the current validatorSet. + proposer_address, }; - let block = Block::new(header, data, evidence, last_commit)?; + tracing::trace!(?header, "built block header"); + + // The next block will use the signatures of this block's header. + let signatures: Vec = if !disable_signatures { + test_node.generate_signatures(&header).collect() + } else { + vec![] + }; + + tracing::trace!( + height=?height.value(), + app_hash=?hex::encode(header.app_hash.clone()), + block_id=?hex::encode(header.hash()), + last_commit_height=?last_commit.as_ref().map(|c| c.height.value()), + "made block" + ); + let block = Block::new(header.clone(), data, evidence, last_commit.clone())?; + + // Now that the block is finalized, we can transition to the next block. + // Generate a commit for the header we just made, that will be + // included in the next header. + test_node.last_commit = Some(Commit { + height: block.header.height, + round: Round::default(), + block_id: block::Id { + hash: block.header.hash().into(), + // The part_set_header seems to be used internally by cometbft + // and the pd node doesn't care about it + part_set_header: block::parts::Header::new(0, Hash::None)?, + }, + // Signatures of the last block + signatures: signatures.clone(), + }); Ok((test_node, block)) } } + +// Allows hashing of commits +pub trait CommitHashingExt: Sized { + fn hash(&self) -> anyhow::Result>; +} + +impl CommitHashingExt for Commit { + // https://github.com/tendermint/tendermint/blob/51dc810d041eaac78320adc6d53ad8b160b06601/types/block.go#L672 + fn hash(&self) -> anyhow::Result> { + // make a vec of the precommit protobuf encodings + // then merkle hash them + // https://github.com/tendermint/tendermint/blob/35581cf54ec436b8c37fabb43fdaa3f48339a170/crypto/merkle/tree.go#L9 + let bs = self + .signatures + .iter() + .map(|precommit| { + tendermint_proto::types::CommitSig::from(precommit.clone()).encode_to_vec() + }) + .collect::>(); + + match bs.len() { + 0 => + // empty hash + { + Ok(Some( + hex::decode( + "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + )? + .try_into()?, + )) + } + _ => Ok(Some( + simple_hash_from_byte_vectors::(&bs) + .to_vec() + .try_into()?, + )), + } + } +} diff --git a/crates/test/mock-consensus/src/block/signature.rs b/crates/test/mock-consensus/src/block/signature.rs index f3195a57be..15ce62589a 100644 --- a/crates/test/mock-consensus/src/block/signature.rs +++ b/crates/test/mock-consensus/src/block/signature.rs @@ -17,11 +17,33 @@ mod sign { /// Returns a [commit signature] saying this validator voted for the block. /// /// [commit signature]: CommitSig - pub(super) fn commit(validator_address: Id, timestamp: Time) -> 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(), + ); + + let signature: tendermint::Signature = validator_key + .sign(sign_bytes.as_slice()) + .try_into() + .unwrap(); + + // 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, - signature: None, + timestamp: canonical.timestamp.expect("timestamp should be present"), + signature: Some(signature.into()), } } @@ -46,18 +68,42 @@ 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| { + /// Signatures sign the given block header. + 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(header.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(), + }; + tracing::trace!(vote=?canonical,"canonical vote constructed"); + + return self + .keyring + .iter() + .map(|(vk, sk)| { ( ::digest(vk).as_slice()[0..20] .try_into() .expect(""), - self.timestamp.clone(), + sk, ) }) - .map(|(a, b)| (self::sign::commit(account::Id::new(a), b))) + .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 8afd9fedef..6718028b56 100644 --- a/crates/test/mock-consensus/src/builder.rs +++ b/crates/test/mock-consensus/src/builder.rs @@ -7,9 +7,11 @@ mod init_chain; use { crate::{Keyring, OnBlockFn, TestNode, TsCallbackFn}, + anyhow::Result, bytes::Bytes, + ed25519_consensus::{SigningKey, VerificationKey}, std::time::Duration, - tendermint::Time, + tendermint::{Genesis, Time}, }; // Default timestamp callback will increment the time by 5 seconds. @@ -26,6 +28,17 @@ pub struct Builder { pub on_block: Option, pub ts_callback: Option, pub initial_timestamp: Option