Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eac8e10
feat(mirror): persist mirror-bond coin ids (#575)
MichaelTaylor3d Sep 6, 2026
08261a4
fix(peer): count accepted relayed circuits in the connected pool (#579)
MichaelTaylor3d Sep 6, 2026
1ad1c51
fix(cli): guard the exit-code namespace shared with diga against coll…
MichaelTaylor3d Sep 6, 2026
194a016
fix(hygiene): port the lost-continuation guard to 4 crates, fix 48 co…
MichaelTaylor3d Sep 6, 2026
93fb452
feat(mirror): detect an IP change daily and reconcile mirror coins to…
MichaelTaylor3d Sep 7, 2026
e11434a
feat(serve): content hosting + serve path batch, v0.255.0 (dig_ecosys…
MichaelTaylor3d Sep 7, 2026
2b1b5d6
chore(develop): merge main (v0.254.89) back into develop after the #5…
MichaelTaylor3d Sep 7, 2026
b27180b
chore(develop): sync develop to main after the v0.255.0 cut (#588 squ…
MichaelTaylor3d Sep 7, 2026
bc9767d
chore: untrack gitnexus-generated agent files (#590)
MichaelTaylor3d Sep 8, 2026
d562aad
feat(rewards): always-on prover loop engine -- honest liveness, type-…
MichaelTaylor3d Sep 9, 2026
8573ecf
feat: serve dig.getRewardProverStatus at Tier::Control (#595)
MichaelTaylor3d Sep 10, 2026
e9f07c4
feat(rewards): peer-side claim loop -- watch distributors, claim on c…
MichaelTaylor3d Sep 10, 2026
56f398d
feat(rewards): chain port + listRewardDistributors (unit 2) (#604)
MichaelTaylor3d Sep 10, 2026
90fff0c
feat(rewards): durable funder-ownership registry (identity only) (#606)
MichaelTaylor3d Sep 10, 2026
49ae2c6
feat(rewards): wire the peer claim loop onto a cadence driver from re…
MichaelTaylor3d Sep 10, 2026
3d2d55b
chore: record main in develop after the v0.257.0 cut (#608)
MichaelTaylor3d Sep 11, 2026
cd8ce7b
chore: record main in develop after the v0.257.0 cut (ancestry link)
MichaelTaylor3d Sep 11, 2026
24cde57
feat(rpc): serve the five reward-distributor RPC methods (#3269)
MichaelTaylor3d Sep 14, 2026
8b81583
chore(release): bump workspace version to 0.258.0
MichaelTaylor3d Sep 14, 2026
0ae3ef4
ci: propagate main's release-cut commitlint depth to develop
MichaelTaylor3d Sep 14, 2026
7902ad7
fix(rewards): clamp an out-of-range claim schedule to the max (#611)
MichaelTaylor3d Sep 14, 2026
22ba90b
feat(rewards): construct and install a real RewardsChainPort over dig…
MichaelTaylor3d Sep 14, 2026
98b29eb
test(peer): lock inbound dial+accept counted once (#591)
MichaelTaylor3d Sep 16, 2026
a2d5c38
fix(rewards): split the three conditions behind REWARD_CHAIN_UNAVAILA…
MichaelTaylor3d Sep 17, 2026
7c9f46b
fix(rewards): reward-distributor reads are OPEN on POST /, doc + pin …
MichaelTaylor3d Sep 18, 2026
4e102c6
fix(rewards-claim): derive both cadences from one raw config value an…
MichaelTaylor3d Sep 18, 2026
44230a7
feat(rewards-claim): real ClaimChainPort over dig-rewards-coin 0.8.0 …
MichaelTaylor3d Sep 20, 2026
a9a9387
chore: record main v0.260.0 in develop after the #622 cut (ancestry l…
MichaelTaylor3d Sep 23, 2026
25a04f0
chore(release): forward-port the v0.260.0 workspace version to develop
MichaelTaylor3d Sep 23, 2026
2ca67dd
fix(rewards-claim): chain_port audit, approval refusal, discovery cap…
MichaelTaylor3d Sep 24, 2026
af8b010
fix(rpc): gate node-local reward reads, rate-bound open chain reads (…
MichaelTaylor3d Sep 24, 2026
6fe8647
chore(release): v0.261.0 -- reward RPC ingress hardening
MichaelTaylor3d Sep 25, 2026
9d0f98a
fix(rewards-claim): remove the #3358 discovery cap from the v0.261.0 cut
MichaelTaylor3d Sep 25, 2026
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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ 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.261.0] - 2026-09-24

### Reward RPC ingress: token gate and rate bound
- Token-gate the three NODE-LOCAL reward reads on `POST /` (`dig.getRewardProverStatus`,
`dig.listRewardDistributors`, `dig.getPayeeRewardClaimStatus`): a master control token or a valid
paired token is now required, `-32030` otherwise. They were already `Tier::Control` (local
dispatch only, never the mTLS peer surface); what changed is that the anonymous `POST /` path no
longer volunteers node-local state, so the served catalogue's `requires_auth` and the enforced
predicate agree (#3352)
Comment thread
MichaelTaylor3d marked this conversation as resolved.
- Rate-bound the two OPEN chain-keyed reward reads per SOURCE at HTTP ingress
(`dig.getRewardDistributor`, `dig.listRewardDistributorCommitments`), `-32034
REWARD_INGRESS_LIMITED`, so an anonymous caller can no longer drive an unbounded number of
upstream chain reads. The limiter is keyed on the source, never on the caller-supplied
`launcher_id` (#3355)
- The served OpenRPC catalogue now reports `requires_auth: true` for five methods that previously
advertised `false` while already being enforced as authenticated: `cache.listCached`,
`cache.fetchAndCache`, `cache.pushCapsule`, `chat.send` and `chat.poll`. No enforcement changed
-- the catalogue was describing these methods wrongly. Every `rpc.discover` consumer that reads
`requires_auth` will see the flip (#3352)

### Reward claim port hardening
- Ban `RewardDistributor::created_slot_value_to_slot` from production code via a workspace
`disallowed-methods` clippy lint (phantom `LineageProof` on a chain-rebuilt distributor); allow
the one legitimate in-process test-fixture use. This is the dig-node HALF of #3357 only --
the ticket also has dig-account and dig-app halves and remains OPEN (#3357)
- Refuse, by name, a distributor requiring payout approval in `submit_initiate_payout` before
building or broadcasting anything (#3362)
- Fix a claim-port regression test to use a non-empty launcher index so it actually exercises the
failing chain source's discovery/submit paths (#3363)

## [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.260.0"
version = "0.261.0"
# 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
29 changes: 22 additions & 7 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ MUST NOT re-declare method names. Each entry carries a `served` class and `requi

| `served` | Meaning |
|---|---|
| `local` | Resolved by the node library (`handle_rpc`). |
| `local` | Resolved by the node library (`handle_rpc`). `requires_auth: false` except the HTTP-token-gated methods named below, which are `requires_auth: true`. |
| `passthrough` | Read path returns `-32601`; relayed verbatim to the upstream WHEN one is configured (§5.4), else returned to the caller as `-32601`. |
| `shell` | Answered by this service itself (`rpc.discover`). |
| `control` | The gated control plane (§7); always `requires_auth: true`. |
Expand All @@ -1056,8 +1056,11 @@ For the current node library (§2.2) the catalogue is:
`dig.getCollection`, `dig.listCollectionItems`, the L7 peer surface (`dig.getNetworkInfo`,
`dig.getPeers`, `dig.announce`, `dig.getAvailability`, `dig.listInventory`, `dig.fetchRange`),
all `cache.*` (`cache.getConfig`, `cache.setCapBytes`, `cache.clear`, `cache.listCached`,
`cache.removeCached`, `cache.fetchAndCache`, `cache.pushCapsule` — §5.5.3), and the chat subsystem
`chat.send` / `chat.poll` (§5.5.2).
`cache.removeCached`, `cache.fetchAndCache`, `cache.pushCapsule` — §5.5.3), the chat subsystem
`chat.send` / `chat.poll` (§5.5.2), and the reward reads `dig.getRewardProverStatus`,
`dig.listRewardDistributors`, `dig.getPayeeRewardClaimStatus`, `dig.getRewardDistributor`,
`dig.listRewardDistributorCommitments` (dig_ecosystem#3352 / #3351 / #3355 — see the
`requires_auth` clause below).
- **passthrough**: `dig.listCapsules` (needs a chain generation walk this node does not perform)
and `dig.getProofStatus` (polls an execution-proof JOB this node does not run — inventing a
status would be the fabrication the anti-fabrication rule forbids: an absent attestation is
Expand All @@ -1077,8 +1080,18 @@ Param/result schemas for the `dig.*`/`cache.*` methods are owned by the digstore
published on docs.dig.net (Protocol → the L7 read/RPC pages); this repo's OpenRPC document is a
method + error **discovery** catalogue with intentionally permissive schemas.

Every non-`control.*` method MUST have `requires_auth: false`; every `control.*` method MUST have
`served: "control"` and `requires_auth: true`.
Every `control.*` method MUST have `served: "control"` and `requires_auth: true`. A non-`control.*`
method MUST have `requires_auth: false` UNLESS the HTTP surface token-gates it — today the
holder-/holdings-revealing `cache.fetchAndCache` / `cache.pushCapsule` / `cache.listCached` (§14.3,
#2108), the node-identity chat pair `chat.send` / `chat.poll` (#1946), and the NODE-LOCAL reward reads
`dig.getRewardProverStatus` / `dig.listRewardDistributors` / `dig.getPayeeRewardClaimStatus`
(dig_ecosystem#3352: each volunteers this node's own prover inventory, funded-distributor set or
payee claim state, failing §7.2's WHO-NAMES-THE-SUBJECT test). Those keep their `served` class and
carry `requires_auth: true`. `requires_auth` is the COMPILED statement of the HTTP token gate: the set
of catalogued methods with `requires_auth: true` MUST equal the set `server.rs` refuses `-32030
UNAUTHORIZED` without a master or paired token (`requires_http_token`), and a test pins the equality.
The two chain-keyed reward reads `dig.getRewardDistributor` / `dig.listRewardDistributorCommitments`
are OPEN (dig_ecosystem#3351) and rate-bounded per source (§10, `-32034`).

#### 5.5.0. `dig.getContent` — the window envelope (#2071)

Expand Down Expand Up @@ -1469,8 +1482,9 @@ Two layers, both REQUIRED:
mismatched credential is answered `UNAUTHORIZED` (`-32030`, §10). Token comparison MUST be
constant-time (`ct_eq`) so verification cannot be probed via a timing oracle.

Exactly the `control.` method prefix is gated (`is_control_method`); unknown `control.*` methods
still pass the auth gate first, then yield `METHOD_NOT_FOUND`. The pairing-administration methods
The `control.` method prefix is token-gated as a class (`is_control_method`); the HTTP surface
additionally token-gates the non-`control.*` methods §5.5 enumerates (`requires_http_token`);
unknown `control.*` methods still pass the auth gate first, then yield `METHOD_NOT_FOUND`. The pairing-administration methods
(`control.pairing.list`/`approve`/`revoke`, §7.11) require the MASTER token specifically — a paired
token is NOT accepted for them. The exceptions are the wallet CHAIN READS — `control.wallet.balance`, `control.wallet.coins`,
`control.wallet.coinById`, `control.wallet.coinSpend`, `control.wallet.coinsByParent`,
Expand Down Expand Up @@ -3315,6 +3329,7 @@ method runs, and it MUST NOT be conflated with the wallet's own `-32043` egress
| -32031 | `NOT_SUPPORTED` | shell | A control operation this build/pin cannot perform (e.g. §21 sync without an identity). |
| -32032 | `CONTROL_ERROR` | shell | A control operation failed at runtime (distinct from bad input / absent capability). |
| -32033 | `CONTROL_INGRESS_LIMITED` | shell | An OPEN, token-less `control.*` read was refused AT INGRESS, before the request reached the dispatcher and before any DB work was done for it: this SOURCE's request bound is exhausted. The open reads present no credential, so without this bound an unauthenticated caller can drive unbounded SQLite work (`.coinById`/`.coinSpend` each run up to two lookups plus an LRU `UPDATE`) simply by asking repeatedly. The bound is PER SOURCE — one flooding source MUST NOT refuse another — and the node's OWN loopback operator is EXEMPT, so this code is only ever seen by a non-loopback caller (i.e. under `DIG_NODE_ALLOW_REMOTE=1`). It MUST stay DISTINCT from `-32043 WALLET_RATE_LIMITED`: that bound is on chain EGRESS and protects the third-party oracle, this one is on REQUESTS and protects this process. They fire for different reasons and have different remedies, so collapsing them would leave a caller unable to tell which bound it hit. Back off and retry. |
| -32034 | `REWARD_INGRESS_LIMITED` | shell | An OPEN reward chain read (`dig.getRewardDistributor` / `dig.listRewardDistributorCommitments`) was refused AT INGRESS: this SOURCE's request bound is exhausted. Each call is one upstream chain read for any caller-supplied launcher_id, so without this bound an anonymous caller drives unbounded upstream work. The bound is PER SOURCE (`RequestorId` — never the launcher id, which the caller controls); the loopback operator is EXEMPT, so only a `DIG_NODE_ALLOW_REMOTE=1` caller ever sees it. Distinct from -32033 (control-read ingress) and -32043 (wallet chain egress). Back off and retry. |
| -32040 | `WALLET_NO_CHAIN_SOURCE` | node | a wallet chain read (`control.wallet.balance`/`.coins`/`.coinById`/`.coinSpend`/`.coinsByParent`/`.peak`) or `control.wallet.broadcast` had NO live chain source able to answer an arbitrary (non-wallet) address. Distinct from a truthful `0`. A read the node can answer WITHOUT a chain source MUST NOT be refused with this code: the replica fast path and the node own chain-read cache both answer from bytes already in hand, so on `.coinById`/`.coinSpend` liveness is consulted only on a cache MISS. Refusing a cached answer because a third party is momentarily unreachable gives availability away for nothing on exactly the rows a lineage walk re-reads (a spent coin record is immutable), and the refusal then cascades into the retries that exhaust the `-32043` bound. The refusal MUST stay for a miss, and `.coinSpend` MUST treat a PARTIAL cache hit (spend cached, coin record not) as a miss, because the heights come from the record. |
| -32041 | `WALLET_NOT_SYNCED` | node | `control.wallet.balance` of the wallet's OWN address while the local DB is still syncing and no live fallback is attached (nothing can answer yet). |
| -32042 | `WALLET_READ_FAILED` | node | `control.wallet.balance`/`.coins`/`.coinById`/`.coinSpend`/`.coinsByParent`/`.peak` failed at the underlying DB / chain-source layer. On `.coinById` this INCLUDES a chain source that answered with a record for a DIFFERENT coin than the id asked for: a coin id is self-certifying (`SHA256(parent ‖ puzzle_hash ‖ amount)`), so a substituted record is a failed READ -- never that coin's record, and never `coin: null`. On `.coinSpend` it likewise INCLUDES a source that answered with another coin's spend, a puzzle reveal that does not tree-hash to the spent coin's own `puzzle_hash` (or will not parse), and a spend the coin record contradicts (no record, or a record calling the coin unspent) -- each fails CLOSED rather than being served unverified. On `.coinsByParent` it INCLUDES a source that returned a child naming a different parent, which fails the WHOLE page rather than being silently filtered (a filtered page is a lineage with an invisible hole). Distinct from `WALLET_NO_CHAIN_SOURCE` and `WALLET_NOT_SYNCED`. |
Expand Down
18 changes: 18 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# DIG-Network/dig_ecosystem#3357: bans calling `RewardDistributor::created_slot_value_to_slot`
# from production code anywhere in this workspace.
#
# That method derives a `LineageProof` from the coin it is called ON. For a distributor rebuilt
# from chain (every production read path), that coin is the TIP -- so a `LineageProof` it derives
# for a slot an EARLIER generation created is a well-formed but PHANTOM proof. The entry slot for a
# real spend must come from a fresh, authenticated chain walk instead (see
# `dig_rewards_coin::ChainEntrySlotSource`, or `DistributorSnapshot::entry_slot` /
# `commitment_slots` / `reward_slots` for a read).
#
# This lint bans the CALL, not the receiver class: clippy cannot tell a chain-rebuilt receiver from
# an in-process one (a distributor built fresh in the same process this same generation, e.g. a
# test fixture reading back the reward slots ITS OWN spend just created -- that use is legitimate).
# Every `#[allow(clippy::disallowed_methods)]` against this entry must carry a comment stating WHY
# its receiver is in-process this generation, not chain-rebuilt.
disallowed-methods = [
Comment thread
MichaelTaylor3d marked this conversation as resolved.
{ path = "chia_sdk_driver::RewardDistributor::created_slot_value_to_slot", reason = "derives a LineageProof from the coin it is called on; on a distributor rebuilt from chain that coin is the TIP, so any earlier generation's slot is a PHANTOM (dig_ecosystem#3357). Use DistributorSnapshot::entry_slot / commitment_slots / reward_slots or ChainEntrySlotSource. This lint bans the CALL, not the receiver class: it cannot tell a chain-rebuilt receiver from an in-process one. Every #[allow] must state in a comment WHY its receiver is in-process." },
]
29 changes: 20 additions & 9 deletions crates/dig-node-core/src/seams/dig_rpc/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,10 +907,11 @@ impl RpcDispatch for Node {
"count": set.len()}});
}
// dig.getRewardProverStatus (dig_ecosystem#3269, dig-rewards-coin SPEC.md
// §2.3/§2.4) — CONTROL plane: loopback admin / in-process FFI ONLY (the token tier of
// this NODE-LOCAL read is dig_ecosystem#3352's decision, not #3351's), NEVER over the
// mTLS peer surface (absent from `is_peer_reachable_method`;
// `reward_methods_tier_guard.rs` fails closed on that). Reads the node's live
// §2.3/§2.4) — `Tier::Control` = local dispatch only, never the mTLS peer surface
// (`reward_methods_tier_guard.rs`); token-GATED on `POST /` at
// `server.rs::is_node_local_reward_read` (master or paired token, `-32030`;
// dig_ecosystem#3352) because it volunteers node-local state; the in-process FFI path
// stays open like `cache.*`. Reads the node's live
// `reward_prover_statuses` registry (empty until dig_ecosystem#3265 spawns a prover
// loop) — a REAL read of a real, currently-empty registry, so
// `{"statuses":{"outcome":"consulted","observed_at":N,"items":[]}}` means "this node
Expand Down Expand Up @@ -1013,7 +1014,11 @@ impl RpcDispatch for Node {
// dig-node-service `tests/server.rs`. Chain-derived state ONLY — never the local prover
// loop's self-reported state (see `GetRewardProverStatus` above for that). Goes entirely
// through `rewards::port::RewardsChainPort`: this crate never calls `dig-rewards-coin`
// itself (dig_ecosystem#3269 unit 0).
// itself (dig_ecosystem#3269 unit 0). Token-LESS does not mean UNBOUNDED: staying open
// per dig_ecosystem#3351 above, `POST /` also rate-bounds this read PER SOURCE at
// ingress (`-32034 REWARD_INGRESS_LIMITED`, dig_ecosystem#3355,
// `server.rs::is_open_reward_chain_read`) — a caller-supplied `launcher_id` is never
// the limiter's key, only the source is.
Some(Method::GetRewardDistributor) => {
let params = req.get("params").cloned().unwrap_or(json!({}));
let launcher_id = match parse_launcher_id_arg(&params) {
Expand Down Expand Up @@ -1095,8 +1100,11 @@ impl RpcDispatch for Node {
};
return json!({"jsonrpc":"2.0","id":id,"result": result});
}
// dig.listRewardDistributors (dig_ecosystem#3269 unit 2, SPEC §2.6) — CONTROL plane,
// same guard shape as the other reward handlers above. Two independently-consulted
// dig.listRewardDistributors (dig_ecosystem#3269 unit 2, SPEC §2.6) — `Tier::Control`
// = local dispatch only, never the mTLS peer surface (`reward_methods_tier_guard.rs`);
// token-GATED on `POST /` at `server.rs::is_node_local_reward_read` (master or paired
// token, `-32030`; dig_ecosystem#3352) because it volunteers node-local state; the
// in-process FFI path stays open like `cache.*`. Two independently-consulted
// halves (`funded` / `claimable`), each a `Half<RewardDistributorRef>` — SPEC §12.5
// clause 6's "reassuring zero" rule applies to EACH half separately.
//
Expand Down Expand Up @@ -1177,8 +1185,11 @@ impl RpcDispatch for Node {
return json!({"jsonrpc":"2.0","id":id,"result": result});
}
// dig.getPayeeRewardClaimStatus (dig_ecosystem#3268/#3269 unit 3, SPEC §12.5) —
// CONTROL plane: loopback admin / in-process FFI ONLY, absent from
// `is_peer_reachable_method` (`reward_methods_tier_guard.rs` fails closed on that).
// `Tier::Control` = local dispatch only, never the mTLS peer surface
// (`reward_methods_tier_guard.rs`); token-GATED on `POST /` at
// `server.rs::is_node_local_reward_read` (master or paired token, `-32030`;
// dig_ecosystem#3352) because it volunteers node-local state; the in-process FFI path
// stays open like `cache.*`.
// Dispatched through `Method::from_name(..)` like every other reward method — never
// the string pre-match above the enum, which bypasses this tier guard entirely
// (dig_ecosystem#3261: a reward RPC reachable by a peer is a money hole).
Expand Down
Loading
Loading