data_anchor_proofs/
slot_hash.rs1use 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#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
15pub struct SlotHashProof {
16 pub(crate) slot: Slot,
18 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#[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 pub fn new(slot: Slot, slot_hashes_inclusion_proof: InclusionProof) -> Self {
67 Self {
68 slot,
69 slot_hashes_inclusion_proof,
70 }
71 }
72
73 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 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 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}