Skip to main content

dig_store/
urn.rs

1//! Store + capsule URN formatting (SPEC §5).
2//!
3//! The DIG content URN scheme is `urn:dig:chia:<store_id>[:<root_hash>]`:
4//!
5//! - the **store URN** (`urn:dig:chia:<store_id>`) is ROOTLESS — it names the store across all its
6//!   generations, the stable handle a resolver uses to find the latest content;
7//! - the **capsule / root URN** (`urn:dig:chia:<store_id>:<root_hash>`) PINS one immutable
8//!   generation — the pair `(store_id, root_hash)` is exactly a capsule.
9//!
10//! The scheme, canonical form, and retrieval-key derivation are owned by the canonical
11//! [`dig_urn_protocol`] crate (the ONE ecosystem definition, re-exported through the `dig-capsule`
12//! `urn` facade). `dig-store` delegates every URN operation to it so the scheme lives in a single
13//! place and cannot drift, converting only between the re-exported [`Bytes32`] and the protocol's own
14//! 32-byte identifier at the boundary.
15
16use dig_urn_protocol::{Bytes32 as UrnBytes32, DigUrn, CANONICAL_CHAIN};
17
18use crate::error::{DigStoreError, DigStoreResult};
19use crate::types::Bytes32;
20
21/// The fixed URN prefix for the Chia-anchored DIG content scheme (`urn:dig:` + the canonical `chia`
22/// chain label). Pinned against the protocol's own prefix + chain by
23/// [`tests::prefix_matches_protocol`].
24pub const URN_PREFIX: &str = "urn:dig:chia:";
25
26/// Converts a store/root [`Bytes32`] into the protocol crate's own 32-byte identifier at the URN
27/// boundary — both are the identical 32 raw bytes.
28fn to_urn_bytes(value: Bytes32) -> UrnBytes32 {
29    let mut raw = [0u8; 32];
30    raw.copy_from_slice(value.as_ref());
31    UrnBytes32(raw)
32}
33
34/// The ROOTLESS store URN: `urn:dig:chia:<store_id>`.
35///
36/// Names the store across every generation — the stable handle for "the latest content of this
37/// store". Use [`capsule_urn`] to pin a specific root.
38pub fn store_urn(store_id: Bytes32) -> String {
39    DigUrn {
40        chain: CANONICAL_CHAIN.to_string(),
41        store_id: to_urn_bytes(store_id),
42        root_hash: None,
43        resource_key: None,
44    }
45    .canonical()
46}
47
48/// The capsule / root URN: `urn:dig:chia:<store_id>:<root_hash>`.
49///
50/// Pins the immutable generation `(store_id, root_hash)` — the on-wire name of one capsule.
51pub fn capsule_urn(store_id: Bytes32, root_hash: Bytes32) -> String {
52    DigUrn {
53        chain: CANONICAL_CHAIN.to_string(),
54        store_id: to_urn_bytes(store_id),
55        root_hash: Some(to_urn_bytes(root_hash)),
56        resource_key: None,
57    }
58    .canonical()
59}
60
61/// The retrieval key of a URN: `SHA-256(canonical(urn))`.
62///
63/// This is the URN-identity key that PINS the content — byte-identical to
64/// [`DigUrn::retrieval_key`] and to the browser verifier. A rootless store URN and a rooted capsule
65/// URN therefore have DISTINCT retrieval keys (the root is part of the canonical string), which is
66/// why a client fetching a pinned generation keys on the capsule URN.
67///
68/// # Errors
69///
70/// Returns [`DigStoreError::InvalidUrn`] if `urn` is not a parseable DIG URN.
71pub fn retrieval_key(urn: &str) -> DigStoreResult<Bytes32> {
72    let parsed =
73        DigUrn::parse(urn).map_err(|error| DigStoreError::InvalidUrn(format!("{urn}: {error}")))?;
74    Ok(Bytes32::new(parsed.retrieval_key().0))
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    fn id(byte: u8) -> Bytes32 {
82        Bytes32::new([byte; 32])
83    }
84
85    #[test]
86    fn prefix_matches_protocol() {
87        assert_eq!(
88            URN_PREFIX,
89            format!("{}{CANONICAL_CHAIN}:", dig_urn_protocol::URN_PREFIX)
90        );
91    }
92
93    #[test]
94    fn store_urn_is_rootless() {
95        let urn = store_urn(id(0xab));
96        assert_eq!(urn, format!("urn:dig:chia:{}", "ab".repeat(32)));
97        assert!(!urn.trim_start_matches(URN_PREFIX).contains(':'));
98    }
99
100    #[test]
101    fn capsule_urn_pins_the_root() {
102        let urn = capsule_urn(id(0x11), id(0x22));
103        assert_eq!(
104            urn,
105            format!("urn:dig:chia:{}:{}", "11".repeat(32), "22".repeat(32))
106        );
107    }
108
109    #[test]
110    fn retrieval_key_matches_dig_urn_protocol() {
111        let urn = store_urn(id(0x01));
112        let expected = DigUrn::parse(&urn).unwrap().retrieval_key();
113        assert_eq!(retrieval_key(&urn).unwrap(), Bytes32::new(expected.0));
114    }
115
116    #[test]
117    fn store_and_capsule_urns_have_distinct_retrieval_keys() {
118        let store = store_urn(id(0x05));
119        let capsule = capsule_urn(id(0x05), id(0x06));
120        assert_ne!(
121            retrieval_key(&store).unwrap(),
122            retrieval_key(&capsule).unwrap()
123        );
124    }
125
126    #[test]
127    fn retrieval_key_rejects_a_non_urn() {
128        assert!(matches!(
129            retrieval_key("not-a-urn"),
130            Err(DigStoreError::InvalidUrn(_))
131        ));
132    }
133}