data_anchor_proofs/
slot_hash.rs

1//! Proof of the state of the [SlotHashes sysvar](https://docs.solanalabs.com/runtime/sysvars#slothashes)
2//! for a given slot. Can be used together with [vote proofs][`crate::vote::single::SingleVoteProof`] to
3//! prove that a specific bank hash was voted on.
4
5use std::{fmt::Debug, sync::Arc};
6
7use serde::{Deserialize, Serialize};
8use solana_sdk::{clock::Slot, slot_hashes::SlotHashes};
9use thiserror::Error;
10
11use crate::{accounts_delta_hash::inclusion::InclusionProof, debug::NoPrettyPrint};
12
13/// A proof for the state of the SlotHashes sysvar for a given slot.
14#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
15pub struct SlotHashProof {
16    /// The slot at which the proven state was captured.
17    pub(crate) slot: Slot,
18    /// An inclusion proof for the SlotHashes sysvar. It might seem superfluous given that the
19    /// sysvar will be present in every slot, but this proof is needed to ensure the state has not
20    /// been taken from another valid (but different) slot.
21    pub(crate) slot_hashes_inclusion_proof: InclusionProof,
22}
23
24impl Debug for SlotHashProof {
25    #[cfg_attr(test, mutants::skip)]
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        let mut s = f.debug_struct("Proof");
28        s.field("slot", &self.slot).field(
29            "slot_hashes_inclusion_proof",
30            &self.slot_hashes_inclusion_proof,
31        );
32        if let Ok(slot_hashes) = self.deserialize_account_data() {
33            s.field(
34                "slot_hashes[..10]",
35                &NoPrettyPrint(slot_hashes.iter().take(10).collect::<Vec<_>>()),
36            );
37            if let Some(last_slot) = slot_hashes.last() {
38                s.field("last_slot", &last_slot.0);
39            }
40        }
41        s.finish()
42    }
43}
44
45/// Failures that can occur when verifying a [`SlotHashProof`].
46#[derive(Debug, Clone, Error)]
47pub enum SlotHashError {
48    #[error(
49        "Slot hash for slot {slot} does not match the expected value, expected {expected}, found {found:?}"
50    )]
51    SlotHashMismatch {
52        slot: Slot,
53        expected: solana_sdk::hash::Hash,
54        found: Option<solana_sdk::hash::Hash>,
55    },
56    #[error("The computed accounts delta hash does not match the provided value")]
57    AccountsDeltaHashMismatch,
58    #[error("The inclusion proof is not for the SlotHashes sysvar")]
59    ProofNotForSlotHashes,
60    #[error(transparent)]
61    BincodeDeserialize(#[from] Arc<bincode::Error>),
62}
63
64impl SlotHashProof {
65    /// Creates a new proof for the state of the SlotHashes sysvar for a given slot.
66    pub fn new(slot: Slot, slot_hashes_inclusion_proof: InclusionProof) -> Self {
67        Self {
68            slot,
69            slot_hashes_inclusion_proof,
70        }
71    }
72
73    /// Verifies that the SlotHashes sysvar contains `bank_hash`.
74    pub fn verify(
75        &self,
76        slot: Slot,
77        bank_hash: solana_sdk::hash::Hash,
78        accounts_delta_hash: solana_sdk::hash::Hash,
79    ) -> Result<(), SlotHashError> {
80        if self.slot_hashes_inclusion_proof.account_pubkey != solana_sdk::sysvar::slot_hashes::ID {
81            return Err(SlotHashError::ProofNotForSlotHashes);
82        }
83
84        if self.hash(slot) != Some(bank_hash) {
85            return Err(SlotHashError::SlotHashMismatch {
86                slot,
87                expected: bank_hash,
88                found: self.hash(slot),
89            });
90        }
91
92        if !self.slot_hashes_inclusion_proof.verify(accounts_delta_hash) {
93            return Err(SlotHashError::AccountsDeltaHashMismatch);
94        }
95
96        Ok(())
97    }
98
99    /// Attempts to deserialize the stored account data into a [`SlotHashes`].
100    pub fn deserialize_account_data(&self) -> Result<SlotHashes, Arc<bincode::Error>> {
101        bincode::deserialize(&self.slot_hashes_inclusion_proof.account_data.data).map_err(Arc::new)
102    }
103
104    /// Attempts to deserialize the stored account data and extracts the bankhash for a specific slot.
105    pub fn hash(&self, slot: Slot) -> Option<solana_sdk::hash::Hash> {
106        self.deserialize_account_data().ok()?.get(&slot).copied()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use std::collections::HashSet;
113
114    use arbtest::arbtest;
115    use solana_sdk::{account::Account, sysvar, sysvar::SysvarId};
116
117    use super::*;
118    use crate::{
119        accounts_delta_hash::{
120            AccountMerkleTree,
121            testing::{ArbAccount, ArbKeypair},
122        },
123        testing::arbitrary_hash,
124    };
125
126    #[test]
127    fn slot_hash_construction() {
128        arbtest(|u| {
129            let mut slot_hashes = u
130                .arbitrary_iter::<(u64, [u8; 32])>()?
131                .map(|tup| Ok((tup?.0, solana_sdk::hash::Hash::new_from_array(tup?.1))))
132                .collect::<Result<HashSet<_>, _>>()?
133                .into_iter()
134                .collect::<Vec<_>>();
135
136            let ((slot, hash), excluded) = if u.ratio(1, 10)? {
137                let slot_hash = slot_hashes.remove(u.choose_index(slot_hashes.len())?);
138                (slot_hash, true)
139            } else {
140                let slot_hash = slot_hashes.get(u.choose_index(slot_hashes.len())?).unwrap();
141                (*slot_hash, false)
142            };
143
144            let slot_hashes = SlotHashes::new(&slot_hashes);
145
146            let mut slot_hashes_account: Account = u.arbitrary::<ArbAccount>()?.into();
147            slot_hashes_account.data = bincode::serialize(&slot_hashes).unwrap();
148
149            let other_key = u.arbitrary::<ArbKeypair>()?.pubkey();
150            let other_account = u.arbitrary::<ArbAccount>()?;
151
152            let mut tree =
153                AccountMerkleTree::builder([sysvar::slot_hashes::ID].into_iter().collect());
154            tree.insert(SlotHashes::id(), slot_hashes_account);
155            tree.insert(other_key, other_account.into());
156            let tree = tree.build();
157
158            let included_id = if u.ratio(1, 10)? {
159                other_key
160            } else {
161                SlotHashes::id()
162            };
163
164            let inclusion_proof = tree.prove_inclusion(included_id).unwrap();
165
166            let proof = SlotHashProof::new(slot, inclusion_proof);
167
168            dbg!(&proof, &included_id, &slot_hashes, &tree);
169            if excluded || included_id != SlotHashes::id() {
170                proof.verify(slot, hash, tree.root()).unwrap_err();
171            } else if u.ratio(1, 10)? {
172                proof.verify(slot, hash, arbitrary_hash(u)?).unwrap_err();
173            } else {
174                proof.verify(slot, hash, tree.root()).unwrap();
175            }
176
177            Ok(())
178        })
179        .size_max(100_000_000);
180    }
181}