snarkvm_ledger_narwhal_batch_header/
lib.rs

1// Copyright (c) 2019-2025 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#![allow(clippy::too_many_arguments)]
19
20extern crate snarkvm_console as console;
21
22mod bytes;
23mod serialize;
24mod string;
25mod to_id;
26
27use console::{
28    account::{Address, PrivateKey, Signature},
29    prelude::*,
30    types::Field,
31};
32use snarkvm_ledger_narwhal_transmission_id::TransmissionID;
33
34use indexmap::IndexSet;
35
36#[cfg(not(feature = "serial"))]
37use rayon::prelude::*;
38
39#[derive(Clone, PartialEq, Eq)]
40pub struct BatchHeader<N: Network> {
41    /// The batch ID, defined as the hash of the author, round number, timestamp, transmission IDs,
42    /// committee ID, and previous batch certificate IDs.
43    batch_id: Field<N>,
44    /// The author of the batch.
45    author: Address<N>,
46    /// The round number.
47    round: u64,
48    /// The timestamp.
49    timestamp: i64,
50    /// The committee ID.
51    committee_id: Field<N>,
52    /// The set of `transmission IDs`.
53    transmission_ids: IndexSet<TransmissionID<N>>,
54    /// The batch certificate IDs of the previous round.
55    previous_certificate_ids: IndexSet<Field<N>>,
56    /// The signature of the batch ID from the creator.
57    signature: Signature<N>,
58}
59
60impl<N: Network> BatchHeader<N> {
61    /// The maximum number of rounds to store before garbage collecting.
62    pub const MAX_GC_ROUNDS: usize = 100;
63    /// The maximum number of transmissions in a batch.
64    /// Note: This limit is set to 50 as part of safety measures to prevent DoS attacks.
65    /// This limit can be increased in the future as performance improves. Alternatively,
66    /// the rate of block production can be sped up to compensate for the limit set here.
67    pub const MAX_TRANSMISSIONS_PER_BATCH: usize = 50;
68}
69
70impl<N: Network> BatchHeader<N> {
71    /// The maximum number of microcredits that can be spent on compute by the transactions in a batch.
72    /// This implies the block spend limit is bounded at batch_spend_limit * N::NUM_MAX_CERTIFICATES` * MAX_GC_ROUNDS.
73    // TODO: div by 20 is temporary until we can dial in what the limit should be.
74    pub fn batch_spend_limit(height: u32) -> u64 {
75        consensus_config_value!(N, TRANSACTION_SPEND_LIMIT, height).unwrap() * Self::MAX_TRANSMISSIONS_PER_BATCH as u64
76            / 20
77    }
78}
79
80impl<N: Network> BatchHeader<N> {
81    /// Initializes a new batch header.
82    pub fn new<R: Rng + CryptoRng>(
83        private_key: &PrivateKey<N>,
84        round: u64,
85        timestamp: i64,
86        committee_id: Field<N>,
87        transmission_ids: IndexSet<TransmissionID<N>>,
88        previous_certificate_ids: IndexSet<Field<N>>,
89        rng: &mut R,
90    ) -> Result<Self> {
91        match round {
92            0 | 1 => {
93                // If the round is zero or one, then there should be no previous certificate IDs.
94                ensure!(previous_certificate_ids.is_empty(), "Invalid round number, must not have certificates");
95            }
96            // If the round is not zero and not one, then there should be at least one previous certificate ID.
97            _ => ensure!(!previous_certificate_ids.is_empty(), "Invalid round number, must have certificates"),
98        }
99
100        // Ensure that the number of transmissions is within bounds.
101        ensure!(
102            transmission_ids.len() <= Self::MAX_TRANSMISSIONS_PER_BATCH,
103            "Invalid number of transmission IDs ({})",
104            transmission_ids.len()
105        );
106        // Ensure that the number of previous certificate IDs is within bounds.
107        ensure!(
108            previous_certificate_ids.len() <= N::LATEST_MAX_CERTIFICATES()? as usize,
109            "Invalid number of previous certificate IDs ({})",
110            previous_certificate_ids.len()
111        );
112
113        // Retrieve the address.
114        let author = Address::try_from(private_key)?;
115        // Compute the batch ID.
116        let batch_id = Self::compute_batch_id(
117            author,
118            round,
119            timestamp,
120            committee_id,
121            &transmission_ids,
122            &previous_certificate_ids,
123        )?;
124        // Sign the preimage.
125        let signature = private_key.sign(&[batch_id], rng)?;
126        // Return the batch header.
127        Ok(Self {
128            batch_id,
129            author,
130            round,
131            timestamp,
132            committee_id,
133            transmission_ids,
134            previous_certificate_ids,
135            signature,
136        })
137    }
138
139    /// Initializes a new batch header.
140    pub fn from(
141        author: Address<N>,
142        round: u64,
143        timestamp: i64,
144        committee_id: Field<N>,
145        transmission_ids: IndexSet<TransmissionID<N>>,
146        previous_certificate_ids: IndexSet<Field<N>>,
147        signature: Signature<N>,
148    ) -> Result<Self> {
149        match round {
150            0 | 1 => {
151                // If the round is zero or one, then there should be no previous certificate IDs.
152                ensure!(previous_certificate_ids.is_empty(), "Invalid round number, must not have certificates");
153            }
154            // If the round is not zero and not one, then there should be at least one previous certificate ID.
155            _ => ensure!(!previous_certificate_ids.is_empty(), "Invalid round number, must have certificates"),
156        }
157
158        // Ensure that the number of transmissions is within bounds.
159        ensure!(
160            transmission_ids.len() <= Self::MAX_TRANSMISSIONS_PER_BATCH,
161            "Invalid number of transmission IDs ({})",
162            transmission_ids.len()
163        );
164        // Ensure that the number of previous certificate IDs is within bounds.
165        ensure!(
166            previous_certificate_ids.len() <= N::LATEST_MAX_CERTIFICATES()? as usize,
167            "Invalid number of previous certificate IDs ({})",
168            previous_certificate_ids.len()
169        );
170
171        // Compute the batch ID.
172        let batch_id = Self::compute_batch_id(
173            author,
174            round,
175            timestamp,
176            committee_id,
177            &transmission_ids,
178            &previous_certificate_ids,
179        )?;
180        // Verify the signature.
181        if !signature.verify(&author, &[batch_id]) {
182            bail!("Invalid signature for the batch header");
183        }
184        // Return the batch header.
185        Ok(Self {
186            author,
187            batch_id,
188            round,
189            timestamp,
190            committee_id,
191            transmission_ids,
192            previous_certificate_ids,
193            signature,
194        })
195    }
196}
197
198impl<N: Network> BatchHeader<N> {
199    /// Returns the batch ID.
200    pub const fn batch_id(&self) -> Field<N> {
201        self.batch_id
202    }
203
204    /// Returns the author.
205    pub const fn author(&self) -> Address<N> {
206        self.author
207    }
208
209    /// Returns the round number.
210    pub const fn round(&self) -> u64 {
211        self.round
212    }
213
214    /// Returns the timestamp.
215    pub const fn timestamp(&self) -> i64 {
216        self.timestamp
217    }
218
219    /// Returns the committee ID.
220    pub const fn committee_id(&self) -> Field<N> {
221        self.committee_id
222    }
223
224    /// Returns the transmission IDs.
225    pub const fn transmission_ids(&self) -> &IndexSet<TransmissionID<N>> {
226        &self.transmission_ids
227    }
228
229    /// Returns the batch certificate IDs for the previous round.
230    pub const fn previous_certificate_ids(&self) -> &IndexSet<Field<N>> {
231        &self.previous_certificate_ids
232    }
233
234    /// Returns the signature.
235    pub const fn signature(&self) -> &Signature<N> {
236        &self.signature
237    }
238}
239
240impl<N: Network> BatchHeader<N> {
241    /// Returns `true` if the batch header is empty.
242    pub fn is_empty(&self) -> bool {
243        self.transmission_ids.is_empty()
244    }
245
246    /// Returns the number of transmissions in the batch header.
247    pub fn len(&self) -> usize {
248        self.transmission_ids.len()
249    }
250
251    /// Returns `true` if the batch contains the specified `transmission ID`.
252    pub fn contains(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
253        self.transmission_ids.contains(&transmission_id.into())
254    }
255}
256
257#[cfg(any(test, feature = "test-helpers"))]
258pub mod test_helpers {
259    use super::*;
260    use console::{account::PrivateKey, network::MainnetV0, prelude::TestRng};
261
262    use time::OffsetDateTime;
263
264    type CurrentNetwork = MainnetV0;
265
266    /// Returns a sample batch header, sampled at random.
267    pub fn sample_batch_header(rng: &mut TestRng) -> BatchHeader<CurrentNetwork> {
268        sample_batch_header_for_round(rng.r#gen(), rng)
269    }
270
271    /// Returns a sample batch header with a given round; the rest is sampled at random.
272    pub fn sample_batch_header_for_round(round: u64, rng: &mut TestRng) -> BatchHeader<CurrentNetwork> {
273        // Sample certificate IDs.
274        let certificate_ids = (0..10).map(|_| Field::<CurrentNetwork>::rand(rng)).collect::<IndexSet<_>>();
275        // Return the batch header.
276        sample_batch_header_for_round_with_previous_certificate_ids(round, certificate_ids, rng)
277    }
278
279    /// Returns a sample batch header with a given round and set of previous certificate IDs; the rest is sampled at random.
280    pub fn sample_batch_header_for_round_with_previous_certificate_ids(
281        round: u64,
282        previous_certificate_ids: IndexSet<Field<CurrentNetwork>>,
283        rng: &mut TestRng,
284    ) -> BatchHeader<CurrentNetwork> {
285        // Sample a private key.
286        let private_key = PrivateKey::new(rng).unwrap();
287        // Generate a new certificated with the key as its author.
288        sample_batch_header_for_round_and_key_with_previous_certificate_ids(
289            round,
290            &private_key,
291            previous_certificate_ids,
292            rng,
293        )
294    }
295
296    /// Returns a sample batch header with a given round, author key, and set of previous certificate IDs; the rest is sampled at random.
297    pub fn sample_batch_header_for_round_and_key_with_previous_certificate_ids(
298        round: u64,
299        private_key: &PrivateKey<CurrentNetwork>,
300        previous_certificate_ids: IndexSet<Field<CurrentNetwork>>,
301        rng: &mut TestRng,
302    ) -> BatchHeader<CurrentNetwork> {
303        // Sample the committee ID.
304        let committee_id = Field::<CurrentNetwork>::rand(rng);
305        // Sample transmission IDs.
306        let transmission_ids = snarkvm_ledger_narwhal_transmission_id::test_helpers::sample_transmission_ids(rng)
307            .into_iter()
308            .collect::<IndexSet<_>>();
309        // Checkpoint the timestamp for the batch.
310        let timestamp = OffsetDateTime::now_utc().unix_timestamp();
311        // Return the batch header.
312        BatchHeader::new(private_key, round, timestamp, committee_id, transmission_ids, previous_certificate_ids, rng)
313            .unwrap()
314    }
315
316    /// Returns a list of sample batch headers, sampled at random.
317    pub fn sample_batch_headers(rng: &mut TestRng) -> Vec<BatchHeader<CurrentNetwork>> {
318        // Initialize a sample vector.
319        let mut sample = Vec::with_capacity(10);
320        // Append sample batches.
321        for _ in 0..10 {
322            // Append the batch header.
323            sample.push(sample_batch_header(rng));
324        }
325        // Return the sample vector.
326        sample
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    use console::network::{CanaryV0, MainnetV0, TestnetV0};
335
336    #[test]
337    fn test_max_synthesis_cost_below_batch_spend_limit() {
338        fn max_synthesis_cost_valid<N: Network>() {
339            let max_synthesis_cost = N::MAX_DEPLOYMENT_VARIABLES.saturating_add(N::MAX_DEPLOYMENT_CONSTRAINTS)
340                * N::SYNTHESIS_FEE_MULTIPLIER
341                / N::ARC_0005_COMPUTE_DISCOUNT;
342            for (_, height) in N::CONSENSUS_VERSION_HEIGHTS().iter() {
343                assert!(max_synthesis_cost < BatchHeader::<N>::batch_spend_limit(*height));
344            }
345        }
346
347        max_synthesis_cost_valid::<CanaryV0>();
348        max_synthesis_cost_valid::<TestnetV0>();
349        max_synthesis_cost_valid::<MainnetV0>();
350    }
351}