fix(rewards): fold observed_at to oldest report; log IoFailed - #628
Conversation
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Verdict: PASS
Reviewed net diff c72a0ebb..a3422a91 (base develop). Read-only worktree at C:/worktrees/dig-node-gate628, removed after this review.
Point-by-point
1. Is min the right fold, and is unwrap_or(now) right in the all-failed case?
min is right — a collection is only as fresh as its stalest member. The "loop ran but every read failed" case cannot reach the unwrap_or(now) fallback at all: dispatch.rs:1191-1194's Err(e) => return reward_chain_port_error_response(&id, &e) short-circuits the entire handler on the first per-identity error, before any Consulted response is built. So unwrap_or(now) only fires when identities is empty (FundsNothing, loop body never runs) — in that case no chain read happened, and now is the honest stamp. Confirmed by reading the full loop at dispatch.rs:1176-1201.
2. Are both NotConsulted arms genuinely untouched, and is FundsNothing distinguishable from consulted-but-empty?
Yes on both. claimable: Half::NotConsulted { observed_at: now } at dispatch.rs:1207 is unchanged. The NotConfigured | PersistedStateCorrupt | IoFailed arm (dispatch.rs:1150-1163, pre-existing, untouched) returns early with both halves NotConsulted { observed_at: now }. FundsNothing falls through to identities = Vec::new(), the loop body never executes, oldest_observed_at stays None, and the result is Consulted { observed_at: now, items: [] } — same clock value as NotConsulted would use, but a structurally different (and correct, per the doc comment above the match) outcome tag. Genuinely distinguishable, verified via funded.determined() semantics.
3. Does report_io_failed log on every IoFailed construction site, including the writer twins?
Yes — verified by grep, all three sites in funded.rs: line 216 (read()), line 234 (refuse_io_failed in the reader-fails-before-merge path), line 246 (refuse_io_failed on a save failure). No bare IoFailed { path, error } construction remains anywhere in dig-node-core (grepped the whole crate). dispatch.rs:1157 is a pattern match on the variant, not a construction site.
4. Does the logging leak anything it shouldn't?
No. FundedDistributorsRead/RecordOutcome derive only Debug, Clone, PartialEq, Eq — no Serialize — so path/error never reach a wire response. dispatch.rs:1157's match arm collapses IoFailed into Half::NotConsulted { observed_at: now }, dropping both fields entirely. path/error reach only tracing::error! at funded.rs:336-352.
5. Salvage risk — did anything from the unverified ec07271f checkpoint survive unreviewed?
One discrepancy worth naming, not blocking: ec07271f's own commit message claims "the fold it asserts is NOT applied — dispatch.rs nets to zero against HEAD, so the handler still stamps its own clock." A direct file-content diff (git show ec07271f:.../dispatch.rs vs. the pre-branch base, md5sum-verified, not read through paraphrased tool output) shows the fold was already present in ec07271f at observed_at: oldest_observed_at.unwrap_or(now). The commit message under-describes its own diff. This doesn't affect correctness of the shipped code — a576ae4b only reworded the comment and a3422a91 only reformatted the test — but the checkpoint's own "state as salvaged" note is inaccurate and shouldn't be trusted at face value in future archaeology on this branch.
The lib.rs test (list_reward_distributors_funded_observed_at_is_the_oldest_report_stamp) and funded.rs test (an_io_failed_read_is_logged_with_its_path_and_error) both carry blind-written bodies from the checkpoint, but both were reformatted only (not logic-changed) by the later commits, and both assertions are non-vacuous: the lib.rs test's expected value (1_700_200_000 + 0x55) cannot be produced by SystemClock::now() (a 2026 wall-clock value, confirmed by the reported red failure 1790508579 vs 1700200085), and it additionally asserts assert_ne! against report_b.observed_at to rule out a "keep max" mutant. The funded.rs test's log-capture assertions go empty (not falsely-positive) under a reverted bare construction, per the reported red run.
Not re-litigated (per brief)
No comment on: chain_port.rs, the NotConsulted collapse itself, the handler-clock stamps at dispatch.rs:996/:1218, the dig-rpc-protocol 0.14.0 bump, or #3274.
Not run
No cargo build/cargo test at any scope — reviewed from the diff and file contents only, per the hard constraint on cold-build cost in this repo.
No blocking findings. No inline threads opened.
Security audit — PASSHead audited: 1 — log reachability ( 2 — unbounded/attacker-driven writes. Same reachability analysis applies: since the read path is Control-tier and not peer-reachable, an attacker cannot drive 3 — 4 — #3261 tier/reachability regression check. Both axes independently confirmed unchanged: 5 — salvage checkpoint ( Out of scope, not re-derived: No live vulnerability found. PASS. |
SALVAGED FROM A LANE KILLED MID-BUILD. At the time of this commit the code had NEVER COMPILED -- the lane died waiting on a cold build before any cargo run finished. It exists so a session cap could not take the work again. CORRECTION to this message's first draft, which claimed the #3323 fold was NOT applied here: it was. `git diff HEAD` reports the WORKING TREE, but a commit records the INDEX, and the dead lane had staged the fold and then reverted it in the tree to confirm its test reddened first. Reading the tree and describing the commit is how that claim went wrong. Read `git diff --cached`, or both columns of `git status --porcelain`. State as salvaged, corrected: - #3324: report_io_failed + refuse_io_failed helpers in rewards/funded.rs and an_io_failed_read_is_logged_with_its_path_and_error. - #3323: both the RED test in lib.rs AND the dispatch.rs fold it asserts. Refs #3323, #3324
…ndler clock dig.listRewardDistributors stamped funded.observed_at with the RPC handler's pre-loop clock, which predates every per-identity chain read (dig-node-service's chain_port.rs stamps report.observed_at AFTER its own read, uncached) -- so the wire understated staleness rather than dating the consultation (dig-rpc-protocol SPEC 4.4.2's first bullet). Fold each report's own observed_at across the loop and keep the oldest -- a collection is only as fresh as its stalest member. Both NotConsulted arms and the empty FundsNothing case keep the handler's clock unchanged: no chain read happened for those, so it remains the honest answer. Refs #3323 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…oint compiles The ec07271 checkpoint was pushed unverified (killed by an account-wide 429 mid-build). Both regression tests now compile and pass: - an_io_failed_read_is_logged_with_its_path_and_error (crates/dig-node-core/src/rewards/funded.rs) -- proves report_io_failed logs path+error; RED under a mutation that reverts the call site to the bare `IoFailed { path, error }` construction, GREEN restored. - list_reward_distributors_funded_observed_at_is_the_oldest_report_stamp (crates/dig-node-core/src/lib.rs) -- was RED against the checkpoint (no fold applied); GREEN after a576ae4's fold. Refs #3323 Refs #3324 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
a3422a9 to
fdea3c3
Compare
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Re-confirmation at rewritten SHA fdea3c34
PASS — metadata-only re-gate. Prior full correctness review at a3422a91 stands unchanged; no code was re-derived.
Head SHA: fdea3c342cbd23f031082a767a37808732a8641f
- Tree diff
a3422a91..fdea3c34is empty (git diff→ 0 lines, exit 0). Byte-identical tree confirmed directly, not paraphrased. - Commit chain matches, same base
c72a0ebb5071, same 3 commits, only messages changed:fdea3c34 test(rewards): fmt the #3323 regression test; verify the #3324 checkpoint compiles(82 chars)307675fa fix(rewards): fold observed_at to the oldest report stamp, not the handler clock(80 chars)11db1759 chore(rewards): UNVERIFIED checkpoint of the #3323/#3324 honesty work(69 chars, waswip(...))
All three types (test,fix,chore) are in the allowed enum; all subjects well under the header length limit.
- Corrected checkpoint message matches its own diff.
git show --stat 11db1759showslib.rs(+53, the RED test),funded.rs(+94, the helpers), anddispatch.rs(+14, the fold) all touched in this single commit — consistent with the corrected claim that both the #3323 RED test and the dispatch.rs fold are present, superseding the retracted "NOT applied" statement from the first draft.
No new findings. No cargo run. No waiting on in-flight CI (commitlint already green per report; that check is out of scope for this re-gate).
Security re-confirmation at rewritten SHA
|
Summary
dig.listRewardDistributorsfoldsfunded.observed_atto the OLDEST per-item chain-report stamp instead of the RPC handler's pre-loop clock. At tip, the handler'snowis captured before everyport.distributor_reportread, and each read's ownobserved_at(stamped bydig-node-service'schain_port.rsafter its chain call, uncached) always postdates it — so the wire understated staleness rather than overstating it, as the ticket title originally claimed (corrected on the ticket).NotConsultedarms and the emptyFundsNothingcase keep the handler's clock: no read happened there, so it remains the honest stamp.report_io_failed/refuse_io_failedhelpers incrates/dig-node-core/src/rewards/funded.rs—IoFailednow logs path + error the same wayreport_corrupt/PersistedStateCorruptalready does (shipped v0.257.0), closing the asymmetry where an I/O fault (permissions, missing mount) was silently swallowed. The wire mapping (dispatch.rs'sNotConfigured|PersistedStateCorrupt|IoFailed→ oneNotConsulted) is unchanged — confirmed correct by ticket research:Half::NotConsultedcarries no reason field at either 0.12.0 or 0.14.0, and the ticket forbids adding one.Checkpoint context
A prior lane was killed by an account-wide 429 mid-build; its uncommitted work was salvaged and pushed as an explicitly unverified checkpoint (
ec07271f, "UNVERIFIED"). This PR is that checkpoint made green:a576ae4bapplies the #3323 fold (the only piece the checkpoint was missing —dispatch.rshad netted to zero against base).a3422a91runscargo fmton the salvaged #3323 test and records that both regression tests now compile and pass.Blast radius
Read via ripgrep + direct read in this worktree (gitnexus not invoked — ticket research already established the mechanism at file:line and the touched surface is two functions in one crate). Touched:
crates/dig-node-core/src/seams/dig_rpc/dispatch.rs(ListRewardDistributorsarm only),crates/dig-node-core/src/rewards/funded.rs(report_io_failed/refuse_io_failed, already in the checkpoint),crates/dig-node-core/src/lib.rs(fmt only). No sibling arm (getRewardProverStatus,getPayeeRewardClaimStatus) touched.chain_port.rs(owned by the #3421 sibling lane) untouched.Tests
list_reward_distributors_funded_observed_at_is_the_oldest_report_stamp(lib.rs) — RED against the checkpoint (no fold applied,observed_at= wall clock), GREEN aftera576ae4b. Mutation proof: reverting the fold (restoringobserved_at: nowand dropping the loop's fold) reproduces RED (cargo test -p dig-node-core --lib <name>→test result: FAILED, exit 101); restoring the fold returns GREEN (exit 0).an_io_failed_read_is_logged_with_its_path_and_error(funded.rs, from the checkpoint) — mutation proof: revertingreport_io_failed's call site to the bareIoFailed { path, error }construction reproduces RED (exit 101); restoring the helper call returns GREEN (exit 0).Both mutations and restores were re-verified with
grep -c/md5sumon the file content directly (not just tool-rendered logs) to rule out a paraphrasing artifact.Not verified
cargo test -p dig-node-core --lib <testname>per ticket, per the hard constraint against a coldcargo build/testat workspace scope (measured 46m21s cold).dispatch.rs:996/:1218's handler-clock stamps ongetRewardProverStatuset al.) are out of scope and untouched, per the ticket.Refs #3323
Refs #3324
🤖 Generated with Claude Code