1use chia_protocol::Bytes32;
20use chia_wallet_sdk::driver::{Did, SingletonInfo};
21
22use crate::error::{DidError, DidResult};
23use crate::resolve::{authenticate_singleton, ChainSource};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum LineageModel {
28 Direct,
30
31 LaunchedFrom {
34 launcher: Bytes32,
36 did_parent: Bytes32,
38 },
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct AncestryProof {
48 coin_id: Bytes32,
49 did_launcher_id: Bytes32,
50 model: LineageModel,
51 did_lineage_tip: Bytes32,
52 authenticated_launcher: Bytes32,
53 chain: Vec<Bytes32>,
54}
55
56impl AncestryProof {
57 pub fn coin_id(&self) -> Bytes32 {
59 self.coin_id
60 }
61
62 pub fn did_launcher_id(&self) -> Bytes32 {
64 self.did_launcher_id
65 }
66
67 pub fn model(&self) -> LineageModel {
69 self.model
70 }
71
72 pub fn did_lineage_tip(&self) -> Bytes32 {
74 self.did_lineage_tip
75 }
76
77 pub fn authenticated_launcher(&self) -> Bytes32 {
80 self.authenticated_launcher
81 }
82
83 pub fn chain(&self) -> &[Bytes32] {
85 &self.chain
86 }
87}
88
89pub fn prove_lineage<S: ChainSource>(
107 coin_id: Bytes32,
108 did: &Did,
109 chain: &S,
110) -> DidResult<AncestryProof> {
111 let did_launcher_id = did.info.launcher_id();
112
113 let did_lineage = chain
116 .resolve_singleton_lineage(did_launcher_id)
117 .map_err(|error| DidError::Chain(error.to_string()))?
118 .ok_or(DidError::NoIdentitySingleton)?;
119
120 let authenticated = authenticate_singleton(coin_id, chain)?;
122
123 let model = if authenticated.launcher_id == did_launcher_id {
125 LineageModel::Direct
126 } else {
127 let did_parent = authenticated.launcher_coin.parent_coin_info;
130 if !did_lineage.contains(did_parent) {
131 return Err(DidError::NotDidRooted);
132 }
133 LineageModel::LaunchedFrom {
134 launcher: authenticated.launcher_id,
135 did_parent,
136 }
137 };
138
139 Ok(AncestryProof {
140 coin_id,
141 did_launcher_id,
142 model,
143 did_lineage_tip: did_lineage.tip(),
144 authenticated_launcher: authenticated.launcher_id,
145 chain: authenticated.trail,
146 })
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::collections::HashMap;
153
154 use chia_protocol::{Coin, CoinSpend};
155 use chia_puzzle_types::singleton::SingletonArgs;
156 use chia_puzzle_types::Memos;
157 use chia_wallet_sdk::driver::{Launcher, SpendContext, StandardLayer};
158 use chia_wallet_sdk::test::Simulator;
159 use chia_wallet_sdk::types::Conditions;
160 use dig_chainsource_interface::CoinRecord;
161
162 use crate::create::create_simple_did;
163 use crate::resolve::{authenticate_singleton_bounded, SingletonLineage};
164 use crate::types::Owner;
165
166 struct SimSource<'a> {
169 sim: &'a Simulator,
170 lineages: HashMap<Bytes32, SingletonLineage>,
171 }
172
173 impl ChainSource for SimSource<'_> {
174 type Error = String;
175
176 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
177 Ok(self.sim.coin_state(coin_id).map(CoinRecord::from))
178 }
179
180 fn coin_records_by_puzzle_hash(
181 &self,
182 _puzzle_hash: Bytes32,
183 _include_spent: bool,
184 ) -> Result<Vec<CoinRecord>, Self::Error> {
185 Ok(Vec::new())
188 }
189
190 fn coin_records_by_parent(
191 &self,
192 _parent_coin_id: Bytes32,
193 ) -> Result<Vec<CoinRecord>, Self::Error> {
194 Ok(Vec::new())
195 }
196
197 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
198 let Some(state) = self.sim.coin_state(coin_id) else {
200 return Ok(None);
201 };
202 let (Some(reveal), Some(solution)) =
203 (self.sim.puzzle_reveal(coin_id), self.sim.solution(coin_id))
204 else {
205 return Ok(None);
206 };
207 Ok(Some(CoinSpend::new(state.coin, reveal, solution)))
208 }
209
210 fn resolve_singleton_lineage(
211 &self,
212 launcher_id: Bytes32,
213 ) -> Result<Option<SingletonLineage>, Self::Error> {
214 Ok(self.lineages.get(&launcher_id).cloned())
215 }
216
217 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
218 Ok(None)
219 }
220
221 fn block_timestamp(&self, _height: u32) -> Result<Option<u64>, Self::Error> {
222 Ok(None)
223 }
224 }
225
226 fn source_with<'a>(
228 sim: &'a Simulator,
229 launcher_id: Bytes32,
230 lineage: SingletonLineage,
231 ) -> SimSource<'a> {
232 SimSource {
233 sim,
234 lineages: HashMap::from([(launcher_id, lineage)]),
235 }
236 }
237
238 fn did_lineage(did: &Did) -> SingletonLineage {
241 SingletonLineage::new(
242 did.coin.coin_id(),
243 [
244 did.info.launcher_id(),
245 did.coin.parent_coin_info,
246 did.coin.coin_id(),
247 ],
248 )
249 }
250
251 #[test]
252 fn model_a_direct_proves_a_did_state() -> anyhow::Result<()> {
253 let mut sim = Simulator::new();
254 let ctx = &mut SpendContext::new();
255 let owner = sim.bls(1);
256
257 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
258 let did = spend.child.expect("create returns a child DID");
259 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
260
261 let launcher_id = did.info.launcher_id();
262 let source = source_with(&sim, launcher_id, did_lineage(&did));
263
264 let proof = prove_lineage(did.coin.coin_id(), &did, &source)?;
265 assert_eq!(proof.model(), LineageModel::Direct);
266 assert_eq!(proof.authenticated_launcher(), launcher_id);
267 assert_eq!(proof.did_launcher_id(), launcher_id);
268 assert_eq!(proof.coin_id(), did.coin.coin_id());
269 assert!(!proof.chain().is_empty());
270 Ok(())
271 }
272
273 #[test]
274 fn model_b_launched_from_proves_a_singleton_launched_by_the_did() -> anyhow::Result<()> {
275 let mut sim = Simulator::new();
276 let ctx = &mut SpendContext::new();
277 let owner = sim.bls(3);
280 let owner_p2 = StandardLayer::new(owner.pk);
281
282 let create = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
283 let did = create.child.expect("create returns a child DID");
284 sim.spend_coins(create.coin_spends, std::slice::from_ref(&owner.sk))?;
285
286 let launcher = Launcher::new(did.coin.coin_id(), 2).with_singleton_amount(1);
288 let launcher_id = launcher.coin().coin_id();
289 let (launch_conditions, eve_coin) = launcher.spend(ctx, owner.puzzle_hash, ())?;
290
291 let memos = ctx.hint(did.info.p2_puzzle_hash)?;
292 let did_spend_conditions =
293 launch_conditions.create_coin(did.info.inner_puzzle_hash().into(), 1, memos);
294 did.spend_with(ctx, &owner_p2, did_spend_conditions)?;
295 sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
296
297 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
298
299 let proof = prove_lineage(eve_coin.coin_id(), &did, &source)?;
300 assert_eq!(
301 proof.model(),
302 LineageModel::LaunchedFrom {
303 launcher: launcher_id,
304 did_parent: did.coin.coin_id(),
305 }
306 );
307 assert_eq!(proof.authenticated_launcher(), launcher_id);
308 assert_eq!(proof.did_launcher_id(), did.info.launcher_id());
309 Ok(())
310 }
311
312 #[test]
313 fn payment_coin_parented_to_a_did_is_not_a_singleton() -> anyhow::Result<()> {
314 let mut sim = Simulator::new();
315 let ctx = &mut SpendContext::new();
316 let owner = sim.bls(3);
317 let owner_p2 = StandardLayer::new(owner.pk);
318
319 let create = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
320 let did = create.child.expect("create returns a child DID");
321 sim.spend_coins(create.coin_spends, std::slice::from_ref(&owner.sk))?;
322
323 let memos = ctx.hint(did.info.p2_puzzle_hash)?;
326 let payment_puzzle_hash = owner.puzzle_hash;
327 let conditions = Conditions::new()
328 .create_coin(did.info.inner_puzzle_hash().into(), 1, memos)
329 .create_coin(payment_puzzle_hash, 2, Memos::None);
330 did.spend_with(ctx, &owner_p2, conditions)?;
331 sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
332
333 let payment_coin = Coin::new(did.coin.coin_id(), payment_puzzle_hash, 2);
334 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
335
336 let error = prove_lineage(payment_coin.coin_id(), &did, &source).unwrap_err();
337 assert!(matches!(error, DidError::NotASingleton));
338 Ok(())
339 }
340
341 #[test]
342 fn attacker_singleton_from_attacker_coin_is_not_did_rooted() -> anyhow::Result<()> {
343 let mut sim = Simulator::new();
344 let ctx = &mut SpendContext::new();
345
346 let victim = sim.bls(1);
347 let victim_spend = create_simple_did(ctx, victim.coin, Owner::Standard(victim.pk))?;
348 let victim_did = victim_spend.child.expect("child DID");
349 sim.spend_coins(victim_spend.coin_spends, std::slice::from_ref(&victim.sk))?;
350
351 let attacker = sim.bls(1);
352 let attacker_spend = create_simple_did(ctx, attacker.coin, Owner::Standard(attacker.pk))?;
353 let attacker_did = attacker_spend.child.expect("child DID");
354 sim.spend_coins(
355 attacker_spend.coin_spends,
356 std::slice::from_ref(&attacker.sk),
357 )?;
358
359 let source = source_with(
361 &sim,
362 victim_did.info.launcher_id(),
363 did_lineage(&victim_did),
364 );
365
366 let error = prove_lineage(attacker_did.coin.coin_id(), &victim_did, &source).unwrap_err();
367 assert!(matches!(error, DidError::NotDidRooted));
368 Ok(())
369 }
370
371 #[test]
372 fn pay_to_coin_wearing_a_singleton_puzzle_hash_is_not_a_singleton() -> anyhow::Result<()> {
373 let mut sim = Simulator::new();
374 let ctx = &mut SpendContext::new();
375
376 let victim = sim.bls(1);
377 let victim_spend = create_simple_did(ctx, victim.coin, Owner::Standard(victim.pk))?;
378 let victim_did = victim_spend.child.expect("child DID");
379 sim.spend_coins(victim_spend.coin_spends, std::slice::from_ref(&victim.sk))?;
380
381 let alice = sim.bls(1);
385 let alice_p2 = StandardLayer::new(alice.pk);
386 let fake_singleton_puzzle_hash: Bytes32 =
387 SingletonArgs::curry_tree_hash(victim_did.info.launcher_id(), alice.puzzle_hash.into())
388 .into();
389 alice_p2.spend(
390 ctx,
391 alice.coin,
392 Conditions::new().create_coin(fake_singleton_puzzle_hash, 1, Memos::None),
393 )?;
394 sim.spend_coins(ctx.take(), std::slice::from_ref(&alice.sk))?;
395
396 let fake_coin = Coin::new(alice.coin.coin_id(), fake_singleton_puzzle_hash, 1);
397 let source = source_with(
398 &sim,
399 victim_did.info.launcher_id(),
400 did_lineage(&victim_did),
401 );
402
403 let error = prove_lineage(fake_coin.coin_id(), &victim_did, &source).unwrap_err();
404 assert!(matches!(error, DidError::NotASingleton));
405 Ok(())
406 }
407
408 #[test]
409 fn melted_did_has_no_identity_singleton() -> anyhow::Result<()> {
410 let mut sim = Simulator::new();
411 let ctx = &mut SpendContext::new();
412 let owner = sim.bls(1);
413
414 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
415 let did = spend.child.expect("child DID");
416 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
417
418 let source = SimSource {
420 sim: &sim,
421 lineages: HashMap::new(),
422 };
423
424 let error = prove_lineage(did.coin.coin_id(), &did, &source).unwrap_err();
425 assert!(matches!(error, DidError::NoIdentitySingleton));
426 Ok(())
427 }
428
429 #[test]
430 fn an_over_deep_lineage_fails_closed() -> anyhow::Result<()> {
431 let mut sim = Simulator::new();
432 let ctx = &mut SpendContext::new();
433 let owner = sim.bls(1);
434
435 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
436 let did = spend.child.expect("child DID");
437 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
438
439 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
440
441 let error = authenticate_singleton_bounded(did.coin.coin_id(), &source, 1).unwrap_err();
444 assert!(matches!(error, DidError::LineageTooDeep));
445 Ok(())
446 }
447
448 #[test]
449 fn walk_did_lineage_to_tip_reconstructs_the_current_did() -> anyhow::Result<()> {
450 let mut sim = Simulator::new();
451 let ctx = &mut SpendContext::new();
452 let owner = sim.bls(1);
453
454 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
455 let did = spend.child.expect("child DID");
456 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
457
458 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
459
460 let tip = crate::resolve::walk_did_lineage_to_tip(&source, did.info.launcher_id())?
461 .expect("a launched DID has a tip");
462 assert_eq!(tip.coin.coin_id(), did.coin.coin_id());
463 assert_eq!(tip.info.launcher_id(), did.info.launcher_id());
464 assert_eq!(tip.did(), did);
465 Ok(())
466 }
467
468 #[test]
469 fn walk_did_lineage_to_tip_returns_none_when_absent() -> anyhow::Result<()> {
470 let sim = Simulator::new();
471 let source = SimSource {
472 sim: &sim,
473 lineages: HashMap::new(),
474 };
475 assert!(
476 crate::resolve::walk_did_lineage_to_tip(&source, Bytes32::new([1u8; 32]))?.is_none()
477 );
478 Ok(())
479 }
480}