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
161 use crate::create::create_simple_did;
162 use crate::resolve::{authenticate_singleton_bounded, SingletonLineage};
163 use crate::types::Owner;
164
165 struct SimSource<'a> {
168 sim: &'a Simulator,
169 lineages: HashMap<Bytes32, SingletonLineage>,
170 }
171
172 impl ChainSource for SimSource<'_> {
173 type Error = String;
174
175 fn resolve_singleton_lineage(
176 &self,
177 launcher_id: Bytes32,
178 ) -> Result<Option<SingletonLineage>, Self::Error> {
179 Ok(self.lineages.get(&launcher_id).cloned())
180 }
181
182 fn parent_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
183 let Some(state) = self.sim.coin_state(coin_id) else {
184 return Ok(None);
185 };
186 let parent_id = state.coin.parent_coin_info;
187 let Some(parent) = self.sim.coin_state(parent_id) else {
188 return Ok(None);
189 };
190 let (Some(reveal), Some(solution)) = (
191 self.sim.puzzle_reveal(parent_id),
192 self.sim.solution(parent_id),
193 ) else {
194 return Ok(None);
195 };
196 Ok(Some(CoinSpend::new(parent.coin, reveal, solution)))
197 }
198 }
199
200 fn source_with<'a>(
202 sim: &'a Simulator,
203 launcher_id: Bytes32,
204 lineage: SingletonLineage,
205 ) -> SimSource<'a> {
206 SimSource {
207 sim,
208 lineages: HashMap::from([(launcher_id, lineage)]),
209 }
210 }
211
212 fn did_lineage(did: &Did) -> SingletonLineage {
215 SingletonLineage::new(
216 did.coin.coin_id(),
217 [
218 did.info.launcher_id(),
219 did.coin.parent_coin_info,
220 did.coin.coin_id(),
221 ],
222 )
223 }
224
225 #[test]
226 fn model_a_direct_proves_a_did_state() -> anyhow::Result<()> {
227 let mut sim = Simulator::new();
228 let ctx = &mut SpendContext::new();
229 let owner = sim.bls(1);
230
231 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
232 let did = spend.child.expect("create returns a child DID");
233 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
234
235 let launcher_id = did.info.launcher_id();
236 let source = source_with(&sim, launcher_id, did_lineage(&did));
237
238 let proof = prove_lineage(did.coin.coin_id(), &did, &source)?;
239 assert_eq!(proof.model(), LineageModel::Direct);
240 assert_eq!(proof.authenticated_launcher(), launcher_id);
241 assert_eq!(proof.did_launcher_id(), launcher_id);
242 assert_eq!(proof.coin_id(), did.coin.coin_id());
243 assert!(!proof.chain().is_empty());
244 Ok(())
245 }
246
247 #[test]
248 fn model_b_launched_from_proves_a_singleton_launched_by_the_did() -> anyhow::Result<()> {
249 let mut sim = Simulator::new();
250 let ctx = &mut SpendContext::new();
251 let owner = sim.bls(3);
254 let owner_p2 = StandardLayer::new(owner.pk);
255
256 let create = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
257 let did = create.child.expect("create returns a child DID");
258 sim.spend_coins(create.coin_spends, std::slice::from_ref(&owner.sk))?;
259
260 let launcher = Launcher::new(did.coin.coin_id(), 2).with_singleton_amount(1);
262 let launcher_id = launcher.coin().coin_id();
263 let (launch_conditions, eve_coin) = launcher.spend(ctx, owner.puzzle_hash, ())?;
264
265 let memos = ctx.hint(did.info.p2_puzzle_hash)?;
266 let did_spend_conditions =
267 launch_conditions.create_coin(did.info.inner_puzzle_hash().into(), 1, memos);
268 did.spend_with(ctx, &owner_p2, did_spend_conditions)?;
269 sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
270
271 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
272
273 let proof = prove_lineage(eve_coin.coin_id(), &did, &source)?;
274 assert_eq!(
275 proof.model(),
276 LineageModel::LaunchedFrom {
277 launcher: launcher_id,
278 did_parent: did.coin.coin_id(),
279 }
280 );
281 assert_eq!(proof.authenticated_launcher(), launcher_id);
282 assert_eq!(proof.did_launcher_id(), did.info.launcher_id());
283 Ok(())
284 }
285
286 #[test]
287 fn payment_coin_parented_to_a_did_is_not_a_singleton() -> anyhow::Result<()> {
288 let mut sim = Simulator::new();
289 let ctx = &mut SpendContext::new();
290 let owner = sim.bls(3);
291 let owner_p2 = StandardLayer::new(owner.pk);
292
293 let create = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
294 let did = create.child.expect("create returns a child DID");
295 sim.spend_coins(create.coin_spends, std::slice::from_ref(&owner.sk))?;
296
297 let memos = ctx.hint(did.info.p2_puzzle_hash)?;
300 let payment_puzzle_hash = owner.puzzle_hash;
301 let conditions = Conditions::new()
302 .create_coin(did.info.inner_puzzle_hash().into(), 1, memos)
303 .create_coin(payment_puzzle_hash, 2, Memos::None);
304 did.spend_with(ctx, &owner_p2, conditions)?;
305 sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
306
307 let payment_coin = Coin::new(did.coin.coin_id(), payment_puzzle_hash, 2);
308 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
309
310 let error = prove_lineage(payment_coin.coin_id(), &did, &source).unwrap_err();
311 assert!(matches!(error, DidError::NotASingleton));
312 Ok(())
313 }
314
315 #[test]
316 fn attacker_singleton_from_attacker_coin_is_not_did_rooted() -> anyhow::Result<()> {
317 let mut sim = Simulator::new();
318 let ctx = &mut SpendContext::new();
319
320 let victim = sim.bls(1);
321 let victim_spend = create_simple_did(ctx, victim.coin, Owner::Standard(victim.pk))?;
322 let victim_did = victim_spend.child.expect("child DID");
323 sim.spend_coins(victim_spend.coin_spends, std::slice::from_ref(&victim.sk))?;
324
325 let attacker = sim.bls(1);
326 let attacker_spend = create_simple_did(ctx, attacker.coin, Owner::Standard(attacker.pk))?;
327 let attacker_did = attacker_spend.child.expect("child DID");
328 sim.spend_coins(
329 attacker_spend.coin_spends,
330 std::slice::from_ref(&attacker.sk),
331 )?;
332
333 let source = source_with(
335 &sim,
336 victim_did.info.launcher_id(),
337 did_lineage(&victim_did),
338 );
339
340 let error = prove_lineage(attacker_did.coin.coin_id(), &victim_did, &source).unwrap_err();
341 assert!(matches!(error, DidError::NotDidRooted));
342 Ok(())
343 }
344
345 #[test]
346 fn pay_to_coin_wearing_a_singleton_puzzle_hash_is_not_a_singleton() -> 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 alice = sim.bls(1);
359 let alice_p2 = StandardLayer::new(alice.pk);
360 let fake_singleton_puzzle_hash: Bytes32 =
361 SingletonArgs::curry_tree_hash(victim_did.info.launcher_id(), alice.puzzle_hash.into())
362 .into();
363 alice_p2.spend(
364 ctx,
365 alice.coin,
366 Conditions::new().create_coin(fake_singleton_puzzle_hash, 1, Memos::None),
367 )?;
368 sim.spend_coins(ctx.take(), std::slice::from_ref(&alice.sk))?;
369
370 let fake_coin = Coin::new(alice.coin.coin_id(), fake_singleton_puzzle_hash, 1);
371 let source = source_with(
372 &sim,
373 victim_did.info.launcher_id(),
374 did_lineage(&victim_did),
375 );
376
377 let error = prove_lineage(fake_coin.coin_id(), &victim_did, &source).unwrap_err();
378 assert!(matches!(error, DidError::NotASingleton));
379 Ok(())
380 }
381
382 #[test]
383 fn melted_did_has_no_identity_singleton() -> anyhow::Result<()> {
384 let mut sim = Simulator::new();
385 let ctx = &mut SpendContext::new();
386 let owner = sim.bls(1);
387
388 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
389 let did = spend.child.expect("child DID");
390 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
391
392 let source = SimSource {
394 sim: &sim,
395 lineages: HashMap::new(),
396 };
397
398 let error = prove_lineage(did.coin.coin_id(), &did, &source).unwrap_err();
399 assert!(matches!(error, DidError::NoIdentitySingleton));
400 Ok(())
401 }
402
403 #[test]
404 fn an_over_deep_lineage_fails_closed() -> anyhow::Result<()> {
405 let mut sim = Simulator::new();
406 let ctx = &mut SpendContext::new();
407 let owner = sim.bls(1);
408
409 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
410 let did = spend.child.expect("child DID");
411 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
412
413 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
414
415 let error = authenticate_singleton_bounded(did.coin.coin_id(), &source, 1).unwrap_err();
418 assert!(matches!(error, DidError::LineageTooDeep));
419 Ok(())
420 }
421
422 #[test]
423 fn walk_did_lineage_to_tip_reconstructs_the_current_did() -> anyhow::Result<()> {
424 let mut sim = Simulator::new();
425 let ctx = &mut SpendContext::new();
426 let owner = sim.bls(1);
427
428 let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
429 let did = spend.child.expect("child DID");
430 sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
431
432 let source = source_with(&sim, did.info.launcher_id(), did_lineage(&did));
433
434 let tip = crate::resolve::walk_did_lineage_to_tip(&source, did.info.launcher_id())?
435 .expect("a launched DID has a tip");
436 assert_eq!(tip.coin.coin_id(), did.coin.coin_id());
437 assert_eq!(tip.info.launcher_id(), did.info.launcher_id());
438 assert_eq!(tip.did(), did);
439 Ok(())
440 }
441
442 #[test]
443 fn walk_did_lineage_to_tip_returns_none_when_absent() -> anyhow::Result<()> {
444 let sim = Simulator::new();
445 let source = SimSource {
446 sim: &sim,
447 lineages: HashMap::new(),
448 };
449 assert!(
450 crate::resolve::walk_did_lineage_to_tip(&source, Bytes32::new([1u8; 32]))?.is_none()
451 );
452 Ok(())
453 }
454}