dig_chainsource_interface/lineage.rs
1//! [`SingletonLineage`] — the authenticated coin set of a Chia singleton, where authority is
2//! **membership**, not tip-equality.
3//!
4//! A singleton (a DID, a DataStore, any Chia singleton) advances one coin per spend: launcher ->
5//! `C1` -> `C2` -> ... -> tip. Any coin minted from a genuine lineage coin is rooted in that
6//! singleton — minting one requires the singleton's key — while an attacker's look-alike coin is
7//! never a member. So a consumer asks "is this coin IN the lineage?" ([`SingletonLineage::contains`]),
8//! never "does this coin equal the tip?".
9//!
10//! This type is byte-coherent with dig-did's `SingletonLineage` so the two crates share one shape.
11
12use std::collections::BTreeSet;
13
14use chia_protocol::Bytes32;
15
16/// The lineage of a Chia singleton: every coin id from the launcher spend forward to the current
17/// unspent tip.
18///
19/// Authority is MEMBERSHIP in this set, not equality with the tip (see the module docs): a coin
20/// launched from ANY genuine lineage coin is rooted in the singleton, while an attacker's coin —
21/// never a member — is not. The [`contains`](Self::contains) test is the authority check consumers
22/// rely on.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SingletonLineage {
25 /// The current unspent singleton tip coin id (the singleton's current on-chain state handle).
26 tip: Bytes32,
27 /// Every coin id in the lineage (launcher -> tip inclusive). Always contains `tip`.
28 members: BTreeSet<Bytes32>,
29}
30
31impl SingletonLineage {
32 /// Builds a lineage from its `tip` and full member set. `tip` is always treated as a member, so
33 /// a caller need not include it in `members` explicitly.
34 pub fn new(tip: Bytes32, members: impl IntoIterator<Item = Bytes32>) -> Self {
35 let mut members: BTreeSet<Bytes32> = members.into_iter().collect();
36 members.insert(tip);
37 Self { tip, members }
38 }
39
40 /// A degenerate single-coin lineage (the tip is the only member) — a singleton never spent
41 /// since launch.
42 pub fn single(tip: Bytes32) -> Self {
43 Self::new(tip, [tip])
44 }
45
46 /// The current unspent singleton tip coin id.
47 pub fn tip(&self) -> Bytes32 {
48 self.tip
49 }
50
51 /// Whether `coin_id` is a genuine coin in this singleton's lineage — the authority membership
52 /// test. A coin that is not a member is NOT rooted in this singleton.
53 pub fn contains(&self, coin_id: Bytes32) -> bool {
54 self.members.contains(&coin_id)
55 }
56
57 /// The number of coins in the lineage (launcher -> tip inclusive).
58 pub fn len(&self) -> usize {
59 self.members.len()
60 }
61
62 /// Whether the lineage has no members. Always `false` for a well-formed lineage (the tip is a
63 /// member), but provided so `len`/`is_empty` stay consistent for lints and callers.
64 pub fn is_empty(&self) -> bool {
65 self.members.is_empty()
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn lineage_membership_includes_tip_and_ancestors() {
75 let launcher = Bytes32::new([1u8; 32]);
76 let cn = Bytes32::new([2u8; 32]);
77 let tip = Bytes32::new([3u8; 32]);
78 let lineage = SingletonLineage::new(tip, [launcher, cn]);
79
80 assert!(lineage.contains(launcher));
81 assert!(lineage.contains(cn));
82 assert!(lineage.contains(tip));
83 assert!(!lineage.contains(Bytes32::new([9u8; 32])));
84 assert_eq!(lineage.tip(), tip);
85 assert_eq!(lineage.len(), 3);
86 assert!(!lineage.is_empty());
87 }
88
89 #[test]
90 fn single_lineage_is_tip_only() {
91 let tip = Bytes32::new([7u8; 32]);
92 let lineage = SingletonLineage::single(tip);
93 assert_eq!(lineage.len(), 1);
94 assert!(lineage.contains(tip));
95 }
96}