use crate::helpers::check_timestamp_for_liveness;
use snarkvm::{
console::{
account::{Address, Signature},
network::Network,
types::Field,
},
ledger::{
committee::Committee,
narwhal::{BatchCertificate, BatchHeader, Transmission, TransmissionID},
},
prelude::{bail, ensure, Itertools, Result},
};
use indexmap::IndexMap;
use std::collections::HashSet;
pub struct Proposal<N: Network> {
batch_header: BatchHeader<N>,
transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
signatures: IndexMap<Signature<N>, i64>,
}
impl<N: Network> Proposal<N> {
pub fn new(
committee: Committee<N>,
batch_header: BatchHeader<N>,
transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
) -> Result<Self> {
ensure!(batch_header.round() >= committee.starting_round(), "Batch round must be >= the committee round");
ensure!(committee.is_committee_member(batch_header.author()), "The batch author is not a committee member");
ensure!(!transmissions.is_empty(), "The transmissions are empty");
ensure!(
batch_header.transmission_ids().len() == transmissions.len(),
"The transmission IDs do not match in the batch header and transmissions"
);
for (a, b) in batch_header.transmission_ids().iter().zip_eq(transmissions.keys()) {
ensure!(a == b, "The transmission IDs do not match in the batch header and transmissions");
}
Ok(Self { batch_header, transmissions, signatures: Default::default() })
}
pub const fn batch_header(&self) -> &BatchHeader<N> {
&self.batch_header
}
pub const fn batch_id(&self) -> Field<N> {
self.batch_header.batch_id()
}
pub const fn round(&self) -> u64 {
self.batch_header.round()
}
pub const fn timestamp(&self) -> i64 {
self.batch_header.timestamp()
}
pub const fn transmissions(&self) -> &IndexMap<TransmissionID<N>, Transmission<N>> {
&self.transmissions
}
pub fn into_transmissions(self) -> IndexMap<TransmissionID<N>, Transmission<N>> {
self.transmissions
}
pub fn signers(&self) -> HashSet<Address<N>> {
self.signatures.keys().chain(Some(self.batch_header.signature())).map(Signature::to_address).collect()
}
pub fn nonsigners(&self, committee: &Committee<N>) -> HashSet<Address<N>> {
let signers = self.signers();
let mut nonsigners = HashSet::new();
for address in committee.members().keys() {
if !signers.contains(address) {
nonsigners.insert(*address);
}
}
nonsigners
}
pub fn is_quorum_threshold_reached(&self, committee: &Committee<N>) -> bool {
committee.is_quorum_threshold_reached(&self.signers())
}
pub fn contains_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
self.transmissions.contains_key(&transmission_id.into())
}
pub fn get_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> Option<&Transmission<N>> {
self.transmissions.get(&transmission_id.into())
}
pub fn add_signature(
&mut self,
signer: Address<N>,
signature: Signature<N>,
timestamp: i64,
committee: &Committee<N>,
) -> Result<()> {
if !committee.is_committee_member(signer) {
bail!("Signature from a non-committee member - '{signer}'")
}
if self.signers().contains(&signer) {
bail!("Duplicate signature from '{signer}'")
}
if !signature.verify(&signer, &[self.batch_id(), Field::from_u64(timestamp as u64)]) {
bail!("Signature verification failed")
}
check_timestamp_for_liveness(timestamp)?;
self.signatures.insert(signature, timestamp);
Ok(())
}
pub fn to_certificate(
&self,
committee: &Committee<N>,
) -> Result<(BatchCertificate<N>, IndexMap<TransmissionID<N>, Transmission<N>>)> {
ensure!(self.is_quorum_threshold_reached(committee), "The quorum threshold has not been reached");
let certificate = BatchCertificate::new(self.batch_header.clone(), self.signatures.clone())?;
Ok((certificate, self.transmissions.clone()))
}
}
#[cfg(test)]
mod prop_tests {
use crate::helpers::{
now,
storage::prop_tests::{AnyTransmission, AnyTransmissionID, CryptoTestRng},
Proposal,
};
use snarkvm::ledger::{
committee::prop_tests::{CommitteeContext, ValidatorSet},
narwhal::BatchHeader,
};
use indexmap::IndexMap;
use proptest::sample::{size_range, Selector};
use test_strategy::proptest;
#[proptest]
fn initialize_proposal(
context: CommitteeContext,
#[any(size_range(1..16).lift())] transmissions: Vec<(AnyTransmissionID, AnyTransmission)>,
selector: Selector,
mut rng: CryptoTestRng,
) {
let CommitteeContext(committee, ValidatorSet(validators)) = context;
let signer = selector.select(&validators);
let mut transmission_map = IndexMap::new();
for (AnyTransmissionID(id), AnyTransmission(t)) in transmissions.iter() {
transmission_map.insert(*id, t.clone());
}
let header = BatchHeader::new(
&signer.private_key,
committee.starting_round(),
now(),
transmission_map.keys().cloned().collect(),
Default::default(),
&mut rng,
)
.unwrap();
let proposal = Proposal::new(committee, header.clone(), transmission_map.clone()).unwrap();
assert_eq!(proposal.batch_id(), header.batch_id());
}
}