use crate::{common::Author, quorum_cert::QuorumCert};
use anyhow::{ensure, Context};
use aptos_crypto::bls12381;
use aptos_crypto_derive::{BCSCryptoHash, CryptoHasher};
use aptos_types::{
block_info::Round, validator_signer::ValidatorSigner, validator_verifier::ValidatorVerifier,
};
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
fmt::{Display, Formatter},
};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TwoChainTimeout {
epoch: u64,
round: Round,
quorum_cert: QuorumCert,
}
impl TwoChainTimeout {
pub fn new(epoch: u64, round: Round, quorum_cert: QuorumCert) -> Self {
Self {
epoch,
round,
quorum_cert,
}
}
pub fn epoch(&self) -> u64 {
self.epoch
}
pub fn round(&self) -> Round {
self.round
}
pub fn hqc_round(&self) -> Round {
self.quorum_cert.certified_block().round()
}
pub fn quorum_cert(&self) -> &QuorumCert {
&self.quorum_cert
}
pub fn sign(&self, signer: &ValidatorSigner) -> bls12381::Signature {
signer.sign(&self.signing_format())
}
pub fn signing_format(&self) -> TimeoutSigningRepr {
TimeoutSigningRepr {
epoch: self.epoch(),
round: self.round(),
hqc_round: self.hqc_round(),
}
}
pub fn verify(&self, validators: &ValidatorVerifier) -> anyhow::Result<()> {
ensure!(
self.hqc_round() < self.round(),
"Timeout round should be larger than the QC round"
);
self.quorum_cert.verify(validators)?;
Ok(())
}
}
impl Display for TwoChainTimeout {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"Timeout: [epoch: {}, round: {}, hqc_round: {}]",
self.epoch,
self.round,
self.hqc_round(),
)
}
}
#[derive(Serialize, Deserialize, CryptoHasher, BCSCryptoHash)]
pub struct TimeoutSigningRepr {
pub epoch: u64,
pub round: Round,
pub hqc_round: Round,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct TwoChainTimeoutCertificate {
timeout: TwoChainTimeout,
signatures: BTreeMap<Author, (Round, bls12381::Signature)>,
}
impl Display for TwoChainTimeoutCertificate {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"TimeoutCertificate[epoch: {}, round: {}, hqc_round: {}]",
self.timeout.epoch(),
self.timeout.round(),
self.timeout.hqc_round(),
)
}
}
impl TwoChainTimeoutCertificate {
pub fn new(timeout: TwoChainTimeout) -> Self {
Self {
timeout,
signatures: BTreeMap::new(),
}
}
pub fn verify(&self, validators: &ValidatorVerifier) -> anyhow::Result<()> {
self.timeout.verify(validators)?;
let hqc_round = self.timeout.hqc_round();
let mut signed_round = 0;
validators.check_voting_power(self.signatures.keys())?;
for (author, (qc_round, signature)) in &self.signatures {
let t = TimeoutSigningRepr {
epoch: self.timeout.epoch(),
round: self.timeout.round(),
hqc_round: *qc_round,
};
validators
.verify(*author, &t, signature)
.with_context(|| format!("Failed to verify {}'s TimeoutSigningRepr", *author))?;
signed_round = std::cmp::max(signed_round, *qc_round);
}
ensure!(
hqc_round == signed_round,
"Inconsistent hqc round, qc has round {}, highest signed round {}",
hqc_round,
signed_round
);
Ok(())
}
pub fn epoch(&self) -> u64 {
self.timeout.epoch()
}
pub fn round(&self) -> Round {
self.timeout.round()
}
pub fn highest_hqc_round(&self) -> Round {
self.timeout.hqc_round()
}
pub fn signers(&self) -> impl Iterator<Item = &Author> {
self.signatures.iter().map(|(k, _)| k)
}
pub fn add(
&mut self,
author: Author,
timeout: TwoChainTimeout,
signature: bls12381::Signature,
) {
debug_assert_eq!(
self.timeout.epoch(),
timeout.epoch(),
"Timeout should have the same epoch as TimeoutCert"
);
debug_assert_eq!(
self.timeout.round(),
timeout.round(),
"Timeout should have the same round as TimeoutCert"
);
let hqc_round = timeout.hqc_round();
if timeout.hqc_round() > self.timeout.hqc_round() {
self.timeout = timeout;
}
self.signatures.insert(author, (hqc_round, signature));
}
}
#[test]
fn test_2chain_timeout_certificate() {
use crate::vote_data::VoteData;
use aptos_crypto::hash::CryptoHash;
use aptos_types::{
block_info::BlockInfo,
ledger_info::{LedgerInfo, LedgerInfoWithSignatures},
validator_verifier::random_validator_verifier,
};
let num_nodes = 4;
let (signers, validators) = random_validator_verifier(num_nodes, None, false);
let quorum_size = validators.quorum_voting_power() as usize;
let generate_quorum = |round, num_of_signature| {
let vote_data = VoteData::new(BlockInfo::random(round), BlockInfo::random(0));
let mut ledger_info = LedgerInfoWithSignatures::new(
LedgerInfo::new(BlockInfo::empty(), vote_data.hash()),
BTreeMap::new(),
);
for signer in &signers[0..num_of_signature] {
let signature = signer.sign(ledger_info.ledger_info());
ledger_info.add_signature(signer.author(), signature);
}
QuorumCert::new(vote_data, ledger_info)
};
let generate_timeout =
|round, qc_round| TwoChainTimeout::new(1, round, generate_quorum(qc_round, quorum_size));
let timeouts: Vec<_> = (1..=3)
.map(|qc_round| generate_timeout(4, qc_round))
.collect();
let mut valid_timeout_cert = TwoChainTimeoutCertificate::new(timeouts[0].clone());
for (timeout, signer) in timeouts.iter().zip(&signers) {
valid_timeout_cert.add(signer.author(), timeout.clone(), timeout.sign(signer));
}
valid_timeout_cert.verify(&validators).unwrap();
let mut invalid_timeout_cert = valid_timeout_cert.clone();
invalid_timeout_cert.timeout.round = 1;
invalid_timeout_cert.verify(&validators).unwrap_err();
let mut invalid_timeout_cert = valid_timeout_cert.clone();
invalid_timeout_cert
.signatures
.get_mut(&signers[0].author())
.unwrap()
.1 = bls12381::Signature::dummy_signature();
invalid_timeout_cert.verify(&validators).unwrap_err();
let mut invalid_timeout_cert = valid_timeout_cert.clone();
invalid_timeout_cert
.signatures
.remove(&signers[0].author())
.unwrap();
invalid_timeout_cert.verify(&validators).unwrap_err();
let mut invalid_timeout_cert = valid_timeout_cert.clone();
invalid_timeout_cert.timeout.quorum_cert = generate_quorum(2, quorum_size);
invalid_timeout_cert.verify(&validators).unwrap_err();
let mut invalid_timeout_cert = valid_timeout_cert;
invalid_timeout_cert.timeout.quorum_cert = generate_quorum(3, quorum_size - 1);
invalid_timeout_cert.verify(&validators).unwrap_err();
}