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