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