snarkos_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)]
43pub struct Proposal<N: Network> {
44 batch_header: BatchHeader<N>,
46 transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
48 signatures: IndexSet<Signature<N>>,
51}
52
53impl<N: Network> Proposal<N> {
54 pub fn new(
60 committee: Committee<N>,
61 batch_header: BatchHeader<N>,
62 transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
63 ) -> Result<Self> {
64 ensure!(batch_header.round() >= committee.starting_round(), "Batch round must be >= the committee round");
66 ensure!(committee.is_committee_member(batch_header.author()), "The batch author is not a committee member");
68 ensure!(
70 batch_header.transmission_ids().len() == transmissions.len(),
71 "The transmission IDs do not match in the batch header and transmissions"
72 );
73 for (a, b) in batch_header.transmission_ids().iter().zip_eq(transmissions.keys()) {
74 ensure!(a == b, "The transmission IDs do not match in the batch header and transmissions");
75 }
76 Ok(Self { batch_header, transmissions, signatures: Default::default() })
78 }
79
80 pub const fn batch_header(&self) -> &BatchHeader<N> {
82 &self.batch_header
83 }
84
85 pub const fn batch_id(&self) -> Field<N> {
87 self.batch_header.batch_id()
88 }
89
90 pub const fn round(&self) -> u64 {
92 self.batch_header.round()
93 }
94
95 pub const fn timestamp(&self) -> i64 {
97 self.batch_header.timestamp()
98 }
99
100 pub const fn transmissions(&self) -> &IndexMap<TransmissionID<N>, Transmission<N>> {
102 &self.transmissions
103 }
104
105 pub fn into_transmissions(self) -> IndexMap<TransmissionID<N>, Transmission<N>> {
107 self.transmissions
108 }
109
110 pub fn signers(&self) -> HashSet<Address<N>> {
112 self.signatures.iter().chain(Some(self.batch_header.signature())).map(Signature::to_address).collect()
113 }
114
115 pub fn nonsigners(&self, committee: &Committee<N>) -> HashSet<Address<N>> {
117 let signers = self.signers();
119 let mut nonsigners = HashSet::new();
121 for address in committee.members().keys() {
123 if !signers.contains(address) {
125 nonsigners.insert(*address);
126 }
127 }
128 nonsigners
130 }
131
132 pub fn is_quorum_threshold_reached(&self, committee: &Committee<N>) -> bool {
135 committee.is_quorum_threshold_reached(&self.signers())
137 }
138
139 pub fn contains_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
141 self.transmissions.contains_key(&transmission_id.into())
142 }
143
144 pub fn get_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> Option<&Transmission<N>> {
146 self.transmissions.get(&transmission_id.into())
147 }
148
149 pub fn add_signature(
153 &mut self,
154 signer: Address<N>,
155 signature: Signature<N>,
156 committee: &Committee<N>,
157 ) -> Result<()> {
158 if !committee.is_committee_member(signer) {
160 bail!("Signature from a non-committee member - '{signer}'")
161 }
162 if self.signers().contains(&signer) {
164 bail!("Duplicate signature from '{signer}'")
165 }
166 if !signature.verify(&signer, &[self.batch_id()]) {
169 bail!("Signature verification failed")
170 }
171 self.signatures.insert(signature);
173 Ok(())
174 }
175
176 pub fn to_certificate(
178 &self,
179 committee: &Committee<N>,
180 ) -> Result<(BatchCertificate<N>, IndexMap<TransmissionID<N>, Transmission<N>>)> {
181 ensure!(self.is_quorum_threshold_reached(committee), "The quorum threshold has not been reached");
183 let certificate = BatchCertificate::from(self.batch_header.clone(), self.signatures.clone())?;
185 Ok((certificate, self.transmissions.clone()))
187 }
188}
189
190impl<N: Network> ToBytes for Proposal<N> {
191 fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
192 self.batch_header.write_le(&mut writer)?;
194 u32::try_from(self.transmissions.len()).map_err(error)?.write_le(&mut writer)?;
196 for (transmission_id, transmission) in &self.transmissions {
198 transmission_id.write_le(&mut writer)?;
199 transmission.write_le(&mut writer)?;
200 }
201 u32::try_from(self.signatures.len()).map_err(error)?.write_le(&mut writer)?;
203 for signature in &self.signatures {
205 signature.write_le(&mut writer)?;
206 }
207 Ok(())
208 }
209}
210
211impl<N: Network> FromBytes for Proposal<N> {
212 fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
213 let batch_header: BatchHeader<N> = FromBytes::read_le(&mut reader)?;
215 let num_transmissions = u32::read_le(&mut reader)?;
217 if num_transmissions as usize > BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
219 return Err(error("Invalid number of transmissions in the proposal"));
220 }
221 let mut transmissions = IndexMap::default();
223 for _ in 0..num_transmissions {
224 let transmission_id = FromBytes::read_le(&mut reader)?;
225 let transmission = FromBytes::read_le(&mut reader)?;
226 transmissions.insert(transmission_id, transmission);
227 }
228 let num_signatures = u32::read_le(&mut reader)?;
230 if num_signatures as usize > Committee::<N>::max_committee_size().map_err(error)? as usize {
232 return Err(error("Invalid number of signatures in the proposal"));
233 }
234 let mut signatures = IndexSet::default();
236 for _ in 0..num_signatures {
237 signatures.insert(FromBytes::read_le(&mut reader)?);
238 }
239
240 if batch_header.transmission_ids().len() != transmissions.len() {
242 return Err(error("The transmission IDs do not match in the batch header and transmissions"));
243 }
244 for (a, b) in batch_header.transmission_ids().iter().zip_eq(transmissions.keys()) {
245 if a != b {
246 return Err(error("The transmission IDs do not match in the batch header and transmissions"));
247 }
248 }
249
250 Ok(Self { batch_header, transmissions, signatures })
251 }
252}
253
254#[cfg(test)]
255pub(crate) mod tests {
256 use super::*;
257 use crate::helpers::storage::tests::sample_transmissions;
258 use snarkvm::{console::network::MainnetV0, utilities::TestRng};
259
260 type CurrentNetwork = MainnetV0;
261
262 const ITERATIONS: usize = 100;
263
264 pub(crate) fn sample_proposal(rng: &mut TestRng) -> Proposal<CurrentNetwork> {
265 let certificate = snarkvm::ledger::narwhal::batch_certificate::test_helpers::sample_batch_certificate(rng);
266 let (_, transmissions) = sample_transmissions(&certificate, rng);
267
268 let transmissions =
269 certificate.transmission_ids().iter().map(|id| (*id, transmissions.get(id).unwrap().0.clone())).collect();
270
271 let batch_header = certificate.batch_header().clone();
272 let signatures = certificate.signatures().copied().collect();
273
274 Proposal { batch_header, transmissions, signatures }
275 }
276
277 #[test]
278 fn test_bytes() {
279 let rng = &mut TestRng::default();
280
281 for _ in 0..ITERATIONS {
282 let expected = sample_proposal(rng);
283 let expected_bytes = expected.to_bytes_le().unwrap();
285 assert_eq!(expected, Proposal::read_le(&expected_bytes[..]).unwrap());
286 }
287 }
288}
289
290#[cfg(test)]
291mod prop_tests {
292 use crate::helpers::{
293 Proposal,
294 now,
295 storage::prop_tests::{AnyTransmission, AnyTransmissionID, CryptoTestRng},
296 };
297 use snarkvm::ledger::{
298 committee::prop_tests::{CommitteeContext, ValidatorSet},
299 narwhal::BatchHeader,
300 };
301
302 use indexmap::IndexMap;
303 use proptest::sample::{Selector, size_range};
304 use test_strategy::proptest;
305
306 #[proptest]
307 fn initialize_proposal(
308 context: CommitteeContext,
309 #[any(size_range(1..16).lift())] transmissions: Vec<(AnyTransmissionID, AnyTransmission)>,
310 selector: Selector,
311 mut rng: CryptoTestRng,
312 ) {
313 let CommitteeContext(committee, ValidatorSet(validators)) = context;
314
315 let signer = selector.select(&validators);
316 let mut transmission_map = IndexMap::new();
317
318 for (AnyTransmissionID(id), AnyTransmission(t)) in transmissions.iter() {
319 transmission_map.insert(*id, t.clone());
320 }
321
322 let header = BatchHeader::new(
323 &signer.private_key,
324 committee.starting_round(),
325 now(),
326 committee.id(),
327 transmission_map.keys().cloned().collect(),
328 Default::default(),
329 &mut rng,
330 )
331 .unwrap();
332 let proposal = Proposal::new(committee, header.clone(), transmission_map.clone()).unwrap();
333 assert_eq!(proposal.batch_id(), header.batch_id());
334 }
335}