From 51485712e12960f6610a5da54ff338b800afafc4 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 8 Jan 2024 12:40:14 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=93=9D=20implement=20cometstub=20?= =?UTF-8?q?crate=20(work-in-progress)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this commit introduces a new library, in `crates/test/`. this library contains a mock implementation of cometbft, for use in cargo integration tests. * this does NOT add the `penumbra-cometstub` crate to the list of crates included in the rust documentation in `deployments/scripts/rust-docs`. the `penumbra-tct-property-test` crate was also not included in that list at the time of writing. /!\ ------------------------------------------------------- /!\ /!\ NOTE: this is a rolling work-in-progress. /!\ /!\ this branch will be force-pushed frequently until it is /!\ /!\ ready for review. proceed accordingly! /!\ /!\ ------------------------------------------------------- /!\ NOTE 24-01-02: i have brought over a test from `core/app/src/tests/spend.rs`. currently, it panics because proving keys could not be loaded. printlns and panics have been left to point out how far we can get through that test before failing. track down the core of this issue next week. use this command to see it break: ``` cargo watch -Bc -x 'test -p penumbra-cometstub spend_happy_path' ``` --- Cargo.lock | 26 +++ Cargo.toml | 1 + crates/crypto/proof-params/src/lib.rs | 1 + crates/test/cometstub/Cargo.toml | 46 +++++ crates/test/cometstub/src/abci.rs | 45 +++++ crates/test/cometstub/src/header.rs | 36 ++++ crates/test/cometstub/src/lib.rs | 182 ++++++++++++++++++ crates/test/cometstub/src/state.rs | 1 + crates/test/cometstub/src/validator.rs | 105 ++++++++++ crates/test/cometstub/tests/common/mod.rs | 32 +++ crates/test/cometstub/tests/ibc_handshake.rs | 20 ++ .../test/cometstub/tests/spend_happy_path.rs | 86 +++++++++ 12 files changed, 581 insertions(+) create mode 100644 crates/test/cometstub/Cargo.toml create mode 100644 crates/test/cometstub/src/abci.rs create mode 100644 crates/test/cometstub/src/header.rs create mode 100644 crates/test/cometstub/src/lib.rs create mode 100644 crates/test/cometstub/src/state.rs create mode 100644 crates/test/cometstub/src/validator.rs create mode 100644 crates/test/cometstub/tests/common/mod.rs create mode 100644 crates/test/cometstub/tests/ibc_handshake.rs create mode 100644 crates/test/cometstub/tests/spend_happy_path.rs diff --git a/Cargo.lock b/Cargo.lock index fe88e71f6d..83097ef112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5073,6 +5073,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "penumbra-cometstub" +version = "0.64.1" +dependencies = [ + "anyhow", + "cnidarium", + "cnidarium-component", + "ed25519-consensus", + "ibc-proto", + "ibc-types", + "penumbra-app", + "penumbra-chain", + "penumbra-compact-block", + "penumbra-keys", + "penumbra-proof-params", + "penumbra-sct", + "penumbra-shielded-pool", + "penumbra-txhash", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "sha2 0.10.8", + "tendermint", + "tendermint-light-client-verifier", + "tokio", +] + [[package]] name = "penumbra-community-pool" version = "0.64.1" diff --git a/Cargo.toml b/Cargo.toml index e6ac21c3c2..773f503d07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "crates/bin/pclientd", "crates/bin/pcli", "crates/wasm", + "crates/test/cometstub", "crates/test/tct-property-test", "crates/misc/measure", "crates/misc/tct-visualize", diff --git a/crates/crypto/proof-params/src/lib.rs b/crates/crypto/proof-params/src/lib.rs index 50c10d77ca..20380e03c5 100644 --- a/crates/crypto/proof-params/src/lib.rs +++ b/crates/crypto/proof-params/src/lib.rs @@ -95,6 +95,7 @@ impl Deref for LazyProvingKey { /// Proving key for the spend proof. pub static SPEND_PROOF_PROVING_KEY: Lazy = Lazy::new(|| { + println!("XXX(kate) loading spend proving key"); let spend_proving_key = LazyProvingKey::new(spend::PROVING_KEY_ID); #[cfg(feature = "bundled-proving-keys")] diff --git a/crates/test/cometstub/Cargo.toml b/crates/test/cometstub/Cargo.toml new file mode 100644 index 0000000000..9205995a65 --- /dev/null +++ b/crates/test/cometstub/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "penumbra-cometstub" +version = "0.64.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["component", "std"] +component = [ + # TODO(kate): are these feature flags needed? maybe. + # "cnidarium", + # "cnidarium-component", + # "penumbra-proto/cnidarium", + # "penumbra-chain/component", +] +std = ["ibc-types/std"] + +[dependencies] +anyhow = "1" +# ed25519 = "2.2.3" +ed25519-consensus = "2.1.0" +ibc-proto = { version = "0.40.0", default-features = false } +ibc-types = { version = "0.11.0", default-features = false } +rand_core = "0.6.3" +sha2 = "0.10.8" +tendermint = "0.34.0" +tendermint-light-client-verifier = "0.34.0" + +[dev-dependencies] +cnidarium = { path = "../../cnidarium" } +cnidarium-component = { path = "../../cnidarium-component" } +penumbra-app = { path = "../../core/app" } +penumbra-chain = { path = "../../core/component/chain", features = ["component"] } +penumbra-compact-block = { path = "../../core/component/compact-block" } +penumbra-keys = { path = "../../core/keys" } +penumbra-proof-params = { path = "../../crypto/proof-params", features = [ + "bundled-proving-keys", + "download-proving-keys", +] } +penumbra-sct = { path = "../../core/component/sct" } +penumbra-shielded-pool = { path = "../../core/component/shielded-pool", features = ["component"] } +penumbra-txhash = { path = "../../core/txhash" } +rand_chacha = "0.3" +rand_core = "0.6" +tokio = { version = "1.21.1", features = ["full", "tracing"] } diff --git a/crates/test/cometstub/src/abci.rs b/crates/test/cometstub/src/abci.rs new file mode 100644 index 0000000000..c44efadcce --- /dev/null +++ b/crates/test/cometstub/src/abci.rs @@ -0,0 +1,45 @@ +//! ABCI- and ABCI++-related facilities. +//! +//! See the [ABCI++ specification][abci-spec] for more information. See ["Methods][abci-methods] +//! for more information on ABCI methods. +//! +//! [abci-spec]: https://github.com/cometbft/cometbft/blob/main/spec/abci/README.md +//! [abci-methods]: https://github.com/cometbft/cometbft/blob/main/spec/abci/abci++_methods.md +// +// TODO(kate): `tendermint::abci::request` types, stub these out as needed. +// - apply_snapshot_chunk::ApplySnapshotChunk, +// - begin_block::BeginBlock, +// - check_tx::{CheckTx, CheckTxKind}, +// - deliver_tx::DeliverTx, +// - echo::Echo, +// - end_block::EndBlock, +// - extend_vote::ExtendVote, +// - finalize_block::FinalizeBlock, +// - info::Info, +// - init_chain::InitChain, +// - load_snapshot_chunk::LoadSnapshotChunk, +// - offer_snapshot::OfferSnapshot, +// - prepare_proposal::PrepareProposal, +// - process_proposal::ProcessProposal, +// - query::Query, +// - set_option::SetOption, +// - verify_vote_extension::VerifyVoteExtension, + +use tendermint::{ + abci::{request::BeginBlock, types}, + block::Round, + Hash, +}; + +#[allow(dead_code)] // XXX(kate) +pub(crate) fn begin_block() -> BeginBlock { + BeginBlock { + hash: Hash::None, + header: crate::header::header(), + last_commit_info: types::CommitInfo { + round: Round::default(), + votes: vec![], + }, + byzantine_validators: vec![], + } +} diff --git a/crates/test/cometstub/src/header.rs b/crates/test/cometstub/src/header.rs new file mode 100644 index 0000000000..4a668c2139 --- /dev/null +++ b/crates/test/cometstub/src/header.rs @@ -0,0 +1,36 @@ +//! Facilities for generating tendermint [`Header`]s + +use tendermint::{ + account, + block::{self, Header}, + chain, + validator::Set, + AppHash, Hash, Time, +}; + +pub(crate) fn header() -> Header { + let validators_hash = Set::new(vec![], None).hash(); + Header { + version: block::header::Version { block: 0, app: 0 }, + chain_id: chain::Id::try_from("test").unwrap(), + height: block::Height::default(), + time: Time::unix_epoch(), + last_block_id: None, + last_commit_hash: None, + data_hash: None, + validators_hash, + next_validators_hash: validators_hash, + consensus_hash: Hash::None, + app_hash: app_hash(), + 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, + ]), + } +} + +// TODO(kate): informalsystems/tendermint-rs#1243 +fn app_hash() -> AppHash { + AppHash::try_from(vec![1, 2, 3]).expect("AppHash::try_from is infallible") +} diff --git a/crates/test/cometstub/src/lib.rs b/crates/test/cometstub/src/lib.rs new file mode 100644 index 0000000000..1ab4b558dc --- /dev/null +++ b/crates/test/cometstub/src/lib.rs @@ -0,0 +1,182 @@ +//! `penumbra-cometstub` is an in-memory consensus engine for integration tests. +// +// TODO(kate): +// - `tests/ibc_handshake.rs` contains a starter test case to build out. + +pub mod validator; + +mod abci; +mod header; +mod state; + +use { + self::validator::Validators, + tendermint::{block, chain, AppHash, Hash, Time}, +}; + +/// An in-memory consensus engine for integration tests. +/// +/// See the crate-level documentation for more information. +pub struct Engine { + /// Inner consensus state. + #[allow(dead_code)] // XXX(kate) + state: State, +} + +/// Consensus state used by the [`Engine`] to generate [`Block`s][tendermint::Block]. +// +// TODO(kate): make this `pub(crate)`. use Engine as the public layer for the caller. +#[allow(dead_code)] // XXX(kate) +pub struct State { + /// The chain identifier. + chain_id: chain::Id, + /// The initial [`block::Height`]. + initial_height: block::Height, + /// Metadata regarding the last block generated. + last_block: Option, + /// The set of validators. + validators: Validators, + /// The latest app hash. + app_hash: AppHash, + /// The merkle root of the result of executing the previous block. + last_results_hash: Option, + // TODO(kate): handle consensus parameters later. + // consensus_params: (), + // last_height_consensus_params_changed: block::Height, +} + +/// Consensus [`State`] metadata regarding the last block generated. +#[allow(dead_code)] // XXX(kate) +pub struct LastBlock { + height: block::Height, + id: block::Id, + time: Time, +} + +/// The initial conditions for the consensus [`Engine`]. +pub struct Genesis { + /// The chain identifier. + chain_id: chain::Id, + /// The timestamp for the initial block. + genesis_time: Time, + /// The initial [`block::Height`]. + initial_height: block::Height, + /// The set of validators. + validators: Validators, + /// The initial app hash. + app_hash: AppHash, + // TODO(kate): handle consensus parameters later. + // - app_state: serde_json::Value, + // - consensus_params: (), +} + +// === impl Engine === + +impl Engine { + /// Returns a new [`Engine`]. + // + // TODO(kate): for now, use a default `Genesis` structure. add a constructor receiving a + // `State` at some point later. + pub fn new() -> Self { + Self { + state: Genesis::default().into_state(), + } + } + + pub fn next_block(&self) -> tendermint::Block { + self.state.next_block() + } +} + +// === impl State === + +impl State { + pub(crate) fn next_block(&self) -> tendermint::Block { + let State { + chain_id, + initial_height, + last_block, + validators, + app_hash, + last_results_hash, + } = self; + + use tendermint::block::header::Header; + use tendermint::Hash; + let header = Header {}; + + todo!("State::next_block"); + } +} + +// === impl Genesis === + +impl Default for Genesis { + fn default() -> Self { + let app_hash: AppHash = b"placeholder-app-hash" + .to_vec() + .try_into() + .expect("infallible"); + Self { + genesis_time: Time::unix_epoch(), + chain_id: chain::Id::try_from("penumbra-cometstub").unwrap(), + initial_height: 0_u32.into(), + validators: Validators::default(), + app_hash, + } + } +} + +impl Genesis { + /// Consumes this genesis information and returns a new [`State`]. + // + // TODO(kate): what do we need at the start? avoid recreating a whole genesis file. + // use a single validator key, no arbitrary genesis height, simple. + // + // TODO(kate): perhaps this should also *generate* the first block. makes the state object + // much nicer to work with going forward. + pub fn into_state(self) -> State { + let Self { + genesis_time: _, // XXX(kate): where do we put this? `next_block` + chain_id, + initial_height, + validators, + app_hash, + } = self; + + State { + chain_id, + initial_height, + last_block: None, + validators, + app_hash, + last_results_hash: None, + } + } + + pub fn with_chain_id(self, chain_id: chain::Id) -> Self { + Self { chain_id, ..self } + } + + pub fn with_genesis_time(self, genesis_time: Time) -> Self { + Self { + genesis_time, + ..self + } + } + + pub fn with_initial_height(self, initial_height: block::Height) -> Self { + Self { + initial_height, + ..self + } + } + + pub fn with_validators(self, validators: Validators) -> Self { + Self { validators, ..self } + } + + pub fn with_app_hash(self, app_hash: AppHash) -> Self { + Self { app_hash, ..self } + } +} diff --git a/crates/test/cometstub/src/state.rs b/crates/test/cometstub/src/state.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/test/cometstub/src/state.rs @@ -0,0 +1 @@ + diff --git a/crates/test/cometstub/src/validator.rs b/crates/test/cometstub/src/validator.rs new file mode 100644 index 0000000000..e1e52fb317 --- /dev/null +++ b/crates/test/cometstub/src/validator.rs @@ -0,0 +1,105 @@ +//! Validator-related facilities. +// +// TODO(kate): +// - `Into`, implementation for converting into a tendermint::..Validator +// - method for getting a key as a tendermint::..SigningKey + +use {rand_core::OsRng, tendermint::vote}; + +/// A validator set. +#[allow(dead_code)] // XXX(kate) +pub struct Validators { + current: Vec, + // TODO(kate): we may want these fields too. + // - next: Vec, + // - last: Vec, + // - last_height_validators_changed: block::Height, +} + +/// A single validator. +/// +/// This is a [`tendermint::abci::types::Validator`], but with signing keys held in-memory. +/// +/// See the [ABCI documentation](https://docs.tendermint.com/master/spec/abci/abci.html#validator) +/// for more information on validators. +#[allow(dead_code)] // XXX(kate) +pub struct Validator { + /// The validator's address (the first 20 bytes of `SHA256(public_key)`). + pub address: [u8; 20], + /// The voting power of the validator. + pub power: vote::Power, + /// The validator's (private) signing key. + signing_key: ed25519_consensus::SigningKey, +} + +// === impl Validators === + +impl Validators { + /// Returns a validator set consisting of a single [`Validator`]. + pub fn single() -> Self { + let power = 100_u32.into(); + Self { + current: vec![Validator::new(power)], + } + } + + /// Returns a new [`Validators`] using the provided collection of validators. + pub fn new(validators: Vec) -> Self { + Self { + current: validators, + } + } +} + +/// A single in-memory [`Validator`] is used by default. +impl Default for Validators { + fn default() -> Self { + Self::single() + } +} + +/// A validator set may be [`collect()`][Iterator::collect]ed from an iterator. +impl FromIterator for Validators { + fn from_iter>(iter: T) -> Self { + let current = iter.into_iter().collect(); + Self { current } + } +} + +// === impl Validator === + +impl Validator { + /// Returns a [`Validator`] with the designated voting power. + /// + /// NB: This will generate a [`ed25519_consensus::SigningKey`] for this validator. + pub fn new(power: vote::Power) -> Self { + let signing_key = ed25519_consensus::SigningKey::new(OsRng); + let address = Self::address_from_public_key(&signing_key); + Validator { + address, + power, + signing_key, + } + } + + // TODO(kate): tendermint-rs says addresses are the first 20 bytes of the sha256 hash of the + // public key. track down where this is described in the spec. + fn address_from_public_key(public_key: impl AsRef<[u8]>) -> [u8; 20] { + use sha2::{Digest as _, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(public_key); + let hash = hasher.finalize(); + hash[..20].try_into().expect("") + } +} + +// === unit tests === + +#[cfg(test)] +mod validator_tests { + use super::*; + #[test] + fn single_validator_can_be_created() { + let _ = Validators::single(); + } +} diff --git a/crates/test/cometstub/tests/common/mod.rs b/crates/test/cometstub/tests/common/mod.rs new file mode 100644 index 0000000000..4e3fb54b6e --- /dev/null +++ b/crates/test/cometstub/tests/common/mod.rs @@ -0,0 +1,32 @@ +//! Common facilities for the `penumbra-cometstub` test suite. + +use tendermint::Time; +use tendermint_light_client_verifier::{ + options::Options, + types::{TrustedBlockState, UntrustedBlockState}, + Verdict, Verifier, +}; + +// XXX(kate): is this actually useful? do we need this? +pub struct TestVerifier; + +impl Verifier for TestVerifier { + fn verify_update_header( + &self, + _untrusted: UntrustedBlockState<'_>, + _trusted: TrustedBlockState<'_>, + _options: &Options, + _now: Time, + ) -> Verdict { + todo!("::verify_update_header") + } + fn verify_misbehaviour_header( + &self, + _untrusted: UntrustedBlockState<'_>, + _trusted: TrustedBlockState<'_>, + _options: &Options, + _now: Time, + ) -> Verdict { + todo!("::verify_misbehaviour_header") + } +} diff --git a/crates/test/cometstub/tests/ibc_handshake.rs b/crates/test/cometstub/tests/ibc_handshake.rs new file mode 100644 index 0000000000..fe3495abb9 --- /dev/null +++ b/crates/test/cometstub/tests/ibc_handshake.rs @@ -0,0 +1,20 @@ +#![allow(unused)] // XXX(kate) + +mod common; + +use { + penumbra_cometstub::Engine, + tendermint_light_client_verifier::{ + options::Options, + types::{TrustedBlockState, UntrustedBlockState}, + Verdict, Verifier, + }, +}; + +#[test] +fn ibc_handshake_can_be_generated() { + let engine = Engine::new(); + let block = engine.next_block(); + + todo!("XXX(kate): fallthrough! nice work. 💕"); +} diff --git a/crates/test/cometstub/tests/spend_happy_path.rs b/crates/test/cometstub/tests/spend_happy_path.rs new file mode 100644 index 0000000000..bbe74c2d02 --- /dev/null +++ b/crates/test/cometstub/tests/spend_happy_path.rs @@ -0,0 +1,86 @@ +//! Reference test case. +//! +//! NB(kate): this is copied from `crates/core/app/src/tests/spend.rs`. it does not exercise the +//! cometstub crate, it is here to use as reference during implementation. if you are reading this +//! in `main`, please remove it. + +use { + cnidarium::{ArcStateDeltaExt, StateDelta, TempStorage}, + cnidarium_component::{ActionHandler as _, Component}, + penumbra_app::{MockClient, TempStorageExt}, + penumbra_chain::component::StateWriteExt, + penumbra_compact_block::component::CompactBlockManager, + penumbra_keys::test_keys, + penumbra_sct::component::SourceContext, + penumbra_shielded_pool::{component::ShieldedPool, SpendPlan}, + penumbra_txhash::{EffectHash, TransactionContext}, + rand_core::SeedableRng, + std::{ops::Deref, sync::Arc}, + tendermint::abci, +}; + +#[tokio::test] +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 mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + + let height = 1; + + // Precondition: This test uses the default genesis which has existing notes for the test keys. + let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone()); + let sk = test_keys::SPEND_KEY.clone(); + client.sync_to(0, state.deref()).await?; + let note = client.notes.values().next().unwrap().clone(); + let note_commitment = note.commit(); + let proof = client.sct.witness(note_commitment).unwrap(); + let root = client.sct.root(); + let tct_position = proof.position(); + + // 1. Simulate BeginBlock + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx.put_block_height(height); + state_tx.put_epoch_by_height( + height, + penumbra_chain::Epoch { + index: 0, + start_height: 0, + }, + ); + state_tx.apply(); + + // 2. Create a Spend action + let spend_plan = SpendPlan::new(&mut rng, note, tct_position); + let dummy_effect_hash = [0u8; 64]; + let rsk = sk.spend_auth_key().randomize(&spend_plan.randomizer); + let auth_sig = rsk.sign(&mut rng, dummy_effect_hash.as_ref()); + let spend = spend_plan.spend(&test_keys::FULL_VIEWING_KEY, auth_sig, proof, root); + let transaction_context = TransactionContext { + anchor: root, + effect_hash: EffectHash(dummy_effect_hash), + }; + + // 3. Simulate execution of the Spend action + spend.check_stateless(transaction_context).await?; + spend.check_stateful(state.clone()).await?; + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx.put_mock_source(1u8); + spend.execute(&mut state_tx).await?; + state_tx.apply(); + + // 4. Execute EndBlock + + let end_block = abci::request::EndBlock { + height: height.try_into().unwrap(), + }; + ShieldedPool::end_block(&mut state, &end_block).await; + + let mut state_tx = state.try_begin_transaction().unwrap(); + // ... and for the App, call `finish_block` to correctly write out the SCT with the data we'll use next. + state_tx.finish_block(false).await.unwrap(); + + state_tx.apply(); + + Ok(()) +}