Skip to main content

dig_store/
error.rs

1//! The `dig-store` error taxonomy (SPEC §6).
2//!
3//! Every fallible operation returns [`DigStoreResult`]. The variants are deliberately coarse and
4//! stable — a consumer matches on the kind, not on a message string — and each carries a
5//! human-readable detail for logs. New variants are added additively; existing ones never change
6//! meaning (CLAUDE.md §5.1 back-compat discipline applies to the public error surface too).
7
8use thiserror::Error;
9
10/// The result type every `dig-store` operation returns.
11pub type DigStoreResult<T> = Result<T, DigStoreError>;
12
13/// Everything that can go wrong composing, spending, or reading a DIG store.
14#[derive(Debug, Error)]
15pub enum DigStoreError {
16    /// A size value is outside the valid ladder (`k ∈ 0..=10`, i.e. 1 MB..1 GB) — see
17    /// [`crate::SizeBucket`]. Mirrors `dig_merkle::MerkleError::InvalidSize`.
18    #[error("invalid store size: {0}")]
19    InvalidSize(String),
20
21    /// A downloaded `.dig`'s real size does not match the size the store anchored on chain, so it
22    /// MUST be discarded rather than stored or served (SPEC §4, NC-9). Carries the anchored bucket
23    /// and the observed byte length for diagnostics.
24    #[error("size-proof mismatch: .dig is {actual_bytes} bytes (bucket {actual_k}) but the store anchored bucket {anchored_k}; discarding")]
25    SizeProofMismatch {
26        /// The power-of-2 exponent the store anchored on chain.
27        anchored_k: u8,
28        /// The power-of-2 exponent the downloaded `.dig`'s real byte length falls into.
29        actual_k: u8,
30        /// The downloaded `.dig`'s real byte length.
31        actual_bytes: u64,
32    },
33
34    /// A URN string could not be parsed or a store id / root hash was malformed.
35    #[error("invalid URN or identifier: {0}")]
36    InvalidUrn(String),
37
38    /// An on-chain read could not prove the store's integrity/validity against the chain (NC-9):
39    /// the coin was absent, the lineage did not verify, or the chain source failed.
40    #[error("on-chain proof failed: {0}")]
41    Proof(String),
42
43    /// The underlying `.dig` capsule (`dig-capsule`) could not be opened, parsed, or read, or its
44    /// declared `store_id` did not match a trusted anchor. Returned by the off-chain capsule getters
45    /// ([`crate::get_capsule_identity`] / [`crate::open_capsule`], SPEC §5/§11): unreadable/tampered
46    /// module bytes and a `store_id`-vs-anchor mismatch both surface here (fail-closed).
47    #[error("capsule error: {0}")]
48    Capsule(String),
49
50    /// A spend could not be constructed by the on-chain anchor (`dig-merkle`).
51    #[error("spend-build error: {0}")]
52    Spend(String),
53}
54
55impl From<dig_merkle::MerkleError> for DigStoreError {
56    /// Maps a `dig-merkle` failure into the store taxonomy. An out-of-range size stays an
57    /// [`DigStoreError::InvalidSize`]; every other `dig-merkle` error is a spend-construction failure
58    /// ([`DigStoreError::Spend`]). Read paths that need the [`DigStoreError::Proof`] framing map
59    /// explicitly at the call site rather than through this conversion.
60    fn from(error: dig_merkle::MerkleError) -> Self {
61        match error {
62            dig_merkle::MerkleError::InvalidSize(message) => DigStoreError::InvalidSize(message),
63            other => DigStoreError::Spend(other.to_string()),
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    /// An out-of-range `dig-merkle` size maps to [`DigStoreError::InvalidSize`], preserving the detail.
73    #[test]
74    fn merkle_invalid_size_maps_to_invalid_size() {
75        let err: DigStoreError = dig_merkle::MerkleError::InvalidSize("too big".into()).into();
76        match err {
77            DigStoreError::InvalidSize(message) => assert_eq!(message, "too big"),
78            other => panic!("expected InvalidSize, got {other:?}"),
79        }
80    }
81
82    /// Every other `dig-merkle` failure maps to [`DigStoreError::Spend`] (a spend-construction error).
83    #[test]
84    fn other_merkle_errors_map_to_spend() {
85        let err: DigStoreError = dig_merkle::MerkleError::NotDataStore.into();
86        assert!(matches!(err, DigStoreError::Spend(_)));
87    }
88
89    /// `MerkleError` is `#[non_exhaustive]`, so the conversion's wildcard arm is what a variant added
90    /// upstream will land in. It must SURFACE that variant as a real error carrying its detail —
91    /// never swallow it into a success or a generic message a caller cannot diagnose.
92    ///
93    /// The two probes are the variants `dig-merkle` 0.6 added guards for, and neither is named
94    /// explicitly by the conversion, so each genuinely exercises the wildcard.
95    #[test]
96    fn a_variant_the_conversion_does_not_name_still_surfaces_its_detail() {
97        for merkle in [
98            dig_merkle::MerkleError::UnsupportedOwner("use mint_datastore_launch_with_kind"),
99            dig_merkle::MerkleError::Chain("the puzzle reveal does not hash".into()),
100        ] {
101            let detail = merkle.to_string();
102            match DigStoreError::from(merkle) {
103                DigStoreError::Spend(message) => assert_eq!(
104                    message, detail,
105                    "the upstream detail must reach the caller verbatim"
106                ),
107                other => panic!("an unnamed merkle variant must surface as Spend, got {other:?}"),
108            }
109        }
110    }
111
112    /// The `Display` rendering carries the mismatch detail for logs.
113    #[test]
114    fn size_proof_mismatch_displays_detail() {
115        let err = DigStoreError::SizeProofMismatch {
116            anchored_k: 7,
117            actual_k: 6,
118            actual_bytes: 64,
119        };
120        assert!(err.to_string().contains("size-proof mismatch"));
121    }
122}