Skip to main content

dig_did/
error.rs

1//! The `dig-did` error taxonomy (SPEC §6).
2//!
3//! Every fallible operation in this crate returns [`DidError`]. It wraps the underlying
4//! chia-wallet-sdk driver error (the byte-source-of-truth for puzzle construction, INV-4) and adds
5//! the DID-domain failure modes this crate raises directly — parse failures, fail-closed hydration
6//! guards, and the `did:chia:` address-codec errors.
7
8use chia_wallet_sdk::driver::DriverError;
9use thiserror::Error;
10
11/// The result type returned by every fallible `dig-did` operation.
12pub type DidResult<T> = Result<T, DidError>;
13
14/// Everything that can go wrong while building or parsing a DID spend.
15///
16/// The variants split into two families: errors *delegated* to the chia-wallet-sdk driver/signer
17/// (wrapped verbatim so the underlying cause is never lost), and DID-domain errors this crate
18/// raises itself (parse/hydration/codec guards, all fail-closed per SPEC §5).
19#[derive(Debug, Error)]
20pub enum DidError {
21    /// A chia-wallet-sdk driver operation failed (puzzle currying, spend construction, CLVM
22    /// evaluation). The wrapped [`DriverError`] carries the precise cause.
23    #[error("chia driver error: {0}")]
24    Driver(#[from] DriverError),
25
26    /// The signing calculator failed to derive the required signatures from the coin spends
27    /// (invalid puzzle/solution, an infinity public key in an `AGG_SIG` condition). The message is
28    /// the underlying signer error rendered as a string, so this crate does not leak the signer's
29    /// error type into its public surface.
30    #[error("signature calculation failed: {0}")]
31    Signer(String),
32
33    /// A coin/puzzle/solution could not be parsed as the expected shape.
34    #[error("failed to parse DID: {0}")]
35    Parse(String),
36
37    /// The supplied puzzle parsed successfully but is not a DID singleton.
38    #[error("coin is not a DID singleton")]
39    NotDid,
40
41    /// A `did:chia:1…` string was malformed or failed bech32m decoding.
42    #[error("invalid did:chia string: {0}")]
43    InvalidDidString(String),
44
45    /// A recovery operation supplied an inconsistent recovery configuration (list hash / required
46    /// verifications mismatch).
47    #[error("invalid recovery configuration: {0}")]
48    InvalidRecovery(String),
49
50    /// Hydration could not establish the lineage proof required to spend the DID (SPEC §5,
51    /// fail-closed).
52    #[error("missing lineage proof for DID")]
53    MissingLineage,
54
55    /// A parsed DID coin was missing the owner hint memo required to recreate its child (SPEC §5,
56    /// fail-closed).
57    #[error("missing owner hint on DID coin")]
58    MissingHint,
59
60    /// A chain-level precondition was violated (e.g. a supplied coin does not match the expected
61    /// launcher). The string states the specific violation. Also carries a [`crate::resolve::ChainSource`]
62    /// read error verbatim — a failed read NEVER degrades to "assume owned" (SPEC §5, fail-closed).
63    #[error("chain precondition failed: {0}")]
64    Chain(String),
65
66    /// The DID's identity singleton has no current on-chain coin — it was never launched, or has been
67    /// melted, so there is no lineage to root a coin against (SPEC §5, fail-closed).
68    #[error("DID singleton has no current on-chain coin (unlaunched or melted)")]
69    NoIdentitySingleton,
70
71    /// The coin under proof could not be authenticated as a genuine singleton: its parent-spend chain
72    /// does not resolve to a singleton launcher (an ordinary payment/change coin, or a pay-to coin that
73    /// merely wears a singleton puzzle hash without a genuine recreation parent spend). SPEC §5.
74    #[error("coin is not a genuine singleton")]
75    NotASingleton,
76
77    /// The coin authenticates as a genuine singleton, but neither IS the DID singleton nor was launched
78    /// from a coin in the DID singleton's lineage — it is not rooted in the DID's identity (SPEC §5).
79    #[error("coin is not rooted in the DID's singleton lineage")]
80    NotDidRooted,
81
82    /// The DID's current tip authenticated as a genuine singleton, but its GENUINE launcher (walked
83    /// from the parent-spend chain) is not the launcher that was requested. This is the money-critical
84    /// guard for [`crate::resolve_xch_address`]: a dishonest [`crate::ChainSource`] can echo an
85    /// attacker DID's tip for a victim launcher, and the curried `launcher_id` on that tip is
86    /// attacker-chosen, so only the parent-walk-authenticated launcher may be trusted. Resolving an
87    /// address from a mismatched launcher would pay the wrong recipient, so this fails closed (SPEC §5).
88    #[error("the DID tip's authenticated launcher does not match the requested launcher")]
89    LauncherMismatch,
90
91    /// The parent-spend walk exceeded [`crate::resolve::MAX_LINEAGE_DEPTH`] — a DoS guard against an
92    /// unbounded (possibly adversarial) lineage. The proof fails closed rather than walk forever.
93    #[error("singleton lineage exceeds the maximum authenticated depth")]
94    LineageTooDeep,
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn display_messages_are_descriptive() {
103        assert_eq!(DidError::NotDid.to_string(), "coin is not a DID singleton");
104        assert_eq!(
105            DidError::MissingLineage.to_string(),
106            "missing lineage proof for DID"
107        );
108        assert_eq!(
109            DidError::MissingHint.to_string(),
110            "missing owner hint on DID coin"
111        );
112        assert_eq!(
113            DidError::Parse("bad".into()).to_string(),
114            "failed to parse DID: bad"
115        );
116        assert_eq!(
117            DidError::InvalidDidString("nope".into()).to_string(),
118            "invalid did:chia string: nope"
119        );
120        assert_eq!(
121            DidError::InvalidRecovery("mismatch".into()).to_string(),
122            "invalid recovery configuration: mismatch"
123        );
124        assert_eq!(
125            DidError::Signer("boom".into()).to_string(),
126            "signature calculation failed: boom"
127        );
128        assert_eq!(
129            DidError::Chain("wrong launcher".into()).to_string(),
130            "chain precondition failed: wrong launcher"
131        );
132    }
133
134    #[test]
135    fn wraps_driver_errors_via_from() {
136        let driver = DriverError::InvalidSingletonStruct;
137        let err: DidError = driver.into();
138        assert!(matches!(err, DidError::Driver(_)));
139        assert!(err.to_string().starts_with("chia driver error:"));
140    }
141}