Skip to main content

snarkos_node_bft/helpers/
proposal_cache.rs

1// Copyright (c) 2019-2025 Provable Inc.
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 crate::helpers::{Proposal, SignedProposals};
17use snarkos_utilities::NodeDataDir;
18
19use snarkvm::{
20    console::{account::Address, network::Network, program::SUBDAG_CERTIFICATES_DEPTH},
21    ledger::narwhal::BatchCertificate,
22    prelude::{FromBytes, IoResult, Read, Result, ToBytes, Write, anyhow, bail, error},
23};
24
25use indexmap::IndexSet;
26use std::{fs, path::PathBuf};
27
28/// Returns the path where a proposal cache file may be stored.
29pub fn proposal_cache_path(node_data_dir: &NodeDataDir) -> PathBuf {
30    node_data_dir.path().join("current-proposal-cache")
31}
32
33/// A helper type for the cache of proposal and signed proposals.
34#[derive(Debug, PartialEq, Eq)]
35pub struct ProposalCache<N: Network> {
36    /// The latest round this node was on prior to the reboot.
37    latest_round: u64,
38    /// The latest proposal this node has created.
39    proposal: Option<Proposal<N>>,
40    /// The signed proposals this node has received.
41    signed_proposals: SignedProposals<N>,
42    /// The pending certificates in storage that have not been included in the ledger.
43    pending_certificates: IndexSet<BatchCertificate<N>>,
44}
45
46impl<N: Network> ProposalCache<N> {
47    /// Initializes a new instance of the proposal cache.
48    pub fn new(
49        latest_round: u64,
50        proposal: Option<Proposal<N>>,
51        signed_proposals: SignedProposals<N>,
52        pending_certificates: IndexSet<BatchCertificate<N>>,
53    ) -> Self {
54        Self { latest_round, proposal, signed_proposals, pending_certificates }
55    }
56
57    /// Ensure that the proposal and every signed proposal is associated with the `expected_signer`.
58    pub fn is_valid(&self, expected_signer: Address<N>) -> bool {
59        self.proposal
60            .as_ref()
61            .map(|proposal| {
62                proposal.batch_header().author() == expected_signer && self.latest_round == proposal.round()
63            })
64            .unwrap_or(true)
65            && self.signed_proposals.is_valid(expected_signer)
66    }
67
68    /// Returns `true` if a proposal cache exists for the given network and `dev`.
69    pub fn exists(node_data_dir: &NodeDataDir) -> bool {
70        proposal_cache_path(node_data_dir).exists()
71    }
72
73    /// Load the proposal cache from the file system and ensure that the proposal cache is valid.
74    pub fn load(expected_signer: Address<N>, node_data_dir: &NodeDataDir) -> Result<Self> {
75        // Construct the proposal cache file system path.
76        let path = proposal_cache_path(node_data_dir);
77
78        // Deserialize the proposal cache from the file system.
79        let proposal_cache = match fs::read(&path) {
80            Ok(bytes) => match Self::from_bytes_le(&bytes) {
81                Ok(proposal_cache) => proposal_cache,
82                Err(_) => bail!("Couldn't deserialize the proposal stored at {}", path.display()),
83            },
84            Err(_) => bail!("Couldn't read the proposal stored at {}", path.display()),
85        };
86
87        // Ensure the proposal cache is valid.
88        if !proposal_cache.is_valid(expected_signer) {
89            bail!("The proposal cache is invalid for the given address {expected_signer}");
90        }
91
92        info!("Loaded the proposal cache from {} at round {}", path.display(), proposal_cache.latest_round);
93
94        Ok(proposal_cache)
95    }
96
97    /// Store the proposal cache to the file system.
98    pub fn store(&self, node_data_dir: &NodeDataDir) -> Result<()> {
99        let path = proposal_cache_path(node_data_dir);
100        info!("Storing the proposal cache to {}...", path.display());
101
102        // Serialize the proposal cache.
103        let bytes = self.to_bytes_le()?;
104        // Store the proposal cache to the file system.
105        fs::write(&path, bytes)
106            .map_err(|err| anyhow!("Couldn't write the proposal cache to {} - {err}", path.display()))?;
107
108        Ok(())
109    }
110
111    /// Returns the latest round, proposal, signed proposals, and pending certificates.
112    pub fn into(self) -> (u64, Option<Proposal<N>>, SignedProposals<N>, IndexSet<BatchCertificate<N>>) {
113        (self.latest_round, self.proposal, self.signed_proposals, self.pending_certificates)
114    }
115}
116
117impl<N: Network> ToBytes for ProposalCache<N> {
118    fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
119        // Serialize the `latest_round`.
120        self.latest_round.write_le(&mut writer)?;
121        // Serialize the `proposal`.
122        self.proposal.is_some().write_le(&mut writer)?;
123        if let Some(proposal) = &self.proposal {
124            proposal.write_le(&mut writer)?;
125        }
126        // Serialize the `signed_proposals`.
127        self.signed_proposals.write_le(&mut writer)?;
128        // Write the number of pending certificates.
129        u32::try_from(self.pending_certificates.len()).map_err(error)?.write_le(&mut writer)?;
130        // Serialize the pending certificates.
131        for certificate in &self.pending_certificates {
132            certificate.write_le(&mut writer)?;
133        }
134
135        Ok(())
136    }
137}
138
139impl<N: Network> FromBytes for ProposalCache<N> {
140    fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
141        // Deserialize `latest_round`.
142        let latest_round = u64::read_le(&mut reader)?;
143        // Deserialize `proposal`.
144        let has_proposal: bool = FromBytes::read_le(&mut reader)?;
145        let proposal = match has_proposal {
146            true => Some(Proposal::read_le(&mut reader)?),
147            false => None,
148        };
149        // Deserialize `signed_proposals`.
150        let signed_proposals = SignedProposals::read_le(&mut reader)?;
151        // Read the number of pending certificates.
152        let num_certificates = u32::read_le(&mut reader)?;
153        // Ensure the number of certificates is within bounds.
154        if num_certificates > 2u32.saturating_pow(SUBDAG_CERTIFICATES_DEPTH as u32) {
155            return Err(error(format!(
156                "Number of certificates ({num_certificates}) exceeds the maximum ({})",
157                2u32.saturating_pow(SUBDAG_CERTIFICATES_DEPTH as u32)
158            )));
159        };
160        // Deserialize the pending certificates.
161        let pending_certificates =
162            (0..num_certificates).map(|_| BatchCertificate::read_le(&mut reader)).collect::<IoResult<IndexSet<_>>>()?;
163
164        Ok(Self::new(latest_round, proposal, signed_proposals, pending_certificates))
165    }
166}
167
168impl<N: Network> Default for ProposalCache<N> {
169    /// Initializes a new instance of the proposal cache.
170    fn default() -> Self {
171        Self::new(0, None, Default::default(), Default::default())
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::helpers::{proposal::tests::sample_proposal, signed_proposals::tests::sample_signed_proposals};
179    use snarkvm::{
180        console::{account::PrivateKey, network::MainnetV0},
181        ledger::narwhal::batch_certificate::test_helpers::sample_batch_certificates,
182        utilities::TestRng,
183    };
184
185    type CurrentNetwork = MainnetV0;
186
187    const ITERATIONS: usize = 100;
188
189    pub(crate) fn sample_proposal_cache(
190        signer: &PrivateKey<CurrentNetwork>,
191        rng: &mut TestRng,
192    ) -> ProposalCache<CurrentNetwork> {
193        let proposal = sample_proposal(rng);
194        let signed_proposals = sample_signed_proposals(signer, rng);
195        let round = proposal.round();
196        let pending_certificates = sample_batch_certificates(rng);
197
198        ProposalCache::new(round, Some(proposal), signed_proposals, pending_certificates)
199    }
200
201    #[test]
202    fn test_bytes() {
203        let rng = &mut TestRng::default();
204        let singer_private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
205
206        for _ in 0..ITERATIONS {
207            let expected = sample_proposal_cache(&singer_private_key, rng);
208            // Check the byte representation.
209            let expected_bytes = expected.to_bytes_le().unwrap();
210            assert_eq!(expected, ProposalCache::read_le(&expected_bytes[..]).unwrap());
211        }
212    }
213}