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)]
150#[allow(clippy::disallowed_methods)]
154mod tests {
155 use super::*;
156 use std::collections::HashMap;
157
158 use chia_protocol::{Coin, CoinSpend};
159 use chia_puzzle_types::singleton::SingletonArgs;
160 use chia_puzzle_types::Memos;
161 use chia_wallet_sdk::driver::{Launcher, SpendContext, StandardLayer};
162 use chia_wallet_sdk::test::Simulator;
163 use chia_wallet_sdk::types::Conditions;
164 use dig_chainsource_interface::CoinRecord;
165
166 use crate::create::create_simple_did;
167 use crate::resolve::{authenticate_singleton_bounded, SingletonLineage};
168 use crate::types::Owner;
169
170 struct SimSource<'a> {
173 sim: &'a Simulator,
174 lineages: HashMap<Bytes32, SingletonLineage>,
175 }
176
177 impl ChainSource for SimSource<'_> {
178 type Error = String;
179
180 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
181 Ok(self.sim.coin_state(coin_id).map(CoinRecord::from))
182 }
183
184 fn coin_records_by_puzzle_hash(
185 &self,
186 _puzzle_hash: Bytes32,
187 _include_spent: bool,
188 ) -> Result<Vec<CoinRecord>, Self::Error> {
189 Ok(Vec::new())
192 }
193
194 fn coin_records_by_parent(
195 &self,
196 _parent_coin_id: Bytes32,
197 ) -> Result<Vec<CoinRecord>, Self::Error> {
198 Ok(Vec::new())
199 }
200
201 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
202 let Some(state) = self.sim.coin_state(coin_id) else {
204 return Ok(None);
205 };
206 let (Some(reveal), Some(solution)) =
207 (self.sim.puzzle_reveal(coin_id), self.sim.solution(coin_id))
208 else {
209 return Ok(None);
210 };
211 Ok(Some(CoinSpend::new(state.coin, reveal, solution)))
212 }
213
214 fn resolve_singleton_lineage(
215 &self,
216 launcher_id: Bytes32,
217 ) -> Result<Option<SingletonLineage>, Self::Error> {
218 Ok(self.lineages.get(&launcher_id).cloned())
219 }
220
221 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
222 Ok(None)
223 }
224
225 fn block_timestamp(&self, _height: u32) -> Result<Option<u64>, Self::Error> {
226 Ok(None)
227 }
228 }
229
230 fn source_with<'a>(
232 sim: &'a Simulator,
233 launcher_id: Bytes32,
234 lineage: SingletonLineage,
235 ) -> SimSource<'a> {
236 SimSource {
237 sim,
238 lineages: HashMap::from([(launcher_id, lineage)]),
239 }
240 }
241
242 fn did_lineage(did: &Did) -> SingletonLineage {
245 SingletonLineage::new(
246 did.coin.coin_id(),
247 [
248 did.info.launcher_id(),
249 did.coin.parent_coin_info,
250 did.coin.coin_id(),
251 ],
252 )
253 }
254
255 #[test]
256 fn model_a_direct_proves_a_did_state() -> anyhow::Result<()> {
257 let mut sim = Simulator::new();
258 let ctx = &mut SpendContext::new();
259 let owner = sim.bls(1);
260
261 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
262 let did = spend.child.expect("create returns a child DID");
263 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
264
265 let launcher_id = did.info.launcher_id();
266 let source = source_with(&sim, launcher_id, did_lineage(&did));
267
268 let proof = prove_lineage(did.coin.coin_id(), &did, &source)?;
269 assert_eq!(proof.model(), LineageModel::Direct);
270 assert_eq!(proof.authenticated_launcher(), launcher_id);
271 assert_eq!(proof.did_launcher_id(), launcher_id);
272 assert_eq!(proof.coin_id(), did.coin.coin_id());
273 assert!(!proof.chain().is_empty());
274 Ok(())
275 }
276
277 #[test]
278 fn model_b_launched_from_proves_a_singleton_launched_by_the_did() -> anyhow::Result<()> {
279 let mut sim = Simulator::new();
280 let ctx = &mut SpendContext::new();
281 let owner = sim.bls(3);
284 let owner_p2 = StandardLayer::new(owner.pk);
285
286 let create = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
287 let did = create.child.expect("create returns a child DID");
288 sim.spend_coins(create.coin_spends, std::slice::from_ref(&owner.sk))?;
289
290 let launcher = Launcher::new(did.coin.coin_id(), 2).with_singleton_amount(1);
292 let launcher_id = launcher.coin().coin_id();
293 let (launch_conditions, eve_coin) = launcher.spend(ctx, owner.puzzle_hash, ())?;
294
295 let memos = ctx.hint(did.info.p2_puzzle_hash)?;
296 let did_spend_conditions =
297 launch_conditions.create_coin(did.info.inner_puzzle_hash().into(), 1, memos);
298 did.spend_with(ctx, &owner_p2, did_spend_conditions)?;
299 sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
300
301 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
302
303 let proof = prove_lineage(eve_coin.coin_id(), &did, &source)?;
304 assert_eq!(
305 proof.model(),
306 LineageModel::LaunchedFrom {
307 launcher: launcher_id,
308 did_parent: did.coin.coin_id(),
309 }
310 );
311 assert_eq!(proof.authenticated_launcher(), launcher_id);
312 assert_eq!(proof.did_launcher_id(), did.info.launcher_id());
313 Ok(())
314 }
315
316 #[test]
317 fn payment_coin_parented_to_a_did_is_not_a_singleton() -> anyhow::Result<()> {
318 let mut sim = Simulator::new();
319 let ctx = &mut SpendContext::new();
320 let owner = sim.bls(3);
321 let owner_p2 = StandardLayer::new(owner.pk);
322
323 let create = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
324 let did = create.child.expect("create returns a child DID");
325 sim.spend_coins(create.coin_spends, std::slice::from_ref(&owner.sk))?;
326
327 let memos = ctx.hint(did.info.p2_puzzle_hash)?;
330 let payment_puzzle_hash = owner.puzzle_hash;
331 let conditions = Conditions::new()
332 .create_coin(did.info.inner_puzzle_hash().into(), 1, memos)
333 .create_coin(payment_puzzle_hash, 2, Memos::None);
334 did.spend_with(ctx, &owner_p2, conditions)?;
335 sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
336
337 let payment_coin = Coin::new(did.coin.coin_id(), payment_puzzle_hash, 2);
338 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
339
340 let error = prove_lineage(payment_coin.coin_id(), &did, &source).unwrap_err();
341 assert!(matches!(error, DidError::NotASingleton));
342 Ok(())
343 }
344
345 #[test]
346 fn attacker_singleton_from_attacker_coin_is_not_did_rooted() -> anyhow::Result<()> {
347 let mut sim = Simulator::new();
348 let ctx = &mut SpendContext::new();
349
350 let victim = sim.bls(1);
351 let victim_spend = create_simple_did(ctx, victim.coin, Owner::Standard(victim.pk))?;
352 let victim_did = victim_spend.child.expect("child DID");
353 sim.spend_coins(victim_spend.coin_spends, std::slice::from_ref(&victim.sk))?;
354
355 let attacker = sim.bls(1);
356 let attacker_spend = create_simple_did(ctx, attacker.coin, Owner::Standard(attacker.pk))?;
357 let attacker_did = attacker_spend.child.expect("child DID");
358 sim.spend_coins(
359 attacker_spend.coin_spends,
360 std::slice::from_ref(&attacker.sk),
361 )?;
362
363 let source = source_with(
365 &sim,
366 victim_did.info.launcher_id(),
367 did_lineage(&victim_did),
368 );
369
370 let error = prove_lineage(attacker_did.coin.coin_id(), &victim_did, &source).unwrap_err();
371 assert!(matches!(error, DidError::NotDidRooted));
372 Ok(())
373 }
374
375 #[test]
376 fn pay_to_coin_wearing_a_singleton_puzzle_hash_is_not_a_singleton() -> anyhow::Result<()> {
377 let mut sim = Simulator::new();
378 let ctx = &mut SpendContext::new();
379
380 let victim = sim.bls(1);
381 let victim_spend = create_simple_did(ctx, victim.coin, Owner::Standard(victim.pk))?;
382 let victim_did = victim_spend.child.expect("child DID");
383 sim.spend_coins(victim_spend.coin_spends, std::slice::from_ref(&victim.sk))?;
384
385 let alice = sim.bls(1);
389 let alice_p2 = StandardLayer::new(alice.pk);
390 let fake_singleton_puzzle_hash: Bytes32 =
391 SingletonArgs::curry_tree_hash(victim_did.info.launcher_id(), alice.puzzle_hash.into())
392 .into();
393 alice_p2.spend(
394 ctx,
395 alice.coin,
396 Conditions::new().create_coin(fake_singleton_puzzle_hash, 1, Memos::None),
397 )?;
398 sim.spend_coins(ctx.take(), std::slice::from_ref(&alice.sk))?;
399
400 let fake_coin = Coin::new(alice.coin.coin_id(), fake_singleton_puzzle_hash, 1);
401 let source = source_with(
402 &sim,
403 victim_did.info.launcher_id(),
404 did_lineage(&victim_did),
405 );
406
407 let error = prove_lineage(fake_coin.coin_id(), &victim_did, &source).unwrap_err();
408 assert!(matches!(error, DidError::NotASingleton));
409 Ok(())
410 }
411
412 #[test]
413 fn melted_did_has_no_identity_singleton() -> anyhow::Result<()> {
414 let mut sim = Simulator::new();
415 let ctx = &mut SpendContext::new();
416 let owner = sim.bls(1);
417
418 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
419 let did = spend.child.expect("child DID");
420 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
421
422 let source = SimSource {
424 sim: &sim,
425 lineages: HashMap::new(),
426 };
427
428 let error = prove_lineage(did.coin.coin_id(), &did, &source).unwrap_err();
429 assert!(matches!(error, DidError::NoIdentitySingleton));
430 Ok(())
431 }
432
433 #[test]
434 fn an_over_deep_lineage_fails_closed() -> anyhow::Result<()> {
435 let mut sim = Simulator::new();
436 let ctx = &mut SpendContext::new();
437 let owner = sim.bls(1);
438
439 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
440 let did = spend.child.expect("child DID");
441 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
442
443 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
444
445 let error = authenticate_singleton_bounded(did.coin.coin_id(), &source, 1).unwrap_err();
448 assert!(matches!(error, DidError::LineageTooDeep));
449 Ok(())
450 }
451
452 #[test]
453 fn walk_did_lineage_to_tip_reconstructs_the_current_did() -> anyhow::Result<()> {
454 let mut sim = Simulator::new();
455 let ctx = &mut SpendContext::new();
456 let owner = sim.bls(1);
457
458 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
459 let did = spend.child.expect("child DID");
460 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
461
462 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
463
464 let tip = crate::resolve::walk_did_lineage_to_tip(&source, did.info.launcher_id())?
465 .expect("a launched DID has a tip");
466 assert_eq!(tip.coin.coin_id(), did.coin.coin_id());
467 assert_eq!(tip.info.launcher_id(), did.info.launcher_id());
468 assert_eq!(tip.did(), did);
469 Ok(())
470 }
471
472 #[test]
473 fn walk_did_lineage_to_tip_returns_none_when_absent() -> anyhow::Result<()> {
474 let sim = Simulator::new();
475 let source = SimSource {
476 sim: &sim,
477 lineages: HashMap::new(),
478 };
479 assert!(
480 crate::resolve::walk_did_lineage_to_tip(&source, Bytes32::new([1u8; 32]))?.is_none()
481 );
482 Ok(())
483 }
484}