Skip to main content

snarkvm_ledger_narwhal_batch_certificate/
lib.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkVM 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
16#![forbid(unsafe_code)]
17#![warn(clippy::cast_possible_truncation)]
18
19extern crate snarkvm_console as console;
20
21mod bytes;
22mod serialize;
23mod string;
24
25use console::{
26    account::{Address, Signature},
27    prelude::*,
28    types::Field,
29};
30use snarkvm_ledger_narwhal_batch_header::BatchHeader;
31use snarkvm_ledger_narwhal_transmission_id::TransmissionID;
32
33use core::hash::{Hash, Hasher};
34use indexmap::IndexSet;
35use std::{collections::HashSet, sync::OnceLock};
36
37#[cfg(not(feature = "serial"))]
38use rayon::prelude::*;
39
40#[derive(Clone)]
41pub struct BatchCertificate<N: Network> {
42    /// The batch header.
43    batch_header: BatchHeader<N>,
44    /// The signatures for the batch ID from the committee.
45    signatures: IndexSet<Signature<N>>,
46    /// The recovered address of each signer, in the same order as `signatures`.
47    ///
48    /// This is derived data, cached because recovering a signer address is expensive: it is a
49    /// fixed-base scalar multiplication per signature (see `Signature::to_address`), and the
50    /// same addresses were previously recomputed at every point that needed them.
51    ///
52    /// It is populated eagerly by `from`, which already recovers the addresses in order to
53    /// validate them, and lazily on first use otherwise (see `signers`). It is never
54    /// serialized, and it takes no part in `PartialEq`, `Eq`, or `Hash`, all of which are
55    /// keyed on the batch ID alone.
56    signers: OnceLock<Vec<Address<N>>>,
57}
58
59impl<N: Network> BatchCertificate<N> {
60    /// The maximum number of signatures in a batch certificate.
61    pub fn max_signatures() -> u16 {
62        N::LATEST_MAX_CERTIFICATES()
63    }
64}
65
66impl<N: Network> BatchCertificate<N> {
67    /// Initializes a new batch certificate.
68    pub fn from(batch_header: BatchHeader<N>, signatures: IndexSet<Signature<N>>) -> Result<Self> {
69        // Ensure that the number of signatures is within bounds.
70        ensure!(signatures.len() <= Self::max_signatures() as usize, "Invalid number of signatures");
71
72        // Collect the signatures so that they can be traversed alongside the recovered
73        // addresses below, and so that the recovery itself can be done in parallel.
74        let signature_list = signatures.iter().collect::<Vec<_>>();
75
76        // Recover the address of each signer, in the same order as `signatures`.
77        //
78        // This is the expensive step: each recovery is a fixed-base scalar multiplication.
79        // It is performed exactly once here, reused by both checks below, and then cached on
80        // the certificate so that later callers can use `signers` instead of recomputing it.
81        let signers = cfg_iter!(signature_list).map(|signature| signature.to_address()).collect::<Vec<_>>();
82
83        // Ensure that the signature is from a unique signer and not from the author.
84        let signature_authors = signers.iter().copied().collect::<HashSet<_>>();
85        ensure!(
86            !signature_authors.contains(&batch_header.author()),
87            "The author's signature was included in the signers"
88        );
89        ensure!(signature_authors.len() == signatures.len(), "A duplicate author was found in the set of signatures");
90
91        // Verify the signatures are valid, reusing the addresses recovered above.
92        cfg_iter!(signature_list).zip(&signers).try_for_each(|(signature, signer)| {
93            if !signature.verify(signer, &[batch_header.batch_id()]) {
94                bail!("Invalid batch certificate signature")
95            }
96            Ok(())
97        })?;
98
99        // Drop the borrows of `signatures` before handing it over.
100        drop(signature_list);
101
102        // Return the batch certificate.
103        let certificate = Self::from_unchecked(batch_header, signatures)?;
104        // Cache the addresses recovered above. `from_unchecked` always returns an empty cache,
105        // so this cannot fail.
106        let _ = certificate.signers.set(signers);
107        Ok(certificate)
108    }
109
110    /// Initializes a new batch certificate.
111    pub fn from_unchecked(batch_header: BatchHeader<N>, signatures: IndexSet<Signature<N>>) -> Result<Self> {
112        // Ensure the signatures are not empty.
113        ensure!(!signatures.is_empty(), "Batch certificate must contain signatures");
114        // Return the batch certificate. Note the signer cache starts out empty here, and is
115        // populated on first use by `signers`.
116        Ok(Self { batch_header, signatures, signers: OnceLock::new() })
117    }
118}
119
120impl<N: Network> PartialEq for BatchCertificate<N> {
121    fn eq(&self, other: &Self) -> bool {
122        self.batch_id() == other.batch_id()
123    }
124}
125
126impl<N: Network> Eq for BatchCertificate<N> {}
127
128impl<N: Network> Hash for BatchCertificate<N> {
129    fn hash<H: Hasher>(&self, state: &mut H) {
130        self.batch_header.batch_id().hash(state);
131    }
132}
133
134impl<N: Network> BatchCertificate<N> {
135    /// Returns the certificate ID.
136    pub const fn id(&self) -> Field<N> {
137        self.batch_header.batch_id()
138    }
139
140    /// Returns the batch header.
141    pub const fn batch_header(&self) -> &BatchHeader<N> {
142        &self.batch_header
143    }
144
145    /// Returns the batch ID.
146    pub const fn batch_id(&self) -> Field<N> {
147        self.batch_header().batch_id()
148    }
149
150    /// Returns the author.
151    pub const fn author(&self) -> Address<N> {
152        self.batch_header().author()
153    }
154
155    /// Returns the round.
156    pub const fn round(&self) -> u64 {
157        self.batch_header().round()
158    }
159
160    /// Returns the timestamp of the batch header.
161    pub fn timestamp(&self) -> i64 {
162        self.batch_header().timestamp()
163    }
164
165    /// Returns the committee ID.
166    pub const fn committee_id(&self) -> Field<N> {
167        self.batch_header().committee_id()
168    }
169
170    /// Returns the transmission IDs.
171    pub const fn transmission_ids(&self) -> &IndexSet<TransmissionID<N>> {
172        self.batch_header().transmission_ids()
173    }
174
175    /// Returns the batch certificate IDs for the previous round.
176    pub const fn previous_certificate_ids(&self) -> &IndexSet<Field<N>> {
177        self.batch_header().previous_certificate_ids()
178    }
179
180    /// Returns the signatures of the batch ID from the committee.
181    pub fn signatures(&self) -> Box<dyn '_ + ExactSizeIterator<Item = &Signature<N>>> {
182        Box::new(self.signatures.iter())
183    }
184
185    /// Returns the address of each signer, in the same order as `signatures`.
186    ///
187    /// Note that this does **not** include the certificate's author, whose signature is not
188    /// part of `signatures`; use `author` for that.
189    ///
190    /// Prefer this over mapping `Signature::to_address` over `signatures`. Recovering a signer
191    /// address is a fixed-base scalar multiplication, and certificates are checked, stored, and
192    /// inspected many times over their lifetime, so recomputing it at each site is significant
193    /// wasted work. Certificates built by `from` already have this populated.
194    ///
195    /// The lazy path takes a lock for the duration of the recovery. It is a leaf: nothing else
196    /// is acquired underneath it, and the work is pure computation, so it cannot participate in
197    /// a lock cycle.
198    pub fn signers(&self) -> &[Address<N>] {
199        self.signers.get_or_init(|| self.signatures.iter().map(|signature| signature.to_address()).collect())
200    }
201}
202
203#[cfg(any(test, feature = "test-helpers"))]
204pub mod test_helpers {
205    use super::*;
206    use console::{account::PrivateKey, network::MainnetV0, prelude::TestRng, types::Field};
207
208    use indexmap::IndexSet;
209
210    type CurrentNetwork = MainnetV0;
211
212    /// Returns a sample batch certificate, sampled at random.
213    pub fn sample_batch_certificate(rng: &mut TestRng) -> BatchCertificate<CurrentNetwork> {
214        sample_batch_certificate_for_round(rng.random(), rng)
215    }
216
217    /// Returns a sample batch certificate with a given round; the rest is sampled at random.
218    pub fn sample_batch_certificate_for_round(round: u64, rng: &mut TestRng) -> BatchCertificate<CurrentNetwork> {
219        // Sample certificate IDs.
220        let certificate_ids = (0..10).map(|_| Field::<CurrentNetwork>::rand(rng)).collect::<IndexSet<_>>();
221        // Return the batch certificate.
222        sample_batch_certificate_for_round_with_previous_certificate_ids(round, certificate_ids, rng)
223    }
224
225    /// Returns a sample batch certificate with a given round and the given certificate ids as predecessors; the rest is sampled at random.
226    pub fn sample_batch_certificate_for_round_with_previous_certificate_ids(
227        round: u64,
228        previous_certificate_ids: IndexSet<Field<CurrentNetwork>>,
229        rng: &mut TestRng,
230    ) -> BatchCertificate<CurrentNetwork> {
231        let committee: Vec<_> = (0..5).map(|_| PrivateKey::new(rng).unwrap()).collect();
232        sample_batch_certificate_for_round_with_committee(
233            round,
234            previous_certificate_ids,
235            &committee[0],
236            &committee[1..],
237            rng,
238        )
239    }
240
241    /// Same as `sample_batch_certificate_for_round_with_previous_certificate_ids`, but also allows you to set the private keys that sign the certificate.
242    pub fn sample_batch_certificate_for_round_with_committee(
243        round: u64,
244        previous_certificate_ids: IndexSet<Field<CurrentNetwork>>,
245        author: &PrivateKey<CurrentNetwork>,
246        signers: &[PrivateKey<CurrentNetwork>],
247        rng: &mut TestRng,
248    ) -> BatchCertificate<CurrentNetwork> {
249        // Sample a batch header.
250        let batch_header =
251            snarkvm_ledger_narwhal_batch_header::test_helpers::sample_batch_header_for_round_and_key_with_previous_certificate_ids(
252                round,
253                author,
254                previous_certificate_ids,
255                rng,
256            );
257        // Generate the endorsements.
258        let signatures: IndexSet<_> =
259            signers.iter().map(|private_key| private_key.sign(&[batch_header.batch_id()], rng).unwrap()).collect();
260
261        // Return the batch certificate.
262        BatchCertificate::from(batch_header, signatures).unwrap()
263    }
264
265    /// Returns a list of sample batch certificates, sampled at random.
266    pub fn sample_batch_certificates(rng: &mut TestRng) -> IndexSet<BatchCertificate<CurrentNetwork>> {
267        // Initialize a sample vector.
268        let mut sample = IndexSet::with_capacity(10);
269        // Append sample batch certificates.
270        for _ in 0..10 {
271            sample.insert(sample_batch_certificate(rng));
272        }
273        // Return the sample vector.
274        sample
275    }
276
277    /// Returns a sample batch certificate with previous certificates, sampled at random.
278    pub fn sample_batch_certificate_with_previous_certificates(
279        round: u64,
280        rng: &mut TestRng,
281    ) -> (BatchCertificate<CurrentNetwork>, Vec<BatchCertificate<CurrentNetwork>>) {
282        assert!(round > 1, "Round must be greater than 1");
283
284        // Initialize the round parameters.
285        let previous_round = round - 1; // <- This must be an even number, for `BFT::update_dag` to behave correctly below.
286        let current_round = round;
287
288        assert_eq!(previous_round % 2, 0, "Previous round must be even");
289
290        // Sample the previous certificates.
291        let previous_certificates = vec![
292            sample_batch_certificate_for_round(previous_round, rng),
293            sample_batch_certificate_for_round(previous_round, rng),
294            sample_batch_certificate_for_round(previous_round, rng),
295            sample_batch_certificate_for_round(previous_round, rng),
296        ];
297        // Construct the previous certificate IDs.
298        let previous_certificate_ids: IndexSet<_> = previous_certificates.iter().map(|c| c.id()).collect();
299        // Sample the leader certificate.
300        let certificate = sample_batch_certificate_for_round_with_previous_certificate_ids(
301            current_round,
302            previous_certificate_ids,
303            rng,
304        );
305
306        (certificate, previous_certificates)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use console::{network::MainnetV0, prelude::TestRng};
314
315    type CurrentNetwork = MainnetV0;
316
317    /// Recovers the signer addresses the naive way, which is what `signers` replaces.
318    fn recompute_signers(certificate: &BatchCertificate<CurrentNetwork>) -> Vec<Address<CurrentNetwork>> {
319        certificate.signatures().map(|signature| signature.to_address()).collect()
320    }
321
322    #[test]
323    fn test_signers_matches_recomputation() {
324        let rng = &mut TestRng::default();
325
326        for _ in 0..8 {
327            let certificate = test_helpers::sample_batch_certificate(rng);
328            // The cache was populated eagerly by `from`; it must agree with recomputation,
329            // and must preserve the order of `signatures`.
330            assert_eq!(certificate.signers(), recompute_signers(&certificate));
331        }
332    }
333
334    #[test]
335    fn test_signers_excludes_the_author() {
336        let rng = &mut TestRng::default();
337
338        let certificate = test_helpers::sample_batch_certificate(rng);
339        assert!(!certificate.signers().is_empty());
340        // `from` rejects a certificate whose author signed it, so the author must never appear.
341        assert!(!certificate.signers().contains(&certificate.author()));
342    }
343
344    #[test]
345    fn test_signers_is_lazily_populated_after_deserialization() {
346        let rng = &mut TestRng::default();
347
348        let certificate = test_helpers::sample_batch_certificate(rng);
349        let expected = certificate.signers().to_vec();
350
351        // `read_le_unchecked` goes through `from_unchecked`, which leaves the cache empty, so
352        // this exercises the lazy path rather than the one populated by `from`.
353        let bytes = certificate.to_bytes_le().unwrap();
354        let recovered = BatchCertificate::<CurrentNetwork>::read_le_unchecked(&bytes[..]).unwrap();
355        assert!(recovered.signers.get().is_none(), "the cache should start out empty");
356
357        assert_eq!(recovered.signers(), expected);
358        // A second call must return the same cached value.
359        assert_eq!(recovered.signers(), expected);
360    }
361
362    #[test]
363    fn test_signers_survives_cloning() {
364        let rng = &mut TestRng::default();
365
366        let certificate = test_helpers::sample_batch_certificate(rng);
367        let expected = certificate.signers().to_vec();
368
369        // Cloning a populated certificate carries the cache over.
370        assert_eq!(certificate.clone().signers(), expected);
371
372        // Cloning an unpopulated one leaves it unpopulated, and it still resolves correctly.
373        let bytes = certificate.to_bytes_le().unwrap();
374        let unpopulated = BatchCertificate::<CurrentNetwork>::read_le_unchecked(&bytes[..]).unwrap();
375        let cloned = unpopulated.clone();
376        assert!(cloned.signers.get().is_none(), "cloning must not populate the cache");
377        assert_eq!(cloned.signers(), expected);
378    }
379}