diff --git a/Cargo.lock b/Cargo.lock index b249ce91d8..6668673740 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5179,10 +5179,12 @@ dependencies = [ name = "penumbra-mock-tendermint-proxy" version = "0.80.0" dependencies = [ + "pbjson-types", "penumbra-mock-consensus", "penumbra-proto", "tap", "tendermint", + "tendermint-proto", "tonic", "tracing", ] diff --git a/crates/core/app/tests/app_blocktimes_increment.rs b/crates/core/app/tests/app_blocktimes_increment.rs new file mode 100644 index 0000000000..6125e70064 --- /dev/null +++ b/crates/core/app/tests/app_blocktimes_increment.rs @@ -0,0 +1,99 @@ +use { + self::common::BuilderExt, + cnidarium::TempStorage, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, + penumbra_mock_consensus::TestNode, + std::time::Duration, + tap::{Tap, TapFallible}, +}; + +mod common; + +/// This is more of a test of test code; this ensures +/// that the mock tendermint's block times are set and increment as expected. +#[tokio::test] +async fn mock_tendermint_block_times_correct() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + + // Fixed start time: + let start_time = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?; + + // Define our application state, and start the test node. + let mut test_node = { + let app_state = AppState::Content( + genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()), + ); + let consensus = Consensus::new(storage.as_ref().clone()); + // This should use the default time callback of 5s + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .with_initial_timestamp(start_time) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + // The test node's time should be the initial timestamp before any blocks are committed + assert_eq!(*test_node.timestamp(), start_time); + + // Test a handful of block executions + for i in 0..10 { + // Execute a block on the test node + test_node.block().execute().await?; + + // Ensure the time has incremented by 5 seconds + assert_eq!( + *test_node.timestamp(), + start_time + .checked_add(Duration::from_secs(5 * (i + 1))) + .unwrap() + ); + } + + // Now do it with a different duration. + let block_duration = Duration::from_secs(13); + let storage = TempStorage::new().await?; + let mut test_node = { + let app_state = AppState::Content( + genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string()), + ); + let consensus = Consensus::new(storage.as_ref().clone()); + // This should use the default time callback of 5s + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .with_initial_timestamp(start_time) + // Set a callback to add 13 seconds instead + .ts_callback(move |t| t.checked_add(block_duration).unwrap()) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + // The test node's time should be the initial timestamp before any blocks are committed + assert_eq!(*test_node.timestamp(), start_time); + + // Test a handful of block executions + for i in 0..10 { + // Execute a block on the test node + test_node.block().execute().await?; + + // Ensure the time has incremented by 5 seconds + assert_eq!( + *test_node.timestamp(), + start_time.checked_add(block_duration * (i + 1)).unwrap() + ); + } + + // Free our temporary storage. + Ok(()) + .tap(|_| drop(test_node)) + .tap(|_| drop(storage)) + .tap(|_| drop(guard)) +} diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index b9a116d23e..af75efeda0 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -10,7 +10,7 @@ use { block::{self, header::Version, Block, Commit, Header, Round}, chain, evidence, v0_37::abci::{ConsensusRequest, ConsensusResponse}, - AppHash, Hash, + AppHash, Hash, Time, }, tower::{BoxError, Service}, tracing::{instrument, trace}, @@ -36,6 +36,8 @@ pub struct Builder<'e, C> { evidence: evidence::List, /// The list of signatures. signatures: Vec, + /// The timestamp of the block. + timestamp: Time, } // === impl TestNode === @@ -47,12 +49,15 @@ impl TestNode { /// 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, } } } @@ -133,6 +138,9 @@ where // 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(()) } @@ -149,6 +157,7 @@ where evidence, test_node, signatures, + timestamp, } = self; let height = { @@ -177,7 +186,7 @@ where version: Version { block: 1, app: 1 }, chain_id: chain::Id::try_from("test".to_owned())?, height, - time: tendermint::Time::now(), + time: timestamp, last_block_id: None, last_commit_hash: None, data_hash: None, diff --git a/crates/test/mock-consensus/src/block/signature.rs b/crates/test/mock-consensus/src/block/signature.rs index 249e91e629..f3195a57be 100644 --- a/crates/test/mock-consensus/src/block/signature.rs +++ b/crates/test/mock-consensus/src/block/signature.rs @@ -17,10 +17,10 @@ mod sign { /// Returns a [commit signature] saying this validator voted for the block. /// /// [commit signature]: CommitSig - pub(super) fn commit(validator_address: Id) -> CommitSig { + pub(super) fn commit(validator_address: Id, timestamp: Time) -> CommitSig { CommitSig::BlockIdFlagCommit { validator_address, - timestamp: timestamp(), + timestamp, signature: None, } } @@ -29,21 +29,13 @@ mod sign { /// /// [commit signature]: CommitSig #[allow(dead_code)] - pub(super) fn nil(validator_address: Id) -> CommitSig { + pub(super) fn nil(validator_address: Id, timestamp: Time) -> CommitSig { CommitSig::BlockIdFlagNil { validator_address, - timestamp: timestamp(), + timestamp, signature: None, } } - - /// Generates a new timestamp, marked at the current time. - // - // TODO(kate): see https://github.com/penumbra-zone/penumbra/issues/3759, re: timestamps. - // eventually, we will add hooks so that we can control these timestamps. - fn timestamp() -> Time { - Time::now() - } } // === impl TestNode === @@ -58,12 +50,14 @@ impl TestNode { self.keyring .keys() .map(|vk| { - ::digest(vk).as_slice()[0..20] - .try_into() - .expect("") + ( + ::digest(vk).as_slice()[0..20] + .try_into() + .expect(""), + self.timestamp.clone(), + ) }) - .map(account::Id::new) - .map(self::sign::commit) + .map(|(a, b)| (self::sign::commit(account::Id::new(a), b))) } } diff --git a/crates/test/mock-consensus/src/builder.rs b/crates/test/mock-consensus/src/builder.rs index dd4b19e5cb..8afd9fedef 100644 --- a/crates/test/mock-consensus/src/builder.rs +++ b/crates/test/mock-consensus/src/builder.rs @@ -6,16 +6,26 @@ mod init_chain; use { - crate::{Keyring, OnBlockFn, TestNode}, + crate::{Keyring, OnBlockFn, TestNode, TsCallbackFn}, bytes::Bytes, + std::time::Duration, + tendermint::Time, }; +// Default timestamp callback will increment the time by 5 seconds. +// can't be const :( +fn default_ts_callback(t: Time) -> Time { + t.checked_add(Duration::from_secs(5)).unwrap() +} + /// A builder, used to prepare and instantiate a new [`TestNode`]. #[derive(Default)] pub struct Builder { pub app_state: Option, pub keyring: Keyring, pub on_block: Option, + pub ts_callback: Option, + pub initial_timestamp: Option