snarkos_node_bft/helpers/
proposal.rsuse snarkvm::{
console::{
account::{Address, Signature},
network::Network,
types::Field,
},
ledger::{
committee::Committee,
narwhal::{BatchCertificate, BatchHeader, Transmission, TransmissionID},
},
prelude::{FromBytes, IoResult, Itertools, Read, Result, ToBytes, Write, bail, ensure, error},
};
use indexmap::{IndexMap, IndexSet};
use std::collections::HashSet;
#[derive(Debug, PartialEq, Eq)]
pub struct Proposal<N: Network> {
batch_header: BatchHeader<N>,
transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
signatures: IndexSet<Signature<N>>,
}
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!(
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.iter().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>,
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()]) {
bail!("Signature verification failed")
}
self.signatures.insert(signature);
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::from(self.batch_header.clone(), self.signatures.clone())?;
Ok((certificate, self.transmissions.clone()))
}
}
impl<N: Network> ToBytes for Proposal<N> {
fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
self.batch_header.write_le(&mut writer)?;
u32::try_from(self.transmissions.len()).map_err(error)?.write_le(&mut writer)?;
for (transmission_id, transmission) in &self.transmissions {
transmission_id.write_le(&mut writer)?;
transmission.write_le(&mut writer)?;
}
u32::try_from(self.signatures.len()).map_err(error)?.write_le(&mut writer)?;
for signature in &self.signatures {
signature.write_le(&mut writer)?;
}
Ok(())
}
}
impl<N: Network> FromBytes for Proposal<N> {
fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
let batch_header = FromBytes::read_le(&mut reader)?;
let num_transmissions = u32::read_le(&mut reader)?;
if num_transmissions as usize > BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
return Err(error("Invalid number of transmissions in the proposal"));
}
let mut transmissions = IndexMap::default();
for _ in 0..num_transmissions {
let transmission_id = FromBytes::read_le(&mut reader)?;
let transmission = FromBytes::read_le(&mut reader)?;
transmissions.insert(transmission_id, transmission);
}
let num_signatures = u32::read_le(&mut reader)?;
if num_signatures as usize > Committee::<N>::MAX_COMMITTEE_SIZE as usize {
return Err(error("Invalid number of signatures in the proposal"));
}
let mut signatures = IndexSet::default();
for _ in 0..num_signatures {
signatures.insert(FromBytes::read_le(&mut reader)?);
}
Ok(Self { batch_header, transmissions, signatures })
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::helpers::storage::tests::sample_transmissions;
use snarkvm::{console::network::MainnetV0, utilities::TestRng};
type CurrentNetwork = MainnetV0;
const ITERATIONS: usize = 100;
pub(crate) fn sample_proposal(rng: &mut TestRng) -> Proposal<CurrentNetwork> {
let certificate = snarkvm::ledger::narwhal::batch_certificate::test_helpers::sample_batch_certificate(rng);
let (_, transmissions) = sample_transmissions(&certificate, rng);
let transmissions = transmissions.into_iter().map(|(id, (t, _))| (id, t)).collect::<IndexMap<_, _>>();
let batch_header = certificate.batch_header().clone();
let signatures = certificate.signatures().copied().collect();
Proposal { batch_header, transmissions, signatures }
}
#[test]
fn test_bytes() {
let rng = &mut TestRng::default();
for _ in 0..ITERATIONS {
let expected = sample_proposal(rng);
let expected_bytes = expected.to_bytes_le().unwrap();
assert_eq!(expected, Proposal::read_le(&expected_bytes[..]).unwrap());
}
}
}
#[cfg(test)]
mod prop_tests {
use crate::helpers::{
Proposal,
now,
storage::prop_tests::{AnyTransmission, AnyTransmissionID, CryptoTestRng},
};
use snarkvm::ledger::{
committee::prop_tests::{CommitteeContext, ValidatorSet},
narwhal::BatchHeader,
};
use indexmap::IndexMap;
use proptest::sample::{Selector, size_range};
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(),
committee.id(),
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());
}
}