Skip to main content

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}