From cf5009ddf764c73b3640302a9ae08527f62f0468 Mon Sep 17 00:00:00 2001 From: mt-dev <5665004+MichaelTaylor3d@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:10:46 -0700 Subject: [PATCH 1/2] refactor(rewards): remove never-emitted ChainPeakUnavailable (dig_ecosystem#3448) (#636) * refactor(rewards): remove never-emitted ChainPeakUnavailable Zero producers and zero consumers; the real adapter already refuses an absent peak via read_distributor (Other/Unavailable). Fix the chain_peak_height doc to name that refusal. Refs DIG-Network/dig_ecosystem#3448 Co-Authored-By: Claude Sonnet 5.5 * test(rewards): pin absent-peak refusal at the chain port Co-Authored-By: Claude Sonnet 5.5 --------- Co-authored-by: Claude Sonnet 5.5 --- crates/dig-node-core/src/rewards/port.rs | 14 ++++------ .../src/seams/dig_rpc/dispatch.rs | 13 ---------- .../tests/common/rewards_fixture.rs | 13 +++++++--- .../tests/rewards_chain_port_a3.rs | 26 +++++++++++++++++-- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/crates/dig-node-core/src/rewards/port.rs b/crates/dig-node-core/src/rewards/port.rs index 5434e321..959bcaf0 100644 --- a/crates/dig-node-core/src/rewards/port.rs +++ b/crates/dig-node-core/src/rewards/port.rs @@ -176,13 +176,6 @@ pub enum ChainPortError { /// SPEC §3.7 clause 4 applies to every attacker-adjacent string, and a chain error is not /// exempt). Other(String), - /// dig-rpc-protocol 0.14 (dig_ecosystem#3262/#3329): the adapter completed its distributor - /// read but could not obtain a chain peak height/timestamp from the SAME read to fill - /// [`DistributorReport::chain_peak_height`]/[`DistributorReport::chain_peak_timestamp`]. Per - /// SPEC §4.5 both fields are required and never `0`-as-absence, so a responder that cannot - /// anchor its answer to a chain view MUST refuse the whole call rather than answer with an - /// invented, stale, or independently-read peak. - ChainPeakUnavailable, } /// One clawback commitment slot, as `dig.listRewardDistributorCommitments` (SPEC §7.4 clause 5) @@ -271,8 +264,11 @@ pub struct DistributorReport { /// independent `peak_height()` call: a peak read separately from the snapshot names a height /// the data did not come from, which is wrong in the most convincing possible way — a plausible /// number beside stale data, with nothing erroring. Required, never `0`-as-absence: an adapter - /// that cannot obtain the peak alongside its read MUST refuse the whole call - /// (`ChainPortError::ChainPeakUnavailable`) instead of reporting one. + /// that cannot obtain the peak alongside its read MUST refuse the whole call instead of + /// reporting one. The real adapter does: `dig_rewards_coin`'s `read_distributor` refuses to + /// build a `ChainObservation` without a peak (`Malformed`, surfaced as + /// `ChainPortError::Other`; a failed read is `ChainUnavailable`, surfaced as + /// `ChainPortError::Unavailable`), so this field is never `0`-as-absence. pub chain_peak_height: u64, /// `block_timestamp(chain_peak_height)` from that SAME chain read — the chain clock /// `entry_set_stale` is computed against, never the wall clock `observed_at` uses. Same diff --git a/crates/dig-node-core/src/seams/dig_rpc/dispatch.rs b/crates/dig-node-core/src/seams/dig_rpc/dispatch.rs index 0c92119a..b318cdb5 100644 --- a/crates/dig-node-core/src/seams/dig_rpc/dispatch.rs +++ b/crates/dig-node-core/src/seams/dig_rpc/dispatch.rs @@ -74,13 +74,6 @@ const REWARD_INVALID_WITHDRAWAL_SHARE_MACHINE: &str = "REWARD_INVALID_WITHDRAWAL /// `REWARD_INVALID_WITHDRAWAL_SHARE_MACHINE`'s sibling shape. const REWARD_ZERO_IDENTITY_MACHINE: &str = "REWARD_ZERO_IDENTITY"; -/// dig_ecosystem#3262/#3329: the machine code for [`ChainPortError::ChainPeakUnavailable`] — the -/// adapter's distributor read succeeded but it could not anchor a `chain_peak_height`/ -/// `chain_peak_timestamp` from that SAME read, so the whole call is refused rather than answered -/// with an invented or independently-read peak (SPEC §4.5). Sibling shape to the other -/// reward-distributor refusals above. -const REWARD_CHAIN_PEAK_UNAVAILABLE_MACHINE: &str = "REWARD_CHAIN_PEAK_UNAVAILABLE"; - /// Maps a [`ChainPortError`] to the JSON-RPC error response for both reward-distributor read /// methods (dig_ecosystem#3269 unit 2) — one mapping so `dig.getRewardDistributor` and /// `dig.listRewardDistributorCommitments` can never disagree about how a given port failure reads @@ -112,12 +105,6 @@ fn reward_chain_port_error_response(id: &Value, error: &ChainPortError) -> Value "message": format!("reward-distributor chain read failed: {msg}"), "data": { "code": "CONTROL_ERROR", "origin": "control" } }}), - ChainPortError::ChainPeakUnavailable => json!({"jsonrpc":"2.0","id":id,"error":{ - "code": CONTROL_ERROR, - "message": "distributor read succeeded but no chain peak height/timestamp from that \ - same read was available to anchor the result", - "data": { "code": REWARD_CHAIN_PEAK_UNAVAILABLE_MACHINE, "origin": "control" } - }}), } } diff --git a/crates/dig-node-service/tests/common/rewards_fixture.rs b/crates/dig-node-service/tests/common/rewards_fixture.rs index e38dd80d..a98107d0 100644 --- a/crates/dig-node-service/tests/common/rewards_fixture.rs +++ b/crates/dig-node-service/tests/common/rewards_fixture.rs @@ -234,6 +234,14 @@ pub fn launch_fixture_with_approval( /// `read_distributor_guarded`/`read_launch_comment` read — mirrors /// `dig-rewards-coin::tests::simulator::chain_source_with_gaps`. pub fn mock_chain_source(fixture: &LaunchedFixture) -> MockChainSource { + let peak = fixture.sim.height(); + mock_chain_source_without_peak(fixture).with_peak(peak) +} + +/// [`mock_chain_source`] minus `with_peak`: every coin, spend, lineage and block timestamp, but a +/// chain that reports no peak height. A peak once set cannot be unset on `MockChainSource`, so +/// the peak-less source has to be built first and the peak added by the caller that wants one. +pub fn mock_chain_source_without_peak(fixture: &LaunchedFixture) -> MockChainSource { let singleton_members = [fixture.launcher_id, fixture.distributor_coin_id]; // The eve coin: `read_distributor` needs the SPEND that consumed it, not any record it @@ -274,11 +282,10 @@ pub fn mock_chain_source(fixture: &LaunchedFixture) -> MockChainSource { ), ); - let peak = fixture.sim.height(); - for height in 0..=peak { + for height in 0..=fixture.sim.height() { source = source.with_timestamp(height, u64::from(height) * 1_000 + 1); } - source.with_peak(peak) + source } // --------------------------------------------------------------------------------------------- diff --git a/crates/dig-node-service/tests/rewards_chain_port_a3.rs b/crates/dig-node-service/tests/rewards_chain_port_a3.rs index def878a1..73976576 100644 --- a/crates/dig-node-service/tests/rewards_chain_port_a3.rs +++ b/crates/dig-node-service/tests/rewards_chain_port_a3.rs @@ -40,16 +40,38 @@ mod common; use std::sync::Arc; use dig_chainsource_interface::MockChainSource; -use dig_node_core::rewards::port::RewardsChainPort; +use dig_node_core::rewards::port::{ChainPortError, RewardsChainPort}; use dig_node_core::Node; use dig_node_service::rewards::RealRewardsChainPort; use dig_rewards_coin::constants::WITHDRAWAL_SHARE_BPS; use common::rewards_fixture::{ launch_fixture, launch_funded_admitted_fixture_with_shape, mock_chain_source, - mock_chain_source_for_funded_fixture_with_clock, FIRST_EPOCH_START, TEST_EPOCH_SECONDS, + mock_chain_source_for_funded_fixture_with_clock, mock_chain_source_without_peak, + FIRST_EPOCH_START, TEST_EPOCH_SECONDS, }; +/// dig_ecosystem#3448: a chain that reports no peak refuses the whole report -- never a `0` peak. +/// Everything else is the real launch, so the read reaches the peak step (a `NotADistributor` or +/// `Unavailable` refusal would fail the message assertion): the refusal is `read_distributor`'s +/// own "no peak height" `Malformed`, surfaced as `ChainPortError::Other`. +#[tokio::test(flavor = "multi_thread")] +async fn distributor_report_refuses_when_the_chain_has_no_peak() { + let fixture = launch_fixture().expect("a real distributor launches cleanly in the simulator"); + let source = mock_chain_source_without_peak(&fixture); + let port = RealRewardsChainPort::::new(Arc::new(source)); + + let result = port.distributor_report(fixture.launcher_id.into()).await; + + match result { + Err(ChainPortError::Other(msg)) => assert!( + msg.contains("no peak height"), + "the refusal must come from the peak read, got: {msg}" + ), + other => panic!("an absent peak must refuse with Other(\"no peak height\"), got {other:?}"), + } +} + /// A3: `RealRewardsChainPort::distributor_report` — the real production adapter, driven by a /// `MockChainSource` loaded from a real simulator launch — reports the values launched with, /// including `store_id`/`root`, which can only be right if the launcher's parent spend was From e8097355ecbcf913b0df4424ad18199462dd775e Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Mon, 5 Oct 2026 14:35:13 -0700 Subject: [PATCH 2/2] chore(release): v0.262.2 -- remove never-emitted ChainPeakUnavailable Refs DIG-Network/dig_ecosystem#3448 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 5 +++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46528fdc..e1dcf740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project are documented here. This project adheres to [Semantic Versioning](https://semver.org) and [Conventional Commits](https://www.conventionalcommits.org). +## [0.262.2] - 2026-10-05 + +### Refactor +- **rewards:** Remove never-emitted `ChainPortError::ChainPeakUnavailable` and its `REWARD_CHAIN_PEAK_UNAVAILABLE` machine code; an absent chain peak keeps refusing as `-32032` `CONTROL_ERROR` and is now pinned by a test (dig_ecosystem#3448) (#636) + ## [0.255.0] - 2026-09-07 ### Chores diff --git a/Cargo.lock b/Cargo.lock index 2d2c8d08..428b152a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3041,7 +3041,7 @@ dependencies = [ [[package]] name = "dig-node-service" -version = "0.262.1" +version = "0.262.2" dependencies = [ "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index 045cf38e..d7ca658b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ edition = "2021" # release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet) # keep their own independent versions — only the released binary tracks the workspace version. -version = "0.262.1" +version = "0.262.2" # Release hardening, matching digstore: keep integer-overflow checks ON in release. # The node parses untrusted serialized input and does offset/length arithmetic over # it, so silent wrapping in release would turn a length bug into a memory/logic hazard.