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
170 changes: 98 additions & 72 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# 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.165.0"
version = "0.166.0"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
65 changes: 65 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -7666,6 +7666,71 @@ authoritative** — not by `control.collateral.requirement`, not by `dig.getColl
into a verification. Every field of such a record parses, so nothing downstream would question its
figures.

### 24.8a. Producing a record: the census

A record for an epoch after the first MUST be produced by counting the chain, and by nothing else.

The node MUST obtain the three census inputs from `dig_mirror_coin::census`, at the height
`dig_mirror_coin::census_height` derives for that epoch — the first transaction block at or after
the epoch's start instant — and MUST derive the record with
`dig_mirror_collateral::EpochRecord::advance`. It MUST NOT restate either. A census height chosen
any other way is a fork, because every node must reach the same height without coordinating.

The chain reads MUST be served through a `dig_chainsource_interface::ChainSource`. The node MUST NOT
open a second connection to the chain for this purpose: it takes a `ChainSource` view of the one
transport that already serves its wallet reads, so a node holds ONE peer pool.

**One pool is not one notion of the peak, and this node has three.** The wallet's peak is settled by
NC-12 agreement across the full nodes this node dialled itself, and their failure to agree is
reported as not knowing. The census's peak is not: it is read through the `ChainSource`, whose router
asks `api.coinset.org` FIRST and consults this node's peers only when that read fails — so on a
reachable oracle the census's peak is one HTTPS endpoint's answer, and when the oracle is
unreachable it is a peer-tracked value carried with NO agreement step. It is that peak the
`CENSUS_FINALITY_DEPTH_BLOCKS` refusal below is measured against.

A census provider MUST therefore be classified by what it can REACH rather than by its type: a
fabric that can fall through to the oracle shares the oracle's independence group, however many
peers it holds. A node MUST NOT count such a provider as an independent chain source, and MUST NOT
describe the census's peak as corroborated.

**The walk is sequential.** Epoch *n* is derived from epoch *n-1*, so the node computes each
intervening epoch in order from the newest record it holds. It MUST NOT skip forward to the current
epoch, and it MUST NOT derive a successor from a record whose `protocol_version` exceeds what the
build implements — the ceiling of §24.8 applies at this boundary too.

**A census that could not be taken MUST record nothing.** Each refusal is reported with its own
reason and its own remedy:

| the census stopped because | remedy |
|---|---|
| a chain read could not be answered | reach a chain source |
| the chain has not yet reached the epoch's start | wait for a block |
| the census height is not yet buried to `CENSUS_FINALITY_DEPTH_BLOCKS` | wait |
| the candidate population exceeds what can be authenticated | refused whole; never censused as a prefix |
| the predecessor record is absent, unreadable, or names an unimplemented ruleset | that epoch first |
| the store already holds a DIFFERENT record for the computed epoch | the held record stands |
| the controller refused to derive the record from the census | its own reason, reported verbatim: the census is not the successor epoch, no activation row governs that epoch, or the version is unimplemented |
| the record store could not be read or written | the state directory |
| the store's own line for the computed epoch cannot be read | repair or remove that line |

The last of these MUST be detected BEFORE the chain is read. An unreadable line is invisible to the
scan that answers "the newest epoch held", so a node that did not check would recensus that epoch on
every attempt — the whole population and its spend executions — only to fail at the write, forever.

None of these MUST EVER become a figure — not a zero, not a default, and not the neighbouring
epoch's answer. The store's own absence then surfaces through §24.2's `unknown` with its reason,
which is the only answer such a node can defend.

**A census that counted nothing MUST say what it examined.** A `stores` of zero is produced
identically by an empty network, by a source answering at a puzzle hash other than the one it was
asked for, and by a source that could not supply the creating spends its candidates needed. Those
call for opposite responses and only the first is a fact about the network, so for every epoch it
records a node MUST report the census's examined count and its per-rule exclusion counts alongside
the recorded figure. Reporting the figure alone renders a broken instrument as an answer.

Re-attempting MUST be cheap in the steady state: a node whose store already holds the target epoch
performs **no chain read at all**.

### 24.9. Serving an epoch to a peer

`dig.getCollateralEpoch` is an OPEN node method taking `{ epoch }` and returning `{ record }` or
Expand Down
16 changes: 16 additions & 0 deletions crates/dig-node-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ dig-node-control-interface = "0.24"
# comes out of this crate so that a second derivation cannot drift from the consensus one.
dig-mirror-collateral = "0.3"

# The chain half of the same model: `census` counts the collateralised network at a block height
# and hands `dig-mirror-collateral` the three integers its controller consumes (dig-node#400).
# Without it a node could only ever record epoch 1, which is derivable from nothing.
dig-mirror-coin = "0.7"

# The canonical `ChainSource` trait `dig-mirror-coin`'s census is generic over. Declared, not
# implemented: `chia-query` already provides the implementation this node uses
# (`ChiaQueryProvider`), reached through `dig-wallet`'s one shared chain transport. Caret-matched
# to the line `chia-query` and `dig-mirror-coin` compiled against, so all three unify on one crate.
dig-chainsource-interface = "0.3"

# The OS CSPRNG for all authorization material — the control token, pairing ids/tokens
# (§7), and the relay loop-probe id (`control::fill_random`). Wraps `getrandom(2)` /
# `/dev/urandom` on Unix and `BCryptGenRandom` on Windows, one code path on every
Expand Down Expand Up @@ -256,6 +267,11 @@ dig-wallet = { path = "../dig-wallet" }
# (its series skips 0.32 -> 0.42) and pins the primitives to its own version, so it cannot
# sit on the chia-wallet-sdk ceiling without re-splitting the family (dig_ecosystem#3161).
chia-bls = "0.36.1"
# The census-runner unit tests implement `ChainSource` over a double, whose method signatures are
# spelled in `chia-protocol` types. The SAME 0.36.1 line `dig-chainsource-interface` and
# `dig-mirror-coin` compile against -- a second line would make the double's `Bytes32` a different
# type from the trait's and the impl would not apply.
chia-protocol = "0.36.1"
hex = "0.4"
# The crate's own integration tests need its test-construction seams (the `testkit` feature above).
# Depending on self with the feature on is what keeps those seams available to `tests/` while staying
Expand Down
Loading
Loading