Skip to content

build(deps): bump dig-rpc-protocol cascade to 0.14.0 - #629

Merged
MichaelTaylor3d merged 7 commits into
developfrom
loop/rpc14-cascade
Sep 27, 2026
Merged

MichaelTaylor3d merged 7 commits into
developfrom
loop/rpc14-cascade

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

Summary

  • Moves the four-pin dig-rpc-protocol 0.14 cascade: dig-rpc-protocol 0.12->0.14, dig-peer 0.15->0.16, dig-download 0.24->0.25 (both entries), dig-peer-selector 0.13->0.14.
  • dig-rewards-coin stays at 0.8 (not part of this cascade).
  • Cargo.lock re-resolved: exactly one entry each at the new versions.
  • Work in progress: compiler-forced field fill, dependency_tree.rs literal update, entry_set_stale compensation check.

Test plan

  • cargo build -p dig-node-core -p dig-node-service
  • dependency_tree.rs green with "0.14." literal, exactly-one assertion unchanged
  • the_peer_client_and_pull_engine_are_not_duplicated status reported
  • cargo fmt --all -- --check exit 0

Refs #3329

🤖 Generated with Claude Code

MichaelTaylor3d and others added 7 commits September 27, 2026 05:13
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 MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 inside build_report (chain_port.rs:152), the exact closure spawn_blocking runs at chain_port.rs:110 (tokio::task::spawn_blocking(move || build_report(source.as_ref(), launcher_id))). The snapshot read (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-1083 and :1131-1134 -- both construction sites assign chain_peak_height: report.chain_peak_height, chain_peak_timestamp: report.chain_peak_timestamp, reading straight off the DistributorReport the port returned. No independent peak_height() call exists anywhere in dispatch.rs (grepped for peak_height in 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) constructs ClaimLoopObservation::NotConsulted { observed_at: now } where now is stamped once, shared with claim_log's NotConsulted { 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) for ClaimLoopObservation -- the only construction site in the crate is this one NotConsulted. There is no Consulted arm anywhere, so nothing can manufacture invented distributors_known/distributors_claimable counts. Confirmed against dig-rpc-protocol 0.14's own type definition (registry cache, types.rs:2147-2166): ClaimLoopObservation is #[serde(deny_unknown_fields)]-tagged with exactly Consulted{observed_at, distributors_known, distributors_claimable, state} / NotConsulted{observed_at} -- the enum shape itself forbids a count riding the NotConsulted arm even if a future edit tried.

Point 4 -- ChainPeakUnavailable refusal, never a 0 substitute

  • crates/dig-node-core/src/rewards/port.rs adds ChainPortError::ChainPeakUnavailable as a new enum variant with a doc explaining 0-as-absence is forbidden (SPEC §4.5).
  • chain_port.rs:197-207 (read_chain_peak): both peak_height() and block_timestamp() reads use .ok_or(ChainPortError::ChainPeakUnavailable)? -- an early return, not a .unwrap_or(0) -- on the None case; a genuine I/O error maps to ChainPortError::Other(...) separately. No 0 literal appears anywhere near this function.
  • The refusal reaches the RPC caller: dispatch.rs:115-121 (reward_chain_port_error_response) adds a dedicated arm mapping ChainPeakUnavailable to a CONTROL_ERROR JSON-RPC error with machine code REWARD_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 every ChainPortError through -- 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-peer 0.16.0, dig-download 0.25.0, dig-peer-selector 0.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 via git 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-key BTreeSet assertion 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 assertion result["claim_loop"]["outcome"] == "not_consulted" and a new negative assertion that claims_submitted_count never rides beside NotConsulted -- strictly more coverage, none removed.
  • list_reward_distributor_commitments_answers_with_real_values_through_dispatch (lib.rs) -- same shape: the exact-key sets for both GetRewardDistributorResult and ListRewardDistributorCommitmentsResult gained "chain_peak_height", "chain_peak_timestamp" as additions, plus new assert_eq! lines actually checking those values against the fixture's report.chain_peak_height/chain_peak_timestamp (which the fixture builder -- lib.rs:9906-9913 -- deliberately derives from distinct seeded constants, 9_000_000 + seed vs 1_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-1256 and lib.rs:10290-10296 -- both doc-comment sites now read "dig-node-service's src/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's RewardsChainPort)". Grepped the full diff and the surrounding crate for stray #3421 claim-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:65 still 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-service and the full cargo test -p dig-node-core --lib (1238 passed) reported in the brief: could not test -- a scoped cargo check on 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 vendored dig-rpc-protocol-0.14.0 source directly from the local cargo registry cache (~/.cargo/registry/src/.../dig-rpc-protocol-0.14.0/src/types.rs) and confirmed GetRewardDistributorResult/ListRewardDistributorCommitmentsResult both require non-optional chain_peak_height: u64/chain_peak_timestamp: u64 (lines 1845, 1852, 1955, 1957) and PayeeClaimStatus requires claim_loop: ClaimLoopObservation (line 2228) with deny_unknown_fields -- matching every construction site in the diff field-for-field. Also confirmed the ChainSource trait signatures (peak_height() -> Result<Option<u32>, E>, block_timestamp(u32) -> Result<Option<u64>, E>, found via grep across existing implementors) match read_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/rustc processes from my own check attempt (tasklist clean 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

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Security audit — PASS

Head audited: 1ccc2ede58463d94b6d0e1ae1e4e7deddef6a685 (matches PR #629's current head; base 2e08447d81420badf56c18b6e7852561193c3d7c)

Read-only worktree at C:/worktrees/dig-node-sec629, removed after the audit.

Per-prompt answers

1. Supply-chain / lock resolution. Cargo.lock resolves exactly one of each: dig-rpc-protocol 0.14.0, dig-peer 0.16.0, dig-download 0.25.0, dig-peer-selector 0.14.0, dig-rewards-coin 0.8.0 (unchanged) — all source = "registry+https://github.com/rust-lang/crates.io-index", no git/path source anywhere in the diff. Package count in the lock is unchanged (800 → 800 [[package]] entries) — no new transitive dependency entered. Diffed the cached extracted sources for all four bumped crates (0.12→0.14, 0.15→0.16, 0.24→0.25, 0.13→0.14 under ~/.cargo/registry/src/…): identical file listing (no added/removed files), and every one carries build = false and no proc-macro = true — no crate gained a build.rs or proc-macro.

2. Duplicate-stack tripwire. the_peer_client_and_pull_engine_are_not_duplicated (crates/dig-node-core/tests/dependency_tree.rs:140-167) still asserts exactly-one for the full six-crate list (dig-peer, dig-download, dig-nat, dig-tls, dig-dht, dig-peer-selector) — that function body is untouched by this PR (only the sibling dig-rpc-protocol test's version-literal doc/assertion changed 0.12→0.14). Confirmed each of the six resolves to exactly one version in the new lock (grep -c '^name = "<crate>"$' = 1 for all six). SealingIdentity::replay_guard sharing is therefore preserved — no second independently-compiled dig-peer/mTLS stack was introduced. Not weakened.

3. Chain-view anchor consistency — swap and mismatch. chain_port.rs:183 binds read_chain_peak(source)?'s (u64, u64) return to chain_peak_height and passes it positionally through report_from_snapshot(..., chain_peak_height) (param type chain_peak: (u64, u64) at line 217), which destructures it at line 219 as let (chain_peak_height, chain_peak_timestamp) = chain_peak; — same order read_chain_peak produced it in (Ok((height, timestamp)) at line 206). No swap exists in this diff; the misleading variable name (holding a tuple, not a scalar) is a real latent hazard the compiler cannot catch on a future edit — worth a follow-up ticket (e.g. a newtype or named struct instead of (u64,u64)), but it is defense-in-depth, not a live defect, since the two positions agree today and both fields flow straight through to dispatch.rs from the same report (verified: no independent re-read at the RPC seam).

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: chain_peak_height/chain_peak_timestamp are purely informational anchor fields on GetRewardDistributorResult/ListRewardDistributorCommitmentsResult — they gate no spend, no clawback decision, no comparison in this codebase (current_distributor_epoch is computed from epoch_end/first_epoch_start/epoch_seconds, all independent of chain_peak). A swap would misinform a caller's own external reasoning about chain freshness, not cause this node to authorize or refuse anything incorrectly. Separately: the snapshot read (read_distributor_guarded) and the peak read (read_chain_peak) are two sequential (non-atomic) reads inside the same spawn_blocking, so a block landing between them could make the peak reflect a height slightly ahead of the distributor snapshot. This is a narrow, one-block-scale skew on an informational field, explicitly weighed against the worse alternative (reading the peak later at the RPC seam) in the code's own comment — not exploitable for fund manipulation, defense-in-depth at most.

4. claim_loop/ChainPeakUnavailable leakage + two-axis check. Both error bodies were read: ChainPortError::ChainPeakUnavailable's JSON-RPC mapping (dispatch.rs) carries only a generic message and the REWARD_CHAIN_PEAK_UNAVAILABLE_MACHINE code — no path, host, or stack detail. claim_loop: NotConsulted { observed_at } carries only a wall-clock stamp, same shape as the pre-existing claim_log. The exact-key-set test (lib.rs::get_payee_reward_claim_status_answers_the_exact_wire_shape, updated in this diff) still asserts the full body key set by equality ({"subject","claim_log","claim_loop"}) and explicitly asserts no monetary amount / payout puzzle hash anywhere — so nothing extra can leak without that test going red. Axis 1 (Tier::Control): GetPayeeRewardClaimStatus is still asserted Tier::Control (lib.rs:10305), unchanged by this PR. Axis 2 (token gate, server.rs::rpc(), untouched by this PR — not in the diff's 8 files): GetPayeeRewardClaimStatus is one of is_node_local_reward_read's three methods and is folded into the same token-gated block as cache.* (server.rs ~line 1289) — requires the local control token or a paired token; it is NOT in the anonymous-OPEN set (GetRewardDistributor/ListRewardDistributorCommitments only, which take a caller-supplied launcher_id by design, #3351, rate-limited separately). Both axes hold; no re-conflation of #3351's shape.

5. Salvage risk. Identified the two salvage commits by commit body text (not paraphrase — read raw git log -1 --format=%B output): f57e46d1 ("UNVERIFIED: not yet compiled... pushed as a checkpoint") and 8c37f158 ("UNVERIFIED CHECKPOINT... This has NOT compiled"). Diffed each commit's changes individually (git show --unified=0): both are pure additive struct-field/doc-comment/test-assertion changes — no hardcoded path or host, no credential-shaped literal, no test writing outside a tempdir, no #[cfg(test)] accessor left ungated, and no visibility widening (the two new pub chain_peak_height/pub chain_peak_timestamp fields are new fields on an already-pub struct, not a widened existing field). 8c37f158's own commit body flagged its own defect (wrong ticket attribution, #3421 vs #3268/#3432) — confirmed the follow-up 559a1cef and current head text now correctly cite #3268/#3432 (never #3421). Nothing security-relevant survived unexamined from either salvage commit.

Scope audited

Files: Cargo.lock, crates/dig-node-core/Cargo.toml, crates/dig-node-core/src/lib.rs, crates/dig-node-core/src/rewards/port.rs, crates/dig-node-core/src/seams/dig_rpc/dispatch.rs, crates/dig-node-core/tests/dependency_tree.rs, crates/dig-node-service/Cargo.toml, crates/dig-node-service/src/rewards/chain_port.rs — the full 8-file diff. Cross-checked crates/dig-node-service/src/server.rs (untouched by this PR) for the token-gate axis.

Not covered / could not test

  • Did not re-run cargo build/cargo test (workspace-scope forbidden per brief, and a scoped run risked outliving the turn); relied on static inspection of Cargo.lock, the cached registry sources, and the test source code itself, which is authoritative for what the reported test runs assert. Treat the correctness gate's reported exit codes (cargo check, cargo test, cargo fmt) as unverified by me directly — could not test.
  • Did not audit the crates.io-hosted source of dig-rpc-protocol/dig-peer/dig-download/dig-peer-selector beyond file-listing/Cargo.toml diffs (i.e., did not line-by-line review the new code inside those external crates) — out of this repo's diff and reasonable given no new files/build scripts appeared.
  • Out of scope per brief: the decision to bump, dig-rewards-coin staying at 0.8, #3274, #3421/#3423/#3432.

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 (u64, u64) so a future edit can't silently swap the pair; not required before merge.

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review September 27, 2026 17:01
@MichaelTaylor3d
MichaelTaylor3d merged commit 7a19d8d into develop Sep 27, 2026
14 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/rpc14-cascade branch September 27, 2026 17:01
MichaelTaylor3d added a commit that referenced this pull request Sep 27, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant