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/common/test_node_ext.rs b/crates/core/app/tests/common/test_node_ext.rs index 48a7f55fd2..d677eb3e24 100644 --- a/crates/core/app/tests/common/test_node_ext.rs +++ b/crates/core/app/tests/common/test_node_ext.rs @@ -34,7 +34,7 @@ where .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); loop { - self.block().execute().await?; + self.block().without_signatures().execute().await?; let current = get_epoch().await?; if current != start { tracing::debug!(end = ?current, ?start, "reached next epoch"); 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..d1e9a12cf7 --- /dev/null +++ b/crates/core/app/tests/mock_consensus_block_proving.rs @@ -0,0 +1,320 @@ +use { + cnidarium::TempStorage, penumbra_app::server::consensus::Consensus, + penumbra_test_subscriber::set_tracing_subscriber, +}; + +// #[tokio::test] +// async fn verify_storage_proof_simple() -> anyhow::Result<()> { +// // Install a test logger, and acquire some temporary storage. +// let guard = set_tracing_subscriber(); + +// let storage = TempStorage::new_with_penumbra_prefixes().await?; + +// let start_time = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?; + +// let proxy = penumbra_mock_tendermint_proxy::TestNodeProxy::new::(); +// let mut node = { +// let genesis = get_verified_genesis()?; +// let consensus = Consensus::new(storage.clone()); +// // let consensus = Consensus::new(storage.as_ref().clone()); +// TestNode::builder() +// .single_validator() +// .with_tendermint_genesis(genesis) +// .on_block(proxy.on_block_callback()) +// .init_chain(consensus) +// .await +// .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? +// }; + +// let client = MockClient::new(test_keys::SPEND_KEY.clone()) +// .with_sync_to_inner_storage(storage.clone()) +// .await? +// .tap( +// |c| tracing::info!(client.notes = %c.notes.len(), "mock client synced to test storage"), +// ); + +// node.block().execute().await?; + +// // Force the node to write an IBC client status into storage +// // so we can retrieve with a proof: +// let plan = { +// let ibc_msg = IbcRelay::CreateClient(MsgCreateClient { +// signer: "test".to_string(), +// client_state: ibc_types::lightclients::tendermint::client_state::ClientState { +// chain_id: TestNode::<()>::CHAIN_ID.to_string().into(), +// trust_level: TrustThreshold { +// numerator: 1, +// denominator: 3, +// }, +// trusting_period: Duration::from_secs(120_000), +// unbonding_period: Duration::from_secs(240_000), +// max_clock_drift: Duration::from_secs(5), +// latest_height: Height { +// revision_height: 55, +// revision_number: 0, +// }, +// proof_specs: IBC_PROOF_SPECS.to_vec(), +// upgrade_path: vec!["upgrade".to_string(), "upgradedIBCState".to_string()], +// allow_update: AllowUpdate { +// after_expiry: false, +// after_misbehaviour: false, +// }, +// frozen_height: None, +// } +// .into(), +// consensus_state: ibc_types::lightclients::tendermint::consensus_state::ConsensusState { +// timestamp: start_time, +// // These values don't matter since we are only checking the proof +// // of the client state. +// root: MerkleRoot { +// hash: vec![0u8; 32], +// }, +// next_validators_hash: Hash::Sha256([0u8; 32]), +// } +// .into(), +// }) +// .into(); +// TransactionPlan { +// actions: vec![ibc_msg], +// // Now fill out the remaining parts of the transaction needed for verification: +// memo: None, +// detection_data: None, // We'll set this automatically below +// transaction_parameters: TransactionParameters { +// chain_id: TestNode::<()>::CHAIN_ID.to_string(), +// ..Default::default() +// }, +// } +// }; +// let tx = client.witness_auth_build(&plan).await?; +// let client_id = "07-tendermint-0".to_string(); +// let key = IBC_COMMITMENT_PREFIX +// .apply_string(ClientStatePath(ClientId::from_str(&client_id)?).to_string()) +// .as_bytes() +// .to_vec(); + +// // Create the fake client +// node.block() +// .with_data(vec![tx.encode_to_vec()]) +// .execute() +// .await?; + +// // Now retrieving the client state directly from storage should succeed: +// let snapshot = storage.latest_snapshot(); + +// let unproven = snapshot +// .get_raw(&String::from_utf8(key.clone())?) +// .await? +// .expect("present in storage"); + +// // The unproven version should be present +// assert!(!unproven.is_empty()); + +// let (cnid_client_state, cnid_proof) = snapshot +// .get_with_proof(key.clone()) +// .await +// .map_err(|e| tonic::Status::aborted(format!("couldn't get connection: {e}")))?; + +// // The proven version should also be present +// let cnid_client_state = cnid_client_state.unwrap(); + +// // The proven version should be the same as the unproven. +// assert_eq!(cnid_client_state, unproven); + +// // Common proof parameters: +// let proof_specs = IBC_PROOF_SPECS.to_vec(); +// // The root will be the latest jmt hash. +// let latest_root = storage.latest_snapshot().root_hash().await.unwrap(); +// let root = MerkleRoot { +// hash: latest_root.0.to_vec(), +// }; +// // Initial path is the key... +// let csp = ClientStatePath(ClientId::from_str(&client_id)?); +// let prefix = &IBC_COMMITMENT_PREFIX; +// // With the prefix applied: +// let merkle_path = prefix.apply(vec![csp.to_string()]); + +// // Verify the proof against the results from calling get_with_proof. +// cnid_proof.verify_membership( +// &proof_specs, +// root.clone(), +// merkle_path.clone(), +// cnid_client_state.clone(), +// 0, +// )?; + +// println!("verified directly from storage"); + +// // now verify the proof retrieved via a gRPC call +// let grpc_url = "http://127.0.0.1:8081" // see #4517 +// .parse::()? +// .tap(|url| tracing::debug!(%url, "parsed grpc url")); +// // Spawn the node's RPC server. +// let _rpc_server = { +// let make_svc = +// penumbra_app::rpc::router(&storage, proxy, false /*enable_expensive_rpc*/)? +// .into_router() +// .layer(tower_http::cors::CorsLayer::permissive()) +// .into_make_service() +// .tap(|_| println!("initialized rpc service")); +// let [addr] = grpc_url +// .socket_addrs(|| None)? +// .try_into() +// .expect("grpc url can be turned into a socket address"); +// let server = axum_server::bind(addr).serve(make_svc); +// tokio::spawn(async { server.await.expect("grpc server returned an error") }) +// .tap(|_| println!("grpc server is running")) +// }; + +// time::sleep(time::Duration::from_secs(1)).await; +// let channel = Channel::from_shared(grpc_url.to_string()) +// .with_context(|| "could not parse node URI")? +// .connect() +// .await +// .with_context(|| "could not connect to grpc server") +// .tap_err(|error| tracing::error!(?error, "could not connect to grpc server"))?; +// let mut cnidarium_client = CnidariumQueryServiceClient::new(channel.clone()); +// let mut ibc_client_query_client = IbcClientQueryClient::new(channel.clone()); +// let mut tendermint_proxy_service_client = TendermintProxyServiceClient::new(channel.clone()); +// let kvr = cnidarium_client +// .key_value(tonic::Request::new(KeyValueRequest { +// key: String::from_utf8(key.clone()).unwrap(), +// proof: true, +// })) +// .await? +// .into_inner(); + +// let proof = kvr.proof.unwrap().try_into()?; +// let value = kvr.value.unwrap().value; + +// // The proof from cnidarium and from the RPC should be the same since nothing has +// // happened on-chain since the cnidarium proof was generated. +// assert_eq!(cnid_proof, proof); +// // Same for the values. +// assert_eq!(value, cnid_client_state); + +// proof.verify_membership( +// &proof_specs, +// root.clone(), +// merkle_path.clone(), +// value.clone(), +// 0, +// )?; + +// let snapshot = storage.latest_snapshot(); +// let storage_revision_height = snapshot.version(); + +// let latest_height = node.height().clone(); +// assert_eq!(u64::from(latest_height), storage_revision_height); + +// // Try fetching the client state via the IBC API +// // height 2 +// // WRONG vvv these don't match what's in the block headers +// let node_last_app_hash = node.last_app_hash(); +// println!( +// "making IBC client state request at height {} and hash {}", +// latest_height, +// // e0c071d4b2198c7e5f9fdee7d6618bf36ea75fdecd56df315ba2ae87b9a50718 (height 3 header app_hash) +// hex::encode(node_last_app_hash) +// ); +// let ibc_client_state_response = ibc_client_query_client +// .client_state(QueryClientStateRequest { +// client_id: "07-tendermint-0".to_string(), +// }) +// .await? +// .into_inner(); + +// let ibc_proof = MerkleProof::decode(ibc_client_state_response.clone().proof.as_slice())?; +// let ibc_value = ibc_client_state_response.client_state.unwrap(); + +// // let cs = ibc_types::lightclients::tendermint::client_state::ClientState::try_from( +// // ibc_value.clone(), +// // )?; +// // println!("client state: {:?}", cs); +// // // let cs2 = ibc_types::lightclients::tendermint::client_state::ClientState::try_from(Any { +// // // type_url: TENDERMINT_CLIENT_STATE_TYPE_URL.to_string(), +// // // value: value.clone().into(), +// // // })?; +// // let client_state = ibc_proto::google::protobuf::Any::decode(value.as_ref())?; +// // let cs2 = ibc_proto::ibc::lightclients::tendermint::v1::ClientState::decode( +// // &*client_state.value.clone(), +// // )?; +// // let cs3 = +// // ibc_types::lightclients::tendermint::client_state::ClientState::try_from(client_state)?; +// // println!("client state2: {:?}", cs2); +// // println!("client state3: {:?}", cs3); + +// // let client_state = ibc_proto::google::protobuf::Any::decode(value.as_ref())?; +// // let cs1 = ibc_proto::ibc::lightclients::tendermint::v1::ClientState::decode(&*client.value)?; +// // let client_state1 = TendermintClientState::try_from(cs1.clone())?; + +// assert_eq!(ibc_value.encode_to_vec(), value); + +// // We should be able to get the block from the proof_height associated with +// // the proof and use the app_hash as the jmt root and succeed in proving: +// let proof_block: penumbra_proto::util::tendermint_proxy::v1::GetBlockByHeightResponse = +// tendermint_proxy_service_client +// .get_block_by_height(GetBlockByHeightRequest { +// height: ibc_client_state_response +// .proof_height +// .clone() +// .unwrap() +// .revision_height +// .try_into()?, +// }) +// .await? +// .into_inner(); + +// // The proof height of the ibc response should be the same as the height of the proof block +// assert_eq!( +// ibc_client_state_response +// .proof_height +// .clone() +// .unwrap() +// .revision_height, +// proof_block.block.clone().unwrap().header.unwrap().height as u64 +// ); +// // The node height when we directly retrieved the last app hash +// // should match the proof height +// assert_eq!( +// ibc_client_state_response +// .proof_height +// .clone() +// .unwrap() +// .revision_height, +// u64::from(latest_height) +// ); +// // the proof block's app hash should match +// // assert_eq!( +// // node_last_app_hash, +// // proof_block.block.clone().unwrap().header.unwrap().app_hash, +// // "node claimed app hash for height {} was {}, however block header contained {}", +// // node_height, +// // hex::encode(node_last_app_hash), +// // hex::encode(proof_block.block.clone().unwrap().header.unwrap().app_hash) +// // ); +// println!( +// "proof height: {} proof_block_root: {:?}", +// ibc_client_state_response +// .proof_height +// .unwrap() +// .revision_height, +// hex::encode(proof_block.block.clone().unwrap().header.unwrap().app_hash) +// ); +// let proof_block_root = MerkleRoot { +// hash: proof_block.block.unwrap().header.unwrap().app_hash, +// }; +// ibc_proof +// .verify_membership( +// &proof_specs, +// proof_block_root, +// merkle_path, +// ibc_value.encode_to_vec(), +// 0, +// ) +// .expect("the ibc proof should validate against the root of the proof_height's block"); + +// Ok(()) +// .tap(|_| drop(node)) +// .tap(|_| drop(storage)) +// .tap(|_| drop(guard)) +// } 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