Skip to main content

dig_rpc_protocol/
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    /// `dig.getModuleInfo` — the whole-`.dig`-module transfer descriptor
69    /// (total size + module hash + per-chunk hashes) for `(store, root)`, the
70    /// handshake a peer reads before range-pulling the module blob.
71    GetModuleInfo,
72    /// `dig.fetchModuleRange` — a single range frame of the whole `.dig` module
73    /// blob for `(store, root)` (reuses [`RangeFrame`](crate::types::RangeFrame)).
74    FetchModuleRange,
75
76    // ---- Control (loopback / in-process only) ----
77    /// `dig.stage` — compile a local folder into a capsule `.dig` in-process.
78    Stage,
79    /// `cache.getConfig` — the local-cache config.
80    CacheGetConfig,
81    /// `cache.setCapBytes` — set the cache size cap.
82    CacheSetCapBytes,
83    /// `cache.clear` — clear the cache.
84    CacheClear,
85    /// `cache.listCached` — the durable module inventory.
86    CacheListCached,
87    /// `cache.removeCached` — remove one cached capsule.
88    CacheRemoveCached,
89    /// `cache.fetchAndCache` — fetch + cache one capsule.
90    CacheFetchAndCache,
91    /// `cache.stats` — cache telemetry: reserved cap + live usage, cached-capsule
92    /// count + total on-disk bytes, session eviction + content-cache hit/miss.
93    CacheStats,
94    /// `control.peerStatus` — the peer-network status snapshot.
95    ControlPeerStatus,
96    /// `control.subscribe` — subscribe the node to a store (persisted): it
97    /// actively watches + gap-fills that store.
98    ControlSubscribe,
99    /// `control.unsubscribe` — remove a store from the node's subscription set.
100    ControlUnsubscribe,
101    /// `control.listSubscriptions` — the node's persisted store subscriptions.
102    ControlListSubscriptions,
103    /// `control.peers.connect` — dial a peer (turn a discovered peer into a
104    /// counted, RPC-reachable connected peer).
105    ControlPeersConnect,
106    /// `control.peers.disconnect` — drop a pooled peer by `peer_id`, closing its
107    /// mTLS link (the inverse of `control.peers.connect`).
108    ControlPeersDisconnect,
109    /// `rpc.discover` — the OpenRPC self-describe document.
110    RpcDiscover,
111}
112
113impl Method {
114    /// The stable JSON-RPC wire name.
115    pub const fn name(self) -> &'static str {
116        match self {
117            Method::GetContent => "dig.getContent",
118            Method::GetCapsule => "dig.getCapsule",
119            Method::GetModule => "dig.getModule",
120            Method::GetManifest => "dig.getManifest",
121            Method::GetMetadata => "dig.getMetadata",
122            Method::ListCapsules => "dig.listCapsules",
123            Method::GetProof => "dig.getProof",
124            Method::GetProofStatus => "dig.getProofStatus",
125            Method::GetAnchoredRoot => "dig.getAnchoredRoot",
126            Method::GetCollection => "dig.getCollection",
127            Method::ListCollectionItems => "dig.listCollectionItems",
128            Method::Health => "dig.health",
129            Method::Methods => "dig.methods",
130            Method::GetNetworkInfo => "dig.getNetworkInfo",
131            Method::GetPeers => "dig.getPeers",
132            Method::Announce => "dig.announce",
133            Method::GetAvailability => "dig.getAvailability",
134            Method::ListInventory => "dig.listInventory",
135            Method::FetchRange => "dig.fetchRange",
136            Method::GetModuleInfo => "dig.getModuleInfo",
137            Method::FetchModuleRange => "dig.fetchModuleRange",
138            Method::Stage => "dig.stage",
139            Method::CacheGetConfig => "cache.getConfig",
140            Method::CacheSetCapBytes => "cache.setCapBytes",
141            Method::CacheClear => "cache.clear",
142            Method::CacheListCached => "cache.listCached",
143            Method::CacheRemoveCached => "cache.removeCached",
144            Method::CacheFetchAndCache => "cache.fetchAndCache",
145            Method::CacheStats => "cache.stats",
146            Method::ControlPeerStatus => "control.peerStatus",
147            Method::ControlSubscribe => "control.subscribe",
148            Method::ControlUnsubscribe => "control.unsubscribe",
149            Method::ControlListSubscriptions => "control.listSubscriptions",
150            Method::ControlPeersConnect => "control.peers.connect",
151            Method::ControlPeersDisconnect => "control.peers.disconnect",
152            Method::RpcDiscover => "rpc.discover",
153        }
154    }
155
156    /// Parse a wire name into a [`Method`], or `None` for an unknown method
157    /// (the caller answers `-32601`).
158    pub fn from_name(name: &str) -> Option<Method> {
159        Method::ALL.iter().copied().find(|m| m.name() == name)
160    }
161
162    /// The method's PRIMARY access [`Tier`].
163    ///
164    /// Note: a `PublicRead` method may still be peer-reachable — see
165    /// [`is_peer_reachable`](Method::is_peer_reachable). The tier is the
166    /// method's canonical home, the allowlist is the security boundary.
167    pub const fn tier(self) -> Tier {
168        match self {
169            Method::GetContent
170            | Method::GetCapsule
171            | Method::GetModule
172            | Method::GetManifest
173            | Method::GetMetadata
174            | Method::ListCapsules
175            | Method::GetProof
176            | Method::GetProofStatus
177            | Method::GetAnchoredRoot
178            | Method::GetCollection
179            | Method::ListCollectionItems
180            | Method::Health
181            | Method::Methods => Tier::PublicRead,
182
183            Method::GetNetworkInfo
184            | Method::GetPeers
185            | Method::Announce
186            | Method::GetAvailability
187            | Method::ListInventory
188            | Method::FetchRange
189            | Method::GetModuleInfo
190            | Method::FetchModuleRange => Tier::Peer,
191
192            Method::Stage
193            | Method::CacheGetConfig
194            | Method::CacheSetCapBytes
195            | Method::CacheClear
196            | Method::CacheListCached
197            | Method::CacheRemoveCached
198            | Method::CacheFetchAndCache
199            | Method::CacheStats
200            | Method::ControlPeerStatus
201            | Method::ControlSubscribe
202            | Method::ControlUnsubscribe
203            | Method::ControlListSubscriptions
204            | Method::ControlPeersConnect
205            | Method::ControlPeersDisconnect
206            | Method::RpcDiscover => Tier::Control,
207        }
208    }
209
210    /// Whether this method is reachable over the mTLS **peer** surface.
211    ///
212    /// This mirrors the canonical node's `is_peer_reachable_method` byte-for-byte
213    /// (digstore `dig-node` `peer.rs`). It is an ALLOWLIST: adding a method here
214    /// is a deliberate security decision — it exposes the method to any remote
215    /// peer (the peer verifier authenticates a `peer_id`, it does NOT authorize).
216    /// Management/mutation methods (`cache.*`, `control.*`, `dig.stage`) are
217    /// NEVER peer-reachable.
218    pub const fn is_peer_reachable(self) -> bool {
219        matches!(
220            self,
221            Method::GetContent
222                | Method::GetNetworkInfo
223                | Method::GetPeers
224                | Method::Announce
225                | Method::GetAvailability
226                | Method::ListInventory
227                | Method::FetchRange
228                | Method::GetModuleInfo
229                | Method::FetchModuleRange
230                | Method::GetAnchoredRoot
231                | Method::GetCollection
232                | Method::ListCollectionItems
233        )
234    }
235
236    /// A one-line human summary (drives the OpenRPC method `summary`).
237    pub const fn summary(self) -> &'static str {
238        match self {
239            Method::GetContent => "Read a verified window of a resource's ciphertext.",
240            Method::GetCapsule => "Fetch the whole .dig module for (store, root).",
241            Method::GetModule => "Alias of dig.getCapsule.",
242            Method::GetManifest => "Fetch the public discovery manifest resource.",
243            Method::GetMetadata => "Fetch the plaintext metadata manifest.",
244            Method::ListCapsules => "List a store's confirmed capsules.",
245            Method::GetProof => "Get the real inclusion proof + execution-proof status.",
246            Method::GetProofStatus => "Poll a real execution-proof job by id.",
247            Method::GetAnchoredRoot => "Resolve a store's chain-anchored tip root.",
248            Method::GetCollection => "Get collection-level facts for NFT launcher ids.",
249            Method::ListCollectionItems => "List resolved NFT collection items (paginated).",
250            Method::Health => "Liveness and capability summary.",
251            Method::Methods => "List the method names this node implements.",
252            Method::GetNetworkInfo => "This node's peer-network posture.",
253            Method::GetPeers => "The peers this node currently knows.",
254            Method::Announce => "Accept a peer announcement (peer_id + addresses).",
255            Method::GetAvailability => "Batch-check whether this node holds items.",
256            Method::ListInventory => "Enumerate the stores / roots this node serves.",
257            Method::FetchRange => "Fetch a single verified range frame of a resource.",
258            Method::GetModuleInfo => {
259                "Get the whole-.dig-module transfer descriptor (size + hashes) for (store, root)."
260            }
261            Method::FetchModuleRange => "Fetch a single range frame of the whole .dig module blob.",
262            Method::Stage => "Compile a local folder into a capsule .dig in-process.",
263            Method::CacheGetConfig => "Get the local-cache configuration.",
264            Method::CacheSetCapBytes => "Set the local-cache size cap.",
265            Method::CacheClear => "Clear the local cache.",
266            Method::CacheListCached => "List the durable cached modules.",
267            Method::CacheRemoveCached => "Remove one cached capsule.",
268            Method::CacheFetchAndCache => "Fetch and cache one capsule.",
269            Method::CacheStats => {
270                "Cache telemetry: cap + usage, entry count + bytes, eviction + hit/miss counters."
271            }
272            Method::ControlPeerStatus => "Snapshot the node's peer network.",
273            Method::ControlSubscribe => {
274                "Subscribe the node to a store (persisted watch + gap-fill)."
275            }
276            Method::ControlUnsubscribe => "Unsubscribe the node from a store.",
277            Method::ControlListSubscriptions => "List the node's persisted store subscriptions.",
278            Method::ControlPeersConnect => "Dial a peer into the connected pool.",
279            Method::ControlPeersDisconnect => "Drop a pooled peer by peer_id.",
280            Method::RpcDiscover => "Return the OpenRPC self-describe document.",
281        }
282    }
283
284    /// Every method, in catalogue order.
285    pub const ALL: &'static [Method] = &[
286        Method::GetContent,
287        Method::GetCapsule,
288        Method::GetModule,
289        Method::GetManifest,
290        Method::GetMetadata,
291        Method::ListCapsules,
292        Method::GetProof,
293        Method::GetProofStatus,
294        Method::GetAnchoredRoot,
295        Method::GetCollection,
296        Method::ListCollectionItems,
297        Method::Health,
298        Method::Methods,
299        Method::GetNetworkInfo,
300        Method::GetPeers,
301        Method::Announce,
302        Method::GetAvailability,
303        Method::ListInventory,
304        Method::FetchRange,
305        Method::GetModuleInfo,
306        Method::FetchModuleRange,
307        Method::Stage,
308        Method::CacheGetConfig,
309        Method::CacheSetCapBytes,
310        Method::CacheClear,
311        Method::CacheListCached,
312        Method::CacheRemoveCached,
313        Method::CacheFetchAndCache,
314        Method::CacheStats,
315        Method::ControlPeerStatus,
316        Method::ControlSubscribe,
317        Method::ControlUnsubscribe,
318        Method::ControlListSubscriptions,
319        Method::ControlPeersConnect,
320        Method::ControlPeersDisconnect,
321        Method::RpcDiscover,
322    ];
323
324    /// The method names reachable over the peer surface, in catalogue order —
325    /// the exact allowlist a server hands its peer responder.
326    pub fn peer_reachable_names() -> Vec<&'static str> {
327        Method::ALL
328            .iter()
329            .copied()
330            .filter(|m| m.is_peer_reachable())
331            .map(Method::name)
332            .collect()
333    }
334}
335
336impl std::fmt::Display for Method {
337    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338        f.write_str(self.name())
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use std::collections::HashSet;
346
347    /// **Proves:** every method name round-trips through `from_name`.
348    /// **Catches:** a name typo or a variant left out of `ALL`.
349    #[test]
350    fn names_round_trip() {
351        for m in Method::ALL {
352            assert_eq!(Method::from_name(m.name()), Some(*m), "{}", m.name());
353        }
354        assert!(Method::from_name("dig.nope").is_none());
355    }
356
357    /// **Proves:** all wire names are unique.
358    /// **Catches:** two variants accidentally sharing a name.
359    #[test]
360    fn names_unique() {
361        let names: HashSet<&str> = Method::ALL.iter().map(|m| m.name()).collect();
362        assert_eq!(names.len(), Method::ALL.len());
363    }
364
365    /// **Proves:** the peer allowlist is EXACTLY the twelve methods the canonical
366    /// node exposes over mTLS — and no `cache.*`/`control.*`/`dig.stage` leaks
367    /// onto the peer surface.
368    /// **Catches:** a drift from digstore `is_peer_reachable_method`, or a
369    /// management method accidentally allowlisted (the audit #179 auth-bypass).
370    #[test]
371    fn peer_allowlist_matches_canonical() {
372        let mut got = Method::peer_reachable_names();
373        got.sort_unstable();
374        let mut want = vec![
375            "dig.getContent",
376            "dig.getNetworkInfo",
377            "dig.getPeers",
378            "dig.announce",
379            "dig.getAvailability",
380            "dig.listInventory",
381            "dig.fetchRange",
382            "dig.getModuleInfo",
383            "dig.fetchModuleRange",
384            "dig.getAnchoredRoot",
385            "dig.getCollection",
386            "dig.listCollectionItems",
387        ];
388        want.sort_unstable();
389        assert_eq!(got, want);
390
391        // No management/mutation method is peer-reachable.
392        for m in [
393            Method::Stage,
394            Method::CacheGetConfig,
395            Method::CacheSetCapBytes,
396            Method::CacheClear,
397            Method::CacheListCached,
398            Method::CacheRemoveCached,
399            Method::CacheFetchAndCache,
400            Method::CacheStats,
401            Method::ControlPeerStatus,
402            Method::ControlSubscribe,
403            Method::ControlUnsubscribe,
404            Method::ControlListSubscriptions,
405            Method::ControlPeersConnect,
406            Method::ControlPeersDisconnect,
407            Method::RpcDiscover,
408        ] {
409            assert!(
410                !m.is_peer_reachable(),
411                "{} must NOT be peer-reachable",
412                m.name()
413            );
414        }
415    }
416
417    /// **Proves:** the chain-anchored public reads are PublicRead yet ALSO
418    /// peer-reachable (the design-brief tier decision §5).
419    #[test]
420    fn anchored_reads_public_but_peer_reachable() {
421        for m in [
422            Method::GetAnchoredRoot,
423            Method::GetCollection,
424            Method::ListCollectionItems,
425        ] {
426            assert_eq!(m.tier(), Tier::PublicRead);
427            assert!(m.is_peer_reachable());
428        }
429    }
430
431    /// **Proves:** the six live-node methods the crate previously lacked are
432    /// present, Control-tiered, and NEVER peer-reachable (the #1075 completeness
433    /// gap: the crate must catalogue every method the dig-node dispatch serves).
434    /// **Catches:** a variant dropped from the catalogue, mis-tiered, or leaked
435    /// onto the peer surface.
436    #[test]
437    fn completed_control_methods_present_and_gated() {
438        let expected = [
439            (Method::CacheStats, "cache.stats"),
440            (Method::ControlSubscribe, "control.subscribe"),
441            (Method::ControlUnsubscribe, "control.unsubscribe"),
442            (
443                Method::ControlListSubscriptions,
444                "control.listSubscriptions",
445            ),
446            (Method::ControlPeersConnect, "control.peers.connect"),
447            (Method::ControlPeersDisconnect, "control.peers.disconnect"),
448        ];
449        for (m, name) in expected {
450            assert_eq!(m.name(), name);
451            assert_eq!(Method::from_name(name), Some(m));
452            assert!(Method::ALL.contains(&m), "{name} missing from ALL");
453            assert_eq!(m.tier(), Tier::Control, "{name} must be Control");
454            assert!(!m.is_peer_reachable(), "{name} must NOT be peer-reachable");
455        }
456    }
457
458    /// **Proves:** the whole-module peer-pull methods (#1576) are present,
459    /// Peer-tiered, and peer-reachable — the wire surface a peer uses to pull a
460    /// complete `.dig` module and reshare it.
461    /// **Catches:** either method dropped from the catalogue, mis-tiered, or left
462    /// off the peer allowlist (which would make peer reshare unreachable).
463    #[test]
464    fn module_pull_methods_present_and_peer_reachable() {
465        for (m, name) in [
466            (Method::GetModuleInfo, "dig.getModuleInfo"),
467            (Method::FetchModuleRange, "dig.fetchModuleRange"),
468        ] {
469            assert_eq!(m.name(), name);
470            assert_eq!(Method::from_name(name), Some(m));
471            assert!(Method::ALL.contains(&m), "{name} missing from ALL");
472            assert_eq!(m.tier(), Tier::Peer, "{name} must be Peer");
473            assert!(m.is_peer_reachable(), "{name} must be peer-reachable");
474        }
475    }
476
477    /// **Proves:** every Control method is NOT peer-reachable (the boundary
478    /// invariant).
479    #[test]
480    fn control_tier_never_peer_reachable() {
481        for m in Method::ALL {
482            if m.tier() == Tier::Control {
483                assert!(
484                    !m.is_peer_reachable(),
485                    "{} is Control but peer-reachable",
486                    m.name()
487                );
488            }
489        }
490    }
491}