Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 5 additions & 9 deletions crates/dig-node-core/src/rewards/port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
13 changes: 0 additions & 13 deletions crates/dig-node-core/src/seams/dig_rpc/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
}}),
}
}

Expand Down
13 changes: 10 additions & 3 deletions crates/dig-node-service/tests/common/rewards_fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

// ---------------------------------------------------------------------------------------------
Expand Down
26 changes: 24 additions & 2 deletions crates/dig-node-service/tests/rewards_chain_port_a3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<MockChainSource>::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
Expand Down
Loading