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