Skip to content

feat(rewards)!: recoverable_base_units can say "not recoverable" - #631

Merged
MichaelTaylor3d merged 4 commits into
developfrom
loop/3442-recoverable-absent
Oct 1, 2026
Merged

MichaelTaylor3d merged 4 commits into
developfrom
loop/3442-recoverable-absent

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

dig_ecosystem#3442 phase A: recoverable_base_units is Option<u64> end to end, so "the chain refuses this clawback" (None) is never rendered as 0 (the #3439 defect).

  • Adopts dig-rpc-protocol 0.14 -> 0.15, dig-peer 0.16 -> 0.17, dig-download 0.25 -> 0.26, dig-peer-selector 0.14 -> 0.15 (exactly one copy of each in Cargo.lock; dependency_tree.rs literals updated).
  • CommitmentSlot.recoverable_base_units is Option<u64>; chain_port.rs carries dig-rewards-coin 0.10's answer unchanged.
  • dig.listRewardDistributorCommitments dispatch maps the Option straight onto the wire; the TEMPORARY whole-call refusal is deleted.
  • ChainPortError::InvalidWithdrawalShare is KEPT: still produced by chain_port.rs (bps does not fit u16 / > 10_000) and by dispatch's range_checked_report.

Tests (each observed RED under its revert)

  • unrecoverable_commitment_serializes_recoverable_as_present_null (dispatch; None -> 0 made it fail)
  • zero_recoverable_commitment_serializes_as_zero (dispatch; Some(0) -> None made it fail)
  • positive_recoverable_commitment_serializes_verbatim (dispatch; Some(x) -> 0 made it fail)
  • an_epoch_started_commitment_is_reported_as_none_not_zero (chain_port, real simulator launch)
  • a_not_started_commitment_with_zero_share_is_reported_as_some_zero (chain_port, real launch with bps 0)

Local: fmt, clippy --workspace --all-targets -D warnings, cargo test -p dig-node-core -p dig-node-service all green.

Out of scope (split per bump-deps-on-touch): dig-dht 0.16 is blocked (dig_ecosystem#3223); the dig-constants split is dig_ecosystem#3193. No version bump (develop flow).

RELEASE ORDERING (loop-decider, #3442): do not release a user-facing dig-node on dig-rpc-protocol 0.15 until dig-app's Option-aware decoder (DIG-Network/dig_ecosystem#3446) has landed.

Refs DIG-Network/dig_ecosystem#3442

🤖 Generated with Claude Code

Refs DIG-Network/dig_ecosystem#3442

Salvaged phase-A WIP; does not yet build -- needs dig-rpc-protocol 0.15 cascade.
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review October 1, 2026 07:13
MichaelTaylor3d and others added 3 commits October 1, 2026 00:14
…ystem#3442)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tem#3442)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d force-pushed the loop/3442-recoverable-absent branch from 1a7f27a to dd5bb22 Compare October 1, 2026 07:14

@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.

Gate verdict: PASS at head dd5bb22 (dig_ecosystem#3442). No blocking findings; zero threads opened.

Checked:

  • No None-to-0 path: unwrap_or / unwrap_or_default / as u64 do not touch recoverable anywhere in the post-PR tree. chain_port.rs:232 carries commitment.recoverable_base_units() verbatim; dispatch.rs:1126 maps the Option straight onto the wire. The temporary whole-call refusal is gone.
  • Tests sit at the decision: three tests drive handle_rpc for dig.listRewardDistributorCommitments and assert the serialized JSON (key present with null, 0, 4500). Two more use a real simulator launch through RealRewardsChainPort. The nearest wrong implementations (None->0, Some(0)->None, Some(x)->0) each fail at least one test.
  • Peak anchor: snapshot.observed() is minted only by dig-rewards-coin 0.10 read_distributor, which refuses an absent peak height or timestamp (state.rs:1441-1448) and holds them as non-Option fields. SPEC 4.5 ("never 0 for an absent peak") holds, and the peak is now from the same read (SPEC 4.6 cl.6).
  • dependency_tree.rs still asserts exactly-one dig-rpc-protocol, now 0.15. Cargo.lock shows rpc 0.15, peer 0.17, download 0.26, selector 0.15.
  • Commit headers are all 100 characters or fewer; the PR body carries the release-ordering line (dig-app #3446). All checks green, merge state CLEAN.

Non-blocking notes (no thread):

  1. ChainPortError::ChainPeakUnavailable (port.rs:185, doc at port.rs:275) is no longer produced by the real adapter, because dig-rewards-coin refuses upstream. The variant is still a valid contract for other adapters and is still mapped in dispatch.rs:115. A follow-up could document that, or add a test that an absent peak refuses the whole call. No dig-node test pins that guarantee now.
  2. The first commit message ("WIP anchor ... does not yet build") is fine only because the PR is squash-merged.

Not run: no build or tests locally (read-only gate); relied on the CI "Test + coverage", Clippy and Rustfmt checks, all pass.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security verdict: PASS

Head audited: dd5bb22 (dig_ecosystem#3442). Read-only, from git objects plus dig-rewards-coin 0.10.0 source in the cargo registry.

Money figure (None vs 0 vs share): clear. chain_port.rs carries commitment.recoverable_base_units() verbatim. In dig-rewards-coin 0.10.0, state.rs:325 and state.rs:1464 compute it as Some(share) only when peak_timestamp < epoch_start, otherwise None. Some(0) is reachable only when the share is 0 bps. The None path cannot produce 0 or the full share. dispatch.rs:1116-1126 maps the Option straight onto the 0.15 wire with no unwrap_or, and the .map no longer allocates an error path. git grep recoverable_base_units shows no other consumer in the tree that could coerce it. Errors still refuse the whole call through range_checked_report (dispatch.rs:156) and reward_chain_port_error_response. They are never rendered as a figure.

Removed second peak read: clear, and better than before. peak_height and peak_timestamp now come from snapshot.observed(), the same ChainObservation the figure was computed against, so they cannot disagree with recoverable. In state.rs:1441-1449, read_distributor refuses (malformed / chain_unavailable) when the peak height or its timestamp is absent, so the "never 0 as stand-in for an absent peak" guarantee (SPEC 4.5) holds inside the library. u64::from(u32) is lossless. Dropping the independent read also closes the skew window between the two reads.

Panics from RPC input: none added. The deleted code had ? only. The new .map has no unwrap or expect. The expect calls inside dig-rewards-coin are guarded by its B1 bps check, which runs before the Commitment is built. InvalidWithdrawalShare is still produced at chain_port.rs (try_into and the >10_000 check) and at dispatch.rs:158.

Dependencies: Cargo.lock diff is exactly 5 version and checksum bumps (dig-download 0.26.0, dig-peer 0.17.0, dig-peer-selector 0.15.0, dig-rewards-coin 0.10.0, dig-rpc-protocol 0.15.0). No package was added or removed. Exactly one each of dig-peer 0.17.0, dig-nat 0.21.2, dig-tls 0.4.1 and dig-rpc-protocol 0.15.0. dig-constants has three versions, but it is outside the diff and was already so. tests/dependency_tree.rs is updated to 0.15.

Authz/exposure: unchanged. The method is still Tier::Control and open on POST /, as before. The only change is that the wire can now say "refused" where the old build refused the whole call.

Tests: the new tests (unrecoverable_commitment_serializes_recoverable_as_present_null, zero_recoverable_commitment_serializes_as_zero, positive_recoverable_commitment_serializes_verbatim, and the two rewards_chain_port_a3.rs tests) sit at the decision. They drive the real adapter against the simulator-backed fixture and assert None against Some(0) at both layers. They are not below the decision.

Defence-in-depth (no gate, ticket suggested):

  1. ChainPortError::ChainPeakUnavailable (port.rs:185, dispatch.rs:115) is no longer produced by chain_port, since the library now refuses an absent peak. The variant and its doc (port.rs:275) are stale. Remove it or document it as reserved.
  2. chain_port.rs observed_at ... .unwrap_or(0) is pre-existing wall-clock metadata, not a money figure. Out of scope.

Not covered: I did not build or run the tests, and I did not read the dig-peer, dig-download or dig-peer-selector source for the dependency-only bumps. I relied on the Cargo.lock diff showing no new transitive packages. The release-ordering constraint (no user-facing 0.15 release until dig-app #3446) is for the orchestrator.

@MichaelTaylor3d
MichaelTaylor3d merged commit c4ea50d into develop Oct 1, 2026
14 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/3442-recoverable-absent branch October 1, 2026 07:40
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