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