penumbra_sdk_mock_consensus/lib.rs
1//! `penumbra-mock-consensus` is a library for testing consensus-driven ABCI applications.
2//!
3//! # Overview
4//!
5//! This library provides facilities that can act as a stand-in for consensus engines like
6//! [CometBFT][cometbft] or [Tendermint][tendermint] in integration tests.
7//!
8//! Testing applications using a mock consensus engine has many benefits. For example, this allows
9//! integration test cases to run as fast as possible, without needing wait real wall-clock time
10//! for blocks to be generated, or for integration test cases to exercise slashing logic related to
11//! byzantine misbehavior (_e.g., double-signing_).
12//!
13//! This library is agnostic with respect to the replicable state transition machine that it
14//! is used to test. This means that, while it may be used to write integration tests for the
15//! [Penumbra][penumbra] network, it can also be used to test other decentralized applications.
16//!
17//! See [`TestNode`] for more information about using `penumbra-mock-consensus`.
18//!
19//! # Alternatives
20//!
21//! Projects implemented in Go may wish to consider using [CometMock][cometmock].
22//! `penumbra-mock-consensus` is primarily oriented towards projects implemented in Rust that wish
23//! to use [`cargo test`][cargo-test] or [`cargo nextest`][cargo-nextest] as a test-runner.
24//!
25//! [cargo-nextest]: https://nexte.st/
26//! [cargo-test]: https://doc.rust-lang.org/cargo/commands/cargo-test.html
27//! [cometbft]: https://github.com/cometbft/cometbft
28//! [cometmock]: https://github.com/informalsystems/CometMock
29//! [penumbra]: https://github.com/penumbra-zone/penumbra
30//! [tendermint]: https://github.com/tendermint/tendermint
31
32use {
33 ed25519_consensus::{SigningKey, VerificationKey},
34 std::collections::BTreeMap,
35 tendermint::{
36 block::{Commit, Height},
37 Time,
38 },
39};
40
41pub mod block;
42pub mod builder;
43
44mod abci;
45
46/// A test node.
47///
48/// A [`TestNode<C>`] represents a validator node containing an instance of the state transition
49/// machine and its accompanying consensus engine.
50///
51/// # Initialization
52///
53/// Construct a new test node by calling [`TestNode::builder()`]. The [`builder::Builder`]
54/// returned by that method can be used to set the initial application state, and configure
55/// validators' consensus keys that should be present at genesis. Use
56/// [`builder::Builder::init_chain()`] to consume the builder and initialize the application.
57///
58/// # Consensus Service
59///
60/// A test node is generic in terms of a consensus service `C`. This service should implement
61/// [`tower::Service`], accepting [`ConsensusRequest`][consensus-request]s, and returning
62/// [`ConsensusResponse`][consensus-response]s.
63///
64/// For [`tower-abci`][tower-abci] users, this should correspond with the `C` parameter of the
65/// [`Server`][tower-abci-server] type.
66///
67/// # Blocks
68///
69/// Blocks can be executed by using [`TestNode::block()`]. This can be used to add transactions,
70/// signatures, and evidence to a [`Block`][tendermint-rs-block], before invoking
71/// [`block::Builder::execute()`] to execute the next block.
72///
73/// [consensus-request]: tendermint::v0_37::abci::ConsensusRequest
74/// [consensus-response]: tendermint::v0_37::abci::ConsensusResponse
75/// [tendermint-rs-block]: tendermint::block::Block
76/// [tower-abci-server]: https://docs.rs/tower-abci/latest/tower_abci/v037/struct.Server.html#
77/// [tower-abci]: https://docs.rs/tower-abci/latest/tower_abci
78pub struct TestNode<C> {
79 /// The inner consensus service being tested.
80 consensus: C,
81 /// The last `app_hash` value.
82 last_app_hash: Vec<u8>,
83 /// The last validator set hash value.
84 last_validator_set_hash: Option<tendermint::Hash>,
85 /// The last tendermint block header commit value.
86 last_commit: Option<tendermint::block::Commit>,
87 /// The consensus params hash.
88 consensus_params_hash: Vec<u8>,
89 /// The current block [`Height`][tendermint::block::Height].
90 height: tendermint::block::Height,
91 /// Validators' consensus keys.
92 ///
93 /// Entries in this keyring consist of a [`VerificationKey`] and a [`SigningKey`].
94 keyring: Keyring,
95 /// A callback that will be invoked when a new block is constructed.
96 on_block: Option<OnBlockFn>,
97 /// A callback that will be invoked when a new block is committed, to produce the next timestamp.
98 ts_callback: TsCallbackFn,
99 /// The current timestamp of the node.
100 timestamp: Time,
101 /// The chain ID.
102 chain_id: tendermint::chain::Id,
103}
104
105/// A type alias for the `TestNode::on_block` callback.
106pub type OnBlockFn = Box<dyn FnMut(tendermint::Block) + Send + Sync + 'static>;
107
108/// A type alias for the `TestNode::ts_callback` callback.
109pub type TsCallbackFn = Box<dyn Fn(Time) -> Time + Send + Sync + 'static>;
110
111/// An ordered map of consensus keys.
112///
113/// Entries in this keyring consist of a [`VerificationKey`] and a [`SigningKey`].
114type Keyring = BTreeMap<VerificationKey, SigningKey>;
115
116/// Accessors.
117impl<C> TestNode<C> {
118 /// A chain ID for use in tests.
119 pub const CHAIN_ID: &'static str = "penumbra-test-chain";
120
121 /// Returns the last `app_hash` value, represented as a slice of bytes.
122 pub fn last_app_hash(&self) -> &[u8] {
123 &self.last_app_hash
124 }
125
126 /// Returns the last `commit` value.
127 pub fn last_commit(&self) -> Option<&Commit> {
128 self.last_commit.as_ref()
129 }
130
131 /// Returns the last `validator_set_hash` value.
132 pub fn last_validator_set_hash(&self) -> Option<&tendermint::Hash> {
133 self.last_validator_set_hash.as_ref()
134 }
135
136 /// Returns the most recent `timestamp` value.
137 pub fn timestamp(&self) -> &Time {
138 &self.timestamp
139 }
140
141 /// Returns the last `app_hash` value, represented as a hexadecimal string.
142 pub fn last_app_hash_hex(&self) -> String {
143 // Use upper-case hexadecimal integers, include leading zeroes.
144 // - https://doc.rust-lang.org/std/fmt/#formatting-traits
145 self.last_app_hash
146 .iter()
147 .map(|b| format!("{:02X}", b).to_string())
148 .collect::<Vec<String>>()
149 .join("")
150 }
151
152 /// Returns a reference to the test node's set of consensus keys.
153 pub fn keyring(&self) -> &Keyring {
154 &self.keyring
155 }
156
157 /// Returns a mutable reference to the test node's set of consensus keys.
158 pub fn keyring_mut(&mut self) -> &mut Keyring {
159 &mut self.keyring
160 }
161
162 pub fn height(&self) -> &Height {
163 &self.height
164 }
165}
166
167/// Fast forward interfaces.
168impl<C> TestNode<C>
169where
170 C: tower::Service<
171 tendermint::v0_37::abci::ConsensusRequest,
172 Response = tendermint::v0_37::abci::ConsensusResponse,
173 Error = tower::BoxError,
174 > + Send
175 + Clone
176 + 'static,
177 C::Future: Send + 'static,
178 C::Error: Sized,
179{
180 /// Fast forwards the given number of blocks.
181 #[tracing::instrument(
182 skip(self),
183 fields(fast_forward.blocks = %blocks)
184 )]
185 pub async fn fast_forward(&mut self, blocks: u64) -> anyhow::Result<()> {
186 use {
187 tap::Tap,
188 tracing::{info, trace, trace_span, Instrument},
189 };
190
191 for i in 1..=blocks {
192 self.block()
193 .execute()
194 .tap(|_| trace!(%i, "executing empty block"))
195 .instrument(trace_span!("executing empty block", %i))
196 .await
197 .tap(|_| trace!(%i, "finished executing empty block"))?;
198 }
199
200 Ok(()).tap(|_| info!("finished fast forward"))
201 }
202}
203
204/// Assert that a [`TestNode`] is both [`Send`] and [`Sync`].
205#[allow(dead_code)]
206mod assert_address_is_send_and_sync {
207 fn is_send<T: Send>() {}
208 fn is_sync<T: Sync>() {}
209 fn f() {
210 is_send::<super::TestNode<()>>();
211 is_sync::<super::TestNode<()>>();
212 }
213}