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/// Generates [`Method`] and [`Method::ALL`] from ONE variant table.
18///
19/// This exists so a new method cannot enter the enum without also entering
20/// `ALL`: the two used to be two hand-written lists (the enum body and a
21/// literal `&[Method::...]` array), and a variant added to one while
22/// forgotten in the other left it invisible to every `ALL`-driven guard in
23/// this module -- including the one enforcing dig_ecosystem#3261 ("no
24/// `*Reward*` method may ever be peer-reachable or non-`Control`"). With one
25/// source table, that omission is unwritable rather than merely testable
26/// (dig_ecosystem#3317).
27///
28/// `name()`, `tier()`, `is_peer_reachable()` and `is_reward()` stay
29/// hand-written exhaustive matches below -- they are already compiler-forced
30/// (adding a variant is `E0004` until every one of them is updated), and
31/// generating them too would hide the method table behind macro grammar for
32/// no completeness gain.
33macro_rules! method_catalogue {
34 ($( $(#[$doc:meta])* $variant:ident ),+ $(,)?) => {
35 /// A DIG-node RPC method.
36 ///
37 /// `#[non_exhaustive]` so adding a method in a minor release is additive.
38 /// Convert to/from the wire name with [`Method::name`] / [`Method::from_name`].
39 #[non_exhaustive]
40 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41 pub enum Method {
42 $( $(#[$doc])* $variant ),+
43 }
44
45 impl Method {
46 /// Every method, in catalogue order. Generated by
47 /// `method_catalogue!` from the same table that defines the enum, so
48 /// a variant cannot exist without also being listed here.
49 pub const ALL: &'static [Method] = &[ $( Method::$variant ),+ ];
50 }
51 };
52}
53
54method_catalogue! {
55 // ---- PublicRead ----
56 /// `dig.getContent` — a verified resource-window read.
57 GetContent,
58 /// `dig.getCapsule` — the whole `.dig` module for `(store, root)`.
59 GetCapsule,
60 /// `dig.getModule` — alias of [`GetCapsule`](Method::GetCapsule).
61 GetModule,
62 /// `dig.getManifest` — the public discovery manifest resource.
63 GetManifest,
64 /// `dig.getMetadata` — the plaintext metadata manifest (never encrypted).
65 GetMetadata,
66 /// `dig.listCapsules` — the confirmed capsule list (discovery metadata).
67 ListCapsules,
68 /// `dig.getProof` — the real inclusion proof + execution-proof status.
69 GetProof,
70 /// `dig.getProofStatus` — poll a real execution-proof job by id.
71 GetProofStatus,
72 /// `dig.getAnchoredRoot` — resolve a store's chain-anchored tip root.
73 /// (Also peer-reachable.)
74 GetAnchoredRoot,
75 /// `dig.getCollection` — collection-level facts for a set of NFT launchers.
76 /// (Also peer-reachable.)
77 GetCollection,
78 /// `dig.listCollectionItems` — a page of resolved collection items.
79 /// (Also peer-reachable.)
80 ListCollectionItems,
81 /// `dig.health` — liveness + capability summary.
82 Health,
83 /// `dig.methods` — the implemented method names (agent self-describe).
84 Methods,
85
86 // ---- Peer ----
87 /// `dig.getNetworkInfo` — this node's peer-network posture.
88 GetNetworkInfo,
89 /// `dig.getPeers` — the peers this node knows (peer exchange).
90 GetPeers,
91 /// `dig.announce` — accept a peer announcement.
92 Announce,
93 /// `dig.getAvailability` — batch presence check across stores/roots/capsules.
94 GetAvailability,
95 /// `dig.listInventory` — enumerate what this node serves.
96 ListInventory,
97 /// `dig.fetchRange` — a single verified range frame of a resource.
98 FetchRange,
99 /// `dig.getModuleInfo` — the whole-`.dig`-module transfer descriptor
100 /// (total size + module hash + per-chunk hashes) for `(store, root)`, the
101 /// handshake a peer reads before range-pulling the module blob.
102 GetModuleInfo,
103 /// `dig.fetchModuleRange` — a single range frame of the whole `.dig` module
104 /// blob for `(store, root)` (reuses [`RangeFrame`](crate::types::RangeFrame)).
105 FetchModuleRange,
106
107 // ---- Control (loopback / in-process only) ----
108 /// `dig.stage` — compile a local folder into a capsule `.dig` in-process.
109 Stage,
110 /// `cache.getConfig` — the local-cache config.
111 CacheGetConfig,
112 /// `cache.setCapBytes` — set the cache size cap.
113 CacheSetCapBytes,
114 /// `cache.clear` — clear the cache.
115 CacheClear,
116 /// `cache.listCached` — the durable module inventory.
117 CacheListCached,
118 /// `cache.removeCached` — remove one cached capsule.
119 CacheRemoveCached,
120 /// `cache.fetchAndCache` — fetch + cache one capsule.
121 CacheFetchAndCache,
122 /// `cache.stats` — cache telemetry: reserved cap + live usage, cached-capsule
123 /// count + total on-disk bytes, session eviction + content-cache hit/miss.
124 CacheStats,
125 /// `control.peerStatus` — the peer-network status snapshot.
126 ControlPeerStatus,
127 /// `control.subscribe` — subscribe the node to a store (persisted): it
128 /// actively watches + gap-fills that store.
129 ControlSubscribe,
130 /// `control.unsubscribe` — remove a store from the node's subscription set.
131 ControlUnsubscribe,
132 /// `control.listSubscriptions` — the node's persisted store subscriptions.
133 ControlListSubscriptions,
134 /// `control.peers.connect` — dial a peer (turn a discovered peer into a
135 /// counted, RPC-reachable connected peer).
136 ControlPeersConnect,
137 /// `control.peers.disconnect` — drop a pooled peer by `peer_id`, closing its
138 /// mTLS link (the inverse of `control.peers.connect`).
139 ControlPeersDisconnect,
140 /// `dig.listRewardDistributors` — the reward distributors this node funds,
141 /// and the ones it has a claim to as a mirror (dig-rewards-coin SPEC §2.6).
142 ListRewardDistributors,
143 /// `dig.getRewardProverStatus` — the always-on reward prover loop's status
144 /// for one or every distributor this node runs (dig-rewards-coin SPEC §2.3).
145 GetRewardProverStatus,
146 /// `dig.getRewardDistributor` — one reward distributor's chain-derived
147 /// state (dig-rewards-coin SPEC §2.6).
148 GetRewardDistributor,
149 /// `dig.listRewardDistributorCommitments` — one distributor's per-epoch
150 /// clawback commitment slots, with the recoverable amount computed per
151 /// slot (dig-rewards-coin SPEC §7.4 clause 5, §7.5).
152 ListRewardDistributorCommitments,
153 /// `dig.getPayeeRewardClaimStatus` — this node's own claim-side posture as a
154 /// **payee** (dig-rewards-coin SPEC §12.5 clause 6, §2.4).
155 GetPayeeRewardClaimStatus,
156 /// `rpc.discover` — the OpenRPC self-describe document.
157 RpcDiscover,
158}
159
160impl Method {
161 // ---- hand-written exhaustive classifiers (compiler-forced, not generated) ----
162
163 /// The stable JSON-RPC wire name.
164 pub const fn name(self) -> &'static str {
165 match self {
166 Method::GetContent => "dig.getContent",
167 Method::GetCapsule => "dig.getCapsule",
168 Method::GetModule => "dig.getModule",
169 Method::GetManifest => "dig.getManifest",
170 Method::GetMetadata => "dig.getMetadata",
171 Method::ListCapsules => "dig.listCapsules",
172 Method::GetProof => "dig.getProof",
173 Method::GetProofStatus => "dig.getProofStatus",
174 Method::GetAnchoredRoot => "dig.getAnchoredRoot",
175 Method::GetCollection => "dig.getCollection",
176 Method::ListCollectionItems => "dig.listCollectionItems",
177 Method::Health => "dig.health",
178 Method::Methods => "dig.methods",
179 Method::GetNetworkInfo => "dig.getNetworkInfo",
180 Method::GetPeers => "dig.getPeers",
181 Method::Announce => "dig.announce",
182 Method::GetAvailability => "dig.getAvailability",
183 Method::ListInventory => "dig.listInventory",
184 Method::FetchRange => "dig.fetchRange",
185 Method::GetModuleInfo => "dig.getModuleInfo",
186 Method::FetchModuleRange => "dig.fetchModuleRange",
187 Method::Stage => "dig.stage",
188 Method::CacheGetConfig => "cache.getConfig",
189 Method::CacheSetCapBytes => "cache.setCapBytes",
190 Method::CacheClear => "cache.clear",
191 Method::CacheListCached => "cache.listCached",
192 Method::CacheRemoveCached => "cache.removeCached",
193 Method::CacheFetchAndCache => "cache.fetchAndCache",
194 Method::CacheStats => "cache.stats",
195 Method::ControlPeerStatus => "control.peerStatus",
196 Method::ControlSubscribe => "control.subscribe",
197 Method::ControlUnsubscribe => "control.unsubscribe",
198 Method::ControlListSubscriptions => "control.listSubscriptions",
199 Method::ControlPeersConnect => "control.peers.connect",
200 Method::ControlPeersDisconnect => "control.peers.disconnect",
201 Method::ListRewardDistributors => "dig.listRewardDistributors",
202 Method::GetRewardProverStatus => "dig.getRewardProverStatus",
203 Method::GetRewardDistributor => "dig.getRewardDistributor",
204 Method::ListRewardDistributorCommitments => "dig.listRewardDistributorCommitments",
205 Method::GetPayeeRewardClaimStatus => "dig.getPayeeRewardClaimStatus",
206 Method::RpcDiscover => "rpc.discover",
207 }
208 }
209
210 /// Parse a wire name into a [`Method`], or `None` for an unknown method
211 /// (the caller answers `-32601`).
212 pub fn from_name(name: &str) -> Option<Method> {
213 Method::ALL.iter().copied().find(|m| m.name() == name)
214 }
215
216 /// The method's PRIMARY access [`Tier`].
217 ///
218 /// Note: a `PublicRead` method may still be peer-reachable — see
219 /// [`is_peer_reachable`](Method::is_peer_reachable). The tier is the
220 /// method's canonical home, the allowlist is the security boundary.
221 pub const fn tier(self) -> Tier {
222 match self {
223 Method::GetContent
224 | Method::GetCapsule
225 | Method::GetModule
226 | Method::GetManifest
227 | Method::GetMetadata
228 | Method::ListCapsules
229 | Method::GetProof
230 | Method::GetProofStatus
231 | Method::GetAnchoredRoot
232 | Method::GetCollection
233 | Method::ListCollectionItems
234 | Method::Health
235 | Method::Methods => Tier::PublicRead,
236
237 Method::GetNetworkInfo
238 | Method::GetPeers
239 | Method::Announce
240 | Method::GetAvailability
241 | Method::ListInventory
242 | Method::FetchRange
243 | Method::GetModuleInfo
244 | Method::FetchModuleRange => Tier::Peer,
245
246 Method::Stage
247 | Method::CacheGetConfig
248 | Method::CacheSetCapBytes
249 | Method::CacheClear
250 | Method::CacheListCached
251 | Method::CacheRemoveCached
252 | Method::CacheFetchAndCache
253 | Method::CacheStats
254 | Method::ControlPeerStatus
255 | Method::ControlSubscribe
256 | Method::ControlUnsubscribe
257 | Method::ControlListSubscriptions
258 | Method::ControlPeersConnect
259 | Method::ControlPeersDisconnect
260 | Method::ListRewardDistributors
261 | Method::GetRewardProverStatus
262 | Method::GetRewardDistributor
263 | Method::ListRewardDistributorCommitments
264 | Method::GetPayeeRewardClaimStatus
265 | Method::RpcDiscover => Tier::Control,
266 }
267 }
268
269 /// Whether this method is reachable over the mTLS **peer** surface.
270 ///
271 /// This mirrors the canonical node's `is_peer_reachable_method` byte-for-byte
272 /// (digstore `dig-node` `peer.rs`). It is an ALLOWLIST: adding a method here
273 /// is a deliberate security decision — it exposes the method to any remote
274 /// peer (the peer verifier authenticates a `peer_id`, it does NOT authorize).
275 /// Management/mutation methods (`cache.*`, `control.*`, `dig.stage`) are
276 /// NEVER peer-reachable.
277 pub const fn is_peer_reachable(self) -> bool {
278 matches!(
279 self,
280 Method::GetContent
281 | Method::GetNetworkInfo
282 | Method::GetPeers
283 | Method::Announce
284 | Method::GetAvailability
285 | Method::ListInventory
286 | Method::FetchRange
287 | Method::GetModuleInfo
288 | Method::FetchModuleRange
289 | Method::GetAnchoredRoot
290 | Method::GetCollection
291 | Method::ListCollectionItems
292 )
293 }
294
295 /// Whether this method answers on the **reward** surface — distributor,
296 /// prover-loop, or payee claim-side state (dig-rewards-coin SPEC §2.3, §2.6,
297 /// §7.4, §7.5, §12.5).
298 ///
299 /// Every method for which this is `true` MUST be `Tier::Control` and MUST
300 /// NOT be peer-reachable; the guard that enforces that drives itself from
301 /// here.
302 ///
303 /// # Why an exhaustive match and not a name filter
304 ///
305 /// The guard previously selected its subjects with
306 /// `name().contains("Reward")`, which is convention-bound: a reward method
307 /// named without that substring escapes the tier assertion silently, and
308 /// nothing tells the author of that method that it has. This match is
309 /// compiler-bound instead — a new variant fails to compile until someone
310 /// classifies it, and the answer they must give is the one the guard needs.
311 /// There is deliberately no `_` arm, for exactly that reason.
312 pub const fn is_reward(self) -> bool {
313 match self {
314 Method::ListRewardDistributors
315 | Method::GetRewardProverStatus
316 | Method::GetRewardDistributor
317 | Method::ListRewardDistributorCommitments
318 | Method::GetPayeeRewardClaimStatus => true,
319 Method::GetContent
320 | Method::GetCapsule
321 | Method::GetModule
322 | Method::GetManifest
323 | Method::GetMetadata
324 | Method::ListCapsules
325 | Method::GetProof
326 | Method::GetProofStatus
327 | Method::GetAnchoredRoot
328 | Method::GetCollection
329 | Method::ListCollectionItems
330 | Method::Health
331 | Method::Methods
332 | Method::GetNetworkInfo
333 | Method::GetPeers
334 | Method::Announce
335 | Method::GetAvailability
336 | Method::ListInventory
337 | Method::FetchRange
338 | Method::GetModuleInfo
339 | Method::FetchModuleRange
340 | Method::Stage
341 | Method::CacheGetConfig
342 | Method::CacheSetCapBytes
343 | Method::CacheClear
344 | Method::CacheListCached
345 | Method::CacheRemoveCached
346 | Method::CacheFetchAndCache
347 | Method::CacheStats
348 | Method::ControlPeerStatus
349 | Method::ControlSubscribe
350 | Method::ControlUnsubscribe
351 | Method::ControlListSubscriptions
352 | Method::ControlPeersConnect
353 | Method::ControlPeersDisconnect
354 | Method::RpcDiscover => false,
355 }
356 }
357
358 /// A one-line human summary (drives the OpenRPC method `summary`).
359 pub const fn summary(self) -> &'static str {
360 match self {
361 Method::GetContent => "Read a verified window of a resource's ciphertext.",
362 Method::GetCapsule => "Fetch the whole .dig module for (store, root).",
363 Method::GetModule => "Alias of dig.getCapsule.",
364 Method::GetManifest => "Fetch the public discovery manifest resource.",
365 Method::GetMetadata => "Fetch the plaintext metadata manifest.",
366 Method::ListCapsules => "List a store's confirmed capsules.",
367 Method::GetProof => "Get the real inclusion proof + execution-proof status.",
368 Method::GetProofStatus => "Poll a real execution-proof job by id.",
369 Method::GetAnchoredRoot => "Resolve a store's chain-anchored tip root.",
370 Method::GetCollection => "Get collection-level facts for NFT launcher ids.",
371 Method::ListCollectionItems => "List resolved NFT collection items (paginated).",
372 Method::Health => "Liveness and capability summary.",
373 Method::Methods => "List the method names this node implements.",
374 Method::GetNetworkInfo => "This node's peer-network posture.",
375 Method::GetPeers => "The peers this node currently knows.",
376 Method::Announce => "Accept a peer announcement (peer_id + addresses).",
377 Method::GetAvailability => "Batch-check whether this node holds items.",
378 Method::ListInventory => "Enumerate the stores / roots this node serves.",
379 Method::FetchRange => "Fetch a single verified range frame of a resource.",
380 Method::GetModuleInfo => {
381 "Get the whole-.dig-module transfer descriptor (size + hashes) for (store, root)."
382 }
383 Method::FetchModuleRange => "Fetch a single range frame of the whole .dig module blob.",
384 Method::Stage => "Compile a local folder into a capsule .dig in-process.",
385 Method::CacheGetConfig => "Get the local-cache configuration.",
386 Method::CacheSetCapBytes => "Set the local-cache size cap.",
387 Method::CacheClear => "Clear the local cache.",
388 Method::CacheListCached => "List the durable cached modules.",
389 Method::CacheRemoveCached => "Remove one cached capsule.",
390 Method::CacheFetchAndCache => "Fetch and cache one capsule.",
391 Method::CacheStats => {
392 "Cache telemetry: cap + usage, entry count + bytes, eviction + hit/miss counters."
393 }
394 Method::ControlPeerStatus => "Snapshot the node's peer network.",
395 Method::ControlSubscribe => {
396 "Subscribe the node to a store (persisted watch + gap-fill)."
397 }
398 Method::ControlUnsubscribe => "Unsubscribe the node from a store.",
399 Method::ControlListSubscriptions => "List the node's persisted store subscriptions.",
400 Method::ControlPeersConnect => "Dial a peer into the connected pool.",
401 Method::ControlPeersDisconnect => "Drop a pooled peer by peer_id.",
402 Method::ListRewardDistributors => {
403 "List the reward distributors this node funds or has a mirror claim to."
404 }
405 Method::GetRewardProverStatus => {
406 "Get the reward prover loop's status for one or every distributor."
407 }
408 Method::GetRewardDistributor => "Get one reward distributor's chain-derived state.",
409 Method::ListRewardDistributorCommitments => {
410 "List one reward distributor's per-epoch clawback commitment slots."
411 }
412 Method::GetPayeeRewardClaimStatus => {
413 "Report this node's own payee-side reward claim posture."
414 }
415 Method::RpcDiscover => "Return the OpenRPC self-describe document.",
416 }
417 }
418
419 /// The method names reachable over the peer surface, in catalogue order —
420 /// the exact allowlist a server hands its peer responder.
421 pub fn peer_reachable_names() -> Vec<&'static str> {
422 Method::ALL
423 .iter()
424 .copied()
425 .filter(|m| m.is_peer_reachable())
426 .map(Method::name)
427 .collect()
428 }
429}
430
431impl std::fmt::Display for Method {
432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433 f.write_str(self.name())
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use std::collections::HashSet;
441
442 /// **Proves:** every method name round-trips through `from_name`.
443 /// **Catches:** a name typo or a variant left out of `ALL`.
444 #[test]
445 fn names_round_trip() {
446 for m in Method::ALL {
447 assert_eq!(Method::from_name(m.name()), Some(*m), "{}", m.name());
448 }
449 assert!(Method::from_name("dig.nope").is_none());
450 }
451
452 /// **Proves:** all wire names are unique.
453 /// **Catches:** two variants accidentally sharing a name.
454 #[test]
455 fn names_unique() {
456 let names: HashSet<&str> = Method::ALL.iter().map(|m| m.name()).collect();
457 assert_eq!(names.len(), Method::ALL.len());
458 }
459
460 /// **Proves:** the peer allowlist is EXACTLY the twelve methods the canonical
461 /// node exposes over mTLS — and no `cache.*`/`control.*`/`dig.stage` leaks
462 /// onto the peer surface.
463 /// **Catches:** a drift from digstore `is_peer_reachable_method`, or a
464 /// management method accidentally allowlisted (the audit #179 auth-bypass).
465 #[test]
466 fn peer_allowlist_matches_canonical() {
467 let mut got = Method::peer_reachable_names();
468 got.sort_unstable();
469 let mut want = vec![
470 "dig.getContent",
471 "dig.getNetworkInfo",
472 "dig.getPeers",
473 "dig.announce",
474 "dig.getAvailability",
475 "dig.listInventory",
476 "dig.fetchRange",
477 "dig.getModuleInfo",
478 "dig.fetchModuleRange",
479 "dig.getAnchoredRoot",
480 "dig.getCollection",
481 "dig.listCollectionItems",
482 ];
483 want.sort_unstable();
484 assert_eq!(got, want);
485
486 // No management/mutation method is peer-reachable.
487 for m in [
488 Method::Stage,
489 Method::CacheGetConfig,
490 Method::CacheSetCapBytes,
491 Method::CacheClear,
492 Method::CacheListCached,
493 Method::CacheRemoveCached,
494 Method::CacheFetchAndCache,
495 Method::CacheStats,
496 Method::ControlPeerStatus,
497 Method::ControlSubscribe,
498 Method::ControlUnsubscribe,
499 Method::ControlListSubscriptions,
500 Method::ControlPeersConnect,
501 Method::ControlPeersDisconnect,
502 Method::RpcDiscover,
503 ] {
504 assert!(
505 !m.is_peer_reachable(),
506 "{} must NOT be peer-reachable",
507 m.name()
508 );
509 }
510 }
511
512 /// **Proves:** the chain-anchored public reads are PublicRead yet ALSO
513 /// peer-reachable (the design-brief tier decision §5).
514 #[test]
515 fn anchored_reads_public_but_peer_reachable() {
516 for m in [
517 Method::GetAnchoredRoot,
518 Method::GetCollection,
519 Method::ListCollectionItems,
520 ] {
521 assert_eq!(m.tier(), Tier::PublicRead);
522 assert!(m.is_peer_reachable());
523 }
524 }
525
526 /// **Proves:** the six live-node methods the crate previously lacked are
527 /// present, Control-tiered, and NEVER peer-reachable (the #1075 completeness
528 /// gap: the crate must catalogue every method the dig-node dispatch serves).
529 /// **Catches:** a variant dropped from the catalogue, mis-tiered, or leaked
530 /// onto the peer surface.
531 #[test]
532 fn completed_control_methods_present_and_gated() {
533 let expected = [
534 (Method::CacheStats, "cache.stats"),
535 (Method::ControlSubscribe, "control.subscribe"),
536 (Method::ControlUnsubscribe, "control.unsubscribe"),
537 (
538 Method::ControlListSubscriptions,
539 "control.listSubscriptions",
540 ),
541 (Method::ControlPeersConnect, "control.peers.connect"),
542 (Method::ControlPeersDisconnect, "control.peers.disconnect"),
543 ];
544 for (m, name) in expected {
545 assert_eq!(m.name(), name);
546 assert_eq!(Method::from_name(name), Some(m));
547 assert!(Method::ALL.contains(&m), "{name} missing from ALL");
548 assert_eq!(m.tier(), Tier::Control, "{name} must be Control");
549 assert!(!m.is_peer_reachable(), "{name} must NOT be peer-reachable");
550 }
551 }
552
553 /// **Proves:** the whole-module peer-pull methods (#1576) are present,
554 /// Peer-tiered, and peer-reachable — the wire surface a peer uses to pull a
555 /// complete `.dig` module and reshare it.
556 /// **Catches:** either method dropped from the catalogue, mis-tiered, or left
557 /// off the peer allowlist (which would make peer reshare unreachable).
558 #[test]
559 fn module_pull_methods_present_and_peer_reachable() {
560 for (m, name) in [
561 (Method::GetModuleInfo, "dig.getModuleInfo"),
562 (Method::FetchModuleRange, "dig.fetchModuleRange"),
563 ] {
564 assert_eq!(m.name(), name);
565 assert_eq!(Method::from_name(name), Some(m));
566 assert!(Method::ALL.contains(&m), "{name} missing from ALL");
567 assert_eq!(m.tier(), Tier::Peer, "{name} must be Peer");
568 assert!(m.is_peer_reachable(), "{name} must be peer-reachable");
569 }
570 }
571
572 /// **Proves:** `dig.getPayeeRewardClaimStatus` is in the catalogue under its
573 /// exact wire name (dig_ecosystem#3269). The tier / peer-reachability half of
574 /// its contract is proven by the catalogue-wide scan below, not restated here.
575 #[test]
576 fn payee_reward_claim_status_present_under_its_wire_name() {
577 assert_eq!(
578 Method::GetPayeeRewardClaimStatus.name(),
579 "dig.getPayeeRewardClaimStatus"
580 );
581 assert_eq!(
582 Method::from_name("dig.getPayeeRewardClaimStatus"),
583 Some(Method::GetPayeeRewardClaimStatus)
584 );
585 assert!(Method::ALL.contains(&Method::GetPayeeRewardClaimStatus));
586 }
587
588 /// **Proves:** EVERY reward method in the catalogue is Control-tiered, not
589 /// peer-reachable, and absent from the peer allowlist — dig-rewards-coin
590 /// SPEC §2.3/§2.6/§7.4/§7.5, §12.5.
591 ///
592 /// **Catches:** a mis-tier that would expose distributor / prover-loop /
593 /// claim-side internals to any remote peer, *including on a reward method
594 /// added after this test was written*. An earlier form named its four
595 /// members explicitly, so a fifth reward method passed it by simply not
596 /// being on the list (#3261: a test over an enumeration can only check the
597 /// enumeration it was given); the form after that filtered on
598 /// `name().contains("Reward")`, which a reward method named without that
599 /// substring escapes silently.
600 ///
601 /// [`Method::is_reward`] is an exhaustive match, so the selection is
602 /// compiler-bound: a new variant fails to compile until it is classified,
603 /// and classifying it as a reward method puts it under every assertion
604 /// below. There is deliberately no expected count here — a count is a second
605 /// thing to forget, and nothing tells it to move.
606 #[test]
607 fn every_reward_method_is_control_and_not_peer_reachable() {
608 let reward_methods: Vec<Method> = Method::ALL
609 .iter()
610 .copied()
611 .filter(|m| m.is_reward())
612 .collect();
613
614 // Non-vacuous: a catalogue that lost the reward methods, or an
615 // `is_reward` that stopped claiming any, must fail here rather than
616 // empty the set and pass silently.
617 assert!(
618 !reward_methods.is_empty(),
619 "no method reported is_reward() -- either the catalogue lost them or \
620 the classification was inverted"
621 );
622
623 let allowlist = Method::peer_reachable_names();
624 for m in reward_methods {
625 let name = m.name();
626 assert_eq!(
627 Method::from_name(name),
628 Some(m),
629 "{name} does not round-trip"
630 );
631 assert_eq!(m.tier(), Tier::Control, "{name} must be Control");
632 assert!(!m.is_peer_reachable(), "{name} must NOT be peer-reachable");
633 assert!(
634 !allowlist.contains(&name),
635 "{name} must not appear in the peer allowlist"
636 );
637 }
638 }
639
640 /// **Proves:** every method whose wire name says "Reward" is classified as
641 /// one.
642 /// **Catches:** the one wrong answer a machine can check. The exhaustive
643 /// match in [`Method::is_reward`] forces an author to answer for a new
644 /// variant, but it cannot tell a wrong answer from a right one; a reward
645 /// method classified `false` would drop straight out of the tier guard, and
646 /// its own name is the evidence against it.
647 #[test]
648 fn reward_named_methods_are_classified_as_reward() {
649 for m in Method::ALL {
650 if m.name().contains("Reward") {
651 assert!(
652 m.is_reward(),
653 "{} is named a reward method but is_reward() says otherwise",
654 m.name()
655 );
656 }
657 }
658 }
659
660 /// **Proves:** every Control method is NOT peer-reachable (the boundary
661 /// invariant).
662 #[test]
663 fn control_tier_never_peer_reachable() {
664 for m in Method::ALL {
665 if m.tier() == Tier::Control {
666 assert!(
667 !m.is_peer_reachable(),
668 "{} is Control but peer-reachable",
669 m.name()
670 );
671 }
672 }
673 }
674
675 /// **Documents:** every [`Method`] variant is named by this exhaustive
676 /// match, same as the test-module's other classifiers. This is no longer
677 /// the completeness guard for `Method::ALL` — `method_catalogue!`
678 /// generates `ALL` directly from the variant table, so a variant missing
679 /// from `ALL` is unwritable, not merely red (dig_ecosystem#3317). This
680 /// match is kept because it is cheap and documents that the test module
681 /// sees the same catalogue as the rest of the crate; deliberately no `_`
682 /// arm, so adding a variant is still a compile error here until an author
683 /// names it.
684 #[test]
685 fn every_variant_is_named_by_an_exhaustive_match() {
686 // The identity mapping is the point: `needless_match` would have us
687 // collapse this into `m`, which erases the exhaustiveness check this
688 // test exists to force. Keep every arm explicit.
689 #[allow(clippy::needless_match)]
690 fn name_it(m: Method) -> Method {
691 match m {
692 Method::GetContent => Method::GetContent,
693 Method::GetCapsule => Method::GetCapsule,
694 Method::GetModule => Method::GetModule,
695 Method::GetManifest => Method::GetManifest,
696 Method::GetMetadata => Method::GetMetadata,
697 Method::ListCapsules => Method::ListCapsules,
698 Method::GetProof => Method::GetProof,
699 Method::GetProofStatus => Method::GetProofStatus,
700 Method::GetAnchoredRoot => Method::GetAnchoredRoot,
701 Method::GetCollection => Method::GetCollection,
702 Method::ListCollectionItems => Method::ListCollectionItems,
703 Method::Health => Method::Health,
704 Method::Methods => Method::Methods,
705 Method::GetNetworkInfo => Method::GetNetworkInfo,
706 Method::GetPeers => Method::GetPeers,
707 Method::Announce => Method::Announce,
708 Method::GetAvailability => Method::GetAvailability,
709 Method::ListInventory => Method::ListInventory,
710 Method::FetchRange => Method::FetchRange,
711 Method::GetModuleInfo => Method::GetModuleInfo,
712 Method::FetchModuleRange => Method::FetchModuleRange,
713 Method::Stage => Method::Stage,
714 Method::CacheGetConfig => Method::CacheGetConfig,
715 Method::CacheSetCapBytes => Method::CacheSetCapBytes,
716 Method::CacheClear => Method::CacheClear,
717 Method::CacheListCached => Method::CacheListCached,
718 Method::CacheRemoveCached => Method::CacheRemoveCached,
719 Method::CacheFetchAndCache => Method::CacheFetchAndCache,
720 Method::CacheStats => Method::CacheStats,
721 Method::ControlPeerStatus => Method::ControlPeerStatus,
722 Method::ControlSubscribe => Method::ControlSubscribe,
723 Method::ControlUnsubscribe => Method::ControlUnsubscribe,
724 Method::ControlListSubscriptions => Method::ControlListSubscriptions,
725 Method::ControlPeersConnect => Method::ControlPeersConnect,
726 Method::ControlPeersDisconnect => Method::ControlPeersDisconnect,
727 Method::ListRewardDistributors => Method::ListRewardDistributors,
728 Method::GetRewardProverStatus => Method::GetRewardProverStatus,
729 Method::GetRewardDistributor => Method::GetRewardDistributor,
730 Method::ListRewardDistributorCommitments => {
731 Method::ListRewardDistributorCommitments
732 }
733 Method::GetPayeeRewardClaimStatus => Method::GetPayeeRewardClaimStatus,
734 Method::RpcDiscover => Method::RpcDiscover,
735 }
736 }
737
738 for m in Method::ALL {
739 assert_eq!(name_it(*m), *m);
740 }
741 }
742
743 /// The number of methods `method_catalogue!` should have generated into
744 /// `Method::ALL`. Bump this when you add a method -- the macro already put
745 /// it in `ALL`; this is the independent oracle that would catch a broken
746 /// macro body (an entry silently dropped from the table), since
747 /// completeness of `ALL` itself is now structural, not this test's job.
748 const CATALOGUE_LEN: usize = 41;
749
750 /// **Proves:** `Method::ALL` has exactly [`CATALOGUE_LEN`] entries.
751 /// **Catches:** a broken `method_catalogue!` body that silently drops an
752 /// entry from the generated `ALL` (the compiler can't catch that -- the
753 /// macro would still expand to a valid, just shorter, array). This is
754 /// NOT the completeness guard for #3317: `Method::ALL` can no longer omit
755 /// a variant that exists in the enum, because `method_catalogue!`
756 /// generates both from the same table -- there is no second list left to
757 /// forget. This test only defends against the table itself losing an
758 /// entry, and forces a visible, reviewable bump on every addition.
759 #[test]
760 fn all_len_is_the_catalogue_count() {
761 assert_eq!(
762 Method::ALL.len(),
763 CATALOGUE_LEN,
764 "Method::ALL has {} entries, expected {CATALOGUE_LEN} -- update CATALOGUE_LEN if a \
765 method was deliberately added or removed, otherwise the macro table lost an entry",
766 Method::ALL.len()
767 );
768
769 // No duplicates in ALL (unchanged from the prior form of this guard).
770 let all_set: HashSet<Method> = Method::ALL.iter().copied().collect();
771 assert_eq!(
772 all_set.len(),
773 Method::ALL.len(),
774 "Method::ALL contains a duplicate variant"
775 );
776 }
777}