Skip to main content

dig_rpc_types/
method.rs

1//! The canonical DIG-node RPC method catalogue.
2//!
3//! [`Method`] enumerates every JSON-RPC method the DIG node surface exposes, its
4//! stable wire name, its primary [`Tier`], and whether it is reachable over the
5//! mTLS peer surface. This is the single method table the OpenRPC generator,
6//! both node dispatchers, and the server's allow-list all read — so the name set,
7//! the tier map, and the peer allowlist can never drift from one another.
8//!
9//! Frame families that are shape-dispatched rather than carried in a JSON-RPC
10//! `method` field — the DHT (`find_node` / `find_providers` / `add_provider` /
11//! `ping`) and PEX (`pex_handshake` / `pex_snapshot` / `pex_delta` /
12//! `pex_error`) wires — are documented in [`crate::frames`] and are not
13//! `Method` variants.
14
15use crate::tier::Tier;
16
17/// A DIG-node RPC method.
18///
19/// `#[non_exhaustive]` so adding a method in a minor release is additive.
20/// Convert to/from the wire name with [`Method::name`] / [`Method::from_name`].
21#[non_exhaustive]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum Method {
24    // ---- PublicRead ----
25    /// `dig.getContent` — a verified resource-window read.
26    GetContent,
27    /// `dig.getCapsule` — the whole `.dig` module for `(store, root)`.
28    GetCapsule,
29    /// `dig.getModule` — alias of [`GetCapsule`](Method::GetCapsule).
30    GetModule,
31    /// `dig.getManifest` — the public discovery manifest resource.
32    GetManifest,
33    /// `dig.getMetadata` — the plaintext metadata manifest (never encrypted).
34    GetMetadata,
35    /// `dig.listCapsules` — the confirmed capsule list (discovery metadata).
36    ListCapsules,
37    /// `dig.getProof` — the real inclusion proof + execution-proof status.
38    GetProof,
39    /// `dig.getProofStatus` — poll a real execution-proof job by id.
40    GetProofStatus,
41    /// `dig.getAnchoredRoot` — resolve a store's chain-anchored tip root.
42    /// (Also peer-reachable.)
43    GetAnchoredRoot,
44    /// `dig.getCollection` — collection-level facts for a set of NFT launchers.
45    /// (Also peer-reachable.)
46    GetCollection,
47    /// `dig.listCollectionItems` — a page of resolved collection items.
48    /// (Also peer-reachable.)
49    ListCollectionItems,
50    /// `dig.health` — liveness + capability summary.
51    Health,
52    /// `dig.methods` — the implemented method names (agent self-describe).
53    Methods,
54
55    // ---- Peer ----
56    /// `dig.getNetworkInfo` — this node's peer-network posture.
57    GetNetworkInfo,
58    /// `dig.getPeers` — the peers this node knows (peer exchange).
59    GetPeers,
60    /// `dig.announce` — accept a peer announcement.
61    Announce,
62    /// `dig.getAvailability` — batch presence check across stores/roots/capsules.
63    GetAvailability,
64    /// `dig.listInventory` — enumerate what this node serves.
65    ListInventory,
66    /// `dig.fetchRange` — a single verified range frame of a resource.
67    FetchRange,
68
69    // ---- Control (loopback / in-process only) ----
70    /// `dig.stage` — compile a local folder into a capsule `.dig` in-process.
71    Stage,
72    /// `cache.getConfig` — the local-cache config.
73    CacheGetConfig,
74    /// `cache.setCapBytes` — set the cache size cap.
75    CacheSetCapBytes,
76    /// `cache.clear` — clear the cache.
77    CacheClear,
78    /// `cache.listCached` — the durable module inventory.
79    CacheListCached,
80    /// `cache.removeCached` — remove one cached capsule.
81    CacheRemoveCached,
82    /// `cache.fetchAndCache` — fetch + cache one capsule.
83    CacheFetchAndCache,
84    /// `control.peerStatus` — the peer-network status snapshot.
85    ControlPeerStatus,
86    /// `rpc.discover` — the OpenRPC self-describe document.
87    RpcDiscover,
88}
89
90impl Method {
91    /// The stable JSON-RPC wire name.
92    pub const fn name(self) -> &'static str {
93        match self {
94            Method::GetContent => "dig.getContent",
95            Method::GetCapsule => "dig.getCapsule",
96            Method::GetModule => "dig.getModule",
97            Method::GetManifest => "dig.getManifest",
98            Method::GetMetadata => "dig.getMetadata",
99            Method::ListCapsules => "dig.listCapsules",
100            Method::GetProof => "dig.getProof",
101            Method::GetProofStatus => "dig.getProofStatus",
102            Method::GetAnchoredRoot => "dig.getAnchoredRoot",
103            Method::GetCollection => "dig.getCollection",
104            Method::ListCollectionItems => "dig.listCollectionItems",
105            Method::Health => "dig.health",
106            Method::Methods => "dig.methods",
107            Method::GetNetworkInfo => "dig.getNetworkInfo",
108            Method::GetPeers => "dig.getPeers",
109            Method::Announce => "dig.announce",
110            Method::GetAvailability => "dig.getAvailability",
111            Method::ListInventory => "dig.listInventory",
112            Method::FetchRange => "dig.fetchRange",
113            Method::Stage => "dig.stage",
114            Method::CacheGetConfig => "cache.getConfig",
115            Method::CacheSetCapBytes => "cache.setCapBytes",
116            Method::CacheClear => "cache.clear",
117            Method::CacheListCached => "cache.listCached",
118            Method::CacheRemoveCached => "cache.removeCached",
119            Method::CacheFetchAndCache => "cache.fetchAndCache",
120            Method::ControlPeerStatus => "control.peerStatus",
121            Method::RpcDiscover => "rpc.discover",
122        }
123    }
124
125    /// Parse a wire name into a [`Method`], or `None` for an unknown method
126    /// (the caller answers `-32601`).
127    pub fn from_name(name: &str) -> Option<Method> {
128        Method::ALL.iter().copied().find(|m| m.name() == name)
129    }
130
131    /// The method's PRIMARY access [`Tier`].
132    ///
133    /// Note: a `PublicRead` method may still be peer-reachable — see
134    /// [`is_peer_reachable`](Method::is_peer_reachable). The tier is the
135    /// method's canonical home, the allowlist is the security boundary.
136    pub const fn tier(self) -> Tier {
137        match self {
138            Method::GetContent
139            | Method::GetCapsule
140            | Method::GetModule
141            | Method::GetManifest
142            | Method::GetMetadata
143            | Method::ListCapsules
144            | Method::GetProof
145            | Method::GetProofStatus
146            | Method::GetAnchoredRoot
147            | Method::GetCollection
148            | Method::ListCollectionItems
149            | Method::Health
150            | Method::Methods => Tier::PublicRead,
151
152            Method::GetNetworkInfo
153            | Method::GetPeers
154            | Method::Announce
155            | Method::GetAvailability
156            | Method::ListInventory
157            | Method::FetchRange => Tier::Peer,
158
159            Method::Stage
160            | Method::CacheGetConfig
161            | Method::CacheSetCapBytes
162            | Method::CacheClear
163            | Method::CacheListCached
164            | Method::CacheRemoveCached
165            | Method::CacheFetchAndCache
166            | Method::ControlPeerStatus
167            | Method::RpcDiscover => Tier::Control,
168        }
169    }
170
171    /// Whether this method is reachable over the mTLS **peer** surface.
172    ///
173    /// This mirrors the canonical node's `is_peer_reachable_method` byte-for-byte
174    /// (digstore `dig-node` `peer.rs`). It is an ALLOWLIST: adding a method here
175    /// is a deliberate security decision — it exposes the method to any remote
176    /// peer (the peer verifier authenticates a `peer_id`, it does NOT authorize).
177    /// Management/mutation methods (`cache.*`, `control.*`, `dig.stage`) are
178    /// NEVER peer-reachable.
179    pub const fn is_peer_reachable(self) -> bool {
180        matches!(
181            self,
182            Method::GetContent
183                | Method::GetNetworkInfo
184                | Method::GetPeers
185                | Method::Announce
186                | Method::GetAvailability
187                | Method::ListInventory
188                | Method::FetchRange
189                | Method::GetAnchoredRoot
190                | Method::GetCollection
191                | Method::ListCollectionItems
192        )
193    }
194
195    /// A one-line human summary (drives the OpenRPC method `summary`).
196    pub const fn summary(self) -> &'static str {
197        match self {
198            Method::GetContent => "Read a verified window of a resource's ciphertext.",
199            Method::GetCapsule => "Fetch the whole .dig module for (store, root).",
200            Method::GetModule => "Alias of dig.getCapsule.",
201            Method::GetManifest => "Fetch the public discovery manifest resource.",
202            Method::GetMetadata => "Fetch the plaintext metadata manifest.",
203            Method::ListCapsules => "List a store's confirmed capsules.",
204            Method::GetProof => "Get the real inclusion proof + execution-proof status.",
205            Method::GetProofStatus => "Poll a real execution-proof job by id.",
206            Method::GetAnchoredRoot => "Resolve a store's chain-anchored tip root.",
207            Method::GetCollection => "Get collection-level facts for NFT launcher ids.",
208            Method::ListCollectionItems => "List resolved NFT collection items (paginated).",
209            Method::Health => "Liveness and capability summary.",
210            Method::Methods => "List the method names this node implements.",
211            Method::GetNetworkInfo => "This node's peer-network posture.",
212            Method::GetPeers => "The peers this node currently knows.",
213            Method::Announce => "Accept a peer announcement (peer_id + addresses).",
214            Method::GetAvailability => "Batch-check whether this node holds items.",
215            Method::ListInventory => "Enumerate the stores / roots this node serves.",
216            Method::FetchRange => "Fetch a single verified range frame of a resource.",
217            Method::Stage => "Compile a local folder into a capsule .dig in-process.",
218            Method::CacheGetConfig => "Get the local-cache configuration.",
219            Method::CacheSetCapBytes => "Set the local-cache size cap.",
220            Method::CacheClear => "Clear the local cache.",
221            Method::CacheListCached => "List the durable cached modules.",
222            Method::CacheRemoveCached => "Remove one cached capsule.",
223            Method::CacheFetchAndCache => "Fetch and cache one capsule.",
224            Method::ControlPeerStatus => "Snapshot the node's peer network.",
225            Method::RpcDiscover => "Return the OpenRPC self-describe document.",
226        }
227    }
228
229    /// Every method, in catalogue order.
230    pub const ALL: &'static [Method] = &[
231        Method::GetContent,
232        Method::GetCapsule,
233        Method::GetModule,
234        Method::GetManifest,
235        Method::GetMetadata,
236        Method::ListCapsules,
237        Method::GetProof,
238        Method::GetProofStatus,
239        Method::GetAnchoredRoot,
240        Method::GetCollection,
241        Method::ListCollectionItems,
242        Method::Health,
243        Method::Methods,
244        Method::GetNetworkInfo,
245        Method::GetPeers,
246        Method::Announce,
247        Method::GetAvailability,
248        Method::ListInventory,
249        Method::FetchRange,
250        Method::Stage,
251        Method::CacheGetConfig,
252        Method::CacheSetCapBytes,
253        Method::CacheClear,
254        Method::CacheListCached,
255        Method::CacheRemoveCached,
256        Method::CacheFetchAndCache,
257        Method::ControlPeerStatus,
258        Method::RpcDiscover,
259    ];
260
261    /// The method names reachable over the peer surface, in catalogue order —
262    /// the exact allowlist a server hands its peer responder.
263    pub fn peer_reachable_names() -> Vec<&'static str> {
264        Method::ALL
265            .iter()
266            .copied()
267            .filter(|m| m.is_peer_reachable())
268            .map(Method::name)
269            .collect()
270    }
271}
272
273impl std::fmt::Display for Method {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        f.write_str(self.name())
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use std::collections::HashSet;
283
284    /// **Proves:** every method name round-trips through `from_name`.
285    /// **Catches:** a name typo or a variant left out of `ALL`.
286    #[test]
287    fn names_round_trip() {
288        for m in Method::ALL {
289            assert_eq!(Method::from_name(m.name()), Some(*m), "{}", m.name());
290        }
291        assert!(Method::from_name("dig.nope").is_none());
292    }
293
294    /// **Proves:** all wire names are unique.
295    /// **Catches:** two variants accidentally sharing a name.
296    #[test]
297    fn names_unique() {
298        let names: HashSet<&str> = Method::ALL.iter().map(|m| m.name()).collect();
299        assert_eq!(names.len(), Method::ALL.len());
300    }
301
302    /// **Proves:** the peer allowlist is EXACTLY the ten methods the canonical
303    /// node exposes over mTLS — and no `cache.*`/`control.*`/`dig.stage` leaks
304    /// onto the peer surface.
305    /// **Catches:** a drift from digstore `is_peer_reachable_method`, or a
306    /// management method accidentally allowlisted (the audit #179 auth-bypass).
307    #[test]
308    fn peer_allowlist_matches_canonical() {
309        let mut got = Method::peer_reachable_names();
310        got.sort_unstable();
311        let mut want = vec![
312            "dig.getContent",
313            "dig.getNetworkInfo",
314            "dig.getPeers",
315            "dig.announce",
316            "dig.getAvailability",
317            "dig.listInventory",
318            "dig.fetchRange",
319            "dig.getAnchoredRoot",
320            "dig.getCollection",
321            "dig.listCollectionItems",
322        ];
323        want.sort_unstable();
324        assert_eq!(got, want);
325
326        // No management/mutation method is peer-reachable.
327        for m in [
328            Method::Stage,
329            Method::CacheGetConfig,
330            Method::CacheSetCapBytes,
331            Method::CacheClear,
332            Method::CacheListCached,
333            Method::CacheRemoveCached,
334            Method::CacheFetchAndCache,
335            Method::ControlPeerStatus,
336            Method::RpcDiscover,
337        ] {
338            assert!(
339                !m.is_peer_reachable(),
340                "{} must NOT be peer-reachable",
341                m.name()
342            );
343        }
344    }
345
346    /// **Proves:** the chain-anchored public reads are PublicRead yet ALSO
347    /// peer-reachable (the design-brief tier decision §5).
348    #[test]
349    fn anchored_reads_public_but_peer_reachable() {
350        for m in [
351            Method::GetAnchoredRoot,
352            Method::GetCollection,
353            Method::ListCollectionItems,
354        ] {
355            assert_eq!(m.tier(), Tier::PublicRead);
356            assert!(m.is_peer_reachable());
357        }
358    }
359
360    /// **Proves:** every Control method is NOT peer-reachable (the boundary
361    /// invariant).
362    #[test]
363    fn control_tier_never_peer_reachable() {
364        for m in Method::ALL {
365            if m.tier() == Tier::Control {
366                assert!(
367                    !m.is_peer_reachable(),
368                    "{} is Control but peer-reachable",
369                    m.name()
370                );
371            }
372        }
373    }
374}