snarkos_node_bft/helpers/
proposal_cache.rsuse crate::helpers::{Proposal, SignedProposals};
use snarkvm::{
console::{account::Address, network::Network, program::SUBDAG_CERTIFICATES_DEPTH},
ledger::narwhal::BatchCertificate,
prelude::{FromBytes, IoResult, Read, Result, ToBytes, Write, anyhow, bail, error},
};
use aleo_std::{StorageMode, aleo_ledger_dir};
use indexmap::IndexSet;
use std::{fs, path::PathBuf};
pub fn proposal_cache_path(network: u16, dev: Option<u16>) -> PathBuf {
const PROPOSAL_CACHE_FILE_NAME: &str = "current-proposal-cache";
let mut path = aleo_ledger_dir(network, StorageMode::from(dev));
path.pop();
match dev {
Some(id) => path.push(format!(".{PROPOSAL_CACHE_FILE_NAME}-{}-{}", network, id)),
None => path.push(format!("{PROPOSAL_CACHE_FILE_NAME}-{}", network)),
}
path
}
#[derive(Debug, PartialEq, Eq)]
pub struct ProposalCache<N: Network> {
latest_round: u64,
proposal: Option<Proposal<N>>,
signed_proposals: SignedProposals<N>,
pending_certificates: IndexSet<BatchCertificate<N>>,
}
impl<N: Network> ProposalCache<N> {
pub fn new(
latest_round: u64,
proposal: Option<Proposal<N>>,
signed_proposals: SignedProposals<N>,
pending_certificates: IndexSet<BatchCertificate<N>>,
) -> Self {
Self { latest_round, proposal, signed_proposals, pending_certificates }
}
pub fn is_valid(&self, expected_signer: Address<N>) -> bool {
self.proposal
.as_ref()
.map(|proposal| {
proposal.batch_header().author() == expected_signer && self.latest_round == proposal.round()
})
.unwrap_or(true)
&& self.signed_proposals.is_valid(expected_signer)
}
pub fn exists(dev: Option<u16>) -> bool {
proposal_cache_path(N::ID, dev).exists()
}
pub fn load(expected_signer: Address<N>, dev: Option<u16>) -> Result<Self> {
let path = proposal_cache_path(N::ID, dev);
let proposal_cache = match fs::read(&path) {
Ok(bytes) => match Self::from_bytes_le(&bytes) {
Ok(proposal_cache) => proposal_cache,
Err(_) => bail!("Couldn't deserialize the proposal stored at {}", path.display()),
},
Err(_) => bail!("Couldn't read the proposal stored at {}", path.display()),
};
if !proposal_cache.is_valid(expected_signer) {
bail!("The proposal cache is invalid for the given address {expected_signer}");
}
info!("Loaded the proposal cache from {} at round {}", path.display(), proposal_cache.latest_round);
Ok(proposal_cache)
}
pub fn store(&self, dev: Option<u16>) -> Result<()> {
let path = proposal_cache_path(N::ID, dev);
info!("Storing the proposal cache to {}...", path.display());
let bytes = self.to_bytes_le()?;
fs::write(&path, bytes)
.map_err(|err| anyhow!("Couldn't write the proposal cache to {} - {err}", path.display()))?;
Ok(())
}
pub fn into(self) -> (u64, Option<Proposal<N>>, SignedProposals<N>, IndexSet<BatchCertificate<N>>) {
(self.latest_round, self.proposal, self.signed_proposals, self.pending_certificates)
}
}
impl<N: Network> ToBytes for ProposalCache<N> {
fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
self.latest_round.write_le(&mut writer)?;
self.proposal.is_some().write_le(&mut writer)?;
if let Some(proposal) = &self.proposal {
proposal.write_le(&mut writer)?;
}
self.signed_proposals.write_le(&mut writer)?;
u32::try_from(self.pending_certificates.len()).map_err(error)?.write_le(&mut writer)?;
for certificate in &self.pending_certificates {
certificate.write_le(&mut writer)?;
}
Ok(())
}
}
impl<N: Network> FromBytes for ProposalCache<N> {
fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
let latest_round = u64::read_le(&mut reader)?;
let has_proposal: bool = FromBytes::read_le(&mut reader)?;
let proposal = match has_proposal {
true => Some(Proposal::read_le(&mut reader)?),
false => None,
};
let signed_proposals = SignedProposals::read_le(&mut reader)?;
let num_certificates = u32::read_le(&mut reader)?;
if num_certificates > 2u32.saturating_pow(SUBDAG_CERTIFICATES_DEPTH as u32) {
return Err(error(format!(
"Number of certificates ({num_certificates}) exceeds the maximum ({})",
2u32.saturating_pow(SUBDAG_CERTIFICATES_DEPTH as u32)
)));
};
let pending_certificates =
(0..num_certificates).map(|_| BatchCertificate::read_le(&mut reader)).collect::<IoResult<IndexSet<_>>>()?;
Ok(Self::new(latest_round, proposal, signed_proposals, pending_certificates))
}
}
impl<N: Network> Default for ProposalCache<N> {
fn default() -> Self {
Self::new(0, None, Default::default(), Default::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::{proposal::tests::sample_proposal, signed_proposals::tests::sample_signed_proposals};
use snarkvm::{
console::{account::PrivateKey, network::MainnetV0},
ledger::narwhal::batch_certificate::test_helpers::sample_batch_certificates,
utilities::TestRng,
};
type CurrentNetwork = MainnetV0;
const ITERATIONS: usize = 100;
pub(crate) fn sample_proposal_cache(
signer: &PrivateKey<CurrentNetwork>,
rng: &mut TestRng,
) -> ProposalCache<CurrentNetwork> {
let proposal = sample_proposal(rng);
let signed_proposals = sample_signed_proposals(signer, rng);
let round = proposal.round();
let pending_certificates = sample_batch_certificates(rng);
ProposalCache::new(round, Some(proposal), signed_proposals, pending_certificates)
}
#[test]
fn test_bytes() {
let rng = &mut TestRng::default();
let singer_private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
for _ in 0..ITERATIONS {
let expected = sample_proposal_cache(&singer_private_key, rng);
let expected_bytes = expected.to_bytes_le().unwrap();
assert_eq!(expected, ProposalCache::read_le(&expected_bytes[..]).unwrap());
}
}
}