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