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