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    /// `dig.listRewardDistributors` — the reward distributors this node funds,
110    /// and the ones it has a claim to as a mirror (dig-rewards-coin SPEC §2.6).
111    ListRewardDistributors,
112    /// `dig.getRewardProverStatus` — the always-on reward prover loop's status
113    /// for one or every distributor this node runs (dig-rewards-coin SPEC §2.3).
114    GetRewardProverStatus,
115    /// `dig.getRewardDistributor` — one reward distributor's chain-derived
116    /// state (dig-rewards-coin SPEC §2.6).
117    GetRewardDistributor,
118    /// `dig.listRewardDistributorCommitments` — one distributor's per-epoch
119    /// clawback commitment slots, with the recoverable amount computed per
120    /// slot (dig-rewards-coin SPEC §7.4 clause 5, §7.5).
121    ListRewardDistributorCommitments,
122    /// `dig.getPayeeRewardClaimStatus` — this node's own claim-side posture as a
123    /// **payee** (dig-rewards-coin SPEC §12.5 clause 6, §2.4).
124    GetPayeeRewardClaimStatus,
125    /// `rpc.discover` — the OpenRPC self-describe document.
126    RpcDiscover,
127}
128
129impl Method {
130    /// The stable JSON-RPC wire name.
131    pub const fn name(self) -> &'static str {
132        match self {
133            Method::GetContent => "dig.getContent",
134            Method::GetCapsule => "dig.getCapsule",
135            Method::GetModule => "dig.getModule",
136            Method::GetManifest => "dig.getManifest",
137            Method::GetMetadata => "dig.getMetadata",
138            Method::ListCapsules => "dig.listCapsules",
139            Method::GetProof => "dig.getProof",
140            Method::GetProofStatus => "dig.getProofStatus",
141            Method::GetAnchoredRoot => "dig.getAnchoredRoot",
142            Method::GetCollection => "dig.getCollection",
143            Method::ListCollectionItems => "dig.listCollectionItems",
144            Method::Health => "dig.health",
145            Method::Methods => "dig.methods",
146            Method::GetNetworkInfo => "dig.getNetworkInfo",
147            Method::GetPeers => "dig.getPeers",
148            Method::Announce => "dig.announce",
149            Method::GetAvailability => "dig.getAvailability",
150            Method::ListInventory => "dig.listInventory",
151            Method::FetchRange => "dig.fetchRange",
152            Method::GetModuleInfo => "dig.getModuleInfo",
153            Method::FetchModuleRange => "dig.fetchModuleRange",
154            Method::Stage => "dig.stage",
155            Method::CacheGetConfig => "cache.getConfig",
156            Method::CacheSetCapBytes => "cache.setCapBytes",
157            Method::CacheClear => "cache.clear",
158            Method::CacheListCached => "cache.listCached",
159            Method::CacheRemoveCached => "cache.removeCached",
160            Method::CacheFetchAndCache => "cache.fetchAndCache",
161            Method::CacheStats => "cache.stats",
162            Method::ControlPeerStatus => "control.peerStatus",
163            Method::ControlSubscribe => "control.subscribe",
164            Method::ControlUnsubscribe => "control.unsubscribe",
165            Method::ControlListSubscriptions => "control.listSubscriptions",
166            Method::ControlPeersConnect => "control.peers.connect",
167            Method::ControlPeersDisconnect => "control.peers.disconnect",
168            Method::ListRewardDistributors => "dig.listRewardDistributors",
169            Method::GetRewardProverStatus => "dig.getRewardProverStatus",
170            Method::GetRewardDistributor => "dig.getRewardDistributor",
171            Method::ListRewardDistributorCommitments => "dig.listRewardDistributorCommitments",
172            Method::GetPayeeRewardClaimStatus => "dig.getPayeeRewardClaimStatus",
173            Method::RpcDiscover => "rpc.discover",
174        }
175    }
176
177    /// Parse a wire name into a [`Method`], or `None` for an unknown method
178    /// (the caller answers `-32601`).
179    pub fn from_name(name: &str) -> Option<Method> {
180        Method::ALL.iter().copied().find(|m| m.name() == name)
181    }
182
183    /// The method's PRIMARY access [`Tier`].
184    ///
185    /// Note: a `PublicRead` method may still be peer-reachable — see
186    /// [`is_peer_reachable`](Method::is_peer_reachable). The tier is the
187    /// method's canonical home, the allowlist is the security boundary.
188    pub const fn tier(self) -> Tier {
189        match self {
190            Method::GetContent
191            | Method::GetCapsule
192            | Method::GetModule
193            | Method::GetManifest
194            | Method::GetMetadata
195            | Method::ListCapsules
196            | Method::GetProof
197            | Method::GetProofStatus
198            | Method::GetAnchoredRoot
199            | Method::GetCollection
200            | Method::ListCollectionItems
201            | Method::Health
202            | Method::Methods => Tier::PublicRead,
203
204            Method::GetNetworkInfo
205            | Method::GetPeers
206            | Method::Announce
207            | Method::GetAvailability
208            | Method::ListInventory
209            | Method::FetchRange
210            | Method::GetModuleInfo
211            | Method::FetchModuleRange => Tier::Peer,
212
213            Method::Stage
214            | Method::CacheGetConfig
215            | Method::CacheSetCapBytes
216            | Method::CacheClear
217            | Method::CacheListCached
218            | Method::CacheRemoveCached
219            | Method::CacheFetchAndCache
220            | Method::CacheStats
221            | Method::ControlPeerStatus
222            | Method::ControlSubscribe
223            | Method::ControlUnsubscribe
224            | Method::ControlListSubscriptions
225            | Method::ControlPeersConnect
226            | Method::ControlPeersDisconnect
227            | Method::ListRewardDistributors
228            | Method::GetRewardProverStatus
229            | Method::GetRewardDistributor
230            | Method::ListRewardDistributorCommitments
231            | Method::GetPayeeRewardClaimStatus
232            | Method::RpcDiscover => Tier::Control,
233        }
234    }
235
236    /// Whether this method is reachable over the mTLS **peer** surface.
237    ///
238    /// This mirrors the canonical node's `is_peer_reachable_method` byte-for-byte
239    /// (digstore `dig-node` `peer.rs`). It is an ALLOWLIST: adding a method here
240    /// is a deliberate security decision — it exposes the method to any remote
241    /// peer (the peer verifier authenticates a `peer_id`, it does NOT authorize).
242    /// Management/mutation methods (`cache.*`, `control.*`, `dig.stage`) are
243    /// NEVER peer-reachable.
244    pub const fn is_peer_reachable(self) -> bool {
245        matches!(
246            self,
247            Method::GetContent
248                | Method::GetNetworkInfo
249                | Method::GetPeers
250                | Method::Announce
251                | Method::GetAvailability
252                | Method::ListInventory
253                | Method::FetchRange
254                | Method::GetModuleInfo
255                | Method::FetchModuleRange
256                | Method::GetAnchoredRoot
257                | Method::GetCollection
258                | Method::ListCollectionItems
259        )
260    }
261
262    /// Whether this method answers on the **reward** surface — distributor,
263    /// prover-loop, or payee claim-side state (dig-rewards-coin SPEC §2.3, §2.6,
264    /// §7.4, §7.5, §12.5).
265    ///
266    /// Every method for which this is `true` MUST be `Tier::Control` and MUST
267    /// NOT be peer-reachable; the guard that enforces that drives itself from
268    /// here.
269    ///
270    /// # Why an exhaustive match and not a name filter
271    ///
272    /// The guard previously selected its subjects with
273    /// `name().contains("Reward")`, which is convention-bound: a reward method
274    /// named without that substring escapes the tier assertion silently, and
275    /// nothing tells the author of that method that it has. This match is
276    /// compiler-bound instead — a new variant fails to compile until someone
277    /// classifies it, and the answer they must give is the one the guard needs.
278    /// There is deliberately no `_` arm, for exactly that reason.
279    pub const fn is_reward(self) -> bool {
280        match self {
281            Method::ListRewardDistributors
282            | Method::GetRewardProverStatus
283            | Method::GetRewardDistributor
284            | Method::ListRewardDistributorCommitments
285            | Method::GetPayeeRewardClaimStatus => true,
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 => false,
322        }
323    }
324
325    /// A one-line human summary (drives the OpenRPC method `summary`).
326    pub const fn summary(self) -> &'static str {
327        match self {
328            Method::GetContent => "Read a verified window of a resource's ciphertext.",
329            Method::GetCapsule => "Fetch the whole .dig module for (store, root).",
330            Method::GetModule => "Alias of dig.getCapsule.",
331            Method::GetManifest => "Fetch the public discovery manifest resource.",
332            Method::GetMetadata => "Fetch the plaintext metadata manifest.",
333            Method::ListCapsules => "List a store's confirmed capsules.",
334            Method::GetProof => "Get the real inclusion proof + execution-proof status.",
335            Method::GetProofStatus => "Poll a real execution-proof job by id.",
336            Method::GetAnchoredRoot => "Resolve a store's chain-anchored tip root.",
337            Method::GetCollection => "Get collection-level facts for NFT launcher ids.",
338            Method::ListCollectionItems => "List resolved NFT collection items (paginated).",
339            Method::Health => "Liveness and capability summary.",
340            Method::Methods => "List the method names this node implements.",
341            Method::GetNetworkInfo => "This node's peer-network posture.",
342            Method::GetPeers => "The peers this node currently knows.",
343            Method::Announce => "Accept a peer announcement (peer_id + addresses).",
344            Method::GetAvailability => "Batch-check whether this node holds items.",
345            Method::ListInventory => "Enumerate the stores / roots this node serves.",
346            Method::FetchRange => "Fetch a single verified range frame of a resource.",
347            Method::GetModuleInfo => {
348                "Get the whole-.dig-module transfer descriptor (size + hashes) for (store, root)."
349            }
350            Method::FetchModuleRange => "Fetch a single range frame of the whole .dig module blob.",
351            Method::Stage => "Compile a local folder into a capsule .dig in-process.",
352            Method::CacheGetConfig => "Get the local-cache configuration.",
353            Method::CacheSetCapBytes => "Set the local-cache size cap.",
354            Method::CacheClear => "Clear the local cache.",
355            Method::CacheListCached => "List the durable cached modules.",
356            Method::CacheRemoveCached => "Remove one cached capsule.",
357            Method::CacheFetchAndCache => "Fetch and cache one capsule.",
358            Method::CacheStats => {
359                "Cache telemetry: cap + usage, entry count + bytes, eviction + hit/miss counters."
360            }
361            Method::ControlPeerStatus => "Snapshot the node's peer network.",
362            Method::ControlSubscribe => {
363                "Subscribe the node to a store (persisted watch + gap-fill)."
364            }
365            Method::ControlUnsubscribe => "Unsubscribe the node from a store.",
366            Method::ControlListSubscriptions => "List the node's persisted store subscriptions.",
367            Method::ControlPeersConnect => "Dial a peer into the connected pool.",
368            Method::ControlPeersDisconnect => "Drop a pooled peer by peer_id.",
369            Method::ListRewardDistributors => {
370                "List the reward distributors this node funds or has a mirror claim to."
371            }
372            Method::GetRewardProverStatus => {
373                "Get the reward prover loop's status for one or every distributor."
374            }
375            Method::GetRewardDistributor => "Get one reward distributor's chain-derived state.",
376            Method::ListRewardDistributorCommitments => {
377                "List one reward distributor's per-epoch clawback commitment slots."
378            }
379            Method::GetPayeeRewardClaimStatus => {
380                "Report this node's own payee-side reward claim posture."
381            }
382            Method::RpcDiscover => "Return the OpenRPC self-describe document.",
383        }
384    }
385
386    /// Every method, in catalogue order.
387    pub const ALL: &'static [Method] = &[
388        Method::GetContent,
389        Method::GetCapsule,
390        Method::GetModule,
391        Method::GetManifest,
392        Method::GetMetadata,
393        Method::ListCapsules,
394        Method::GetProof,
395        Method::GetProofStatus,
396        Method::GetAnchoredRoot,
397        Method::GetCollection,
398        Method::ListCollectionItems,
399        Method::Health,
400        Method::Methods,
401        Method::GetNetworkInfo,
402        Method::GetPeers,
403        Method::Announce,
404        Method::GetAvailability,
405        Method::ListInventory,
406        Method::FetchRange,
407        Method::GetModuleInfo,
408        Method::FetchModuleRange,
409        Method::Stage,
410        Method::CacheGetConfig,
411        Method::CacheSetCapBytes,
412        Method::CacheClear,
413        Method::CacheListCached,
414        Method::CacheRemoveCached,
415        Method::CacheFetchAndCache,
416        Method::CacheStats,
417        Method::ControlPeerStatus,
418        Method::ControlSubscribe,
419        Method::ControlUnsubscribe,
420        Method::ControlListSubscriptions,
421        Method::ControlPeersConnect,
422        Method::ControlPeersDisconnect,
423        Method::ListRewardDistributors,
424        Method::GetRewardProverStatus,
425        Method::GetRewardDistributor,
426        Method::ListRewardDistributorCommitments,
427        Method::GetPayeeRewardClaimStatus,
428        Method::RpcDiscover,
429    ];
430
431    /// The method names reachable over the peer surface, in catalogue order —
432    /// the exact allowlist a server hands its peer responder.
433    pub fn peer_reachable_names() -> Vec<&'static str> {
434        Method::ALL
435            .iter()
436            .copied()
437            .filter(|m| m.is_peer_reachable())
438            .map(Method::name)
439            .collect()
440    }
441}
442
443impl std::fmt::Display for Method {
444    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445        f.write_str(self.name())
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use std::collections::HashSet;
453
454    /// **Proves:** every method name round-trips through `from_name`.
455    /// **Catches:** a name typo or a variant left out of `ALL`.
456    #[test]
457    fn names_round_trip() {
458        for m in Method::ALL {
459            assert_eq!(Method::from_name(m.name()), Some(*m), "{}", m.name());
460        }
461        assert!(Method::from_name("dig.nope").is_none());
462    }
463
464    /// **Proves:** all wire names are unique.
465    /// **Catches:** two variants accidentally sharing a name.
466    #[test]
467    fn names_unique() {
468        let names: HashSet<&str> = Method::ALL.iter().map(|m| m.name()).collect();
469        assert_eq!(names.len(), Method::ALL.len());
470    }
471
472    /// **Proves:** the peer allowlist is EXACTLY the twelve methods the canonical
473    /// node exposes over mTLS — and no `cache.*`/`control.*`/`dig.stage` leaks
474    /// onto the peer surface.
475    /// **Catches:** a drift from digstore `is_peer_reachable_method`, or a
476    /// management method accidentally allowlisted (the audit #179 auth-bypass).
477    #[test]
478    fn peer_allowlist_matches_canonical() {
479        let mut got = Method::peer_reachable_names();
480        got.sort_unstable();
481        let mut want = vec![
482            "dig.getContent",
483            "dig.getNetworkInfo",
484            "dig.getPeers",
485            "dig.announce",
486            "dig.getAvailability",
487            "dig.listInventory",
488            "dig.fetchRange",
489            "dig.getModuleInfo",
490            "dig.fetchModuleRange",
491            "dig.getAnchoredRoot",
492            "dig.getCollection",
493            "dig.listCollectionItems",
494        ];
495        want.sort_unstable();
496        assert_eq!(got, want);
497
498        // No management/mutation method is peer-reachable.
499        for m in [
500            Method::Stage,
501            Method::CacheGetConfig,
502            Method::CacheSetCapBytes,
503            Method::CacheClear,
504            Method::CacheListCached,
505            Method::CacheRemoveCached,
506            Method::CacheFetchAndCache,
507            Method::CacheStats,
508            Method::ControlPeerStatus,
509            Method::ControlSubscribe,
510            Method::ControlUnsubscribe,
511            Method::ControlListSubscriptions,
512            Method::ControlPeersConnect,
513            Method::ControlPeersDisconnect,
514            Method::RpcDiscover,
515        ] {
516            assert!(
517                !m.is_peer_reachable(),
518                "{} must NOT be peer-reachable",
519                m.name()
520            );
521        }
522    }
523
524    /// **Proves:** the chain-anchored public reads are PublicRead yet ALSO
525    /// peer-reachable (the design-brief tier decision §5).
526    #[test]
527    fn anchored_reads_public_but_peer_reachable() {
528        for m in [
529            Method::GetAnchoredRoot,
530            Method::GetCollection,
531            Method::ListCollectionItems,
532        ] {
533            assert_eq!(m.tier(), Tier::PublicRead);
534            assert!(m.is_peer_reachable());
535        }
536    }
537
538    /// **Proves:** the six live-node methods the crate previously lacked are
539    /// present, Control-tiered, and NEVER peer-reachable (the #1075 completeness
540    /// gap: the crate must catalogue every method the dig-node dispatch serves).
541    /// **Catches:** a variant dropped from the catalogue, mis-tiered, or leaked
542    /// onto the peer surface.
543    #[test]
544    fn completed_control_methods_present_and_gated() {
545        let expected = [
546            (Method::CacheStats, "cache.stats"),
547            (Method::ControlSubscribe, "control.subscribe"),
548            (Method::ControlUnsubscribe, "control.unsubscribe"),
549            (
550                Method::ControlListSubscriptions,
551                "control.listSubscriptions",
552            ),
553            (Method::ControlPeersConnect, "control.peers.connect"),
554            (Method::ControlPeersDisconnect, "control.peers.disconnect"),
555        ];
556        for (m, name) in expected {
557            assert_eq!(m.name(), name);
558            assert_eq!(Method::from_name(name), Some(m));
559            assert!(Method::ALL.contains(&m), "{name} missing from ALL");
560            assert_eq!(m.tier(), Tier::Control, "{name} must be Control");
561            assert!(!m.is_peer_reachable(), "{name} must NOT be peer-reachable");
562        }
563    }
564
565    /// **Proves:** the whole-module peer-pull methods (#1576) are present,
566    /// Peer-tiered, and peer-reachable — the wire surface a peer uses to pull a
567    /// complete `.dig` module and reshare it.
568    /// **Catches:** either method dropped from the catalogue, mis-tiered, or left
569    /// off the peer allowlist (which would make peer reshare unreachable).
570    #[test]
571    fn module_pull_methods_present_and_peer_reachable() {
572        for (m, name) in [
573            (Method::GetModuleInfo, "dig.getModuleInfo"),
574            (Method::FetchModuleRange, "dig.fetchModuleRange"),
575        ] {
576            assert_eq!(m.name(), name);
577            assert_eq!(Method::from_name(name), Some(m));
578            assert!(Method::ALL.contains(&m), "{name} missing from ALL");
579            assert_eq!(m.tier(), Tier::Peer, "{name} must be Peer");
580            assert!(m.is_peer_reachable(), "{name} must be peer-reachable");
581        }
582    }
583
584    /// **Proves:** `dig.getPayeeRewardClaimStatus` is in the catalogue under its
585    /// exact wire name (dig_ecosystem#3269). The tier / peer-reachability half of
586    /// its contract is proven by the catalogue-wide scan below, not restated here.
587    #[test]
588    fn payee_reward_claim_status_present_under_its_wire_name() {
589        assert_eq!(
590            Method::GetPayeeRewardClaimStatus.name(),
591            "dig.getPayeeRewardClaimStatus"
592        );
593        assert_eq!(
594            Method::from_name("dig.getPayeeRewardClaimStatus"),
595            Some(Method::GetPayeeRewardClaimStatus)
596        );
597        assert!(Method::ALL.contains(&Method::GetPayeeRewardClaimStatus));
598    }
599
600    /// **Proves:** EVERY reward method in the catalogue is Control-tiered, not
601    /// peer-reachable, and absent from the peer allowlist — dig-rewards-coin
602    /// SPEC §2.3/§2.6/§7.4/§7.5, §12.5.
603    ///
604    /// **Catches:** a mis-tier that would expose distributor / prover-loop /
605    /// claim-side internals to any remote peer, *including on a reward method
606    /// added after this test was written*. An earlier form named its four
607    /// members explicitly, so a fifth reward method passed it by simply not
608    /// being on the list (#3261: a test over an enumeration can only check the
609    /// enumeration it was given); the form after that filtered on
610    /// `name().contains("Reward")`, which a reward method named without that
611    /// substring escapes silently.
612    ///
613    /// [`Method::is_reward`] is an exhaustive match, so the selection is
614    /// compiler-bound: a new variant fails to compile until it is classified,
615    /// and classifying it as a reward method puts it under every assertion
616    /// below. There is deliberately no expected count here — a count is a second
617    /// thing to forget, and nothing tells it to move.
618    #[test]
619    fn every_reward_method_is_control_and_not_peer_reachable() {
620        let reward_methods: Vec<Method> = Method::ALL
621            .iter()
622            .copied()
623            .filter(|m| m.is_reward())
624            .collect();
625
626        // Non-vacuous: a catalogue that lost the reward methods, or an
627        // `is_reward` that stopped claiming any, must fail here rather than
628        // empty the set and pass silently.
629        assert!(
630            !reward_methods.is_empty(),
631            "no method reported is_reward() -- either the catalogue lost them or \
632             the classification was inverted"
633        );
634
635        let allowlist = Method::peer_reachable_names();
636        for m in reward_methods {
637            let name = m.name();
638            assert_eq!(
639                Method::from_name(name),
640                Some(m),
641                "{name} does not round-trip"
642            );
643            assert_eq!(m.tier(), Tier::Control, "{name} must be Control");
644            assert!(!m.is_peer_reachable(), "{name} must NOT be peer-reachable");
645            assert!(
646                !allowlist.contains(&name),
647                "{name} must not appear in the peer allowlist"
648            );
649        }
650    }
651
652    /// **Proves:** every method whose wire name says "Reward" is classified as
653    /// one.
654    /// **Catches:** the one wrong answer a machine can check. The exhaustive
655    /// match in [`Method::is_reward`] forces an author to answer for a new
656    /// variant, but it cannot tell a wrong answer from a right one; a reward
657    /// method classified `false` would drop straight out of the tier guard, and
658    /// its own name is the evidence against it.
659    #[test]
660    fn reward_named_methods_are_classified_as_reward() {
661        for m in Method::ALL {
662            if m.name().contains("Reward") {
663                assert!(
664                    m.is_reward(),
665                    "{} is named a reward method but is_reward() says otherwise",
666                    m.name()
667                );
668            }
669        }
670    }
671
672    /// **Proves:** every Control method is NOT peer-reachable (the boundary
673    /// invariant).
674    #[test]
675    fn control_tier_never_peer_reachable() {
676        for m in Method::ALL {
677            if m.tier() == Tier::Control {
678                assert!(
679                    !m.is_peer_reachable(),
680                    "{} is Control but peer-reachable",
681                    m.name()
682                );
683            }
684        }
685    }
686}