dig_evidence/
read_integrity.rs1use chia_protocol::Bytes32;
18use dig_capsule::format::Bytes32 as CapsuleBytes32;
19use dig_capsule::merkle::ProofStep;
20use dig_chainsource_interface::ChainSource;
21
22use crate::error::{EvidenceError, EvidenceResult};
23use crate::evidence::Evidence;
24use crate::range_inclusion::{RangeInclusionClaim, RangeInclusionEvidence};
25use crate::root_anchor::{RootAnchorClaim, RootAnchorEvidence};
26
27fn to_capsule_bytes(bytes: Bytes32) -> CapsuleBytes32 {
31 let mut raw = [0u8; 32];
32 raw.copy_from_slice(bytes.as_ref());
33 CapsuleBytes32(raw)
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ReadIntegrityClaim {
40 pub store_id: Bytes32,
42 pub generation_root: Bytes32,
44 pub range_leaf: CapsuleBytes32,
46 pub range_path: Vec<ProofStep>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ReadIntegrityEvidence {
58 range: RangeInclusionEvidence,
59 anchor: RootAnchorEvidence,
60}
61
62impl ReadIntegrityEvidence {
63 pub fn range(&self) -> &RangeInclusionEvidence {
65 &self.range
66 }
67
68 pub fn anchor(&self) -> &RootAnchorEvidence {
70 &self.anchor
71 }
72
73 pub fn store_id(&self) -> Bytes32 {
75 self.anchor.store_id()
76 }
77
78 pub fn generation_root(&self) -> Bytes32 {
80 self.anchor.generation_root()
81 }
82}
83
84impl Evidence for ReadIntegrityEvidence {
85 type Claim = ReadIntegrityClaim;
86
87 fn gather<S: ChainSource>(claim: &Self::Claim, chain: &S) -> EvidenceResult<Self> {
91 let anchor = RootAnchorEvidence::gather(
94 &RootAnchorClaim {
95 store_id: claim.store_id,
96 generation_root: claim.generation_root,
97 },
98 chain,
99 )?;
100
101 let range = RangeInclusionEvidence::gather(
104 &RangeInclusionClaim {
105 leaf: claim.range_leaf,
106 path: claim.range_path.clone(),
107 generation_root: to_capsule_bytes(anchor.generation_root()),
108 },
109 chain,
110 )?;
111
112 Ok(Self { range, anchor })
113 }
114
115 fn verify(&self) -> EvidenceResult<()> {
117 self.range.verify()?;
118 self.anchor.verify()?;
119 if self.range.generation_root() != to_capsule_bytes(self.anchor.generation_root()) {
120 return Err(EvidenceError::RootMismatch);
121 }
122 Ok(())
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use chia_puzzle_types::standard::StandardArgs;
130 use chia_wallet_sdk::test::Simulator;
131 use dig_capsule::merkle::MerkleTree;
132 use dig_chainsource_interface::MockChainSource;
133 use dig_chainsource_interface::{CoinRecord, SingletonLineage};
134 use dig_merkle::{mint_datastore, Owner};
135
136 struct Fixture {
139 source: MockChainSource,
140 store_id: Bytes32,
141 root: Bytes32,
142 leaf: CapsuleBytes32,
143 path: Vec<ProofStep>,
144 }
145
146 fn record(coin: chia_protocol::Coin, spent: Option<u32>) -> CoinRecord {
147 CoinRecord {
148 coin,
149 confirmed_height: Some(1),
150 spent_height: spent,
151 timestamp: None,
152 coinbase: false,
153 }
154 }
155
156 fn fixture() -> anyhow::Result<Fixture> {
157 let chunks: Vec<Vec<u8>> = (0..6u8).map(|i| vec![i; 16]).collect();
159 let tree = MerkleTree::build(&chunks);
160 let root_capsule = tree.root();
161 let proof = tree.prove(3).expect("index in range");
162 let root = Bytes32::new(root_capsule.0);
163
164 let mut sim = Simulator::new();
166 let owner = sim.bls(1_000_000);
167 let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
168 let built = mint_datastore(
169 owner.coin,
170 Owner::Standard(owner.pk),
171 root,
172 None,
173 None,
174 None,
175 None,
176 None,
177 owner_ph,
178 vec![],
179 0,
180 )?;
181 sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
182 let minted = built.child.expect("child");
183 let store_id = minted.info.launcher_id;
184 let launcher_spend = built
185 .coin_spends
186 .iter()
187 .find(|s| s.coin.coin_id() == store_id)
188 .expect("launcher spend")
189 .clone();
190 let launcher = launcher_spend.coin;
191 let eve = minted.coin;
192
193 let source = MockChainSource::new()
194 .with_coin(store_id, record(launcher, Some(2)))
195 .with_coin(eve.coin_id(), record(eve, None))
196 .with_spend(store_id, launcher_spend)
197 .with_lineage(
198 store_id,
199 SingletonLineage::new(eve.coin_id(), [store_id, eve.coin_id()]),
200 );
201
202 Ok(Fixture {
203 source,
204 store_id,
205 root,
206 leaf: proof.leaf,
207 path: proof.path,
208 })
209 }
210
211 #[test]
212 fn a_genuine_read_gathers_and_verifies() -> anyhow::Result<()> {
213 let f = fixture()?;
214 let claim = ReadIntegrityClaim {
215 store_id: f.store_id,
216 generation_root: f.root,
217 range_leaf: f.leaf,
218 range_path: f.path.clone(),
219 };
220 let evidence = ReadIntegrityEvidence::gather(&claim, &f.source).expect("genuine read");
221 assert_eq!(evidence.store_id(), f.store_id);
222 assert_eq!(evidence.generation_root(), f.root);
223 assert!(evidence.verify().is_ok());
224 Ok(())
225 }
226
227 #[test]
228 fn a_range_under_an_unanchored_root_is_rejected() -> anyhow::Result<()> {
229 let f = fixture()?;
230 let claim = ReadIntegrityClaim {
232 store_id: f.store_id,
233 generation_root: Bytes32::new([0xAB; 32]), range_leaf: f.leaf,
235 range_path: f.path.clone(),
236 };
237 assert_eq!(
238 ReadIntegrityEvidence::gather(&claim, &f.source).unwrap_err(),
239 EvidenceError::RootNotCommitted
240 );
241 Ok(())
242 }
243
244 #[test]
245 fn a_tampered_range_leaf_is_rejected_even_with_a_genuine_anchor() -> anyhow::Result<()> {
246 let f = fixture()?;
247 let claim = ReadIntegrityClaim {
248 store_id: f.store_id,
249 generation_root: f.root, range_leaf: CapsuleBytes32([0xff; 32]), range_path: f.path.clone(),
252 };
253 assert_eq!(
254 ReadIntegrityEvidence::gather(&claim, &f.source).unwrap_err(),
255 EvidenceError::ProofDoesNotFold
256 );
257 Ok(())
258 }
259}