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}