chore: open lane for #3269 (reward RPC methods, unit 3) - #609
chore: open lane for #3269 (reward RPC methods, unit 3)#609MichaelTaylor3d wants to merge 14 commits into
Conversation
Adds the reward-distributor chain reader (read_distributor, DistributorSnapshot, ChainObservation) as a dependency, unpinned beyond the 0.4 line dig-rewards-coin ships it on. Resolves alongside chia-sdk-driver 0.36.0 / chia-sdk-types 0.36.0 / chia-puzzle-types 0.36.1, matching the cohort_lock_guard pins dig-rewards-coin itself asserts. No consumer yet -- see the accompanying finding on the PR: dig-node-core has no reachable dig_chainsource_interface::ChainSource implementation, so none of #3269's four RPC methods can be wired to a live chain read from this crate without a further architecture decision. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…orCommitments Both at Tier::Control, behind dig-node-core's own RewardsChainPort. The adapter that talks to chain (CorroboratedChainSource + dig_rewards_coin::read_distributor) is built in dig-node-service and injected downward, so no dependency cycle is created and dig_chainsource_interface::ChainSource never enters this crate. - port: one additive `distributor_report` method feeding both handlers. DistributorChainState is deliberately NOT widened -- the prover cycle engine consumes it. UnavailableChainPort returns the chain-unavailable error, never an empty report: a failed read must not render as "no entries". - dispatch: both arms route through the Method::from_name enum match, never the string pre-match, so the tier guard cannot be bypassed. - deps: dig-rewards-coin removed from dig-node-core; it belongs to the service crate that builds the adapter. No chia-* pin touched. - #3261: the guard predicate is duplicated in peer.rs and in tests/reward_methods_tier_guard.rs; both widened, with a count assertion so the filter cannot silently start matching fewer methods than exist. Money handling for the recoverable figure: withdraw_committed_incentives is never called -- it returns #3286's wrapped u64 uncorrected (#3303). The figure is computed with dig_rewards_coin::recoverable_base_units, whose u128 intermediate is correct exactly where the driver wraps. withdrawal_share_bps is echoed from chain-derived state, never recomputed (SPEC 2.6 clause 2), and narrowed with u16::try_from rather than `as` -- a silent cast wraps 65_536 to 0. When the figure cannot be honestly computed the WHOLE CALL refuses. It is never a zero and never an omitted field left to default. bps is launch-curried per distributor, so a refusal cannot blind one row while sparing another. listRewardDistributors and getPayeeRewardClaimStatus remain -32601 with no stub: the first would have to emit `claimable: []`, indistinguishable from "no claims", until #3268's mirror-admission registry exists; the second has no Method variant in dig-rpc-protocol v0.11.0. Checkpoint push by the orchestrator; CI is the compile. Refs #3269 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cargo fmt --all` touched only `crates/dig-node-core/src/lib.rs` -- the eight diffs CI reported in the pushed test block. No other file in the workspace wanted reformatting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e age `get_reward_distributor_threads_entry_set_stale_both_ways` installed a chain port twice on one `Node`, so the second `install_reward_chain_port` correctly returned false and its `assert!` fired. `install_reward_chain_port` is single-shot on purpose and stays that way -- loosening it so a test could pass would let a real double-install through in production. Instead the fake port, which is already keyed per launcher id, now serves both cases from ONE install on ONE node, answered by the requested launcher id. Two reports straddle the staleness bound: one whose last entry write is exactly `STALE_ENTRY_SET_SECONDS` old (stale), one a second inside it (fresh). The test asserts the fixtures really do straddle the bound before it asserts anything about the wire. Both expectations are DERIVED through `rewards::staleness::is_entry_set_stale` against the exported constant, so the bound's numeric value (172_800) appears nowhere in the test and the fixture can no longer carry a staleness flag its own chain-derived times contradict. That derivation also removes the `seed % 2 == 0` parity trick clippy flagged as a manual `is_multiple_of`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial gate (third leg) — MERGE-WITH-CONDITIONSHead read: Verdict: MERGE-WITH-CONDITIONS. Two pre-merge edits (one shared re-gate), one ticket to file before merge, one follow-up. Not "wait for the adapter" — see 1. 1. Working feature, or a handler nothing can call? — GROUNDWORK. Report it as groundwork.Nothing in the production graph installs a That is not a reason to hold it. The failure is honest — a named unavailability, never a zero, never an empty 2. Third
|
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
CHANGES-REQUIRED - head a77ed707
Correctness gate for #3269 unit 3. CI at this SHA is 13/13 green and the Test + coverage job reports 3402 tests run: 3402 passed (17 slow), 4 skipped, TOTAL line coverage 90.13% against the >=80% gate. Blast radius is exactly the five intended files under crates/dig-node-core/**; crates/dig-node-service/**, Cargo.toml, Cargo.lock and tests/dependency_tree.rs are untouched, and no chia-* pin moved. The shape is right, the refusal discipline is right, and the money figure never touches the wrapped path. Two things block the merge, both on the wire contract, plus two doc claims that are false as merged.
Blocking
-32033is already taken, on this node, by an unrelated condition.dispatch.rs:51mintsREWARD_INVALID_WITHDRAWAL_SHARE = -32033, which isErrorCode::ControlIngressLimitedincrates/dig-node-service/src/meta.rs:840(asserted atcrates/dig-node-service/src/server.rs:3480) - the shell that fronts this very dispatch. One number, two opposite instructions to the caller: "you were rate-limited at ingress, back off and retry" versus "this distributor's own curried constant is out of range, never retry". dig-app 15.6.0's clawback is the caller.- The guard that exists to catch exactly that was not told about the new number.
LOCAL_WIRE_CODES(lib.rs:5434) is documented as "every JSON-RPC error number this node PUTS ON THE WIRE";-32033is absent, which is whyno_local_wire_code_collides_with_a_different_canonical_code(job log1134/3402, PASS) is green over the collision in (1).
Doc truth (blocking, per this epic's five born-false claims)
port.rs:151-153asserts in the present tense thatdig-node-serviceis the crate that depends ondig-rewards-coinand computes the figure withdig_rewards_coin::recoverable_base_units. At this SHAcrates/dig-node-service/Cargo.tomlhas nodig-rewards-coindependency and no such adapter exists.- Two test doc comments (
lib.rs:9899,lib.rs:9994) cite "the accompanying report" for their mutation-probe evidence - an out-of-tree artifact a reader holding only the repo cannot check.
Answers to the six checks
- Money figure never from the wrapped path - HOLDS.
withdraw_committed_incentives,recovered_base_unitsandread_distributorappear nowhere incrates/dig-node-core(grep at this SHA); there is nodig-rewards-coindependency and nodig_chainsource_interface::ChainSourcereference. All three architectural negatives hold.recoverable_base_unitsis carried verbatim fromCommitmentSlotto the wire row and never recomputed in this crate. withdrawal_share_bpsechoed, never recomputed - HOLDS, but the narrowing is not in this PR at all.DistributorReport.withdrawal_share_bpsis alreadyu16and the port's contract puts both theu16::try_fromand the<= 10_000check on the adapter (port.rs:139,port.rs:151-166). So there is noas u16here to get wrong - and equally nou16::try_fromin-tree to verify. The65_536 -> 0risk (#3284) transfers whole to #3268; the port doc states the obligation, which is the most this crate can do.- Refusal is whole-call and never a zero - HOLDS for both paths reachable from this crate. No-adapter-installed and
Unavailableboth return anerrorwithdata.code = REWARD_CHAIN_UNAVAILABLEand noresultmember;InvalidWithdrawalSharereturns anerrorwith its own machine string. Both handlers build their result with a total struct literal (no..Default::default(), every field assigned), so no field can be silently defaulted. The two out-of-range sub-cases (> u16::MAXand> 10_000) collapse into one wire code, which is acceptable: the whole-call-refusal reasoning still holds in the code, becausewithdrawal_share_bpsis read once per distributor and every row of a single response belongs to that one distributor. entrySetStalechain-derived - HOLDS. The handler echoesreport.entry_set_stale;172_800appears nowhere in the diff; the fixture derives both sides of the bound throughrewards::staleness::is_entry_set_staleagainstrewards::spec_constants::STALE_ENTRY_SET_SECONDS, so the numeric bound is written down nowhere in the test.- Single-install invariant - the TEST was fixed, not the install.
install_reward_chain_portstill readsself.reward_chain_port.set(port).is_ok()over aOnceLock(lib.rs:684-689).get_reward_distributor_threads_entry_set_stale_both_waysnow installs ONE port holding a two-entryHashMapkeyed by launcher id and asserts each launcher gets its own staleness - strictly stronger than two installs, since it also catches a handler answering a different distributor's row. - #3261 guards in both places - HOLD. Both are rule-scans over
Method::ALLfiltered on.contains("Reward"), so they cover the new surface without naming it; both are non-vacuous (!is_empty()) and both now pin the count at 4, which matchesdig-rpc-protocolv0.11.0 exactly (method.rs:352-355). Both new arms are in theMethod::from_name(method)match atdispatch.rs:384; neither name appears in the string pre-match atdispatch.rs:335-373(chat.send,chat.poll,cache.pushCapsule,dig.getPublicManifest), so neither bypasses the tier guard.
Executed reward tests, from the job log (not the check mark)
| # | test | result |
|---|---|---|
| 235/3402 | peer::tests::reward_methods_are_absent_from_the_node_peer_allowlist |
PASS |
| 1014/3402 | tests::commitment_money_figures_stay_attributed_to_their_own_distributor |
PASS |
| 1097/3402 | tests::get_reward_distributor_answers_with_real_values_through_dispatch |
PASS |
| 1098/3402 | tests::get_reward_distributor_threads_entry_set_stale_both_ways |
PASS |
| 1125/3402 | tests::list_reward_distributor_commitments_answers_with_real_values_through_dispatch |
PASS |
| 1134/3402 | tests::no_local_wire_code_collides_with_a_different_canonical_code |
PASS (vacuous over the new code - see blocking 2) |
| 1162/3402 | tests::reward_distributor_methods_chain_unavailable_is_a_distinct_error_never_a_zero_or_empty |
PASS |
| 1163/3402 | tests::reward_distributor_methods_refuse_whole_call_on_invalid_withdrawal_share |
PASS |
| 1309/3402 | reward_methods_tier_guard::every_reward_method_is_tier_control |
PASS |
| 1310/3402 | reward_methods_tier_guard::no_reward_method_is_peer_reachable |
PASS |
| 1311/3402 | reward_methods_tier_guard::reward_methods_exist_and_are_found_by_the_prefix_scan |
PASS |
| 1243-1245/3402 | dig-node-core::dependency_tree (unmodified) |
PASS |
The subject test in the two-distributor shape exists and passes (1014/3402): distinct recoverable_base_units of 4_500 and 6_300 read back under their own launcher ids, with the summed (10_800) and swapped values asserted absent from the field. All assertions in the new tests are on the serialized JSON body - key SETs, not substrings - and the expected keys are snake_case, which is correct: v0.11.0's GetRewardDistributorResult, ListRewardDistributorCommitmentsResult and RewardDistributorCommitment carry no rename_all. No internal type is serialized onto the wire; every field crosses through a dig_rpc_protocol::types struct.
Stubs
Neither dig.listRewardDistributors nor dig.getPayeeRewardClaimStatus has a stub, a todo!() or an unimplemented!() anywhere in dispatch.rs or port.rs. Nothing to flag.
Not run
No local build, no local test run, no mutation probe re-executed. Findings are from the diff, the blobs at a77ed707, the dig-rpc-protocol v0.11.0 tag source, and the Test + coverage job log for this SHA.
| /// `0..=10_000` range. Refuses the WHOLE call — see `rewards::port::ChainPortError::InvalidWithdrawalShare`'s | ||
| /// doc for why a per-distributor curried value makes that the correct shape, never a `0` or an | ||
| /// omitted field. | ||
| const REWARD_INVALID_WITHDRAWAL_SHARE: i64 = -32033; |
There was a problem hiding this comment.
BLOCKING - -32033 is already assigned, in this repo, to a different condition.
ErrorCode::ControlIngressLimited is -32033 at crates/dig-node-service/src/meta.rs:840, asserted at crates/dig-node-service/src/server.rs:3480. That is the service shell that fronts this dispatch, so both numbers reach the same client on the same wire.
Consequence: dig-app 15.6.0's clawback receives -32033 and cannot choose between two opposite instructions - "an open, token-less control read was refused at ingress, back off and retry" and "this distributor's curried withdrawal_share_bps is out of range, never retry". The ambiguity is in the contract, so no retry policy recovers it, and the surface is money.
Fix: either mint a number in the -32030..-32039 control band that nothing in this workspace emits, or do what the arm two lines below already does and reuse CONTROL_ERROR (-32032) while keeping the distinct data.code machine string - the chain-unavailable arm proves that pattern is sufficient for a client to branch.
The fix must NOT renumber ControlIngressLimited (released, and consumer-held), and must NOT merely document the overlap.
| /// `0..=10_000` range. Refuses the WHOLE call — see `rewards::port::ChainPortError::InvalidWithdrawalShare`'s | ||
| /// doc for why a per-distributor curried value makes that the correct shape, never a `0` or an | ||
| /// omitted field. | ||
| const REWARD_INVALID_WITHDRAWAL_SHARE: i64 = -32033; |
There was a problem hiding this comment.
BLOCKING (second half of the collision) - register the new number in LOCAL_WIRE_CODES.
LOCAL_WIRE_CODES at lib.rs:5434 is documented as "Every JSON-RPC error number this node PUTS ON THE WIRE, paired with the condition it names", and its test no_local_wire_code_collides_with_a_different_canonical_code exists, per its own doc, because this defect class was found twice in one review. -32033 is not in the table, so that test passed at 1134/3402 while the collision above shipped.
Add the row ((REWARD_INVALID_WITHDRAWAL_SHARE, "REWARD_INVALID_WITHDRAWAL_SHARE")) alongside the fix, so the guard fails on a collision rather than being told about it afterwards.
The fix must NOT be to raise the len() >= 10 floor instead of adding the row - the floor measures the table's size, not its completeness.
| /// | ||
| /// `recoverable_base_units` is the adapter's PRE-COMPUTED share — never restated by a caller of | ||
| /// this port, and never recomputed by `dig-node-core` itself. The production adapter | ||
| /// (`dig-node-service`, dig_ecosystem#3268) is the one crate in this seam that depends on |
There was a problem hiding this comment.
BLOCKING (doc truth) - false as merged.
"The production adapter (dig-node-service, dig_ecosystem#3268) is the one crate in this seam that depends on dig-rewards-coin ... it computes this figure with dig_rewards_coin::recoverable_base_units".
At this SHA, crates/dig-node-service/Cargo.toml declares no dig-rewards-coin dependency (its chain dep is dig-chainsource-interface = "0.3", line 129) and no such adapter exists in the tree. A reader holding only this repo checks the claim and finds it false - the born-false-doc class this epic has already shipped five times.
Rewrite as an obligation rather than a description: the adapter that WILL implement this port (#3268, in dig-node-service) must compute the figure with dig_rewards_coin::recoverable_base_units. Keep the arithmetic argument and the u128-intermediate reference verbatim; only the tense and the existence claim change.
The fix must NOT add a dig-rewards-coin dependency to either crate to make the sentence true.
| /// side: doesn't fit `u16`, the caller narrows before calling this port, or the adapter's own | ||
| /// `0..=10_000` domain check), BOTH methods refuse the WHOLE call with a distinct machine code | ||
| /// — never a `0`, never an empty `commitments` list standing in for the refusal. | ||
| /// **Mutation-probe:** flipping `reward_chain_port_error_response`'s |
There was a problem hiding this comment.
BLOCKING (doc truth) - cites an out-of-tree artifact.
"see the accompanying report for the before/after run" points at something no reader of this repo can open, so the mutation-probe claim is unverifiable where it is written. Same at lib.rs:9994.
Make it checkable in place: name the mutation and the assertion it breaks (e.g. "replacing the InvalidWithdrawalShare arm of reward_chain_port_error_response with a withdrawal_share_bps: 0 result fails the resp.get(\"result\").is_none() assertion below, for both methods"), and drop the citation.
The fix must NOT delete the mutation-probe note altogether - the property it records is the reason the test is not vacuous.
| /// dig_ecosystem#3268's files (`dig-node-service`), so clippy's non-test lib target sees no | ||
| /// production caller yet. | ||
| #[cfg_attr(not(test), allow(dead_code))] | ||
| pub(crate) fn install_reward_chain_port( |
There was a problem hiding this comment.
MEDIUM - the seam as shipped is unreachable from the crate its own doc says installs it.
install_reward_chain_port is pub(crate), and the doc above says "the startup path that would install the real adapter belongs to dig_ecosystem#3268's files (dig-node-service)". dig-node-service cannot call a pub(crate) function of dig-node-core, so as merged there is no path by which the injected adapter this PR was designed around can ever be installed - and #[cfg_attr(not(test), allow(dead_code))] suppresses the compiler's own report of that.
It also creates a single-writer problem: #3268 would have to re-edit this file, which this lane owns.
Either widen to pub now (the OnceLock keeps the one-shot invariant regardless of visibility, and reward_chain_port() can stay pub(crate)), or say explicitly in the doc that widening the visibility is part of #3268's diff. The fix must NOT replace the OnceLock or add a second installer.
…unregistered wire code The reward-distributor refusal path minted its own numeric error code locally. That number is not registered in the shared cross-implementation wire taxonomy, whose canonical catalogue in lib.rs stops at -32032, and lib.rs states that this numbering IS the byte-identical contract this node declares it follows. Minting a code locally puts the node outside that contract: another implementation would have no way to read it. The number is not even free -- dig-node-service already spends it on an unrelated ingress refusal, so two surfaces of the same product would have disagreed about what it means. Return -32032 with a REWARD_INVALID_WITHDRAWAL_SHARE data.code instead, which is exactly the shape the chain-unavailable arm beside it already uses. The two refusals stay fully distinguishable in the body, and the refusal test needed no change -- it asserts the absence of `result`, not a numeral. Also corrects a born-false doc attribution: three comments asserted that the chain adapter and its install_reward_chain_port call site belong to #3268. #3268's scope is the claim loop and ClaimStatus, and it names neither distributor_report nor this installer, so those comments pointed a reader at a ticket that will not do the work. They now say the ownership is an open question tracked separately rather than naming a number nobody verified. Same defect shape as #3292. Refs #3269 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Security gate: PASSHead audited: Severity limiter, stated up front: nothing constructs a 1. #3303 — a wrapped figure reaching a money surface: clear
The figure that reaches the wire is The residue: that direction is a doc obligation on #3268, not an enforced one. Nothing in this crate can tell which rewards-coin function produced the number. Correct layering for unit 3; name it in #3268's brief as the one thing its gate must read. 2. The
|
|
Adversarial condition C3 satisfied by filing #3310 — the adapter and its Both are ticket-only; neither requires a code change at this PR's head. |
…istributor seam dig_ecosystem#3284. Both `dig.getRewardDistributor` and `dig.listRewardDistributorCommitments` emitted `report.withdrawal_share_bps` with no range check. The port's adapter is contracted to refuse a value outside `0..=10_000`, but no adapter exists yet and the contract was enforced nowhere at this seam, so `10_001..=65_535` reached the wire unexamined: a chain constant of `74_536` narrowed into the wire's `u16` renders as `9_000` (an ordinary-looking 90% share) and `65_536` renders as `0`. Either is a figure a funder reads before deciding whether to claw back, and neither looks wrong. Refuses the whole call through the existing `InvalidWithdrawalShare` path (`CONTROL_ERROR` with `data.code = REWARD_INVALID_WITHDRAWAL_SHARE`) rather than clamping: clamping `74_536` to `10_000` would report a confident 100% recoverable share for a distributor whose real constant is nonsense. A whole-call refusal costs no good data - `withdrawal_share_bps` is curried once per distributor at launch, so every commitment slot in one response shares the one invalid value. Two tests, a selectivity pair: `10_001` refuses on both methods with neither the raw figure nor a clamped `10_000` anywhere in the body, and `10_000` - a legitimate 100% - still answers on both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fake TEMPORARY, reverted in the next commit. Proves `commitment_money_figures_stay_attributed_to_their_own_distributor` is not vacuously green: with the two reports swapped behind their launcher ids the test must go RED against the green baseline at a77ed70. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… in the fake" Reverts 0b07be6. The probe commit was pushed onto a branch a SECOND writer is committing to concurrently (3dd1d67, d5e1509 appeared under this worktree's HEAD mid-task), so the probe is withdrawn rather than left to redden that writer's CI. The tree is byte-identical to d5e1509. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing one fails `no_local_wire_code_collides_with_a_different_canonical_code` passed at 1134/3402 while this crate really was emitting `-32033`, a number that is already `dig-node-service`'s `ControlIngressLimited`. It passed because the number was never added to `LOCAL_WIRE_CODES` - and the guard's own side-effect assertion was `len() >= 10`, which measures the table's SIZE, not its completeness. A code nobody registers is invisible to a guard built over the registry. Adds `every_wire_code_this_crate_mentions_is_classified`, which SCANS this crate's sources for every `-32xxx` in a non-comment line and requires each to be either declared by the taxonomy owner (resolved through `seams::dig_rpc::errors::taxonomy_code`, so the taxonomy is not restated) or registered in `LOCAL_WIRE_CODES`. The list is therefore derived from the emitting sites and cannot fall behind them: forgetting to register a number is what fails. Chose the derived form over a hand-maintained "everything we emit" list because such a list is the same defect one level up. Also: registers the two numbers this crate occupies that upstream does not declare (`-32001` push authorization, `-32002` `ENGINE_WARMING`), both previously unregistered; replaces the `len() >= 10` floor with an exact count; and drops the table doc's now-false claim that the `-32050..-32052` chat band is undeclared upstream - `dig-rpc-protocol` 0.11 declares all three. The test carries its own non-vacuity checks: a scanner-liveness floor plus three codes known to be present, and a proof that the classifier rejects the number that slipped through, built by arithmetic because writing it as a literal would itself be a mention the scan must classify. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he service crate
Two repairs to the two commits before this one, then three doc-truth findings.
Repairs:
* the wire-code assertion message lost its line continuations, baking a 10+
space run into the string - the exact defect `continuation_guard` exists to
catch, and the only red on the branch. Restated as one line, so there is no
continuation to lose. The message a reader gets at the moment they are told a
wire code is unregistered is now a clean sentence.
* the out-of-range withdrawal-share test asserted the digits `10000` appear
nowhere in the body, which the refusal MESSAGE legitimately contains ("must
fit u16 and be <= 10000"). Matched in a JSON value position (`:10000`,
`:10001`) instead, so the assertion is about the figure rather than the prose.
Findings:
* `install_reward_chain_port` is now `pub`. Its own doc said the installing
startup path lives in `dig-node-service`, while `pub(crate)` meant that crate
could not call it - both could not be true. The decided architecture builds
the adapter in the service crate and injects it downward, so the setter is the
injection point and says so, citing dig_ecosystem#3310 (which names both
`distributor_report` and this function). The single-install `OnceLock`
discipline is untouched: `set(port).is_ok()` stays, and a second install still
returns `false` and changes nothing. The `allow(dead_code)` is dropped - it was
hiding the unreachability, not standing in for an absent caller.
* the two mutation-probe doc comments cited "the accompanying report", an
artifact that does not exist in this repository. Each now states the mechanism
and the expected outcome - which edit, which command, which assertion fails -
so a reader holding only the repo can re-run it.
* `rewards::port` asserted in the present tense that `dig-node-service` depends
on `dig-rewards-coin` and computes `recoverable_base_units` with it. It does
not: that manifest has no such dependency and no adapter exists. Rewritten in
the future tense against #3310, and it now also names the seam-side range
check so the two statements of that rule cannot be read as rivals.
* three comments attributed the funder-registry installer to #3268, whose scope
is the claim loop and `ClaimStatus` and which names neither the registry nor
the call. They now say the owner is tracked separately and is not #3268; the
installer work itself stays out of this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-- repoint port.rs Finding E's defect class, in the one file of the three it was reported in that this lane owns. `rewards/port.rs`'s module doc was written against `dig-rewards-coin` 0.2.0 and still asserted, in the present tense, that the crate "ships no chain reader -- blocker 1". It does. Verified first-hand rather than from a report: `index.crates.io` lists 0.4.1 as the latest publish, and 0.4.0 in the local registry cache carries `state::read_distributor(&impl ChainSource, launcher_id) -> Result<Option<DistributorSnapshot>, RewardsError>` and `clawback::recoverable_base_units(rewards_base_units, withdrawal_share_bps) -> Option<u64>`, the latter refusing above `10_000` bps exactly as this seam's own range check does. No dependency is added: the crate stays absent from `dig-node-core`'s manifest, which is the injected-downward architecture, and the doc now says so as an instruction rather than as a pending chore. The two `UnavailableChainPort` citations deferred the production adapter to #3249, the `dig-rewards-coin` DRIVER ticket. That is a dead pointer, not a stale one: the driver does no socket I/O -- its reader takes a caller-supplied `ChainSource` -- so it can never be the adapter, and a blocker filed on a ticket that structurally cannot ship the change is never read. Repointed to #3310, which owns the `RewardsChainPort` adapter in `dig-node-service` and names both `distributor_report` and `install_reward_chain_port`; #3307's claim seam is a different trait and is not what these sentences are about. The correction is recorded in the doc rather than silently applied, because the next reader will otherwise re-derive the same wrong pointer from the ticket title. Two dependent claims corrected with it: the paragraph that said no adapter could answer any of the four trait methods "yet" now makes the narrower true claim (none can live in THIS crate, which holds no `ChainSource`), and the settlement condition that waited on "a reader (0.3.0+) and the funder-ownership registry" now records that the reader half is met and blocker 2 is not. Out of fence, reported upward rather than edited: the same dead #3249 pointer sits in `rewards/mod.rs:11,:16`, `rewards/spec_constants.rs:7` and `rewards/writes.rs:244`, and the first two also carry the SPEC-only claim about `dig-rewards-coin` that this commit retires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Salvage, not new work. The derived scan landed in 2f81a0e, but the three hunks it depends on - the two unregistered codes it needs registered (`-32001` push authorization, `-32002` `ENGINE_WARMING`), the exact-count assertion replacing the `len() >= 10` floor, and the table doc - were swept into a sibling lane's probe commit (0b07be6) and removed again by its revert (a937f81) while this lane's edits were uncommitted. The scan was therefore correct and RED: `-32002` really was unclassified. Verified locally at this tree rather than from a CI log: `every_wire_code_this_crate_mentions_is_classified`, `no_local_wire_code_collides_with_a_different_canonical_code` and `no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence` all pass, and `cargo fmt --check` is clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nly claim
The four cited sites pointed at #3249, the `dig-rewards-coin` DRIVER ticket.
That crate does no socket I/O -- `state::read_distributor` takes a
caller-supplied `&impl ChainSource` -- so it structurally cannot ever ship the
chain adapter these comments were waiting on. Repointed by seam:
- `writes.rs` and `mod.rs` (the `RewardsChainPort` seam) -> #3310, which names
both `distributor_report` and `install_reward_chain_port`.
- `mod.rs` also notes the claim-side chain adapter is tracked in #3307.
`mod.rs` and `spec_constants.rs` additionally claimed the crate is "SPEC-only"
with an empty `pub mod distributor {}`. False since 0.2.0 and three releases
stale: 0.4.1 ships `state::read_distributor(&impl ChainSource, launcher_id)
-> Result<Option<DistributorSnapshot>, RewardsError>`, `DistributorSnapshot`,
`ChainObservation` and `clawback::recoverable_base_units`, and has no
`distributor` module at all. Both now say what is true: the crate ships the
reader and the share arithmetic; what dig-node lacks is the adapter wiring that
reader to a `ChainSource`.
The `spec_constants.rs` migration note no longer attributes the constants to
#3249 either: 0.4.1's `constants` module carries distributor-side values only,
not these prover-side ones, so no ticket is claimed to be about to publish them.
Comments only -- no code, test or dependency change.
Refs #3269
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refs #3269
DO NOT MERGE — gate round in progress.
The test this PR exists to make possible
commitment_money_figures_stay_attributed_to_their_own_distributor— two distributors, 4,500 and 6,300, each figure returned under its ownlauncher_id, with the summed value (10,800) and the swapped values asserted absent from the response.That assertion is the structural answer to this epic's central defect. A sibling gate found
total_paid_out_base_units— a figure paid to all mirrors, sourced from the funder's record — rendered as one operator's personal earnings, overstating by up to 250x. It passed security review and 11/11 CI. The adversarial leg's explanation of why three legs missed it:A number can be correctly typed, read-only, in range, perfectly formatted — and false about whose money it is. Type checks, lints and coverage are all silent on that property, because it is not a property of the value. It is a property of the mapping from subject to value, and only an assertion that names two subjects and denies their sum can see it.
This PR's handlers are where that subject is established on the wire, so the test lives here.
What this PR does and does not deliver
Serves two methods, both
Tier::Control:dig.getRewardDistributoranddig.listRewardDistributorCommitments.In production both answer chain-unavailable, because nothing constructs a
RewardsChainPortyet — the adapter and itsinstall_reward_chain_portcall site are tracked in #3310. This is groundwork, and the failure is honest: a named unavailability, never a zero and never an empty list.Two methods deliberately stay
-32601, with no stub and notodo!():dig.listRewardDistributors— its result type derivedDefaultwith bareVechalves, so a funder-only answer had to emitclaimable: [], indistinguishable from "you have no mirror claims". Fixed upstream indig-rpc-protocol0.12.0 by a dated-absence field; this PR waits for that publish.dig.getPayeeRewardClaimStatus— noMethodvariant and no result type existed in v0.11.0. Also added in 0.12.0.Money handling
withdraw_committed_incentivesis never called and itsrecovered_base_unitsnever read: it returns #3286's wrappedu64uncorrected (#3303), and the exactchia-*pins stop that overflow moving without correcting it. The recoverable figure is computed withrecoverable_base_units, whose u128 intermediate is correct exactly where the driver wraps.withdrawal_share_bpsis echoed from chain-derived state, never recomputed (SPEC 2.6 clause 2 forbids it by name).An unhonest figure refuses the whole call rather than emitting a value — never a zero, never a field omitted and defaulted. Safe wholesale because
bpsis launch-curried per distributor, so no good row is ever blinded by the refusal.