Skip to content
Draft
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
4 changes: 2 additions & 2 deletions Cargo.lock

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

19 changes: 14 additions & 5 deletions crates/dig-node-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ chia-sdk-types = { version = "=0.36.0", features = ["chip-0035", "action-layer"]
# Same chia 0.36 set as the crates around it -- a mirror coin's collateral is $DIG, so the returned
# coin sits at the CAT puzzle hash and never at the bare owner puzzle hash.
chia-puzzle-types = "=0.36.1"
# `SINGLETON_LAUNCHER_HASH` -- `rewards_claim::chain_port::HintedLauncherIndex` filters a hinted
# coin candidate down to an actual singleton-launcher coin before ever handing its id to
# `dig_rewards_coin::discover_distributor` (dig_ecosystem#3347). Promoted from a dev-only edge to
# a normal one at the SAME `=0.20.3` pin the dev-dependency section below already resolves -- no
# second version enters the tree.
chia-puzzles = "=0.20.3"
# `ToTreeHash`, to re-derive an owner puzzle hash from the key a mirror create is built for, so the
# signer can refuse spends that are not its own wallet's rather than trusting the call site to have
# passed the right key. Part of the same chia set above -- `chia-sdk-driver` already resolves it.
Expand Down Expand Up @@ -202,11 +208,14 @@ dig-wallet = { path = "../dig-wallet" }
# `ChainSource`. This is the ONE crate in this seam that depends on it — `dig-node-core`
# deliberately does not (see `dig_node_core::rewards::port`'s module doc) because the
# reader takes a `ChainSource` it has no seam to hold; this crate does (`dig-wallet`'s
# `CorroboratedChainSource`), so the adapter lives here. `0.5`, not the `0.4` the ticket
# names (stale): 0.5.0 is the release that already refuses `epoch_seconds == 0` inside
# `read_distributor` itself (see `rewards/chain_source.rs`'s module doc for the exact
# line, and why this crate ALSO refuses at its own edge as defence in depth).
dig-rewards-coin = "0.5"
# `CorroboratedChainSource`), so the adapter lives here. 0.5.0 was the release that first
# refused `epoch_seconds == 0` inside `read_distributor` itself (see `rewards/chain_source.rs`'s
# module doc for the exact line, and why this crate ALSO refuses at its own edge as defence in
# depth). Bumped to 0.7 for dig_ecosystem#3347: 0.7.0 adds `discovery::discover_distributor` (the
# real parent-spend memo decode `rewards_claim::chain_port::RealClaimChainPort` needs) and stays
# on the same chia 0.36 line (chia-sdk-driver `=0.36.0`, chia-protocol 0.36.1) every other
# dependency in this crate is already pinned to.
dig-rewards-coin = "0.7"

# HTTP stack: the same axum/tokio the node itself uses, so there is one async runtime
# and one server framework across the node and the service shell. `ws` enables
Expand Down
368 changes: 368 additions & 0 deletions crates/dig-node-service/src/rewards_claim/chain_port.rs

Large diffs are not rendered by default.

180 changes: 148 additions & 32 deletions crates/dig-node-service/src/rewards_claim/driver.rs

Large diffs are not rendered by default.

34 changes: 33 additions & 1 deletion crates/dig-node-service/src/rewards_claim/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ impl<P: ClaimChainPort, H: DistributorHintSource> ClaimEngine<P, H> {
self.rotation_cursor
}

/// Which [`ClaimChainPort`] adapter this engine is driving against -- surfaced so a running
/// node's own log line can name it (DIG-Network/dig_ecosystem#3347), never left implicit.
#[must_use]
pub fn port_kind(&self) -> &'static str {
self.port.kind()
}

/// F7: restores the persisted aggregate-fee-budget window and cadence clock from `dir` and
/// arms this engine to keep persisting them there after every submission and every completed
/// cycle (never batched to cycle end — see [`Self::run_cycle`]'s "F7" doc section for why).
Expand Down Expand Up @@ -979,7 +986,8 @@ mod tests {
}

/// A full in-memory fake standing in for the real chain adapter (see the module doc's "chain
/// seam" section) — the ONLY thing #3249 landing changes is which struct implements this trait.
/// seam" section) — #3249 landed `RealClaimChainPort` as the production implementer of this
/// trait; this fake stays as the engine's own unit-test double.
struct FakeChainPort {
distributors: Mutex<HashMap<Bytes32, FakeDistributor>>,
submitted: Mutex<Vec<(Bytes32, Bytes32, u64)>>,
Expand Down Expand Up @@ -1140,6 +1148,10 @@ mod tests {
.push((launcher_id, payout_puzzle_hash, fee_mojos));
Ok(())
}

fn kind(&self) -> &'static str {
"test-fake"
}
}

fn one_distributor(
Expand Down Expand Up @@ -1452,6 +1464,10 @@ mod tests {
) -> Result<(), ClaimPortError> {
self.0.submit_initiate_payout(l, p, f).await
}

fn kind(&self) -> &'static str {
self.0.kind()
}
}

let port = HintOnlyPort(FakeChainPort::new(vec![d]));
Expand Down Expand Up @@ -1537,6 +1553,10 @@ mod tests {
) -> Result<(), ClaimPortError> {
Err(ClaimPortError::Other("unreachable".into()))
}

fn kind(&self) -> &'static str {
"test-always-faulting-discovery"
}
}

/// Defect A1/A2 regression -- THE anti-green test for this defect: a port that errors on
Expand Down Expand Up @@ -2850,6 +2870,10 @@ mod tests {
.submit_initiate_payout(launcher_id, payout_puzzle_hash, fee_mojos)
.await
}

fn kind(&self) -> &'static str {
self.inner.kind()
}
}

/// **F1 regression -- the anti-latch test.** `ChainSourceUnavailable` must be a PER-CYCLE
Expand Down Expand Up @@ -2955,6 +2979,10 @@ mod tests {
.submit_initiate_payout(launcher_id, payout_puzzle_hash, fee_mojos)
.await
}

fn kind(&self) -> &'static str {
self.inner.kind()
}
}

/// **F3 regression -- staleness under a fresh timestamp.** Cycle 1 is healthy and submits a
Expand Down Expand Up @@ -3074,6 +3102,10 @@ mod tests {
.submit_initiate_payout(launcher_id, payout_puzzle_hash, fee_mojos)
.await
}

fn kind(&self) -> &'static str {
self.0.kind()
}
}

/// **F4 (non-blocking, cheap) -- a duplicated launcher id must submit EXACTLY ONCE.** Without
Expand Down
40 changes: 22 additions & 18 deletions crates/dig-node-service/src/rewards_claim/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,33 @@
//!
//! # The chain seam
//!
//! `dig-rewards-coin` is v0.1.3, published on crates.io, and still SPEC-only (`src/` is
//! `error.rs` + `lib.rs`); its driver is
//! DIG-Network/dig_ecosystem#3249, still open. So the whole engine here is built against the narrow
//! [`ClaimChainPort`] trait derived from the SPEC's described surface, tested with a full in-memory
//! fake, and the production adapter — until #3249 ships — is [`UnavailableClaimChainPort`], which
//! reports the named state `ChainSourceUnavailable` and runs zero cycles. This mirrors #3250's own
//! `UnavailableChainPort` exactly. When #3249 lands, one adapter is written against
//! `ClaimChainPort` and nothing above this seam changes.
//! `dig-rewards-coin` 0.7.0 ships a real driver (`discovery`, `payout`, `state`), landed by
//! DIG-Network/dig_ecosystem#3249. The production adapter is [`RealClaimChainPort`]
//! (`chain_port.rs`), built over this node's own corroborated chain source; [`UnavailableClaimChainPort`]
//! remains only as the engine's test double now. Two methods still refuse rather than answer:
//! `own_entry`'s accrued amount and `submit_initiate_payout` both need a chain-backed spendable
//! entry slot and a real reserve lineage proof that 0.7.0's read model does not carry — see
//! `chain_port.rs`'s own module doc, blocked on DIG-Network/dig_ecosystem#3356.
//!
//! A silent no-op that reported progress instead would be the exact defect this ticket exists to
//! prevent (SPEC §2.4): with the unavailable adapter wired, zero claims IS the true state, so the
//! status surface must say so by name, not by omission.
//! prevent (SPEC §2.4): a refused method reports a NAMED [`ClaimPortError`] or
//! [`super::types::ClaimOutcome::Faulted`], never a fabricated success.
//!
//! # Wired into node startup (DIG-Network/dig_ecosystem#3268)
//! [`driver::spawn_claim_driver_from_config`] is the one call `dig-node-service::server`'s
//! `serve_with_shutdown` makes: it is gated on `RewardsClaimConfig::enabled` AND
//! `Config::enable_chain_sync` (the same flag `spawn_collateral_census` and
//! `mirror::bond_verify::spawn_bond_verifier_install` already gate on), and when both are true it
//! spawns a detached task that drives [`ClaimEngine::run_cycle`] on a jittered cadence forever.
//! [`driver::handle`] is the IN-PROCESS accessor a future RPC can read once DIG-Network/dig_ecosystem#3249
//! lands a real [`ClaimChainPort`] adapter and the `ClaimStatus` wire semantics are re-derived
//! against it — this module puts nothing on the wire itself (see `driver`'s own module doc for
//! why). Until #3249 lands, the only production adapter is still [`UnavailableClaimChainPort`], so
//! every real cycle reports [`ClaimLoopState::ChainSourceUnavailable`] and submits nothing the
//! honest state, not a silent no-op.
//! spawns a detached task that drives [`ClaimEngine::run_cycle`] on a jittered cadence forever,
//! over a [`RealClaimChainPort`] built from `state.wallet_chain`'s corroborated source. If that
//! source cannot be built (offline, no peers), the loop reports the named refusal
//! `ClaimDriverRefusal::ChainSourceUnbuildable` and runs zero cycles rather than installing
//! [`UnavailableClaimChainPort`] silently. [`driver::handle`] is the IN-PROCESS accessor a future
//! RPC can read against the `ClaimStatus` wire semantics — this module puts nothing on the wire
//! itself (see `driver`'s own module doc for why).

mod cadence;
mod chain_port;
mod config;
mod driver;
mod engine;
Expand All @@ -55,11 +55,15 @@ mod port;
mod types;

pub use cadence::{next_interval_seconds, FixedJitter, JitterSource, CLAIM_JITTER_SECONDS_DEFAULT};
pub use chain_port::{HintedLauncherIndex, LauncherIndex, RealClaimChainPort};
pub use config::{
RewardsClaimConfig, CLAIM_CADENCE_SECONDS_DEFAULT, CLAIM_CYCLE_FEE_BUDGET_MOJOS_DEFAULT,
CLAIM_FEE_CEILING_MOJOS_DEFAULT,
};
pub use driver::{handle, spawn_claim_driver_from_config, ClaimDriverRefusal, ClaimLoopHandle};
pub use driver::{
handle, run_claim_driver_in, spawn_claim_driver_from_config, ClaimDriverRefusal,
ClaimLoopHandle,
};
pub use engine::ClaimEngine;
pub use hints::{DistributorHint, DistributorHintSource, NoHintSource};
pub use parser::parse_launch_comment;
Expand Down
16 changes: 13 additions & 3 deletions crates/dig-node-service/src/rewards_claim/port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,17 @@ pub trait ClaimChainPort: Send + Sync {
payout_puzzle_hash: Bytes32,
fee_mojos: u64,
) -> Result<(), ClaimPortError>;

/// Names which adapter is installed, so a running node's own log line can say which one --
/// no default impl, so a new adapter must choose its own name rather than silently inheriting
/// one that describes a different adapter.
fn kind(&self) -> &'static str;
}

/// The production adapter until DIG-Network/dig_ecosystem#3249 lands: reports
/// [`ClaimPortError::Unavailable`] on every call and runs zero cycles — the named state
/// `ChainSourceUnavailable` (see the module doc), never a silent no-op.
/// The engine's test double: reports [`ClaimPortError::Unavailable`] on every call and runs zero
/// cycles — the named state `ChainSourceUnavailable` (see the module doc), never a silent no-op.
/// No production path constructs this any more; the real adapter is
/// [`super::chain_port::RealClaimChainPort`].
pub struct UnavailableClaimChainPort;

#[async_trait]
Expand Down Expand Up @@ -115,6 +121,10 @@ impl ClaimChainPort for UnavailableClaimChainPort {
) -> Result<(), ClaimPortError> {
Err(ClaimPortError::Unavailable)
}

fn kind(&self) -> &'static str {
"unavailable"
}
}

#[cfg(test)]
Expand Down
18 changes: 13 additions & 5 deletions crates/dig-node-service/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2231,16 +2231,24 @@ where
// (a tested unit, #1864) so it cannot be silently flipped to always- or never-spawn.
crate::self_heal::spawn_driver_if_service();

// The peer reward-claim loop (DIG-Network/dig_ecosystem#3268, #3251): drives
// The peer reward-claim loop (DIG-Network/dig_ecosystem#3268, #3251, #3347): drives
// `rewards_claim::ClaimEngine::run_cycle` on a jittered cadence so
// `RewardsClaimConfig::enabled = true` stops being a false statement. Gated the same way the
// census and bond-verifier spawns above are -- `enable_chain_sync` already means "this node
// talks to the Chia network", and a harness sets it false precisely so nothing dials. The
// service-gate lives inside the seam (a tested unit, mirroring `self_heal::spawn_driver_if`)
// so it cannot be silently flipped to always- or never-spawn. The only production chain
// adapter until DIG-Network/dig_ecosystem#3249 lands is `UnavailableClaimChainPort`, so every
// real cycle reports `ChainSourceUnavailable` and submits nothing -- the honest state.
crate::rewards_claim::spawn_claim_driver_from_config(config.enable_chain_sync);
// so it cannot be silently flipped to always- or never-spawn. The production chain adapter is
// `rewards_claim::RealClaimChainPort`, built from this node's own `wallet_chain`'s
// corroborated source (the same call the funder-side reward-chain-port install above makes) --
// a source that fails to build is a named refusal (`ClaimDriverRefusal::ChainSourceUnbuildable`),
// never a silent `UnavailableClaimChainPort` substitution. Two methods still refuse:
// `own_entry`'s accrued amount and `submit_initiate_payout`, both blocked on
// DIG-Network/dig_ecosystem#3356 (`dig-rewards-coin` 0.7.0 has no chain-backed spendable entry
// slot or real reserve lineage proof).
crate::rewards_claim::spawn_claim_driver_from_config(
config.enable_chain_sync,
state.wallet_chain.clone(),
);

// Best-effort wallet mTLS listener (#368, Sage byte-parity, node-class clients, §5.3). Binds
// loopback only on [`DEFAULT_MTLS_PORT`], which is deliberately NOT Sage's own RPC port
Expand Down
4 changes: 4 additions & 0 deletions crates/dig-node-service/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Shared integration-test fixtures — deliberately thin: each submodule owns one fixture, no
//! cross-fixture coupling.

pub mod rewards_fixture;
Loading
Loading