fix(rewards): reward-distributor reads are OPEN on POST /, doc + pin (dig_ecosystem#3351) - #619
Conversation
…t token-gated dig.getRewardDistributor / dig.listRewardDistributorCommitments are Tier::Control in dig-rpc-protocol's sense (loopback/in-process dispatch only, never over the mTLS peer surface) -- that does NOT mean token-gated. Both disclose only public on-chain state keyed by the caller's launcher_id, the same class control::is_open_control_read already treats as open (#1851). The prior comment read as operator/token-gated, which the enforced behaviour never was. Refs DIG-Network/dig_ecosystem#3351 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…token Adds reward_distributor_reads_answer_on_post_slash_without_a_token: POSTs dig.getRewardDistributor and dig.listRewardDistributorCommitments with no X-Dig-Control-Token / _control_token and asserts the request passed every ingress gate (not -32030 / UNAUTHORIZED) and reached the reward handler itself (REWARD_CHAIN_UNAVAILABLE on this ephemeral node's unwired chain port), not a passthrough relay or a method-not-found stub. Refs DIG-Network/dig_ecosystem#3351 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…(dig_ecosystem#3351) The ws_dispatch fall-through routes an unrecognized method to WalletBackend::dispatch, whose match has no dig.* arm. Pins both dig.getRewardDistributor and dig.listRewardDistributorCommitments as unroutable over /ws so a future wallet-backend or ws_dispatch change cannot silently open a second, WS-reachable path to the reward-chain reads without the tier decision being revisited. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mutation proof for the two guards this PR pins (head
|
…ecosystem#3351) `ws_err` renders `error.code` as `ErrorCode::code()` (-32030), not the string name, so comparing only against "UNAUTHORIZED" could never fire for a gate added through `ws_err`. Reject both spellings. Refs DIG-Network/dig_ecosystem#3351 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Verdict: PASS
Reviewed at head 5efd8a9c1a5e4c60a659f71744498b2cc4db10df (confirmed current via gh pr view --json headRefOid; matches the brief's SHA).
Q1 — Newly-false neighbour claims
Checked every sentence in crates/dig-node-core/src/seams/dig_rpc/dispatch.rs L995-1062, kept and new:
control::is_open_control_readexists (crates/dig-node-service/src/control.rs:149) and its doc explicitly documentscontrol.wallet.balanceas an open read gated by the same predicate. The new comment uses it as an analogy ("for the same reason ... is open"), not a claim that reward reads are literally added to that function's match arm (they aren't — different namespace, different dispatch path viaRpcDispatch::dispatch, notcontrol::dispatch_control). Accurate as written.reward_distributor_reads_answer_on_post_slash_without_a_tokenexists verbatim incrates/dig-node-service/tests/server.rs:3931. Name matches exactly.- "same guard shape as
GetRewardDistributorabove" forListRewardDistributorCommitments— both handlers use the identicalreward_chain_port_absent_responsefallback and sameTier::Control/open-read framing; still true. - No sentence found that claims behavior the code doesn't have.
Q2 — Test honesty
- Verified
ErrorCode::Unauthorized.code() == -32030and.name() == "UNAUTHORIZED"(dig-rpc-protocol meta, asserted at its own test site, line ~1571-1572). - Verified
REWARD_CHAIN_UNAVAILABLEis emitted only fromreward_chain_port_absent_response(dispatch.rs:116-121), called from both handlers'let Some(port) = … elsearms (dispatch.rs:1020, 1063) — reaching it does prove the reward handler ran past dispatch/param-parsing, not a passthrough or method-not-found stub. - WS test: confirmed
ws_dispatch(server.rs:1549) has nodig.*arm —control::is_control_methodonly matchescontrol.*,is_gated_chat_methodonlychat.send/chat.poll,wallet_authz::requires_authorizationonly matches retired (wallet./auth.prefixes), gated mutations, or master-tier aliases — none matchdig.getRewardDistributor/dig.listRewardDistributorCommitments. Falls through tostate.wallet.dispatch()→WalletBackend::dispatch_inner(dig-wallet/sage/rpc.rs), whose match has nodig.*arm and returnsError::not_foundon theother =>branch, surfaced as{"ok":false,"error":{"code":<numeric HTTP status>,...}}— never the string"UNAUTHORIZED"nor-32030. The test's dual-spelling check is not vacuous: mutation proof (PR comment 5724073551) shows the author caught and fixed a real gap (the original WS test only checked the string form; aws_err(.., ErrorCode::Unauthorized, ..)-shaped regression would have passed it) — commit5efd8a9cis exactly that fix. start_node_wallet(tests/server.rs:274) andnext_ws_json(tests/server.rs:3649) exist and are used as described; the test drains one initial sync-status frame before sending, consistent with existing WS test patterns elsewhere in the file.- POST / test:
-32030confirmed to beErrorCode::Unauthorized's wire code; the mutation proof (M1: cache-trio gate condition mutated to also catchdig.getRewardDistributor) reproduces the exact assertion failure attests/server.rs:3944, and M2 (earlyMethodNotFoundstub) fails at line 3954 — both proofs are RED on the named lines with the fix reverted, satisfying the test-vacuity property for "would this test catch a silent re-gate" and "would this test catch a stub/relay answering instead of the real handler."
Q3 — rustdoc / doc lint
No new [...]-bracket intra-doc links added — all references in the new/edited comments use plain backticks (`control::is_open_control_read`, `reward_distributor_reads_answer_on_post_slash_without_a_token`, etc.), which rustdoc does not attempt to resolve as links. No cargo doc -D warnings regression risk. Clippy and Rustfmt CI checks both pass on this head.
Q4 — Commit messages
All 5 commits checked (git log origin/develop..5efd8a9c): types are docs, test (x3), style — all in the allowed set. Header lengths: 82, 74, 47, 89, 84 chars — all ≤ 100. Lint commit messages CI check passes.
Q5 — No server.rs code-path change
git diff origin/develop..5efd8a9c -- crates/dig-node-service/src/server.rs is empty. Confirmed zero production code-path change; the diff is doc-only in dispatch.rs plus two new tests in tests/server.rs.
Not run
Did not run the test suite myself (read-only worktree per brief; relied on the CI Test + coverage run cited in the mutation-proof comment, run 35278713056, 2534-2535/3435, plus the current run's other green checks). Did not build.
No blocking findings. No inline threads opened — zero to drive to zero.
loop-decider — adversarial third leg: CHANGES-REQUIRED (option 2 stands; two doc-line fixes before merge)Head judged: Decision: Option 2 (OPEN read, never peer-reachable, not token-gated) is the correct call and is consistent with dig-node's own normative SPEC. Two lines of the PR's doc over-claim / leave the file self-contradictory; fix them, then merge. Option 1 is rejected. Q1 — Is "Tier::Control ≠ token-gated" defensible? YES, and it is the SPEC's own rule, not a convenience.
Q2 — Does the correction falsify a neighbour? YES, mildly: fix with one line.
Q3 — Is the caller measurement sound? Sound enough; one gap noted, needs no anonymity change.
Q4 — A third mutation both tests miss? YES — and the doc claims it.Mutation M3: gate the two methods only when the requestor is non-loopback (a Q5 — Irreversibility: gate now, open later? Argued both ways; NO.
Required before merge (both in
|
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
loop-security verdict: PASS
Head SHA judged: 5efd8a9c1a5e4c60a659f71744498b2cc4db10df (matches gh pr view --json headRefOid).
Scope confirmed: diff touches only (1) doc comments in crates/dig-node-core/src/seams/dig_rpc/dispatch.rs around the GetRewardDistributor / ListRewardDistributorCommitments arms, and (2) two new tests in crates/dig-node-service/tests/server.rs. No handler logic, no gate, no dispatch-table entry changed. The "zero production code-path change" claim holds.
Q1 — Is OPEN the right tier for exactly these two?
Yes for both. Read GetRewardDistributorResult / ListRewardDistributorCommitmentsResult / RewardDistributorCommitment in dig-rpc-protocol-0.12.0/src/types.rs (L1752-1889): every field (launcher_id, store_id, root, epoch_seconds, first_epoch_start, payout_threshold, fee_bps, withdrawal_share_bps, reserve_base_units, entry_count, current_distributor_epoch, last_entry_write_at, entry_set_stale, and the commitment rows' clawback_puzzle_hash/rewards_base_units/recoverable_base_units) is chain-derived. entry_set_stale's own doc comment says it explicitly: "computed at read time from the singleton's own on-chain spend history — it is not self-reported." last_entry_write_at is likewise "the most recent entry-set write on chain." observed_at is only the local wall-clock time the response was assembled, not sensitive.
Cross-checked against the actual handler (dispatch.rs, Method::GetRewardDistributor / Method::ListRewardDistributorCommitments arms): every wire field is copied 1:1 from report: DistributorReport returned by node.reward_chain_port().distributor_report(launcher_id) — no local registry, no config, no operator identity feeds either result. Nothing here originates from this node's own state. The OPEN decision is correct for exactly these two methods, and the sibling ticket #3352's three node-local reads (prover status, funded registry, payee claim status) are rightly left CONTROL/gated — GetRewardProverStatusResult and GetPayeeRewardClaimStatus are self-reported/local, visibly different in kind from these two.
Q2 — Resource cost as a DoS primitive
No per-call bound found on this path. crate::rate_limit::RequestorId is threaded through RpcDispatch::dispatch's signature but the GetRewardDistributor/ListRewardDistributorCommitments arms never call try_acquire or consult it — unlike the miss→DHT-lookup path (rate_limit.rs's own doc header scopes that limiter explicitly to dig.getContent/fetchRange/getAvailability, nothing reward-related). distributor_report is a real chain-backed read (currently Unavailable pending #3310's production adapter over dig-rewards-coin); once wired, an anonymous caller can drive one chain read per POST / call with no bucket in front of it. This is pre-existing — the code path is unchanged by this PR (comment/test-only diff) — so it does not block this PR, but it is a live gap: recommend a ticket ("bound anonymous callers on dig.getRewardDistributor/dig.listRewardDistributorCommitments the same way the miss-lookup path is bounded") rather than folding it into #3352, since #3352's three methods are a different set with different local-state costs.
Q3 — Does the new test create a false sense of a guard?
No, both new tests are load-bearing, not decorative:
reward_distributor_reads_answer_on_post_slash_without_a_token: asserts the response is neither-32030nordata.code == "UNAUTHORIZED", AND asserts it reachesREWARD_CHAIN_UNAVAILABLE(proving the reward handler itself ran, not a passthrough/method-not-found stub). This can't pass under a hidden gate — a gate at the ingress would short-circuit beforeREWARD_CHAIN_UNAVAILABLEis producible.reward_distributor_reads_are_not_routable_over_ws: tracedws_dispatch(server.rsL1549+) —dig.getRewardDistributor/dig.listRewardDistributorCommitmentsare notcontrol.*, not chat, not wallet-authz-gated, so they fall through tostate.wallet.dispatch(method, body)(dig-wallet/src/sage/rpc.rs'sdispatch_inner), whose default arm returnsError::not_found→ HTTP-shaped status (404-class), not 401. The test's numeric-vs-string dual check (-32030or"UNAUTHORIZED") is real: a future gate wired through eitherws_err's numericErrorCode::code()orws_from_jsonrpc's string path would trip it. Could not construct a scenario where a future WS-gated-and-routable change would pass both tests simultaneously — the openness test is HTTP-scoped and the WS test assertsok:falsespecifically, so a WS path that became both routable and gated would flipokin a way the malformed-frame case doesn't (a malformed frame errors before method dispatch, different code path/message shape, not conflated here).
Q4 — Doc/enforcement agreement
Checked every remaining "loopback admin" hit at head SHA (git grep): all other dispatch.rs hits (L830, 850, 871, 910) are control.*/cache methods, and L1176 is dig.getPayeeRewardClaimStatus (#3352's, correctly still CONTROL/loopback-only, untouched here as the brief expects). Neither ecosystem SPEC.md nor crates/dig-node-core/SPEC.md mentions RewardDistributor at all (checked via grep -n RewardDistributor), so there is no stale SPEC clause asserting admin-only for these two methods to contradict the new doc. No remaining sentence anywhere in scope still claims admin/loopback-only for GetRewardDistributor/ListRewardDistributorCommitments specifically.
Q5 — DIG_NODE_ALLOW_REMOTE=1 framing
Accurate. dig-node-service/src/config.rs (L1539+, #1662) refuses a non-loopback DIG_NODE_HOST unless DIG_NODE_ALLOW_REMOTE=1; control.rs L133/L2740 and server.rs L904/L953/L965 independently describe that escape hatch as making "this open read network-reachable and unauthenticated." The new comment's claim — that on such a bind, anonymous remote callers are included among those who can reach POST / — matches existing, unrelated code documented the same way elsewhere in the repo. Not a new claim invented for this PR.
Pre-existing issue to file separately (not this PR's scope)
Ticket content: "dig.getRewardDistributor / dig.listRewardDistributorCommitments have no per-requestor rate limit once #3310 wires the production chain-read adapter — an anonymous caller can drive one chain read per POST / call unbounded. Extend the existing miss-lookup TokenBucket (dig-node-core/src/rate_limit.rs) to cover these two methods (or a dedicated bucket), keyed on the same non-spoofable RequestorId already threaded through RpcDispatch::dispatch." This is defense-in-depth against a cost-amplification vector, not a live break introduced by this PR — does not gate.
KG: NONE (gate)
…prover-status tier at #3352 The getRewardDistributor comment asserted that anonymous REMOTE callers are answered on a DIG_NODE_ALLOW_REMOTE=1 bind -- a path no test reaches (both pins post to 127.0.0.1), so an origin-scoped gate could falsify it while every test stayed green. State the tested fact and its SPEC §7.2 reason instead. The neighbouring getRewardProverStatus comment keeps its loopback-admin wording and now names dig_ecosystem#3352 as the ticket that decides that node-local read's token tier, so the two blocks stop reading as a contradiction. Refs DIG-Network/dig_ecosystem#3351 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
loop-decider RE-GATE —
|
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
loop-security delta re-gate — PASS
Head audited: 2791bf43f0d3be862c5a943162a4748f4968701c
Scope: git diff 5efd8a9c..2791bf43 — one file, crates/dig-node-core/src/seams/dig_rpc/dispatch.rs, +5/-4, comment-only.
-
Byte-identical security surface confirmed.
git diff 5efd8a9c..2791bf43 | grep '^[-+]' | grep -v '^[-+][-+]' | grep -v '^[-+]\s*//'returns empty — every changed line is a//comment. No code, no test, no config changed since the PASSed5efd8a9c. -
Doc-honesty call on dropping the
DIG_NODE_ALLOW_REMOTE=1clause: honest, not a hide. The old wording ("anonymous remote callers included on a node bound withDIG_NODE_ALLOW_REMOTE=1") named one specific transport condition under which the open read is reachable remotely, which reads as scoping the exposure to that bind mode — implying a default/loopback-only bind is safe from it. That implication was the actual risk: the code makesdig.getRewardDistributoropen (requires_auth: false) unconditionally for anyPOST /caller, regardless of bind mode;DIG_NODE_ALLOW_REMOTEcontrols whether the socket accepts remote connections at all, not whether this specific method is gated once it does. The new wording — "answered to any caller that reachesPOST /with no token ... the disclosure rule SPEC §7.2 applies to every non-control.*method" — states the true, unconditional scope and points to the actual authorization rule rather than a single environment variable that could be read as a boundary. This is the more honest formulation: it removes a clause that could mislead an operator into thinking the exposure is conditional, without removing any information about who can reach the read (the reachability is still fully governed by ordinary network exposure ofPOST /, which the comment still calls out). -
SPEC §7.2 cross-reference is accurate.
SPEC.mdline 1080 (as of2791bf43) reads verbatim: "Every non-control.*method MUST haverequires_auth: false; everycontrol.*method MUST haveserved: "control"andrequires_auth: true." This is the general disclosure rule the comment cites, anddig.getRewardDistributoris a non-control.*method, so the citation is correct. §7.2 itself ("Authorization model — loopback + local capability token", line 1461) is the section that defines the control-token gating this open read is explicitly exempted from. The #3352 cross-ref added to thedig.getRewardProverStatuscomment is a forward pointer to a separate open decision (token tier of a CONTROL-plane, not open, method) and makes no claim this diff needs to substantiate.
No non-comment lines, no behavior change, no new exposure. Comment wording is more precise than before and matches SPEC.md.
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
PASS -- delta re-gate at 2791bf43f0d3be862c5a943162a4748f4968701c
Scope confirmed: git diff 5efd8a9c..2791bf43 --stat is exactly one file
(crates/dig-node-core/src/seams/dig_rpc/dispatch.rs, +5/-4), and every changed line is a //
comment line -- no code, no test, no config changed.
- Comment accuracy vs SPEC/code:
dig.getRewardDistributorcomment now reads "the disclosure rule SPEC SS7.2 applies to every
non-control.*method" (dropping theDIG_NODE_ALLOW_REMOTE=1clause). SPEC SS7.2
("Authorization model -- loopback + local capability token") states "Exactly thecontrol.
method prefix is gated (is_control_method)" -- i.e. every non-control.*method is
unauthenticated by construction. That is a correct paraphrase of SS7.2's rule (SS5.5 line 1080
restates the same rule for the OpenRPC catalogue: "Every non-control.*method MUST have
requires_auth: false" -- consistent, not contradictory).control::is_open_control_readstill exists,dig-node-service/src/control.rs:149, unchanged.dig.getRewardProverStatuscomment gains "(the token tier of this NODE-LOCAL read is
dig_ecosystem#3352's decision, not #3351's)" -- verified againstgh issue view 3352: OPEN,
and its body is exactly about the three NODE-LOCAL reward reads (getRewardProverStatus
included) answering anonymously, explicitly distinguishing itself from #3351's public-state
reads. Accurate.
- Encoding: em dashes in both changed comment blocks render as U+2014 (
--glyph), confirmed
via raw byte inspection -- no cp1252/mangled-escape damage. - Commit header:
docs(rewards): claim only the transport the pins exercise; point the prover-status tier at #3352-- 96 chars (<=100), typedocsis in the allowed set. - Blast radius:
git diff 5efd8a9c..2791bf43 --statis exactly the one file; the
code-line-only diff (excluding//lines) is empty -- nothing outside comments changed.
Zero threads opened -- delta is comment-only and every claim checks out against current SPEC/code/issue
state.
Head: 2791bf43f0d3be862c5a943162a4748f4968701c
…sure rule" it does not name The previous wording attributed a "disclosure rule" for every non-control.* method to SPEC §7.2. §7.2 states two things: the `control.` prefix is the only token gate (`is_control_method`; §5.5 `requires_auth: false` for the rest), and WHO-NAMES-THE-SUBJECT is the membership test for the open `control.wallet.*` reads. Say exactly that, and say why this read passes the subject test: the launcher_id arrives in the request, so the answer discloses no node-local association. Refs DIG-Network/dig_ecosystem#3351 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
loop-decider RE-GATE —
|
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
PASS — delta re-gate on 262551c68821ae1729285a149d284c6ea4fdaf0f.
Scope: crates/dig-node-core/src/seams/dig_rpc/dispatch.rs, comment-only, +6/-3, per git diff 2791bf43..262551c6.
Check 1 — SPEC attribution (read from git show 262551c6:SPEC.md, not memory):
- (a) "only the
control.prefix is token-gated (SPEC §7.2is_control_method)" — matches SPEC.md:1472, "Exactly thecontrol.method prefix is gated (is_control_method)". - (b) "§5.5
requires_auth: falsefor every non-control.*method" — matches SPEC.md:1080 under the confirmed### 5.5. Method catalogueheading (SPEC.md:1039), verbatim: "Every non-control.*method MUST haverequires_auth: false". - (c) "§7.2's WHO-NAMES-THE-SUBJECT test … the subject arrives in the request, so the answer discloses no node-local association" — matches SPEC.md:1480, "WHO NAMES THE SUBJECT: an open read's subject arrives in the request as a public address or coin id, so the answer discloses no node-to-address association" (paraphrase preserves the claim, not a fabrication). Cross-checked against
crates/dig-node-service/src/control.rsdoc comment onis_open_control_read(~L149), which states the same test forcontrol.wallet.balanceand named siblings.
The prior gates' defect — attributing a "disclosure rule for every non-control.* method" to §7.2, which names no such rule — is gone; the new wording cites §7.2 only for the prefix-gating and WHO-NAMES-THE-SUBJECT claims it actually makes, and cites §5.5 (not §7.2) for the requires_auth: false claim.
Check 2 — comment-only diff: git diff 2791bf43..262551c6 | grep '^[-+]' | grep -v '^[-+][-+]' | grep -v '^[-+]\s*//' returns empty (exit 1, no match). Em dashes elsewhere in the file intact (135 occurrences of U+2014 in the post-commit file).
Check 3 — commit: header docs(rewards): cite the rule SPEC §7.2 actually states, not a "disclosure rule" it does not name is 97 chars (≤100), type docs is valid conventional-commit type, scope rewards reasonable for the touched code.
No code semantics changed; nothing else in scope for this delta.
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
loop-security RE-GATE — PASS
head audited: 262551c68821ae1729285a149d284c6ea4fdaf0f · delta: 2791bf43..262551c6 (1 file, +6/-3, crates/dig-node-core/src/seams/dig_rpc/dispatch.rs, comment lines only)
1. Byte-identical security surface. git diff 2791bf43..262551c6 -- crates/dig-node-core/src/seams/dig_rpc/dispatch.rs | grep '^[-+]' | grep -v '^[-+][-+]' | grep -vE '^[-+]\s*//' is empty — every changed line is a // comment line. Confirmed.
2. The disclosure claim is TRUE for both methods at this head. dig.getRewardDistributor and dig.listRewardDistributorCommitments (dispatch.rs L1012/L1057 handlers) both go through port.distributor_report(launcher_id) → RealRewardsChainPort::distributor_report → build_report (crates/dig-node-service/src/rewards/chain_port.rs L100-178). That path consults only ChainSource reads keyed on the caller-supplied launcher_id: read_distributor_guarded, read_launch_comment, read_launch_constants — all chain-derived. No node-local registry, config, funder-ownership table, or prover state is touched. Notably funded_distributors (the one method shaped like a node-local registry lookup) is hard-coded Err(ChainPortError::Unavailable) in this same impl (L79-84) — the registry the sentence would need to worry about doesn't exist yet. So "the subject arrives in the request, so the answer discloses no node-local association" holds.
3. §7.2 does state WHO-NAMES-THE-SUBJECT, and applying it here is the fair analogy, not a category error. SPEC.md §7.2 (L1478-1483, git show 262551c6:SPEC.md) defines the test as the membership rule for the is_open_control_read exceptions among control.* methods (control.wallet.balance etc.). dig.getRewardDistributor/ListRewardDistributorCommitments are non-control.* methods, already open by construction (§7.2 "Exactly the control. prefix is gated"; §5.5 requires_auth: false) — the new wording correctly does NOT claim §7.2 gates or exempts them. It uses the test only as an independent disclosure-safety argument, by analogy, for why being open is safe, not as the reason it is open. This is exactly the wording the adversarial gate (PR comment 5724296746) prescribed verbatim after flagging the prior sentence's mis-attribution ("the disclosure rule SPEC §7.2 applies to every non-control.* method" — false, refuted by GetRewardProverStatus sitting node-local and ungated, #3352). The new commit's added lines match that prescribed replacement byte-for-byte.
No live finding. Scope audited: crates/dig-node-core/src/seams/dig_rpc/dispatch.rs diff 2791bf43..262551c6, crates/dig-node-service/src/rewards/chain_port.rs L1-240, SPEC.md §7.2 at 262551c6. Not re-covered: everything already PASSed at 5efd8a9c and 2791bf43 (no code changed since).
Refs DIG-Network/dig_ecosystem#3351
Decision (option 2 of the ticket)
Tier::Controlin dig-rpc-protocol's sense means "loopback / in-processdispatch only, never reachable over the mTLS peer surface" -- it does NOT
mean token-gated.
rpc.discoverandcache.getConfigare alsoTier::Controland are open to any caller ofPOST /. Token gating inthis repo is a separate axis:
control.*(minuscontrol::is_open_control_read),wallet_authz::requires_authorization,the cache trio (
server.rs:1262), andchat.send/poll.dig.getRewardDistributoranddig.listRewardDistributorCommitmentsdisclose only public on-chain state keyed by a caller-supplied
launcher_id-- the same classcontrol::is_open_control_readalreadytreats as open (#1851, "reads of PUBLIC chain state ... are exposed like
the other reads rather than behind the control-token gate"). No live RPC
caller depends on either being gated: dig-app's only
RewardsClientimplis a test fake, and the peer claim loop uses the in-process
ClaimChainPort, never RPC. Contract + precedent -> the enforced tier isOPEN; the doc that read "CONTROL plane: loopback admin / in-process FFI
ONLY" was the thing that was wrong, since
POST /answers anonymousremote callers on a node bound with
DIG_NODE_ALLOW_REMOTE=1.Changes
dispatch.rs: corrected the comment on both handlers to state theenforced tier accurately, pointing at the new pinning test.
port.rs: no tier claim found there at this tip (grep below) -- noedit made, reported rather than invented.
tests/server.rs: new integration testreward_distributor_reads_answer_on_post_slash_without_a_token--POSTs both methods with no control token and asserts the request
passed every ingress gate (not
-32030/UNAUTHORIZED) and reached thereward handler (
REWARD_CHAIN_UNAVAILABLE, not a passthrough ormethod-not-found stub).
module in
server.rsnever constructs anAppStateandws_dispatchis a private fn unreachable from
tests/server.rs; per the brief, nonew fixture was built for this.
crates/dig-node-core/tests/reward_methods_tier_guard.rsis unchanged --it polices the peer-surface half (
Tier::Control+ not peer-reachable),which this PR does not touch.
Zero behaviour change
No edits to any code path in
server.rsordispatch.rsoutsidecomments; the new test is additive.
🤖 Generated with Claude Code