diff --git a/node/actors/bft/src/leader/tests.rs b/node/actors/bft/src/leader/tests.rs index 3c068336..e6453384 100644 --- a/node/actors/bft/src/leader/tests.rs +++ b/node/actors/bft/src/leader/tests.rs @@ -564,7 +564,7 @@ async fn replica_commit_bad_chain() { assert_matches!( res, Err(replica_commit::Error::InvalidMessage( - validator::ReplicaCommitVerifyError::View(_) + validator::ReplicaCommitVerifyError::BadView(_) )) ); Ok(()) diff --git a/node/actors/bft/src/replica/tests.rs b/node/actors/bft/src/replica/tests.rs index 10d7407d..afb2a8aa 100644 --- a/node/actors/bft/src/replica/tests.rs +++ b/node/actors/bft/src/replica/tests.rs @@ -550,7 +550,7 @@ async fn leader_commit_bad_chain() { res, Err(leader_commit::Error::InvalidMessage( validator::CommitQCVerifyError::InvalidMessage( - validator::ReplicaCommitVerifyError::View(_) + validator::ReplicaCommitVerifyError::BadView(_) ) )) ); diff --git a/node/libs/crypto/src/keccak256/mod.rs b/node/libs/crypto/src/keccak256/mod.rs index 1c94c243..ce7bd753 100644 --- a/node/libs/crypto/src/keccak256/mod.rs +++ b/node/libs/crypto/src/keccak256/mod.rs @@ -7,7 +7,7 @@ mod test; pub mod testonly; /// Keccak256 hash. -#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Keccak256(pub(crate) [u8; 32]); impl Keccak256 { diff --git a/node/libs/roles/src/validator/keys/mod.rs b/node/libs/roles/src/validator/keys/mod.rs index f8d18c94..a4816015 100644 --- a/node/libs/roles/src/validator/keys/mod.rs +++ b/node/libs/roles/src/validator/keys/mod.rs @@ -4,6 +4,8 @@ mod aggregate_signature; mod public_key; mod secret_key; mod signature; +#[cfg(test)] +mod tests; pub use aggregate_signature::AggregateSignature; pub use public_key::PublicKey; diff --git a/node/libs/roles/src/validator/keys/signature.rs b/node/libs/roles/src/validator/keys/signature.rs index 76e3419c..d5ed9583 100644 --- a/node/libs/roles/src/validator/keys/signature.rs +++ b/node/libs/roles/src/validator/keys/signature.rs @@ -19,14 +19,9 @@ impl Signature { } } -/// Proof of possession of a validator secret key. -#[derive(Clone, PartialEq, Eq)] -pub struct ProofOfPossession(pub(crate) bls12_381::ProofOfPossession); - -impl ProofOfPossession { - /// Verifies the proof against the public key. - pub fn verify(&self, pk: &PublicKey) -> anyhow::Result<()> { - self.0.verify(&pk.0) +impl fmt::Debug for Signature { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(&TextFmt::encode(self)) } } @@ -39,15 +34,6 @@ impl ByteFmt for Signature { } } -impl ByteFmt for ProofOfPossession { - fn encode(&self) -> Vec { - ByteFmt::encode(&self.0) - } - fn decode(bytes: &[u8]) -> anyhow::Result { - ByteFmt::decode(bytes).map(Self) - } -} - impl TextFmt for Signature { fn encode(&self) -> String { format!( @@ -62,6 +48,32 @@ impl TextFmt for Signature { } } +/// Proof of possession of a validator secret key. +#[derive(Clone, PartialEq, Eq)] +pub struct ProofOfPossession(pub(crate) bls12_381::ProofOfPossession); + +impl ProofOfPossession { + /// Verifies the proof against the public key. + pub fn verify(&self, pk: &PublicKey) -> anyhow::Result<()> { + self.0.verify(&pk.0) + } +} + +impl fmt::Debug for ProofOfPossession { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(&TextFmt::encode(self)) + } +} + +impl ByteFmt for ProofOfPossession { + fn encode(&self) -> Vec { + ByteFmt::encode(&self.0) + } + fn decode(bytes: &[u8]) -> anyhow::Result { + ByteFmt::decode(bytes).map(Self) + } +} + impl TextFmt for ProofOfPossession { fn encode(&self) -> String { format!( @@ -75,15 +87,3 @@ impl TextFmt for ProofOfPossession { .map(Self) } } - -impl fmt::Debug for ProofOfPossession { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.write_str(&TextFmt::encode(self)) - } -} - -impl fmt::Debug for Signature { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.write_str(&TextFmt::encode(self)) - } -} diff --git a/node/libs/roles/src/validator/keys/tests.rs b/node/libs/roles/src/validator/keys/tests.rs new file mode 100644 index 00000000..b52e8778 --- /dev/null +++ b/node/libs/roles/src/validator/keys/tests.rs @@ -0,0 +1,60 @@ +use super::*; +use crate::validator::MsgHash; +use rand::Rng as _; +use std::vec; +use zksync_concurrency::ctx; + +#[test] +fn test_signature_verify() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let msg1: MsgHash = rng.gen(); + let msg2: MsgHash = rng.gen(); + + let key1: SecretKey = rng.gen(); + let key2: SecretKey = rng.gen(); + + let sig1 = key1.sign_hash(&msg1); + + // Matching key and message. + sig1.verify_hash(&msg1, &key1.public()).unwrap(); + + // Mismatching message. + assert!(sig1.verify_hash(&msg2, &key1.public()).is_err()); + + // Mismatching key. + assert!(sig1.verify_hash(&msg1, &key2.public()).is_err()); +} + +#[test] +fn test_agg_signature_verify() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let msg1: MsgHash = rng.gen(); + let msg2: MsgHash = rng.gen(); + + let key1: SecretKey = rng.gen(); + let key2: SecretKey = rng.gen(); + + let sig1 = key1.sign_hash(&msg1); + let sig2 = key2.sign_hash(&msg2); + + let agg_sig = AggregateSignature::aggregate(vec![&sig1, &sig2]); + + // Matching key and message. + agg_sig + .verify_hash([(msg1, &key1.public()), (msg2, &key2.public())].into_iter()) + .unwrap(); + + // Mismatching message. + assert!(agg_sig + .verify_hash([(msg2, &key1.public()), (msg1, &key2.public())].into_iter()) + .is_err()); + + // Mismatching key. + assert!(agg_sig + .verify_hash([(msg1, &key2.public()), (msg2, &key1.public())].into_iter()) + .is_err()); +} diff --git a/node/libs/roles/src/validator/messages/block.rs b/node/libs/roles/src/validator/messages/block.rs index cf107070..7db2628b 100644 --- a/node/libs/roles/src/validator/messages/block.rs +++ b/node/libs/roles/src/validator/messages/block.rs @@ -102,6 +102,7 @@ impl FinalBlock { /// Creates a new finalized block. pub fn new(payload: Payload, justification: CommitQC) -> Self { assert_eq!(justification.header().payload, payload.hash()); + Self { payload, justification, diff --git a/node/libs/roles/src/validator/messages/committee.rs b/node/libs/roles/src/validator/messages/committee.rs index d5f1fd64..018dbb4f 100644 --- a/node/libs/roles/src/validator/messages/committee.rs +++ b/node/libs/roles/src/validator/messages/committee.rs @@ -24,7 +24,8 @@ impl std::ops::Deref for Committee { } impl Committee { - /// Creates a new Committee from a list of validator public keys. + /// Creates a new Committee from a list of validator public keys. Note that the order of the given validators + /// is NOT preserved in the committee. pub fn new(validators: impl IntoIterator) -> anyhow::Result { let mut map = BTreeMap::new(); let mut total_weight: u64 = 0; diff --git a/node/libs/roles/src/validator/messages/consensus.rs b/node/libs/roles/src/validator/messages/consensus.rs index a13426e1..1b723714 100644 --- a/node/libs/roles/src/validator/messages/consensus.rs +++ b/node/libs/roles/src/validator/messages/consensus.rs @@ -114,6 +114,14 @@ impl View { number: ViewNumber(self.number.0 + 1), } } + + /// Decrements the view number. + pub fn prev(self) -> Option { + self.number.prev().map(|number| Self { + genesis: self.genesis, + number, + }) + } } /// A struct that represents a view number. @@ -125,6 +133,11 @@ impl ViewNumber { pub fn next(self) -> Self { Self(self.0 + 1) } + + /// Get the previous view number. + pub fn prev(self) -> Option { + self.0.checked_sub(1).map(Self) + } } impl fmt::Display for ViewNumber { diff --git a/node/libs/roles/src/validator/messages/genesis.rs b/node/libs/roles/src/validator/messages/genesis.rs index 57e5e1cd..9af95411 100644 --- a/node/libs/roles/src/validator/messages/genesis.rs +++ b/node/libs/roles/src/validator/messages/genesis.rs @@ -1,5 +1,5 @@ //! Messages related to the consensus protocol. -use super::{BlockNumber, Committee, LeaderSelectionMode, ViewNumber}; +use super::{BlockNumber, LeaderSelectionMode, ViewNumber}; use crate::{attester, validator}; use std::{fmt, hash::Hash}; use zksync_consensus_crypto::{keccak256::Keccak256, ByteFmt, Text, TextFmt}; @@ -17,7 +17,7 @@ pub struct GenesisRaw { /// First block of a fork. pub first_block: BlockNumber, /// Set of validators of the chain. - pub validators: Committee, + pub validators: validator::Committee, /// Set of attesters of the chain. pub attesters: Option, /// The mode used for selecting leader for a given view. @@ -33,7 +33,7 @@ impl GenesisRaw { } /// Hash of the genesis specification. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct GenesisHash(pub(crate) Keccak256); impl TextFmt for GenesisHash { diff --git a/node/libs/roles/src/validator/messages/replica_commit.rs b/node/libs/roles/src/validator/messages/replica_commit.rs index 6d4b98f4..76f86606 100644 --- a/node/libs/roles/src/validator/messages/replica_commit.rs +++ b/node/libs/roles/src/validator/messages/replica_commit.rs @@ -15,7 +15,7 @@ impl ReplicaCommit { pub fn verify(&self, genesis: &Genesis) -> Result<(), ReplicaCommitVerifyError> { self.view .verify(genesis) - .map_err(ReplicaCommitVerifyError::View)?; + .map_err(ReplicaCommitVerifyError::BadView)?; if self.proposal.number < genesis.first_block { return Err(ReplicaCommitVerifyError::BadBlockNumber); @@ -30,7 +30,7 @@ impl ReplicaCommit { pub enum ReplicaCommitVerifyError { /// Invalid view. #[error("view: {0:#}")] - View(anyhow::Error), + BadView(anyhow::Error), /// Bad block number. #[error("block number < first block")] BadBlockNumber, diff --git a/node/libs/roles/src/validator/messages/tests.rs b/node/libs/roles/src/validator/messages/tests.rs deleted file mode 100644 index 7f58184c..00000000 --- a/node/libs/roles/src/validator/messages/tests.rs +++ /dev/null @@ -1,375 +0,0 @@ -use crate::{ - attester::{self, WeightedAttester}, - validator::*, -}; -use anyhow::Context as _; -use rand::{prelude::StdRng, Rng, SeedableRng}; -use zksync_concurrency::ctx; -use zksync_consensus_crypto::Text; -use zksync_consensus_utils::enum_util::Variant as _; - -/// Hardcoded view numbers. -fn views() -> impl Iterator { - [8394532, 2297897, 9089304, 7203483, 9982111] - .into_iter() - .map(ViewNumber) -} - -/// Hardcoded validator secret keys. -fn validator_keys() -> Vec { - [ - "validator:secret:bls12_381:27cb45b1670a1ae8d376a85821d51c7f91ebc6e32788027a84758441aaf0a987", - "validator:secret:bls12_381:20132edc08a529e927f155e710ae7295a2a0d249f1b1f37726894d1d0d8f0d81", - "validator:secret:bls12_381:0946901f0a6650284726763b12de5da0f06df0016c8ec2144cf6b1903f1979a6", - "validator:secret:bls12_381:3143a64c079b2f50545288d7c9b282281e05c97ac043228830a9660ddd63fea3", - "validator:secret:bls12_381:5512f40d33844c1c8107aa630af764005ab6e13f6bf8edb59b4ca3683727e619", - ] - .iter() - .map(|raw| Text::new(raw).decode().unwrap()) - .collect() -} - -/// Hardcoded attester secret keys. -fn attester_keys() -> Vec { - [ - "attester:secret:secp256k1:27cb45b1670a1ae8d376a85821d51c7f91ebc6e32788027a84758441aaf0a987", - "attester:secret:secp256k1:20132edc08a529e927f155e710ae7295a2a0d249f1b1f37726894d1d0d8f0d81", - "attester:secret:secp256k1:0946901f0a6650284726763b12de5da0f06df0016c8ec2144cf6b1903f1979a6", - ] - .iter() - .map(|raw| Text::new(raw).decode().unwrap()) - .collect() -} - -/// Hardcoded validator committee. -fn validator_committee() -> Committee { - Committee::new( - validator_keys() - .iter() - .enumerate() - .map(|(i, key)| WeightedValidator { - key: key.public(), - weight: i as u64 + 10, - }), - ) - .unwrap() -} - -/// Hardcoded attester committee. -fn attester_committee() -> attester::Committee { - attester::Committee::new( - attester_keys() - .iter() - .enumerate() - .map(|(i, key)| WeightedAttester { - key: key.public(), - weight: i as u64 + 10, - }), - ) - .unwrap() -} - -/// Hardcoded payload. -fn payload() -> Payload { - Payload( - hex::decode("57b79660558f18d56b5196053f64007030a1cb7eeadb5c32d816b9439f77edf5f6bd9d") - .unwrap(), - ) -} - -/// Checks that the order of validators in a committee is stable. -#[test] -fn validator_committee_change_detector() { - let committee = validator_committee(); - let got: Vec = validator_keys() - .iter() - .map(|k| committee.index(&k.public()).unwrap()) - .collect(); - assert_eq!(vec![0, 1, 4, 3, 2], got); -} - -#[test] -fn payload_hash_change_detector() { - let want: PayloadHash = Text::new( - "payload:keccak256:ba8ffff2526cae27a9e8e014749014b08b80e01905c8b769159d02d6579d9b83", - ) - .decode() - .unwrap(); - assert_eq!(want, payload().hash()); -} - -#[test] -fn test_sticky() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - let committee = validator_committee(); - let want = committee - .get(rng.gen_range(0..committee.len())) - .unwrap() - .key - .clone(); - let sticky = LeaderSelectionMode::Sticky(want.clone()); - for _ in 0..100 { - assert_eq!(want, committee.view_leader(rng.gen(), &sticky)); - } -} - -#[test] -fn test_rota() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - let committee = validator_committee(); - let mut want = Vec::new(); - for _ in 0..3 { - want.push( - committee - .get(rng.gen_range(0..committee.len())) - .unwrap() - .key - .clone(), - ); - } - let rota = LeaderSelectionMode::Rota(want.clone()); - for _ in 0..100 { - let vn: ViewNumber = rng.gen(); - let pk = &want[vn.0 as usize % want.len()]; - assert_eq!(*pk, committee.view_leader(vn, &rota)); - } -} - -/// Checks that leader schedule is stable. -#[test] -fn roundrobin_change_detector() { - let committee = validator_committee(); - let mode = LeaderSelectionMode::RoundRobin; - let got: Vec<_> = views() - .map(|view| { - let got = committee.view_leader(view, &mode); - committee.index(&got).unwrap() - }) - .collect(); - assert_eq!(vec![2, 2, 4, 3, 1], got); -} - -/// Checks that leader schedule is stable. -#[test] -fn weighted_change_detector() { - let committee = validator_committee(); - let mode = LeaderSelectionMode::Weighted; - let got: Vec<_> = views() - .map(|view| { - let got = committee.view_leader(view, &mode); - committee.index(&got).unwrap() - }) - .collect(); - assert_eq!(vec![4, 2, 2, 2, 1], got); -} - -mod version1 { - use super::*; - - /// Hardcoded genesis with no attesters. - fn genesis_empty_attesters() -> Genesis { - GenesisRaw { - chain_id: ChainId(1337), - fork_number: ForkNumber(402598740274745173), - first_block: BlockNumber(8902834932452), - - protocol_version: ProtocolVersion(1), - validators: validator_committee(), - attesters: None, - leader_selection: LeaderSelectionMode::Weighted, - } - .with_hash() - } - - /// Hardcoded genesis with attesters. - fn genesis_with_attesters() -> Genesis { - GenesisRaw { - chain_id: ChainId(1337), - fork_number: ForkNumber(402598740274745173), - first_block: BlockNumber(8902834932452), - - protocol_version: ProtocolVersion(1), - validators: validator_committee(), - attesters: attester_committee().into(), - leader_selection: LeaderSelectionMode::Weighted, - } - .with_hash() - } - - /// Note that genesis is NOT versioned by ProtocolVersion. - /// Even if it was, ALL versions of genesis need to be supported FOREVER, - /// unless we introduce dynamic regenesis. - #[test] - fn genesis_hash_change_detector_empty_attesters() { - let want: GenesisHash = Text::new( - "genesis_hash:keccak256:13a16cfa758c6716b4c4d40a5fe71023a016c7507b7893c7dc775f4420fc5d61", - ) - .decode() - .unwrap(); - assert_eq!(want, genesis_empty_attesters().hash()); - } - - /// Note that genesis is NOT versioned by ProtocolVersion. - /// Even if it was, ALL versions of genesis need to be supported FOREVER, - /// unless we introduce dynamic regenesis. - #[test] - fn genesis_hash_change_detector_nonempty_attesters() { - let want: GenesisHash = Text::new( - "genesis_hash:keccak256:47a52a5491873fa4ceb369a334b4c09833a06bd34718fb22e530ab4d70b4daf7", - ) - .decode() - .unwrap(); - assert_eq!(want, genesis_with_attesters().hash()); - } - - #[test] - fn genesis_verify_leader_pubkey_not_in_committee() { - let mut rng = StdRng::seed_from_u64(29483920); - let mut genesis = rng.gen::(); - genesis.leader_selection = LeaderSelectionMode::Sticky(rng.gen()); - let genesis = genesis.with_hash(); - assert!(genesis.verify().is_err()) - } - - /// Hardcoded view. - fn view() -> View { - View { - genesis: genesis_empty_attesters().hash(), - number: ViewNumber(9136573498460759103), - } - } - - /// Hardcoded `BlockHeader`. - fn block_header() -> BlockHeader { - BlockHeader { - number: BlockNumber(772839452345), - payload: payload().hash(), - } - } - - /// Hardcoded `ReplicaCommit`. - fn replica_commit() -> ReplicaCommit { - ReplicaCommit { - view: view(), - proposal: block_header(), - } - } - - /// Hardcoded `CommitQC`. - fn commit_qc() -> CommitQC { - let genesis = genesis_empty_attesters(); - let replica_commit = replica_commit(); - let mut x = CommitQC::new(replica_commit.clone(), &genesis); - for k in validator_keys() { - x.add(&k.sign_msg(replica_commit.clone()), &genesis) - .unwrap(); - } - x - } - - /// Hardcoded `ReplicaNewView`. - fn replica_new_view() -> ReplicaNewView { - ReplicaNewView { - justification: ProposalJustification::Commit(commit_qc()), - } - } - - /// Hardcoded `ReplicaTimeout` - fn replica_timeout() -> ReplicaTimeout { - ReplicaTimeout { - view: view(), - high_vote: Some(replica_commit()), - high_qc: Some(commit_qc()), - } - } - - /// Hardcoded `TimeoutQC`. - fn timeout_qc() -> TimeoutQC { - let mut x = TimeoutQC::new(view()); - let genesis = genesis_empty_attesters(); - let replica_timeout = replica_timeout(); - for k in validator_keys() { - x.add(&k.sign_msg(replica_timeout.clone()), &genesis) - .unwrap(); - } - x - } - - /// Hardcoded `LeaderProposal`. - fn leader_proposal() -> LeaderProposal { - LeaderProposal { - proposal: block_header(), - proposal_payload: Some(payload()), - justification: ProposalJustification::Timeout(timeout_qc()), - } - } - - /// Asserts that msg.hash()==hash and that sig is a - /// valid signature of msg (signed by `keys()[0]`). - #[track_caller] - fn change_detector(msg: Msg, hash: &str, sig: &str) { - let key = validator_keys()[0].clone(); - - (|| { - // Decode hash and signature. - let hash: MsgHash = Text::new(hash).decode()?; - let sig: Signature = Text::new(sig).decode()?; - - // Check if msg.hash() is equal to hash. - if msg.hash() != hash { - return Err(anyhow::anyhow!("Hash mismatch")); - } - - // Check if sig is a valid signature of hash. - sig.verify_hash(&hash, &key.public())?; - - anyhow::Ok(()) - })() - .with_context(|| { - format!( - "\nIntended hash: {:?}\nIntended signature: {:?}", - msg.hash(), - key.sign_hash(&msg.hash()), - ) - }) - .unwrap(); - } - - #[test] - fn replica_commit_change_detector() { - change_detector( - replica_commit().insert(), - "validator_msg:keccak256:2ec798684e539d417fac1caba74ed1a27a033bc18058ba0a4632f6bb0ae4fe1c", - "validator:signature:bls12_381:8de9ad850d78eb4f918c8c3a02310be49fc9ac35f2b1fdd6489293db1d5128f0d4c8389674e6bc2eee4c6e16f58e0b51", - ); - } - - #[test] - fn replica_new_view_change_detector() { - change_detector( - replica_new_view().insert(), - "validator_msg:keccak256:be1d87c8fcabeb1dd6aebef8564994830f206997d3cf240ad19281a8ef84fbd1", - "validator:signature:bls12_381:92640683902cd5c092fe8732f556d2bb3e0e209665ba4fe8e6f9ce2572a43700a63e82c3fbfd803fd217b3027ca843ca", - ); - } - - #[test] - fn replica_timeout_change_detector() { - change_detector( - replica_timeout().insert(), - "validator_msg:keccak256:c3a2dbbb01fa4884effdbad3cdfea7c03f3fa0d1b8ae15b221ac83e1d7abb9df", - "validator:signature:bls12_381:acb18d66816a0e7e2c1fbaa2d5045ead1a41eba8692ef0cdbf3f3d3f5a5616fa77903cc45326090cca1468912419d824", - ); - } - - #[test] - fn leader_proposal_change_detector() { - change_detector( - leader_proposal().insert(), - "validator_msg:keccak256:ecf7cd55c9b44ae5c361d23067af4f3bf5aa0f4d170f4d33b8ce4214c9963c6b", - "validator:signature:bls12_381:b650efd18dd800363aafd8ca67e3b6f14963f99b094fb497a3edad56816108a4e835ffcec714320e6fe1b42c30057005", - ); - } -} diff --git a/node/libs/roles/src/validator/messages/tests/block.rs b/node/libs/roles/src/validator/messages/tests/block.rs new file mode 100644 index 00000000..e3a3ea52 --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/block.rs @@ -0,0 +1,71 @@ +use super::*; +use assert_matches::assert_matches; +use rand::Rng; +use validator::testonly::Setup; +use zksync_concurrency::ctx; +use zksync_consensus_crypto::{keccak256::Keccak256, Text}; + +#[test] +fn payload_hash_change_detector() { + let want: PayloadHash = Text::new( + "payload:keccak256:ba8ffff2526cae27a9e8e014749014b08b80e01905c8b769159d02d6579d9b83", + ) + .decode() + .unwrap(); + assert_eq!(want, payload().hash()); +} + +#[test] +fn test_payload_hash() { + let data = vec![1, 2, 3, 4]; + let payload = Payload(data.clone()); + let hash = payload.hash(); + assert_eq!(hash.0, Keccak256::new(&data)); +} + +#[test] +fn test_block_number_next() { + let block_number = BlockNumber(5); + assert_eq!(block_number.next(), BlockNumber(6)); +} + +#[test] +fn test_block_number_prev() { + let block_number = BlockNumber(5); + assert_eq!(block_number.prev(), Some(BlockNumber(4))); + + let block_number_zero = BlockNumber(0); + assert_eq!(block_number_zero.prev(), None); +} + +#[test] +fn test_block_number_add() { + let block_number = BlockNumber(5); + assert_eq!(block_number + 3, BlockNumber(8)); +} + +#[test] +fn test_final_block_verify() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + let setup = Setup::new(rng, 2); + + let payload: Payload = rng.gen(); + let view_number = rng.gen(); + let commit_qc = setup.make_commit_qc_with_payload(&payload, view_number); + let mut final_block = FinalBlock::new(payload.clone(), commit_qc.clone()); + + assert!(final_block.verify(&setup.genesis).is_ok()); + + final_block.payload = rng.gen(); + assert_matches!( + final_block.verify(&setup.genesis), + Err(BlockValidationError::HashMismatch { .. }) + ); + + final_block.justification.message.proposal.payload = final_block.payload.hash(); + assert_matches!( + final_block.verify(&setup.genesis), + Err(BlockValidationError::Justification(_)) + ); +} diff --git a/node/libs/roles/src/validator/messages/tests/committee.rs b/node/libs/roles/src/validator/messages/tests/committee.rs new file mode 100644 index 00000000..0b9968f9 --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/committee.rs @@ -0,0 +1,198 @@ +use super::*; +use rand::Rng; +use zksync_concurrency::ctx; + +/// Checks that the order of validators in a committee is stable. +#[test] +fn test_committee_order_change_detector() { + let committee = validator_committee(); + let got: Vec = validator_keys() + .iter() + .map(|k| committee.index(&k.public()).unwrap()) + .collect(); + assert_eq!(vec![0, 1, 4, 3, 2], got); +} + +fn create_validator(weight: u64) -> WeightedValidator { + WeightedValidator { + key: validator::SecretKey::generate().public(), + weight, + } +} + +#[test] +fn test_committee_new() { + let validators = vec![create_validator(10), create_validator(20)]; + let committee = Committee::new(validators).unwrap(); + assert_eq!(committee.len(), 2); + assert_eq!(committee.total_weight(), 30); +} + +#[test] +fn test_committee_new_duplicate_validator() { + let mut validators = vec![create_validator(10), create_validator(20)]; + validators[1].key = validators[0].key.clone(); + let result = Committee::new(validators); + assert!(result.is_err()); +} + +#[test] +fn test_committee_new_zero_weight() { + let validators = vec![create_validator(10), create_validator(0)]; + let result = Committee::new(validators); + assert!(result.is_err()); +} + +#[test] +fn test_committee_weights_overflow_check() { + let validators: Vec = [u64::MAX / 5; 6] + .iter() + .map(|w| create_validator(*w)) + .collect(); + let result = Committee::new(validators); + assert!(result.is_err()); +} + +#[test] +fn test_committee_new_empty() { + let validators = vec![]; + let result = Committee::new(validators); + assert!(result.is_err()); +} + +#[test] +fn test_committee_contains() { + let validators = vec![create_validator(10), create_validator(20)]; + let committee = Committee::new(validators.clone()).unwrap(); + assert!(committee.contains(&validators[0].key)); + assert!(!committee.contains(&validator::SecretKey::generate().public())); +} + +#[test] +fn test_committee_get() { + let validators = validator_keys() + .into_iter() + .map(|x| x.public()) + .collect::>(); + let committee = validator_committee(); + assert_eq!(committee.get(0).unwrap().key, validators[0]); + assert_eq!(committee.get(1).unwrap().key, validators[1]); + assert_eq!(committee.get(2).unwrap().key, validators[4]); + assert_eq!(committee.get(3).unwrap().key, validators[3]); + assert_eq!(committee.get(4).unwrap().key, validators[2]); + assert!(committee.get(5).is_none()); +} + +#[test] +fn test_committee_index() { + let validators = validator_keys() + .into_iter() + .map(|x| x.public()) + .collect::>(); + let committee = validator_committee(); + assert_eq!(committee.index(&validators[0]), Some(0)); + assert_eq!(committee.index(&validators[1]), Some(1)); + assert_eq!(committee.index(&validators[4]), Some(2)); + assert_eq!(committee.index(&validators[3]), Some(3)); + assert_eq!(committee.index(&validators[2]), Some(4)); + assert_eq!( + committee.index(&validator::SecretKey::generate().public()), + None + ); +} + +#[test] +fn test_committee_view_leader_round_robin() { + let committee = validator_committee(); + let mode = LeaderSelectionMode::RoundRobin; + let got: Vec<_> = views() + .map(|view| { + let got = committee.view_leader(view, &mode); + committee.index(&got).unwrap() + }) + .collect(); + assert_eq!(vec![2, 3, 4, 4, 1], got); +} + +#[test] +fn test_committee_view_leader_weighted() { + let committee = validator_committee(); + let mode = LeaderSelectionMode::Weighted; + let got: Vec<_> = views() + .map(|view| { + let got = committee.view_leader(view, &mode); + committee.index(&got).unwrap() + }) + .collect(); + assert_eq!(vec![2, 3, 2, 1, 3], got); +} + +#[test] +fn test_committee_view_leader_sticky() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + let committee = validator_committee(); + let want = committee + .get(rng.gen_range(0..committee.len())) + .unwrap() + .key + .clone(); + let sticky = LeaderSelectionMode::Sticky(want.clone()); + for _ in 0..100 { + assert_eq!(want, committee.view_leader(rng.gen(), &sticky)); + } +} + +#[test] +fn test_committee_view_leader_rota() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + let committee = validator_committee(); + let mut want = Vec::new(); + for _ in 0..3 { + want.push( + committee + .get(rng.gen_range(0..committee.len())) + .unwrap() + .key + .clone(), + ); + } + let rota = LeaderSelectionMode::Rota(want.clone()); + for _ in 0..100 { + let vn: ViewNumber = rng.gen(); + let pk = &want[vn.0 as usize % want.len()]; + assert_eq!(*pk, committee.view_leader(vn, &rota)); + } +} + +#[test] +fn test_committee_quorum_threshold() { + let validators = vec![create_validator(10), create_validator(20)]; + let committee = Committee::new(validators).unwrap(); + assert_eq!(committee.quorum_threshold(), 25); // 30 - (30 - 1) / 5 +} + +#[test] +fn test_committee_subquorum_threshold() { + let validators = vec![create_validator(10), create_validator(20)]; + let committee = Committee::new(validators).unwrap(); + assert_eq!(committee.subquorum_threshold(), 15); // 30 - 3 * (30 - 1) / 5 +} + +#[test] +fn test_committee_max_faulty_weight() { + let validators = vec![create_validator(10), create_validator(20)]; + let committee = Committee::new(validators).unwrap(); + assert_eq!(committee.max_faulty_weight(), 5); // (30 - 1) / 5 +} + +#[test] +fn test_committee_weight() { + let committee = validator_committee(); + let mut signers = Signers::new(5); + signers.0.set(1, true); + signers.0.set(2, true); + signers.0.set(4, true); + assert_eq!(committee.weight(&signers), 37); +} diff --git a/node/libs/roles/src/validator/messages/tests/consensus.rs b/node/libs/roles/src/validator/messages/tests/consensus.rs new file mode 100644 index 00000000..632f7508 --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/consensus.rs @@ -0,0 +1,92 @@ +use super::*; + +#[test] +fn test_view_next() { + let view = View { + genesis: GenesisHash::default(), + number: ViewNumber(1), + }; + let next_view = view.next(); + assert_eq!(next_view.number, ViewNumber(2)); +} + +#[test] +fn test_view_prev() { + let view = View { + genesis: GenesisHash::default(), + number: ViewNumber(1), + }; + let prev_view = view.prev(); + assert_eq!(prev_view.unwrap().number, ViewNumber(0)); + let view = View { + genesis: GenesisHash::default(), + number: ViewNumber(0), + }; + let prev_view = view.prev(); + assert!(prev_view.is_none()); +} + +#[test] +fn test_view_verify() { + let genesis = genesis_with_attesters(); + let view = View { + genesis: genesis.hash(), + number: ViewNumber(1), + }; + assert!(view.verify(&genesis).is_ok()); + assert!(view.verify(&genesis_empty_attesters()).is_err()); + let view = View { + genesis: GenesisHash::default(), + number: ViewNumber(1), + }; + assert!(view.verify(&genesis).is_err()); +} + +#[test] +fn test_signers_new() { + let signers = Signers::new(10); + assert_eq!(signers.len(), 10); + assert!(signers.is_empty()); +} + +#[test] +fn test_signers_count() { + let mut signers = Signers::new(10); + signers.0.set(0, true); + signers.0.set(1, true); + assert_eq!(signers.count(), 2); +} + +#[test] +fn test_signers_empty() { + let mut signers = Signers::new(10); + assert!(signers.is_empty()); + signers.0.set(1, true); + assert!(!signers.is_empty()); + signers.0.set(1, false); + assert!(signers.is_empty()); +} + +#[test] +fn test_signers_bitor_assign() { + let mut signers1 = Signers::new(10); + let mut signers2 = Signers::new(10); + signers1.0.set(0, true); + signers1.0.set(3, true); + signers2.0.set(1, true); + signers2.0.set(3, true); + signers1 |= &signers2; + assert_eq!(signers1.count(), 3); +} + +#[test] +fn test_signers_bitand_assign() { + let mut signers1 = Signers::new(10); + let mut signers2 = Signers::new(10); + signers1.0.set(0, true); + signers1.0.set(3, true); + signers2.0.set(1, true); + signers2.0.set(3, true); + signers1 &= &signers2; + assert_eq!(signers1.count(), 1); +} diff --git a/node/libs/roles/src/validator/messages/tests/genesis.rs b/node/libs/roles/src/validator/messages/tests/genesis.rs new file mode 100644 index 00000000..cdeac273 --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/genesis.rs @@ -0,0 +1,30 @@ +use super::*; +use rand::{prelude::StdRng, Rng, SeedableRng}; +use validator::testonly::Setup; +use zksync_concurrency::ctx; +use zksync_protobuf::ProtoFmt as _; + +#[test] +fn genesis_verify_leader_pubkey_not_in_committee() { + let mut rng = StdRng::seed_from_u64(29483920); + let mut genesis = rng.gen::(); + genesis.leader_selection = LeaderSelectionMode::Sticky(rng.gen()); + let genesis = genesis.with_hash(); + assert!(genesis.verify().is_err()) +} + +#[test] +fn test_genesis_verify() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let genesis = Setup::new(rng, 1).genesis.clone(); + assert!(genesis.verify().is_ok()); + assert!(Genesis::read(&genesis.build()).is_ok()); + + let mut genesis = (*genesis).clone(); + genesis.leader_selection = LeaderSelectionMode::Sticky(rng.gen()); + let genesis = genesis.with_hash(); + assert!(genesis.verify().is_err()); + assert!(Genesis::read(&genesis.build()).is_err()) +} diff --git a/node/libs/roles/src/validator/messages/tests/leader_proposal.rs b/node/libs/roles/src/validator/messages/tests/leader_proposal.rs new file mode 100644 index 00000000..3c1fdac3 --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/leader_proposal.rs @@ -0,0 +1,97 @@ +use super::*; +use assert_matches::assert_matches; +use zksync_concurrency::ctx; + +#[test] +fn test_leader_proposal_verify() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + // This will create equally weighted validators + let mut setup = Setup::new(rng, 6); + setup.push_blocks(rng, 3); + + // Valid proposal + let payload: Payload = rng.gen(); + let block_header = BlockHeader { + number: setup.next(), + payload: payload.hash(), + }; + let commit_qc = match setup.blocks.last().unwrap() { + Block::Final(block) => block.justification.clone(), + _ => unreachable!(), + }; + let justification = ProposalJustification::Commit(commit_qc); + let proposal = LeaderProposal { + proposal: block_header, + proposal_payload: Some(payload.clone()), + justification, + }; + + assert!(proposal.verify(&setup.genesis).is_ok()); + + // Invalid justification + let mut wrong_proposal = proposal.clone(); + wrong_proposal.justification = ProposalJustification::Timeout(rng.gen()); + + assert_matches!( + wrong_proposal.verify(&setup.genesis), + Err(LeaderProposalVerifyError::Justification(_)) + ); + + // Invalid block number + let mut wrong_proposal = proposal.clone(); + wrong_proposal.proposal.number = BlockNumber(1); + + assert_matches!( + wrong_proposal.verify(&setup.genesis), + Err(LeaderProposalVerifyError::BadBlockNumber { .. }) + ); + + // Wrong reproposal + let mut wrong_proposal = proposal.clone(); + wrong_proposal.proposal_payload = None; + + assert_matches!( + wrong_proposal.verify(&setup.genesis), + Err(LeaderProposalVerifyError::ReproposalWhenPreviousFinalized) + ); + + // Invalid payload + let mut wrong_proposal = proposal.clone(); + wrong_proposal.proposal.payload = rng.gen(); + + assert_matches!( + wrong_proposal.verify(&setup.genesis), + Err(LeaderProposalVerifyError::MismatchedPayload { .. }) + ); + + // New leader proposal with a reproposal + let timeout_qc = setup.make_timeout_qc(rng, ViewNumber(7), Some(&payload)); + let justification = ProposalJustification::Timeout(timeout_qc); + let proposal = LeaderProposal { + proposal: block_header, + proposal_payload: None, + justification, + }; + + assert!(proposal.verify(&setup.genesis).is_ok()); + + // Invalid payload hash + let mut wrong_proposal = proposal.clone(); + wrong_proposal.proposal.payload = rng.gen(); + + assert_matches!( + wrong_proposal.verify(&setup.genesis), + Err(LeaderProposalVerifyError::BadPayloadHash { .. }) + ); + + // Wrong new proposal + let mut wrong_proposal = proposal.clone(); + wrong_proposal.proposal_payload = Some(rng.gen()); + + assert_matches!( + wrong_proposal.verify(&setup.genesis), + Err(LeaderProposalVerifyError::NewProposalWhenPreviousNotFinalized) + ); +} diff --git a/node/libs/roles/src/validator/messages/tests/mod.rs b/node/libs/roles/src/validator/messages/tests/mod.rs new file mode 100644 index 00000000..b8b703b7 --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/mod.rs @@ -0,0 +1,192 @@ +use super::*; +use crate::{ + attester::{self, WeightedAttester}, + validator::{self, testonly::Setup}, +}; +use rand::Rng; +use zksync_consensus_crypto::Text; + +mod block; +mod committee; +mod consensus; +mod genesis; +mod leader_proposal; +mod replica_commit; +mod replica_timeout; +mod versions; + +/// Hardcoded view. +fn view() -> View { + View { + genesis: genesis_empty_attesters().hash(), + number: ViewNumber(9136), + } +} + +/// Hardcoded view numbers. +fn views() -> impl Iterator { + [2297, 7203, 8394, 9089, 99821].into_iter().map(ViewNumber) +} + +/// Hardcoded payload. +fn payload() -> Payload { + Payload( + hex::decode("57b79660558f18d56b5196053f64007030a1cb7eeadb5c32d816b9439f77edf5f6bd9d") + .unwrap(), + ) +} + +/// Hardcoded `BlockHeader`. +fn block_header() -> BlockHeader { + BlockHeader { + number: BlockNumber(7728), + payload: payload().hash(), + } +} + +/// Hardcoded validator secret keys. +fn validator_keys() -> Vec { + [ + "validator:secret:bls12_381:27cb45b1670a1ae8d376a85821d51c7f91ebc6e32788027a84758441aaf0a987", + "validator:secret:bls12_381:20132edc08a529e927f155e710ae7295a2a0d249f1b1f37726894d1d0d8f0d81", + "validator:secret:bls12_381:0946901f0a6650284726763b12de5da0f06df0016c8ec2144cf6b1903f1979a6", + "validator:secret:bls12_381:3143a64c079b2f50545288d7c9b282281e05c97ac043228830a9660ddd63fea3", + "validator:secret:bls12_381:5512f40d33844c1c8107aa630af764005ab6e13f6bf8edb59b4ca3683727e619", + ] + .iter() + .map(|raw| Text::new(raw).decode().unwrap()) + .collect() +} + +/// Hardcoded attester secret keys. +fn attester_keys() -> Vec { + [ + "attester:secret:secp256k1:27cb45b1670a1ae8d376a85821d51c7f91ebc6e32788027a84758441aaf0a987", + "attester:secret:secp256k1:20132edc08a529e927f155e710ae7295a2a0d249f1b1f37726894d1d0d8f0d81", + "attester:secret:secp256k1:0946901f0a6650284726763b12de5da0f06df0016c8ec2144cf6b1903f1979a6", + ] + .iter() + .map(|raw| Text::new(raw).decode().unwrap()) + .collect() +} + +/// Hardcoded validator committee. +fn validator_committee() -> Committee { + Committee::new( + validator_keys() + .iter() + .enumerate() + .map(|(i, key)| WeightedValidator { + key: key.public(), + weight: i as u64 + 10, + }), + ) + .unwrap() +} + +/// Hardcoded attester committee. +fn attester_committee() -> attester::Committee { + attester::Committee::new( + attester_keys() + .iter() + .enumerate() + .map(|(i, key)| WeightedAttester { + key: key.public(), + weight: i as u64 + 10, + }), + ) + .unwrap() +} + +/// Hardcoded genesis with no attesters. +fn genesis_empty_attesters() -> Genesis { + GenesisRaw { + chain_id: ChainId(1337), + fork_number: ForkNumber(42), + first_block: BlockNumber(2834), + + protocol_version: ProtocolVersion(1), + validators: validator_committee(), + attesters: None, + leader_selection: LeaderSelectionMode::Weighted, + } + .with_hash() +} + +/// Hardcoded genesis with attesters. +fn genesis_with_attesters() -> Genesis { + GenesisRaw { + chain_id: ChainId(1337), + fork_number: ForkNumber(42), + first_block: BlockNumber(2834), + + protocol_version: ProtocolVersion(1), + validators: validator_committee(), + attesters: attester_committee().into(), + leader_selection: LeaderSelectionMode::Weighted, + } + .with_hash() +} + +/// Hardcoded `LeaderProposal`. +fn leader_proposal() -> LeaderProposal { + LeaderProposal { + proposal: block_header(), + proposal_payload: Some(payload()), + justification: ProposalJustification::Timeout(timeout_qc()), + } +} + +/// Hardcoded `ReplicaCommit`. +fn replica_commit() -> ReplicaCommit { + ReplicaCommit { + view: view(), + proposal: block_header(), + } +} + +/// Hardcoded `CommitQC`. +fn commit_qc() -> CommitQC { + let genesis = genesis_empty_attesters(); + let replica_commit = replica_commit(); + let mut x = CommitQC::new(replica_commit.clone(), &genesis); + for k in validator_keys() { + x.add(&k.sign_msg(replica_commit.clone()), &genesis) + .unwrap(); + } + x +} + +/// Hardcoded `ReplicaTimeout` +fn replica_timeout() -> ReplicaTimeout { + ReplicaTimeout { + view: View { + genesis: genesis_empty_attesters().hash(), + number: ViewNumber(9169), + }, + high_vote: Some(replica_commit()), + high_qc: Some(commit_qc()), + } +} + +/// Hardcoded `TimeoutQC`. +fn timeout_qc() -> TimeoutQC { + let mut x = TimeoutQC::new(View { + genesis: genesis_empty_attesters().hash(), + number: ViewNumber(9169), + }); + let genesis = genesis_empty_attesters(); + let replica_timeout = replica_timeout(); + for k in validator_keys() { + x.add(&k.sign_msg(replica_timeout.clone()), &genesis) + .unwrap(); + } + x +} + +/// Hardcoded `ReplicaNewView`. +fn replica_new_view() -> ReplicaNewView { + ReplicaNewView { + justification: ProposalJustification::Commit(commit_qc()), + } +} diff --git a/node/libs/roles/src/validator/messages/tests/replica_commit.rs b/node/libs/roles/src/validator/messages/tests/replica_commit.rs new file mode 100644 index 00000000..06516ba5 --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/replica_commit.rs @@ -0,0 +1,95 @@ +use super::*; +use assert_matches::assert_matches; +use zksync_concurrency::ctx; + +#[test] +fn test_commit_qc() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + // This will create equally weighted validators + let setup1 = Setup::new(rng, 6); + let setup2 = Setup::new(rng, 6); + let mut genesis3 = (*setup1.genesis).clone(); + genesis3.validators = + Committee::new(setup1.genesis.validators.iter().take(3).cloned()).unwrap(); + let genesis3 = genesis3.with_hash(); + + for i in 0..setup1.validator_keys.len() + 1 { + let view = rng.gen(); + let mut qc = CommitQC::new(setup1.make_replica_commit(rng, view), &setup1.genesis); + for key in &setup1.validator_keys[0..i] { + qc.add(&key.sign_msg(qc.message.clone()), &setup1.genesis) + .unwrap(); + } + let expected_weight: u64 = setup1 + .genesis + .validators + .iter() + .take(i) + .map(|w| w.weight) + .sum(); + if expected_weight >= setup1.genesis.validators.quorum_threshold() { + qc.verify(&setup1.genesis).unwrap(); + } else { + assert_matches!( + qc.verify(&setup1.genesis), + Err(CommitQCVerifyError::NotEnoughSigners { .. }) + ); + } + + // Mismatching validator sets. + assert!(qc.verify(&setup2.genesis).is_err()); + assert!(qc.verify(&genesis3).is_err()); + } +} + +#[test] +fn test_commit_qc_add_errors() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + let setup = Setup::new(rng, 2); + let view = rng.gen(); + let mut qc = CommitQC::new(setup.make_replica_commit(rng, view), &setup.genesis); + let msg = qc.message.clone(); + // Add the message + assert_matches!( + qc.add( + &setup.validator_keys[0].sign_msg(msg.clone()), + &setup.genesis + ), + Ok(()) + ); + + // Try to add a message for a different view + let mut msg1 = msg.clone(); + msg1.view.number = view.next(); + assert_matches!( + qc.add(&setup.validator_keys[0].sign_msg(msg1), &setup.genesis), + Err(CommitQCAddError::InconsistentMessages { .. }) + ); + + // Try to add a message from a signer not in committee + assert_matches!( + qc.add( + &rng.gen::().sign_msg(msg.clone()), + &setup.genesis + ), + Err(CommitQCAddError::SignerNotInCommittee { .. }) + ); + + // Try to add the same message already added by same validator + assert_matches!( + qc.add( + &setup.validator_keys[0].sign_msg(msg.clone()), + &setup.genesis + ), + Err(CommitQCAddError::DuplicateSigner { .. }) + ); + + // Add same message signed by another validator. + assert_matches!( + qc.add(&setup.validator_keys[1].sign_msg(msg), &setup.genesis), + Ok(()) + ); +} diff --git a/node/libs/roles/src/validator/messages/tests/replica_timeout.rs b/node/libs/roles/src/validator/messages/tests/replica_timeout.rs new file mode 100644 index 00000000..f69b08a6 --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/replica_timeout.rs @@ -0,0 +1,174 @@ +use super::*; +use assert_matches::assert_matches; +use rand::seq::SliceRandom as _; +use zksync_concurrency::ctx; + +#[test] +fn test_timeout_qc() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + // This will create equally weighted validators + let setup1 = Setup::new(rng, 6); + let setup2 = Setup::new(rng, 6); + let mut genesis3 = (*setup1.genesis).clone(); + genesis3.validators = + Committee::new(setup1.genesis.validators.iter().take(3).cloned()).unwrap(); + let genesis3 = genesis3.with_hash(); + + let view: ViewNumber = rng.gen(); + let msgs: Vec<_> = (0..3) + .map(|_| setup1.make_replica_timeout(rng, view)) + .collect(); + + for n in 0..setup1.validator_keys.len() + 1 { + let mut qc = TimeoutQC::new(msgs[0].view.clone()); + for key in &setup1.validator_keys[0..n] { + qc.add( + &key.sign_msg(msgs.choose(rng).unwrap().clone()), + &setup1.genesis, + ) + .unwrap(); + } + let expected_weight: u64 = setup1 + .genesis + .validators + .iter() + .take(n) + .map(|w| w.weight) + .sum(); + if expected_weight >= setup1.genesis.validators.quorum_threshold() { + qc.verify(&setup1.genesis).unwrap(); + } else { + assert_matches!( + qc.verify(&setup1.genesis), + Err(TimeoutQCVerifyError::NotEnoughSigners { .. }) + ); + } + + // Mismatching validator sets. + assert!(qc.verify(&setup2.genesis).is_err()); + assert!(qc.verify(&genesis3).is_err()); + } +} + +#[test] +fn test_timeout_qc_high_vote() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + // This will create equally weighted validators + let setup = Setup::new(rng, 6); + + let view_num: ViewNumber = rng.gen(); + let msg_a = setup.make_replica_timeout(rng, view_num); + let msg_b = setup.make_replica_timeout(rng, view_num); + let msg_c = setup.make_replica_timeout(rng, view_num); + + // Case with 1 subquorum. + let mut qc = TimeoutQC::new(msg_a.view.clone()); + + for key in &setup.validator_keys { + qc.add(&key.sign_msg(msg_a.clone()), &setup.genesis) + .unwrap(); + } + + assert!(qc.high_vote(&setup.genesis).is_some()); + + // Case with 2 subquorums. + let mut qc = TimeoutQC::new(msg_a.view.clone()); + + for key in &setup.validator_keys[0..3] { + qc.add(&key.sign_msg(msg_a.clone()), &setup.genesis) + .unwrap(); + } + + for key in &setup.validator_keys[3..6] { + qc.add(&key.sign_msg(msg_b.clone()), &setup.genesis) + .unwrap(); + } + + assert!(qc.high_vote(&setup.genesis).is_none()); + + // Case with no subquorums. + let mut qc = TimeoutQC::new(msg_a.view.clone()); + + for key in &setup.validator_keys[0..2] { + qc.add(&key.sign_msg(msg_a.clone()), &setup.genesis) + .unwrap(); + } + + for key in &setup.validator_keys[2..4] { + qc.add(&key.sign_msg(msg_b.clone()), &setup.genesis) + .unwrap(); + } + + for key in &setup.validator_keys[4..6] { + qc.add(&key.sign_msg(msg_c.clone()), &setup.genesis) + .unwrap(); + } + + assert!(qc.high_vote(&setup.genesis).is_none()); +} + +#[test] +fn test_timeout_qc_add_errors() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + let setup = Setup::new(rng, 2); + let view = rng.gen(); + let msg = setup.make_replica_timeout(rng, view); + let mut qc = TimeoutQC::new(msg.view.clone()); + let msg = setup.make_replica_timeout(rng, view); + + // Add the message + assert_matches!( + qc.add( + &setup.validator_keys[0].sign_msg(msg.clone()), + &setup.genesis + ), + Ok(()) + ); + + // Try to add a message for a different view + let mut msg1 = msg.clone(); + msg1.view.number = view.next(); + assert_matches!( + qc.add(&setup.validator_keys[0].sign_msg(msg1), &setup.genesis), + Err(TimeoutQCAddError::InconsistentViews { .. }) + ); + + // Try to add a message from a signer not in committee + assert_matches!( + qc.add( + &rng.gen::().sign_msg(msg.clone()), + &setup.genesis + ), + Err(TimeoutQCAddError::SignerNotInCommittee { .. }) + ); + + // Try to add the same message already added by same validator + assert_matches!( + qc.add( + &setup.validator_keys[0].sign_msg(msg.clone()), + &setup.genesis + ), + Err(TimeoutQCAddError::DuplicateSigner { .. }) + ); + + // Try to add a message for a validator that already added another message + let msg2 = setup.make_replica_timeout(rng, view); + assert_matches!( + qc.add(&setup.validator_keys[0].sign_msg(msg2), &setup.genesis), + Err(TimeoutQCAddError::DuplicateSigner { .. }) + ); + + // Add same message signed by another validator. + assert_matches!( + qc.add( + &setup.validator_keys[1].sign_msg(msg.clone()), + &setup.genesis + ), + Ok(()) + ); +} diff --git a/node/libs/roles/src/validator/messages/tests/versions.rs b/node/libs/roles/src/validator/messages/tests/versions.rs new file mode 100644 index 00000000..1fa2c46d --- /dev/null +++ b/node/libs/roles/src/validator/messages/tests/versions.rs @@ -0,0 +1,102 @@ +use super::*; +use anyhow::Context as _; +use zksync_consensus_crypto::Text; + +mod version1 { + use zksync_consensus_utils::enum_util::Variant as _; + + use super::*; + + /// Note that genesis is NOT versioned by ProtocolVersion. + /// Even if it was, ALL versions of genesis need to be supported FOREVER, + /// unless we introduce dynamic regenesis. + #[test] + fn genesis_hash_change_detector_empty_attesters() { + let want: GenesisHash = Text::new( + "genesis_hash:keccak256:75cfa582fcda9b5da37af8fb63a279f777bb17a97a50519e1a61aad6c77a522f", + ) + .decode() + .unwrap(); + assert_eq!(want, genesis_empty_attesters().hash()); + } + + /// Note that genesis is NOT versioned by ProtocolVersion. + /// Even if it was, ALL versions of genesis need to be supported FOREVER, + /// unless we introduce dynamic regenesis. + #[test] + fn genesis_hash_change_detector_nonempty_attesters() { + let want: GenesisHash = Text::new( + "genesis_hash:keccak256:586a4bc6167c084d7499cead9267b224ab04a4fdeff555630418bcd2df5d186d", + ) + .decode() + .unwrap(); + assert_eq!(want, genesis_with_attesters().hash()); + } + + /// Asserts that msg.hash()==hash and that sig is a + /// valid signature of msg (signed by `keys()[0]`). + #[track_caller] + fn msg_change_detector(msg: Msg, hash: &str, sig: &str) { + let key = validator_keys()[0].clone(); + + (|| { + // Decode hash and signature. + let hash: MsgHash = Text::new(hash).decode()?; + let sig: validator::Signature = Text::new(sig).decode()?; + + // Check if msg.hash() is equal to hash. + if msg.hash() != hash { + return Err(anyhow::anyhow!("Hash mismatch")); + } + + // Check if sig is a valid signature of hash. + sig.verify_hash(&hash, &key.public())?; + + anyhow::Ok(()) + })() + .with_context(|| { + format!( + "\nIntended hash: {:?}\nIntended signature: {:?}", + msg.hash(), + key.sign_hash(&msg.hash()), + ) + }) + .unwrap(); + } + + #[test] + fn replica_commit_change_detector() { + msg_change_detector( + replica_commit().insert(), + "validator_msg:keccak256:ccbb11a6b3f4e06840a2a06abc2a245a2b3de30bb951e759a9ec6920f74f0632", + "validator:signature:bls12_381:8e41b89c89c0de8f83102966596ab95f6bdfdc18fceaceb224753b3ff495e02d5479c709829bd6d0802c5a1f24fa96b5", + ); + } + + #[test] + fn replica_new_view_change_detector() { + msg_change_detector( + replica_new_view().insert(), + "validator_msg:keccak256:2be143114cd3442b96d5f6083713c4c338a1c18ef562ede4721ebf037689a6ad", + "validator:signature:bls12_381:9809b66d44509cf7847baaa03a35ae87062f9827cf1f90c8353f057eee45b79fde0f4c4c500980b69c59263b51b6d072", + ); + } + + #[test] + fn replica_timeout_change_detector() { + msg_change_detector( + replica_timeout().insert(), + "validator_msg:keccak256:615fa6d2960b48e30ab88fe195bbad161b8a6f9a59a45ca86b5e2f20593f76cd", + "validator:signature:bls12_381:ac9b6d340bf1b04421455676b8a28a8de079cd9b40f75f1009aa3da32981690bc520d4ec0284ae030fc8b036d86ca307", + ); + } + + #[test] + fn leader_proposal_change_detector() { + msg_change_detector( + leader_proposal().insert(), + "validator_msg:keccak256:7b079e4ca3021834fa35745cb042fea6dd5bb89a91ca5ba31ed6ba1765a1e113", + "validator:signature:bls12_381:98ca0f24d87f938b22ac9c2a2720466cd157a502b31ae5627ce5fdbda6de0ad6d2e9b159cf816cd1583644f2f69ecb84", + ); + } +} diff --git a/node/libs/roles/src/validator/testonly.rs b/node/libs/roles/src/validator/testonly.rs index c9800a4f..b88768d9 100644 --- a/node/libs/roles/src/validator/testonly.rs +++ b/node/libs/roles/src/validator/testonly.rs @@ -38,10 +38,6 @@ pub struct SetupSpec { pub leader_selection: LeaderSelectionMode, } -/// Test setup. -#[derive(Debug, Clone)] -pub struct Setup(SetupInner); - impl SetupSpec { /// New `SetupSpec`. pub fn new(rng: &mut impl Rng, validators: usize) -> Self { @@ -68,6 +64,10 @@ impl SetupSpec { } } +/// Test setup. +#[derive(Debug, Clone)] +pub struct Setup(SetupInner); + impl Setup { /// New `Setup`. pub fn new(rng: &mut impl Rng, validators: usize) -> Self { @@ -81,6 +81,51 @@ impl Setup { Self::from_spec(rng, spec) } + /// Generates a new `Setup` from the given `SetupSpec`. + pub fn from_spec(rng: &mut impl Rng, spec: SetupSpec) -> Self { + let mut this = Self(SetupInner { + genesis: GenesisRaw { + chain_id: spec.chain_id, + fork_number: spec.fork_number, + first_block: spec.first_block, + + protocol_version: spec.protocol_version, + validators: Committee::new(spec.validator_weights.iter().map(|(k, w)| { + WeightedValidator { + key: k.public(), + weight: *w, + } + })) + .unwrap(), + attesters: attester::Committee::new(spec.attester_weights.iter().map(|(k, w)| { + attester::WeightedAttester { + key: k.public(), + weight: *w, + } + })) + .unwrap() + .into(), + leader_selection: spec.leader_selection, + } + .with_hash(), + validator_keys: spec.validator_weights.into_iter().map(|(k, _)| k).collect(), + attester_keys: spec.attester_weights.into_iter().map(|(k, _)| k).collect(), + blocks: vec![], + }); + // Populate pregenesis blocks. + for block in spec.first_pregenesis_block.0..spec.first_block.0 { + this.0.blocks.push( + PreGenesisBlock { + number: BlockNumber(block), + payload: rng.gen(), + justification: rng.gen(), + } + .into(), + ); + } + this + } + /// Next block to finalize. pub fn next(&self) -> BlockNumber { match self.0.blocks.last() { @@ -144,52 +189,107 @@ impl Setup { let first = self.0.blocks.first()?.number(); self.0.blocks.get(n.0.checked_sub(first.0)? as usize) } -} -impl Setup { - /// Generates a new `Setup` from the given `SetupSpec`. - pub fn from_spec(rng: &mut impl Rng, spec: SetupSpec) -> Self { - let mut this = Self(SetupInner { - genesis: GenesisRaw { - chain_id: spec.chain_id, - fork_number: spec.fork_number, - first_block: spec.first_block, + /// Creates a View with the given view number. + pub fn make_view(&self, number: ViewNumber) -> View { + View { + genesis: self.genesis.hash(), + number, + } + } - protocol_version: spec.protocol_version, - validators: Committee::new(spec.validator_weights.iter().map(|(k, w)| { - WeightedValidator { - key: k.public(), - weight: *w, - } - })) - .unwrap(), - attesters: attester::Committee::new(spec.attester_weights.iter().map(|(k, w)| { - attester::WeightedAttester { - key: k.public(), - weight: *w, - } - })) - .unwrap() - .into(), - leader_selection: spec.leader_selection, + /// Creates a ReplicaCommt with a random payload. + pub fn make_replica_commit(&self, rng: &mut impl Rng, view: ViewNumber) -> ReplicaCommit { + ReplicaCommit { + view: self.make_view(view), + proposal: BlockHeader { + number: self.next(), + payload: rng.gen(), + }, + } + } + + /// Creates a ReplicaCommt with the given payload. + pub fn make_replica_commit_with_payload( + &self, + payload: &Payload, + view: ViewNumber, + ) -> ReplicaCommit { + ReplicaCommit { + view: self.make_view(view), + proposal: BlockHeader { + number: self.next(), + payload: payload.hash(), + }, + } + } + + /// Creates a CommitQC with a random payload. + pub fn make_commit_qc(&self, rng: &mut impl Rng, view: ViewNumber) -> CommitQC { + let mut qc = CommitQC::new(self.make_replica_commit(rng, view), &self.genesis); + for key in &self.validator_keys { + qc.add(&key.sign_msg(qc.message.clone()), &self.genesis) + .unwrap(); + } + qc + } + + /// Creates a CommitQC with the given payload. + pub fn make_commit_qc_with_payload(&self, payload: &Payload, view: ViewNumber) -> CommitQC { + let mut qc = CommitQC::new( + self.make_replica_commit_with_payload(payload, view), + &self.genesis, + ); + for key in &self.validator_keys { + qc.add(&key.sign_msg(qc.message.clone()), &self.genesis) + .unwrap(); + } + qc + } + + /// Creates a ReplicaTimeout with a random payload. + pub fn make_replica_timeout(&self, rng: &mut impl Rng, view: ViewNumber) -> ReplicaTimeout { + let high_vote_view = ViewNumber(rng.gen_range(0..view.0)); + let high_qc_view = ViewNumber(rng.gen_range(0..high_vote_view.0)); + ReplicaTimeout { + view: self.make_view(view), + high_vote: Some(self.make_replica_commit(rng, high_vote_view)), + high_qc: Some(self.make_commit_qc(rng, high_qc_view)), + } + } + + /// Creates a TimeoutQC. If a payload is given, the QC will contain a + /// re-proposal for that payload + pub fn make_timeout_qc( + &self, + rng: &mut impl Rng, + view: ViewNumber, + payload_opt: Option<&Payload>, + ) -> TimeoutQC { + let mut vote = if let Some(payload) = payload_opt { + self.make_replica_commit_with_payload(payload, view.prev().unwrap()) + } else { + self.make_replica_commit(rng, view.prev().unwrap()) + }; + let commit_qc = match self.0.blocks.last().unwrap() { + Block::Final(block) => block.justification.clone(), + _ => unreachable!(), + }; + + let mut qc = TimeoutQC::new(self.make_view(view)); + for key in &self.validator_keys { + if payload_opt.is_none() { + vote.proposal.payload = rng.gen(); } - .with_hash(), - validator_keys: spec.validator_weights.into_iter().map(|(k, _)| k).collect(), - attester_keys: spec.attester_weights.into_iter().map(|(k, _)| k).collect(), - blocks: vec![], - }); - // Populate pregenesis blocks. - for block in spec.first_pregenesis_block.0..spec.first_block.0 { - this.0.blocks.push( - PreGenesisBlock { - number: BlockNumber(block), - payload: rng.gen(), - justification: rng.gen(), - } - .into(), - ); + let msg = ReplicaTimeout { + view: self.make_view(view), + high_vote: Some(vote.clone()), + high_qc: Some(commit_qc.clone()), + }; + qc.add(&key.sign_msg(msg), &self.genesis).unwrap(); } - this + + qc } } diff --git a/node/libs/roles/src/validator/tests.rs b/node/libs/roles/src/validator/tests.rs index f2616d49..95d0f190 100644 --- a/node/libs/roles/src/validator/tests.rs +++ b/node/libs/roles/src/validator/tests.rs @@ -1,11 +1,8 @@ use super::*; -use crate::validator::testonly::Setup; -use assert_matches::assert_matches; -use rand::{seq::SliceRandom, Rng}; -use std::vec; +use rand::Rng; use zksync_concurrency::ctx; use zksync_consensus_crypto::{ByteFmt, Text, TextFmt}; -use zksync_protobuf::{testonly::test_encode_random, ProtoFmt}; +use zksync_protobuf::testonly::test_encode_random; #[test] fn test_byte_encoding() { @@ -102,432 +99,3 @@ fn test_schema_encoding() { test_encode_random::(rng); test_encode_random::(rng); } - -#[test] -fn test_genesis_verify() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - let genesis = Setup::new(rng, 1).genesis.clone(); - assert!(genesis.verify().is_ok()); - assert!(Genesis::read(&genesis.build()).is_ok()); - - let mut genesis = (*genesis).clone(); - genesis.leader_selection = LeaderSelectionMode::Sticky(rng.gen()); - let genesis = genesis.with_hash(); - assert!(genesis.verify().is_err()); - assert!(Genesis::read(&genesis.build()).is_err()) -} - -#[test] -fn test_signature_verify() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - let msg1: MsgHash = rng.gen(); - let msg2: MsgHash = rng.gen(); - - let key1: SecretKey = rng.gen(); - let key2: SecretKey = rng.gen(); - - let sig1 = key1.sign_hash(&msg1); - - // Matching key and message. - sig1.verify_hash(&msg1, &key1.public()).unwrap(); - - // Mismatching message. - assert!(sig1.verify_hash(&msg2, &key1.public()).is_err()); - - // Mismatching key. - assert!(sig1.verify_hash(&msg1, &key2.public()).is_err()); -} - -#[test] -fn test_agg_signature_verify() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - let msg1: MsgHash = rng.gen(); - let msg2: MsgHash = rng.gen(); - - let key1: SecretKey = rng.gen(); - let key2: SecretKey = rng.gen(); - - let sig1 = key1.sign_hash(&msg1); - let sig2 = key2.sign_hash(&msg2); - - let agg_sig = AggregateSignature::aggregate(vec![&sig1, &sig2]); - - // Matching key and message. - agg_sig - .verify_hash([(msg1, &key1.public()), (msg2, &key2.public())].into_iter()) - .unwrap(); - - // Mismatching message. - assert!(agg_sig - .verify_hash([(msg2, &key1.public()), (msg1, &key2.public())].into_iter()) - .is_err()); - - // Mismatching key. - assert!(agg_sig - .verify_hash([(msg1, &key2.public()), (msg2, &key1.public())].into_iter()) - .is_err()); -} - -fn make_view(number: ViewNumber, setup: &Setup) -> View { - View { - genesis: setup.genesis.hash(), - number, - } -} - -fn make_replica_commit(rng: &mut impl Rng, view: ViewNumber, setup: &Setup) -> ReplicaCommit { - ReplicaCommit { - view: make_view(view, setup), - proposal: rng.gen(), - } -} - -fn make_commit_qc(rng: &mut impl Rng, view: ViewNumber, setup: &Setup) -> CommitQC { - let mut qc = CommitQC::new(make_replica_commit(rng, view, setup), &setup.genesis); - for key in &setup.validator_keys { - qc.add(&key.sign_msg(qc.message.clone()), &setup.genesis) - .unwrap(); - } - qc -} - -fn make_replica_timeout(rng: &mut impl Rng, view: ViewNumber, setup: &Setup) -> ReplicaTimeout { - ReplicaTimeout { - view: make_view(view, setup), - high_vote: { - let view = ViewNumber(rng.gen_range(0..view.0)); - Some(make_replica_commit(rng, view, setup)) - }, - high_qc: { - let view = ViewNumber(rng.gen_range(0..view.0)); - Some(make_commit_qc(rng, view, setup)) - }, - } -} - -#[test] -fn test_commit_qc() { - use CommitQCVerifyError as Error; - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - // This will create equally weighted validators - let setup1 = Setup::new(rng, 6); - let setup2 = Setup::new(rng, 6); - let mut genesis3 = (*setup1.genesis).clone(); - genesis3.validators = - Committee::new(setup1.genesis.validators.iter().take(3).cloned()).unwrap(); - let genesis3 = genesis3.with_hash(); - - for i in 0..setup1.validator_keys.len() + 1 { - let view = rng.gen(); - let mut qc = CommitQC::new(make_replica_commit(rng, view, &setup1), &setup1.genesis); - for key in &setup1.validator_keys[0..i] { - qc.add(&key.sign_msg(qc.message.clone()), &setup1.genesis) - .unwrap(); - } - let expected_weight: u64 = setup1 - .genesis - .validators - .iter() - .take(i) - .map(|w| w.weight) - .sum(); - if expected_weight >= setup1.genesis.validators.quorum_threshold() { - qc.verify(&setup1.genesis).unwrap(); - } else { - assert_matches!( - qc.verify(&setup1.genesis), - Err(Error::NotEnoughSigners { .. }) - ); - } - - // Mismatching validator sets. - assert!(qc.verify(&setup2.genesis).is_err()); - assert!(qc.verify(&genesis3).is_err()); - } -} - -#[test] -fn test_commit_qc_add_errors() { - use CommitQCAddError as Error; - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - let setup = Setup::new(rng, 2); - let view = rng.gen(); - let mut qc = CommitQC::new(make_replica_commit(rng, view, &setup), &setup.genesis); - let msg = qc.message.clone(); - // Add the message - assert_matches!( - qc.add( - &setup.validator_keys[0].sign_msg(msg.clone()), - &setup.genesis - ), - Ok(()) - ); - - // Try to add a message for a different view - let mut msg1 = msg.clone(); - msg1.view.number = view.next(); - assert_matches!( - qc.add(&setup.validator_keys[0].sign_msg(msg1), &setup.genesis), - Err(Error::InconsistentMessages { .. }) - ); - - // Try to add a message from a signer not in committee - assert_matches!( - qc.add( - &rng.gen::().sign_msg(msg.clone()), - &setup.genesis - ), - Err(Error::SignerNotInCommittee { .. }) - ); - - // Try to add the same message already added by same validator - assert_matches!( - qc.add( - &setup.validator_keys[0].sign_msg(msg.clone()), - &setup.genesis - ), - Err(Error::DuplicateSigner { .. }) - ); - - // Add same message signed by another validator. - assert_matches!( - qc.add(&setup.validator_keys[1].sign_msg(msg), &setup.genesis), - Ok(()) - ); -} - -#[test] -fn test_timeout_qc() { - use TimeoutQCVerifyError as Error; - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - // This will create equally weighted validators - let setup1 = Setup::new(rng, 6); - let setup2 = Setup::new(rng, 6); - let mut genesis3 = (*setup1.genesis).clone(); - genesis3.validators = - Committee::new(setup1.genesis.validators.iter().take(3).cloned()).unwrap(); - let genesis3 = genesis3.with_hash(); - - let view: ViewNumber = rng.gen(); - let msgs: Vec<_> = (0..3) - .map(|_| make_replica_timeout(rng, view, &setup1)) - .collect(); - - for n in 0..setup1.validator_keys.len() + 1 { - let mut qc = TimeoutQC::new(msgs[0].view.clone()); - for key in &setup1.validator_keys[0..n] { - qc.add( - &key.sign_msg(msgs.choose(rng).unwrap().clone()), - &setup1.genesis, - ) - .unwrap(); - } - let expected_weight: u64 = setup1 - .genesis - .validators - .iter() - .take(n) - .map(|w| w.weight) - .sum(); - if expected_weight >= setup1.genesis.validators.quorum_threshold() { - qc.verify(&setup1.genesis).unwrap(); - } else { - assert_matches!( - qc.verify(&setup1.genesis), - Err(Error::NotEnoughSigners { .. }) - ); - } - - // Mismatching validator sets. - assert!(qc.verify(&setup2.genesis).is_err()); - assert!(qc.verify(&genesis3).is_err()); - } -} - -#[test] -fn test_prepare_qc_high_vote() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - // This will create equally weighted validators - let setup = Setup::new(rng, 6); - - let view_num: ViewNumber = rng.gen(); - let msg_a = make_replica_timeout(rng, view_num, &setup); - let msg_b = make_replica_timeout(rng, view_num, &setup); - let msg_c = make_replica_timeout(rng, view_num, &setup); - - // Case with 1 subquorum. - let mut qc = TimeoutQC::new(msg_a.view.clone()); - - for key in &setup.validator_keys { - qc.add(&key.sign_msg(msg_a.clone()), &setup.genesis) - .unwrap(); - } - - assert!(qc.high_vote(&setup.genesis).is_some()); - - // Case with 2 subquorums. - let mut qc = TimeoutQC::new(msg_a.view.clone()); - - for key in &setup.validator_keys[0..3] { - qc.add(&key.sign_msg(msg_a.clone()), &setup.genesis) - .unwrap(); - } - - for key in &setup.validator_keys[3..6] { - qc.add(&key.sign_msg(msg_b.clone()), &setup.genesis) - .unwrap(); - } - - assert!(qc.high_vote(&setup.genesis).is_none()); - - // Case with no subquorums. - let mut qc = TimeoutQC::new(msg_a.view.clone()); - - for key in &setup.validator_keys[0..2] { - qc.add(&key.sign_msg(msg_a.clone()), &setup.genesis) - .unwrap(); - } - - for key in &setup.validator_keys[2..4] { - qc.add(&key.sign_msg(msg_b.clone()), &setup.genesis) - .unwrap(); - } - - for key in &setup.validator_keys[4..6] { - qc.add(&key.sign_msg(msg_c.clone()), &setup.genesis) - .unwrap(); - } - - assert!(qc.high_vote(&setup.genesis).is_none()); -} - -#[test] -fn test_prepare_qc_add_errors() { - use TimeoutQCAddError as Error; - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - let setup = Setup::new(rng, 2); - let view = rng.gen(); - let msg = make_replica_timeout(rng, view, &setup); - let mut qc = TimeoutQC::new(msg.view.clone()); - let msg = make_replica_timeout(rng, view, &setup); - - // Add the message - assert_matches!( - qc.add( - &setup.validator_keys[0].sign_msg(msg.clone()), - &setup.genesis - ), - Ok(()) - ); - - // Try to add a message for a different view - let mut msg1 = msg.clone(); - msg1.view.number = view.next(); - assert_matches!( - qc.add(&setup.validator_keys[0].sign_msg(msg1), &setup.genesis), - Err(Error::InconsistentViews { .. }) - ); - - // Try to add a message from a signer not in committee - assert_matches!( - qc.add( - &rng.gen::().sign_msg(msg.clone()), - &setup.genesis - ), - Err(Error::SignerNotInCommittee { .. }) - ); - - // Try to add the same message already added by same validator - assert_matches!( - qc.add( - &setup.validator_keys[0].sign_msg(msg.clone()), - &setup.genesis - ), - Err(Error::DuplicateSigner { .. }) - ); - - // Try to add a message for a validator that already added another message - let msg2 = make_replica_timeout(rng, view, &setup); - assert_matches!( - qc.add(&setup.validator_keys[0].sign_msg(msg2), &setup.genesis), - Err(Error::DuplicateSigner { .. }) - ); - - // Add same message signed by another validator. - assert_matches!( - qc.add( - &setup.validator_keys[1].sign_msg(msg.clone()), - &setup.genesis - ), - Ok(()) - ); -} - -#[test] -fn test_validator_committee_weights() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - // Validators with non-uniform weights - let setup = Setup::new_with_weights(rng, vec![1000, 600, 800, 6000, 900, 700]); - // Expected sum of the validators weights - let sums = [1000, 1600, 2400, 8400, 9300, 10000]; - - let view: ViewNumber = rng.gen(); - let msg = make_replica_timeout(rng, view, &setup); - let mut qc = TimeoutQC::new(msg.view.clone()); - for (n, weight) in sums.iter().enumerate() { - let key = &setup.validator_keys[n]; - qc.add(&key.sign_msg(msg.clone()), &setup.genesis).unwrap(); - let signers = &qc.map[&msg]; - assert_eq!(setup.genesis.validators.weight(signers), *weight); - } -} - -#[test] -fn test_committee_weights_overflow_check() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - let validators: Vec = [u64::MAX / 5; 6] - .iter() - .map(|w| WeightedValidator { - key: rng.gen::().public(), - weight: *w, - }) - .collect(); - - // Creation should overflow - assert_matches!(Committee::new(validators), Err(_)); -} - -#[test] -fn test_committee_with_zero_weights() { - let ctx = ctx::test_root(&ctx::RealClock); - let rng = &mut ctx.rng(); - - let validators: Vec = [1000, 0, 800, 6000, 0, 700] - .iter() - .map(|w| WeightedValidator { - key: rng.gen::().public(), - weight: *w, - }) - .collect(); - - // Committee creation should error on zero weight validators - assert_matches!(Committee::new(validators), Err(_)); -}