amareleo_node_bft/helpers/
proposal.rs

1// Copyright 2024 Aleo Network Foundation
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use 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    /// The proposed batch header.
35    batch_header: BatchHeader<N>,
36    /// The proposed transmissions.
37    transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
38    /// The set of signatures.
39    signatures: IndexSet<Signature<N>>,
40}
41
42impl<N: Network> Proposal<N> {
43    /// Initializes a new instance of the proposal.
44    pub fn new(
45        committee: Committee<N>,
46        batch_header: BatchHeader<N>,
47        transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
48    ) -> Result<Self> {
49        // Ensure the committee is for the batch round.
50        ensure!(batch_header.round() >= committee.starting_round(), "Batch round must be >= the committee round");
51        // Ensure the batch author is a member of the committee.
52        ensure!(committee.is_committee_member(batch_header.author()), "The batch author is not a committee member");
53        // Ensure the transmission IDs match in the batch header and transmissions.
54        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        // Return the proposal.
62        Ok(Self { batch_header, transmissions, signatures: Default::default() })
63    }
64
65    /// Returns the proposed batch header.
66    pub const fn batch_header(&self) -> &BatchHeader<N> {
67        &self.batch_header
68    }
69
70    /// Returns the proposed batch ID.
71    pub const fn batch_id(&self) -> Field<N> {
72        self.batch_header.batch_id()
73    }
74
75    /// Returns the round.
76    pub const fn round(&self) -> u64 {
77        self.batch_header.round()
78    }
79
80    /// Returns the timestamp.
81    pub const fn timestamp(&self) -> i64 {
82        self.batch_header.timestamp()
83    }
84
85    /// Returns the transmissions.
86    pub const fn transmissions(&self) -> &IndexMap<TransmissionID<N>, Transmission<N>> {
87        &self.transmissions
88    }
89
90    /// Returns the transmissions.
91    pub fn into_transmissions(self) -> IndexMap<TransmissionID<N>, Transmission<N>> {
92        self.transmissions
93    }
94
95    /// Returns the signers.
96    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    /// Returns the nonsigners.
101    pub fn nonsigners(&self, committee: &Committee<N>) -> HashSet<Address<N>> {
102        // Retrieve the current signers.
103        let signers = self.signers();
104        // Initialize a set for the non-signers.
105        let mut nonsigners = HashSet::new();
106        // Iterate through the committee members.
107        for address in committee.members().keys() {
108            // Insert the address if it is not a signer.
109            if !signers.contains(address) {
110                nonsigners.insert(*address);
111            }
112        }
113        // Return the non-signers.
114        nonsigners
115    }
116
117    /// Returns `true` if the quorum threshold has been reached for the proposed batch.
118    pub fn is_quorum_threshold_reached(&self, committee: &Committee<N>) -> bool {
119        // Check if the batch has reached the quorum threshold.
120        committee.is_quorum_threshold_reached(&self.signers())
121    }
122
123    /// Returns `true` if the proposal contains the given transmission ID.
124    pub fn contains_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
125        self.transmissions.contains_key(&transmission_id.into())
126    }
127
128    /// Returns the `transmission` for the given `transmission ID`.
129    pub fn get_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> Option<&Transmission<N>> {
130        self.transmissions.get(&transmission_id.into())
131    }
132
133    /// Adds a signature to the proposal, if the signature is valid.
134    pub fn add_signature(
135        &mut self,
136        signer: Address<N>,
137        signature: Signature<N>,
138        committee: &Committee<N>,
139    ) -> Result<()> {
140        // Ensure the signer is in the committee.
141        if !committee.is_committee_member(signer) {
142            bail!("Signature from a non-committee member - '{signer}'")
143        }
144        // Ensure the signer is new.
145        if self.signers().contains(&signer) {
146            bail!("Duplicate signature from '{signer}'")
147        }
148        // Verify the signature. If the signature is not valid, return an error.
149        // Note: This check ensures the peer's address matches the address of the signature.
150        if !signature.verify(&signer, &[self.batch_id()]) {
151            bail!("Signature verification failed")
152        }
153        // Insert the signature.
154        self.signatures.insert(signature);
155        Ok(())
156    }
157
158    /// Returns the batch certificate and transmissions.
159    pub fn to_certificate(
160        &self,
161        committee: &Committee<N>,
162    ) -> Result<(BatchCertificate<N>, IndexMap<TransmissionID<N>, Transmission<N>>)> {
163        // Ensure the quorum threshold has been reached.
164        ensure!(self.is_quorum_threshold_reached(committee), "The quorum threshold has not been reached");
165        // Create the batch certificate.
166        let certificate = BatchCertificate::from(self.batch_header.clone(), self.signatures.clone())?;
167        // Return the certificate and transmissions.
168        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        // Write the batch header.
175        self.batch_header.write_le(&mut writer)?;
176        // Write the number of transmissions.
177        u32::try_from(self.transmissions.len()).map_err(error)?.write_le(&mut writer)?;
178        // Write the transmissions.
179        for (transmission_id, transmission) in &self.transmissions {
180            transmission_id.write_le(&mut writer)?;
181            transmission.write_le(&mut writer)?;
182        }
183        // Write the number of signatures.
184        u32::try_from(self.signatures.len()).map_err(error)?.write_le(&mut writer)?;
185        // Write the signatures.
186        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        // Read the batch header.
196        let batch_header = FromBytes::read_le(&mut reader)?;
197        // Read the number of transmissions.
198        let num_transmissions = u32::read_le(&mut reader)?;
199        // Ensure the number of transmissions is within bounds (this is an early safety check).
200        if num_transmissions as usize > BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
201            return Err(error("Invalid number of transmissions in the proposal"));
202        }
203        // Read the transmissions.
204        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        // Read the number of signatures.
211        let num_signatures = u32::read_le(&mut reader)?;
212        // Ensure the number of signatures is within bounds (this is an early safety check).
213        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        // Read the signatures.
217        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            // Check the byte representation.
254            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}