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..084f2455df --- /dev/null +++ b/crates/test/cometstub/src/lib.rs @@ -0,0 +1,186 @@ +//! `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. + +mod abci; +mod header; +mod state; + +use { + rand_core::OsRng, + tendermint::{block, chain, vote, 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]. +#[allow(dead_code)] // XXX(kate) +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) +struct LastBlock { + height: block::Height, + id: block::Id, + time: Time, +} + +/// Consensus [`State`] fields regarding for the 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 validator address with voting power. +/// +/// This is a [`tendermint::abci::types::Validator`], but with signing keys held in-memory. +/// +/// [ABCI documentation](https://docs.tendermint.com/master/spec/abci/abci.html#validator) +#[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 Engine === + +impl Engine { + /// Returns a new [`Engine`]. + pub fn new() -> Self { + Self { + state: State::new(), + } + } +} + +// === impl State === + +impl State { + /// 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. + pub fn new() -> Self { + let app_hash: AppHash = b"placeholder-app-hash" + .to_vec() + .try_into() + .expect("infallible"); + + let validators = { + /// A hard-coded voting power assignment for our mock validator. + // TODO(kate): upstream a constant `Power` constructor, if possible. + const POWER: u32 = 100; + let v = Validator::new(POWER.into()); + Validators { current: vec![v] } + }; + + State { +chain_id: chain::Id::try_from("penumbra-cometstub").unwrap(), +initial_height: 0_u32.into(), + last_block: None, + validators, + app_hash, + last_results_hash: None, + } + } +} + +// === impl Validators === + +impl Validators { + /// Returns a validator containing a single [`Validator`]. + pub fn single() -> Self { + let power = 100_u32.into(); + Self { + current: vec![ + Validator::new(power), + ], + } + } + + pub fn new(validators: Vec) -> Self { + Self { + current: validators, + } + } +} + +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("") + } +} + +// TODO(kate): `Into`, implementation for converting this into a tendermint::..Validator, and +// for getting its key as a tendermint::..SigningKey. write those when needed. + +// === 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/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/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..5571a9898c --- /dev/null +++ b/crates/test/cometstub/tests/ibc_handshake.rs @@ -0,0 +1,19 @@ +#![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(); + + 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(()) +}