amareleo_node_bft/helpers/
proposal.rs1use snarkvm::{
17 console::{
18 account::{Address, Signature},
19 network::Network,
20 types::Field,
21 },
22 ledger::{
23 committee::Committee,
24 narwhal::{BatchCertificate, BatchHeader, Transmission, TransmissionID},
25 },
26 prelude::{FromBytes, IoResult, Itertools, Read, Result, ToBytes, Write, bail, ensure, error},
27};
28
29use indexmap::{IndexMap, IndexSet};
30use std::collections::HashSet;
31
32#[derive(Debug, PartialEq, Eq)]
33pub struct Proposal<N: Network> {
34 batch_header: BatchHeader<N>,
36 transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
38 signatures: IndexSet<Signature<N>>,
40}
41
42impl<N: Network> Proposal<N> {
43 pub fn new(
45 committee: Committee<N>,
46 batch_header: BatchHeader<N>,
47 transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
48 ) -> Result<Self> {
49 ensure!(batch_header.round() >= committee.starting_round(), "Batch round must be >= the committee round");
51 ensure!(committee.is_committee_member(batch_header.author()), "The batch author is not a committee member");
53 ensure!(
55 batch_header.transmission_ids().len() == transmissions.len(),
56 "The transmission IDs do not match in the batch header and transmissions"
57 );
58 for (a, b) in batch_header.transmission_ids().iter().zip_eq(transmissions.keys()) {
59 ensure!(a == b, "The transmission IDs do not match in the batch header and transmissions");
60 }
61 Ok(Self { batch_header, transmissions, signatures: Default::default() })
63 }
64
65 pub const fn batch_header(&self) -> &BatchHeader<N> {
67 &self.batch_header
68 }
69
70 pub const fn batch_id(&self) -> Field<N> {
72 self.batch_header.batch_id()
73 }
74
75 pub const fn round(&self) -> u64 {
77 self.batch_header.round()
78 }
79
80 pub const fn timestamp(&self) -> i64 {
82 self.batch_header.timestamp()
83 }
84
85 pub const fn transmissions(&self) -> &IndexMap<TransmissionID<N>, Transmission<N>> {
87 &self.transmissions
88 }
89
90 pub fn into_transmissions(self) -> IndexMap<TransmissionID<N>, Transmission<N>> {
92 self.transmissions
93 }
94
95 pub fn signers(&self) -> HashSet<Address<N>> {
97 self.signatures.iter().chain(Some(self.batch_header.signature())).map(Signature::to_address).collect()
98 }
99
100 pub fn nonsigners(&self, committee: &Committee<N>) -> HashSet<Address<N>> {
102 let signers = self.signers();
104 let mut nonsigners = HashSet::new();
106 for address in committee.members().keys() {
108 if !signers.contains(address) {
110 nonsigners.insert(*address);
111 }
112 }
113 nonsigners
115 }
116
117 pub fn is_quorum_threshold_reached(&self, committee: &Committee<N>) -> bool {
119 committee.is_quorum_threshold_reached(&self.signers())
121 }
122
123 pub fn contains_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
125 self.transmissions.contains_key(&transmission_id.into())
126 }
127
128 pub fn get_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> Option<&Transmission<N>> {
130 self.transmissions.get(&transmission_id.into())
131 }
132
133 pub fn add_signature(
135 &mut self,
136 signer: Address<N>,
137 signature: Signature<N>,
138 committee: &Committee<N>,
139 ) -> Result<()> {
140 if !committee.is_committee_member(signer) {
142 bail!("Signature from a non-committee member - '{signer}'")
143 }
144 if self.signers().contains(&signer) {
146 bail!("Duplicate signature from '{signer}'")
147 }
148 if !signature.verify(&signer, &[self.batch_id()]) {
151 bail!("Signature verification failed")
152 }
153 self.signatures.insert(signature);
155 Ok(())
156 }
157
158 pub fn to_certificate(
160 &self,
161 committee: &Committee<N>,
162 ) -> Result<(BatchCertificate<N>, IndexMap<TransmissionID<N>, Transmission<N>>)> {
163 ensure!(self.is_quorum_threshold_reached(committee), "The quorum threshold has not been reached");
165 let certificate = BatchCertificate::from(self.batch_header.clone(), self.signatures.clone())?;
167 Ok((certificate, self.transmissions.clone()))
169 }
170}
171
172impl<N: Network> ToBytes for Proposal<N> {
173 fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
174 self.batch_header.write_le(&mut writer)?;
176 u32::try_from(self.transmissions.len()).map_err(error)?.write_le(&mut writer)?;
178 for (transmission_id, transmission) in &self.transmissions {
180 transmission_id.write_le(&mut writer)?;
181 transmission.write_le(&mut writer)?;
182 }
183 u32::try_from(self.signatures.len()).map_err(error)?.write_le(&mut writer)?;
185 for signature in &self.signatures {
187 signature.write_le(&mut writer)?;
188 }
189 Ok(())
190 }
191}
192
193impl<N: Network> FromBytes for Proposal<N> {
194 fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
195 let batch_header = FromBytes::read_le(&mut reader)?;
197 let num_transmissions = u32::read_le(&mut reader)?;
199 if num_transmissions as usize > BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
201 return Err(error("Invalid number of transmissions in the proposal"));
202 }
203 let mut transmissions = IndexMap::default();
205 for _ in 0..num_transmissions {
206 let transmission_id = FromBytes::read_le(&mut reader)?;
207 let transmission = FromBytes::read_le(&mut reader)?;
208 transmissions.insert(transmission_id, transmission);
209 }
210 let num_signatures = u32::read_le(&mut reader)?;
212 if num_signatures as usize > Committee::<N>::max_committee_size().map_err(error)? as usize {
214 return Err(error("Invalid number of signatures in the proposal"));
215 }
216 let mut signatures = IndexSet::default();
218 for _ in 0..num_signatures {
219 signatures.insert(FromBytes::read_le(&mut reader)?);
220 }
221
222 Ok(Self { batch_header, transmissions, signatures })
223 }
224}
225
226#[cfg(test)]
227pub(crate) mod tests {
228 use super::*;
229 use crate::helpers::storage::tests::sample_transmissions;
230 use snarkvm::{console::network::MainnetV0, utilities::TestRng};
231
232 type CurrentNetwork = MainnetV0;
233
234 const ITERATIONS: usize = 100;
235
236 pub(crate) fn sample_proposal(rng: &mut TestRng) -> Proposal<CurrentNetwork> {
237 let certificate = snarkvm::ledger::narwhal::batch_certificate::test_helpers::sample_batch_certificate(rng);
238 let (_, transmissions) = sample_transmissions(&certificate, rng);
239
240 let transmissions = transmissions.into_iter().map(|(id, (t, _))| (id, t)).collect::<IndexMap<_, _>>();
241 let batch_header = certificate.batch_header().clone();
242 let signatures = certificate.signatures().copied().collect();
243
244 Proposal { batch_header, transmissions, signatures }
245 }
246
247 #[test]
248 fn test_bytes() {
249 let rng = &mut TestRng::default();
250
251 for _ in 0..ITERATIONS {
252 let expected = sample_proposal(rng);
253 let expected_bytes = expected.to_bytes_le().unwrap();
255 assert_eq!(expected, Proposal::read_le(&expected_bytes[..]).unwrap());
256 }
257 }
258}
259
260#[cfg(test)]
261mod prop_tests {
262 use crate::helpers::{
263 Proposal,
264 now,
265 storage::prop_tests::{AnyTransmission, AnyTransmissionID, CryptoTestRng},
266 };
267 use snarkvm::ledger::{
268 committee::prop_tests::{CommitteeContext, ValidatorSet},
269 narwhal::BatchHeader,
270 };
271
272 use indexmap::IndexMap;
273 use proptest::sample::{Selector, size_range};
274 use test_strategy::proptest;
275
276 #[proptest]
277 fn initialize_proposal(
278 context: CommitteeContext,
279 #[any(size_range(1..16).lift())] transmissions: Vec<(AnyTransmissionID, AnyTransmission)>,
280 selector: Selector,
281 mut rng: CryptoTestRng,
282 ) {
283 let CommitteeContext(committee, ValidatorSet(validators)) = context;
284
285 let signer = selector.select(&validators);
286 let mut transmission_map = IndexMap::new();
287
288 for (AnyTransmissionID(id), AnyTransmission(t)) in transmissions.iter() {
289 transmission_map.insert(*id, t.clone());
290 }
291
292 let header = BatchHeader::new(
293 &signer.private_key,
294 committee.starting_round(),
295 now(),
296 committee.id(),
297 transmission_map.keys().cloned().collect(),
298 Default::default(),
299 &mut rng,
300 )
301 .unwrap();
302 let proposal = Proposal::new(committee, header.clone(), transmission_map.clone()).unwrap();
303 assert_eq!(proposal.batch_id(), header.batch_id());
304 }
305}