build(deps): bump dig-rpc-protocol cascade to 0.14.0 - #629
Conversation
Moves dig-rpc-protocol 0.12->0.14, dig-peer 0.15->0.16, dig-download 0.24->0.25, dig-peer-selector 0.13->0.14 across crates/dig-node-core and crates/dig-node-service Cargo.toml, with the resolved Cargo.lock. dig-rewards-coin stays pinned at 0.8 (not part of this cascade). Refs #3329 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The exactly-one/version-floor assertion in the_workspace_carries_exactly_one_module_wire_crate stays exactly as strong -- only the expected literal moves from "0.12." to "0.14." to match the resolved dig-rpc-protocol cascade. Refs #3329 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds chain_peak_height/chain_peak_timestamp to DistributorReport (port.rs) and GetRewardDistributorResult's struct literal (dispatch.rs), with a new ChainPortError::ChainPeakUnavailable refusal path when a read can't anchor to a chain peak. chain_port.rs reads the peak in the SAME synchronous spawn_blocking body that reads the distributor snapshot, never a later independent call, so the anchor never names a height the rest of the report was not read against. UNVERIFIED: not yet compiled. A cargo check on dig-node-core + dig-node-service is running; this commit is pushed as a checkpoint before that check returns, per this epic's push-early rule. Refs #3329 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d result UNVERIFIED CHECKPOINT. Salvaged from a lane killed by a rate limit before its cargo check returned. This has NOT compiled. Committed so a cap cannot take it again -- the previous lane on this task lost everything it had not pushed. Content, described from `git diff --cached` (the index, which is what a commit records) rather than from the working tree: - dispatch.rs: fills dig-rpc-protocol 0.14's chain-view anchor on the second result type, from `report` -- the SAME chain read that produced the rest of the result, never a fresh peak lookup, which would name a height the data did not come from. - dispatch.rs: answers 0.14's required `claim_loop` as `NotConsulted`, dated when the responder established it has nothing to read. This crate holds no claim loop, so a manufactured `Consulted` with invented counts would be the lie this epic keeps producing. - lib.rs: extends the exhaustive-key tripwire's doc comment for `claim_loop`. KNOWN DEFECT IN THIS DIFF, for whoever picks it up: the new comments attribute the claim loop to dig_ecosystem#3421. That is wrong. #3421 is the PROVER's RewardsChainPort. The claim loop lives in dig-node-service/src/rewards_claim/ and belongs to #3268 (wiring, landed) and #3432 (the 13.2 off-chain seam). A false ticket attribution in a doc comment is the exact defect #3292 was filed for; fix it before this merges. Refs #3329 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claim_loop exhaustive-key gap
`8c37f158` was a salvage checkpoint pushed unverified. It compiles now
(cargo check -p dig-node-core -p dig-node-service: clean, 1m30s). Two
follow-ups landed with it:
- The doc comments in lib.rs and dispatch.rs attributed the claim loop
to dig_ecosystem#3421 (the prover's RewardsChainPort). Wrong: the
claim loop lives in dig-node-service's src/rewards_claim/**, owned by
#3268 (wiring, landed) and #3432 (the SPEC 13.2 off-chain seam).
Corrected both.
- get_payee_reward_claim_status_answers_the_exact_wire_shape asserted
an exhaustive key set of exactly {subject, claim_log} — stale since
8c37f15 added claim_loop to the wire result. Extended the
enumeration to include claim_loop and asserted its NotConsulted
shape, same strength as before (no widening).
Refs #3329
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t left unformatted cargo fmt --all -- --check was red on one line in the f57e46d/8c37f158 salvage; cargo fmt --all now passes clean. Refs #3329 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gone stale under 0.14 cargo test -p dig-node-core --lib caught it: list_reward_distributor_commitments_answers_with_real_values_through_dispatch still asserted the pre-0.14 key set on GetListRewardDistributorCommitments's result, so it failed the moment chain_peak_height/chain_peak_timestamp landed on the wire. Extended the enumeration and added value assertions for both fields, same strength as the sibling GetRewardDistributorResult test above it. Full `cargo test -p dig-node-core --lib`: 1238 passed, 0 failed (was 1237 passed, 1 failed before this commit). Refs #3329 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Verdict: PASS
Independent read-only review at head 1ccc2ede58463d94b6d0e1ae1e4e7deddef6a685 (confirmed unmoved via gh pr view 629 --json headRefOid immediately before this review). Reviewed in a detached worktree at C:/worktrees/dig-node-gate629, diffed against base c72a0ebb50714b8a4d6efdb79987355bb3ebaa7f.
Point 1 -- chain-view anchor from the SAME chain read
Confirmed on both halves.
crates/dig-node-service/src/rewards/chain_port.rs:183--let chain_peak_height = read_chain_peak(source)?;sits insidebuild_report(chain_port.rs:152), the exact closurespawn_blockingruns atchain_port.rs:110(tokio::task::spawn_blocking(move || build_report(source.as_ref(), launcher_id))). Thesnapshotread (read_distributor_guarded, chain_port.rs:163) and the peak read are both synchronous calls inside that one closure body -- same thread, same call, no reordering hazard across an await point.crates/dig-node-core/src/seams/dig_rpc/dispatch.rs:1080-1083and:1131-1134-- both construction sites assignchain_peak_height: report.chain_peak_height, chain_peak_timestamp: report.chain_peak_timestamp,reading straight off theDistributorReportthe port returned. No independentpeak_height()call exists anywhere indispatch.rs(grepped forpeak_heightin that file -- zero hits outside the struct field name itself).
Point 2 -- the two-u64 positional hazard: my verdict is FIX NOW, and the hazard is one join downstream of where the brief points
Confirmed the shape exactly as reported: chain_port.rs:183 binds read_chain_peak's (u64, u64) return to a variable literally named chain_peak_height (singular, height-shaped name for a pair), then chain_port.rs:185-190 passes it positionally as the 4th, single-tuple argument to report_from_snapshot, whose signature (chain_port.rs:212-219) takes it as one parameter chain_peak: (u64, u64) and destructures it inside: let (chain_peak_height, chain_peak_timestamp) = chain_peak; at line 219.
This is real but narrower than the brief frames it -- the hazard is not "if anyone reorders report_from_snapshot's call site": that call has exactly one caller (build_report), one call site, and the whole pair moves as a single opaque tuple value from bind to destructure with no reordering opportunity in between; nothing in this diff swaps height and timestamp. The actual hazard is at the destructuring line (219): if a future edit reorders the tuple's construction in read_chain_peak (line 206, Ok((u64::from(height), timestamp))) without updating the destructure order at line 219, or a second call site is added that destructures the pair in the wrong order, both fields are the same u64 type and nothing type-checks the pairing -- exactly the class of defect the brief is right to worry about, just one join downstream of where it currently points.
Given that: fix now, in this PR. The rename is one line and zero risk, and "later" for a naming lie over two same-typed, order-sensitive values is exactly the deferred-cleanup pattern that turns into a live incident on the next edit that touches this file. Minimal fix: rename the outer binding at line 183 from chain_peak_height to chain_peak (matching the parameter name it is already being passed as). A newtype pair (struct ChainPeak { height: u64, timestamp: u64 }) would be stronger -- it would make a reordered destructure a compile error instead of a silent swap -- but is a bigger change than this PR's stated blast radius; accept the plain rename here and leave the newtype as a follow-up if a second call site to read_chain_peak/report_from_snapshot is ever added.
This is a naming/readability finding, not a correctness defect in the code as it stands today -- I traced every value flow from read_chain_peak's construction to both consumption sites (report_from_snapshot's destructure and the two dispatch.rs echo sites) and the height and timestamp are never swapped anywhere in this diff. Not blocking the PR on it, but recommend the rename be landed before merge since it is a trivial fixup -- non-gating suggestion, not CHANGES-REQUIRED.
Point 3 -- claim_loop: NotConsulted is honest
dispatch.rs:1263-1268(approximate final lines after the fix) constructsClaimLoopObservation::NotConsulted { observed_at: now }wherenowis stamped once, shared withclaim_log'sNotConsulted { observed_at: now }right above it -- both fields dated at the same instant this responder established it has nothing to read.- Grepped the whole workspace (
dig-node-core) forClaimLoopObservation-- the only construction site in the crate is this oneNotConsulted. There is noConsultedarm anywhere, so nothing can manufacture inventeddistributors_known/distributors_claimablecounts. Confirmed againstdig-rpc-protocol 0.14's own type definition (registry cache,types.rs:2147-2166):ClaimLoopObservationis#[serde(deny_unknown_fields)]-tagged with exactlyConsulted{observed_at, distributors_known, distributors_claimable, state}/NotConsulted{observed_at}-- the enum shape itself forbids a count riding theNotConsultedarm even if a future edit tried.
Point 4 -- ChainPeakUnavailable refusal, never a 0 substitute
crates/dig-node-core/src/rewards/port.rsaddsChainPortError::ChainPeakUnavailableas a new enum variant with a doc explaining0-as-absence is forbidden (SPEC §4.5).chain_port.rs:197-207(read_chain_peak): bothpeak_height()andblock_timestamp()reads use.ok_or(ChainPortError::ChainPeakUnavailable)?-- an early return, not a.unwrap_or(0)-- on theNonecase; a genuine I/O error maps toChainPortError::Other(...)separately. No0literal appears anywhere near this function.- The refusal reaches the RPC caller:
dispatch.rs:115-121(reward_chain_port_error_response) adds a dedicated arm mappingChainPeakUnavailableto aCONTROL_ERRORJSON-RPC error with machine codeREWARD_CHAIN_PEAK_UNAVAILABLE_MACHINE = "REWARD_CHAIN_PEAK_UNAVAILABLE", reusing the same one dispatch point (reward_chain_port_error_response) both reward-read methods already funnel everyChainPortErrorthrough -- not a new, possibly-divergent error path.
Point 5 -- the three tripwires
dependency_tree.rs::the_workspace_carries_exactly_one_module_wire_crate-- the enumeration (exactly-one-resolved-version assertion) is unchanged in kind; only the literal moved"0.12."to"0.14."(line ~128) to track the bump, and the doc above it was updated to name the new upstream versions (dig-peer0.16.0,dig-download0.25.0,dig-peer-selector0.14.0). The assertion still refuses to widen to accept a set (comment explicitly reaffirms#836/#1576). This is exactly what a version-tracking literal should do on a legitimate bump -- not a weakening.the_peer_client_and_pull_engine_are_not_duplicated-- not touched by this diff at all (confirmed viagit diff--dependency_tree.rs's only hunk is the one above). Its assertion is unchanged.get_payee_reward_claim_status_answers_the_exact_wire_shape(lib.rs) -- the exact-keyBTreeSetassertion was extended, not weakened:["subject", "claim_log"]to["subject", "claim_log", "claim_loop"](an addition to the set, the old two keys still required), plus a new positive assertionresult["claim_loop"]["outcome"] == "not_consulted"and a new negative assertion thatclaims_submitted_countnever rides besideNotConsulted-- strictly more coverage, none removed.list_reward_distributor_commitments_answers_with_real_values_through_dispatch(lib.rs) -- same shape: the exact-key sets for bothGetRewardDistributorResultandListRewardDistributorCommitmentsResultgained"chain_peak_height", "chain_peak_timestamp"as additions, plus newassert_eq!lines actually checking those values against the fixture'sreport.chain_peak_height/chain_peak_timestamp(which the fixture builder --lib.rs:9906-9913-- deliberately derives from distinct seeded constants,9_000_000 + seedvs1_700_190_000 + seed, specifically so the two fields cannot pass by coincidentally sharing a value). This is a real vacuity-gate pass: reverting only the fix (e.g. swapping which field gets which value, or omitting one) would fail this test, because the two source values are provably distinct by construction.
Point 6 -- ticket attribution correction
dispatch.rs:1252-1256andlib.rs:10290-10296-- both doc-comment sites now read "dig-node-service'ssrc/rewards_claim/**(dig_ecosystem#3268 wired it, landed; #3432 is the SPEC §13.2 off-chain seam), never dig_ecosystem#3421 (that ticket is the prover'sRewardsChainPort)". Grepped the full diff and the surrounding crate for stray#3421claim-loop attributions -- none remain; both sites are corrected consistently and match the brief's stated correction (#3268/#3432, not #3421).- Noted for awareness, out of scope for this PR (file not touched by this diff):
dig-node-service/src/rewards_claim/config.rs:65still says "(DIG-Network/dig_ecosystem#3268, not yet landed)", which reads as inconsistent with this PR's new "#3268 wired it, landed" language elsewhere. This predates this PR (not in the diff) and is not one of the four numbered facts in scope -- flagging only so it does not get missed as a future doc-drift ticket, not blocking this PR.
Evidence I could not independently reproduce
cargo check -p dig-node-core -p dig-node-serviceand the fullcargo test -p dig-node-core --lib(1238 passed) reported in the brief: could not test -- a scopedcargo checkon this cold worktree did not complete inside a bounded 100s window (first-run transitive rebuild, consistent with the brief's own warning about cold-worktree cost) and I did not extend the wait, per the hard constraint against ending a turn on a build. I verified type-correctness statically instead: read the vendoreddig-rpc-protocol-0.14.0source directly from the local cargo registry cache (~/.cargo/registry/src/.../dig-rpc-protocol-0.14.0/src/types.rs) and confirmedGetRewardDistributorResult/ListRewardDistributorCommitmentsResultboth require non-optionalchain_peak_height: u64/chain_peak_timestamp: u64(lines 1845, 1852, 1955, 1957) andPayeeClaimStatusrequiresclaim_loop: ClaimLoopObservation(line 2228) withdeny_unknown_fields-- matching every construction site in the diff field-for-field. Also confirmed theChainSourcetrait signatures (peak_height() -> Result<Option<u32>, E>,block_timestamp(u32) -> Result<Option<u64>, E>, found via grep across existing implementors) matchread_chain_peak's usage including the u32-to-u64 widening at line 206.cargo fmt --all -- --check: reproduced myself, exit 0 (fast, no compile required).- No leftover
cargo/rustcprocesses from my own check attempt (tasklistclean after timeout).
Summary
No blocking defects found. Point 2's naming issue is real and I recommend fixing it in this PR (see above), but it is not a correctness defect as shipped -- non-gating. PASS at 1ccc2ede58463d94b6d0e1ae1e4e7deddef6a685.
🤖 Generated with Claude Code
Security audit — PASSHead audited: Read-only worktree at Per-prompt answers1. Supply-chain / lock resolution. 2. Duplicate-stack tripwire. 3. Chain-view anchor consistency — swap and mismatch. Answer to "is a swapped height/timestamp exploitable, or merely wrong": as shipped, not swapped, so not currently exploitable. If it WERE swapped, impact is bounded: 4. 5. Salvage risk. Identified the two salvage commits by commit body text (not paraphrase — read raw Scope auditedFiles: Not covered / could not test
No LIVE vulnerability found. One defense-in-depth note (prompt 3's tuple-typed anchor pair — name it, don't gate on it): recommend a follow-up ticket for a named struct/newtype instead of |
Both members of read_chain_peak's (u64, u64) return are u64, so the misleading name would let a future construction/destructure swap go unseen by the compiler. Rename before rewriting around these lines for #3421 (gate finding on PR #629). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
dig-rpc-protocol0.14 cascade:dig-rpc-protocol0.12->0.14,dig-peer0.15->0.16,dig-download0.24->0.25 (both entries),dig-peer-selector0.13->0.14.dig-rewards-coinstays at 0.8 (not part of this cascade).Cargo.lockre-resolved: exactly one entry each at the new versions.dependency_tree.rsliteral update,entry_set_stalecompensation check.Test plan
cargo build -p dig-node-core -p dig-node-servicedependency_tree.rsgreen with"0.14."literal, exactly-one assertion unchangedthe_peer_client_and_pull_engine_are_not_duplicatedstatus reportedcargo fmt --all -- --checkexit 0Refs #3329
🤖 Generated with Claude Code