Skip to content

Commit

Permalink
tests(app): 🧪 test that uptime tracks signed blocks (#4070)
Browse files Browse the repository at this point in the history
**NB:** this is based on #4099.

this adds test coverage, complementary to the work in #4061, which
asserts that we properly track the _affirmative_ case of validators
signing blocks.

fixes #4040. see #3995.
  • Loading branch information
cratelyn authored Mar 26, 2024
1 parent 8f55a18 commit c7c0223
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 17 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use {
component::validator_handler::validator_store::ValidatorDataRead, validator::Validator,
},
tap::Tap,
tracing::{error_span, info, Instrument},
tracing::{error_span, info, trace, Instrument},
};

#[tokio::test]
Expand Down Expand Up @@ -60,10 +60,15 @@ async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Res

// Jump ahead a few blocks.
let height = 4;
node.fast_forward(height)
.instrument(error_span!("fast forwarding test node {height} blocks"))
.await
.context("fast forwarding {height} blocks")?;
for i in 1..=height {
node.block()
.with_signatures(Default::default())
.execute()
.tap(|_| trace!(%i, "executing block with no signatures"))
.instrument(error_span!("executing block with no signatures", %i))
.await
.context("executing block with no signatures")?;
}

// Check the validator's uptime once more. We should have uptime data up to the fourth block,
// and the validator should have missed all of the blocks between genesis and now.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
mod common;

use {
self::common::BuilderExt,
anyhow::Context,
cnidarium::TempStorage,
penumbra_app::server::consensus::Consensus,
penumbra_genesis::AppState,
penumbra_mock_consensus::TestNode,
penumbra_stake::{
component::validator_handler::validator_store::ValidatorDataRead, validator::Validator,
},
tap::Tap,
tracing::{error_span, info, Instrument},
};

#[tokio::test]
async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Result<()> {
// Install a test logger, acquire some temporary storage, and start the test node.
let guard = common::set_tracing_subscriber();
let storage = TempStorage::new().await?;

// Start the test node.
let mut node = {
let app_state = AppState::default();
let consensus = Consensus::new(storage.as_ref().clone());
TestNode::builder()
.single_validator()
.with_penumbra_auto_app_state(app_state)?
.init_chain(consensus)
.await
}?;

// Retrieve the validator definition from the latest snapshot.
let Validator { identity_key, .. } = match storage
.latest_snapshot()
.validator_definitions()
.tap(|_| info!("getting validator definitions"))
.await?
.as_slice()
{
[v] => v.clone(),
unexpected => panic!("there should be one validator, got: {unexpected:?}"),
};
let get_uptime = || async {
storage
.latest_snapshot()
.get_validator_uptime(&identity_key)
.await
.expect("should be able to get a validator uptime")
.expect("validator uptime should exist")
};

// Jump ahead a few blocks.
// TODO TODO TODO have the validator sign blocks here.
let height = 4;
node.fast_forward(height)
.instrument(error_span!("fast forwarding test node {height} blocks"))
.await
.context("fast forwarding {height} blocks")?;

// Check the validator's uptime once more. We should have uptime data up to the fourth block,
// and the validator should have missed all of the blocks between genesis and now.
{
let uptime = get_uptime().await;
assert_eq!(uptime.as_of_height(), height);
assert_eq!(
uptime.num_missed_blocks(),
0,
"validator should have signed the last {height} blocks"
);
}

Ok(())
.tap(|_| drop(node))
.tap(|_| drop(storage))
.tap(|_| drop(guard))
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<(
);
assert_eq!(
existing.num_missed_blocks(),
(EPOCH_DURATION - 1) as usize,
"genesis validator has missed all blocks in the previous epoch"
0,
"genesis validator has signed all blocks in the previous epoch"
);
}

Expand Down
1 change: 1 addition & 0 deletions crates/test/mock-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ anyhow = { workspace = true }
bytes = { workspace = true }
ed25519-consensus = { workspace = true }
rand_core = { workspace = true }
sha2 = { workspace = true }
tap = { workspace = true }
tendermint = { workspace = true }
tower = { workspace = true, features = ["full"] }
Expand Down
8 changes: 3 additions & 5 deletions crates/test/mock-consensus/src/abci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use {
tap::{Tap, TapFallible},
tendermint::{
abci::types::CommitInfo,
block::{Header, Round},
block::Header,
v0_37::abci::{request, response, ConsensusRequest, ConsensusResponse},
},
tower::{BoxError, Service},
Expand Down Expand Up @@ -41,14 +41,12 @@ where
pub async fn begin_block(
&mut self,
header: Header,
last_commit_info: CommitInfo,
) -> Result<response::BeginBlock, anyhow::Error> {
let request = ConsensusRequest::BeginBlock(request::BeginBlock {
hash: tendermint::Hash::None,
header,
last_commit_info: CommitInfo {
round: Round::from(1_u8),
votes: Default::default(),
},
last_commit_info,
byzantine_validators: Default::default(),
});
let service = self.service().await?;
Expand Down
24 changes: 19 additions & 5 deletions crates/test/mock-consensus/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
//!
//! Builders are acquired by calling [`TestNode::block()`].
/// Interfaces for generating commit signatures.
mod signature;

use {
crate::TestNode,
tap::Tap,
Expand All @@ -28,15 +31,22 @@ pub struct Builder<'e, C> {

/// Evidence of malfeasance.
evidence: evidence::List,

/// The list of signatures.
signatures: Vec<block::CommitSig>,
}

// === impl TestNode ===

impl<C> TestNode<C> {
/// Returns a new [`Builder`].
pub fn block<'e>(&'e mut self) -> Builder<'e, C> {
let signatures = self.generate_signatures().collect();
Builder {
test_node: self,
data: Default::default(),
evidence: Default::default(),
signatures,
}
}
}
Expand All @@ -60,8 +70,10 @@ impl<'e, C> Builder<'e, C> {
Self { evidence, ..self }
}

// TODO(kate): add more `with_` setters for fields in the header.
// TODO(kate): set some fields using state in the test node.
/// Sets the [`CommitSig`][block::CommitSig] commit signatures for this block.
pub fn with_signatures(self, signatures: Vec<block::CommitSig>) -> Self {
Self { signatures, ..self }
}
}

impl<'e, C> Builder<'e, C>
Expand All @@ -84,16 +96,17 @@ where
header,
data,
evidence: _,
last_commit: _,
last_commit,
..
} = block.tap(|block| {
tracing::span::Span::current()
.record("height", block.header.height.value())
.record("time", block.header.time.unix_timestamp());
});
let last_commit_info = Self::last_commit_info(last_commit);

trace!("sending block");
test_node.begin_block(header).await?;
test_node.begin_block(header, last_commit_info).await?;
for tx in data {
let tx = tx.into();
test_node.deliver_tx(tx).await?;
Expand All @@ -117,6 +130,7 @@ where
data,
evidence,
test_node,
signatures,
} = self;

let height = {
Expand All @@ -135,7 +149,7 @@ where
height,
round: Round::default(),
block_id,
signatures: Vec::default(),
signatures,
})
} else {
None // The first block has no previous commit to speak of.
Expand Down
124 changes: 124 additions & 0 deletions crates/test/mock-consensus/src/block/signature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use {
super::Builder,
crate::TestNode,
sha2::{Digest, Sha256},
tendermint::{
abci::types::{BlockSignatureInfo, CommitInfo, VoteInfo},
account,
block::{BlockIdFlag, Commit, CommitSig, Round},
vote::Power,
},
};

/// Helper functions for generating [commit signatures].
mod sign {
use tendermint::{account::Id, block::CommitSig, time::Time};

/// Returns a [commit signature] saying this validator voted for the block.
///
/// [commit signature]: CommitSig
pub(super) fn commit(validator_address: Id) -> CommitSig {
CommitSig::BlockIdFlagCommit {
validator_address,
timestamp: timestamp(),
signature: None,
}
}

/// Returns a [commit signature] saying this validator voted nil.
///
/// [commit signature]: CommitSig
#[allow(dead_code)]
pub(super) fn nil(validator_address: Id) -> CommitSig {
CommitSig::BlockIdFlagNil {
validator_address,
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 ===

impl<C> TestNode<C> {
// TODO(kate): other interfaces may be helpful to add in the future, and these may eventually
// warrant being made `pub`. we defer doing so for now, only defining what is needed to provide
// 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<Item = CommitSig> + '_ {
self.keyring
.iter()
// Compute the address of this validator.
.map(|(vk, _)| -> [u8; 20] {
<Sha256 as Digest>::digest(vk).as_slice()[0..20]
.try_into()
.expect("")
})
.map(account::Id::new)
.map(self::sign::commit)
}
}

// === impl Builder ===

impl<'e, C: 'e> Builder<'e, C> {
/// Returns [`CommitInfo`] given a block's [`Commit`].
pub(super) fn last_commit_info(last_commit: Option<Commit>) -> CommitInfo {
let Some(Commit {
round, signatures, ..
}) = last_commit
else {
// If there is no commit information about the last block, return an empty object.
return CommitInfo {
round: Round::default(),
votes: Vec::default(),
};
};

CommitInfo {
round,
votes: signatures
.into_iter()
.map(Self::vote)
.filter_map(|v| v)
.collect(),
}
}

/// Returns a [`VoteInfo`] for this [`CommitSig`].
///
/// If no validator voted, returns [`None`].
fn vote(commit_sig: CommitSig) -> Option<VoteInfo> {
use tendermint::abci::types::Validator;

// TODO(kate): upstream this into the `tendermint` library.
let sig_info = BlockSignatureInfo::Flag(match commit_sig {
CommitSig::BlockIdFlagAbsent => BlockIdFlag::Absent,
CommitSig::BlockIdFlagCommit { .. } => BlockIdFlag::Commit,
CommitSig::BlockIdFlagNil { .. } => BlockIdFlag::Nil,
});

let address: [u8; 20] = commit_sig
.validator_address()?
// TODO(kate): upstream an accessor to retrieve this as the [u8; 20] that it is.
.as_bytes()
.try_into()
.expect("validator address should be 20 bytes");
let power = Power::from(1_u8); // TODO(kate): for now, hard-code voting power to 1.
let validator = Validator { address, power };

Some(VoteInfo {
validator,
sig_info,
})
}
}

0 comments on commit c7c0223

Please sign in to comment.