chain/net — implementation receipt, 2026-10-15
What specs/chain-wire.md asked for, what crate/src/chain/net/ and the node
wiring deliver, and the evidence: unit/conformance tests, the three-node
harness, the adversarial tests. Branch feat/chain-net, PR soft3#187,
built in stand .stands/N2 against soft3 main at 8525b17 (after #184/#185),
radio main 50600cc (≥ bc8b633), hemera/mudra/tade/bbg stand defaults.
modules
| file | spec | lines |
|---|---|---|
chain/net/codec.rs |
chain.md §1–2: 168-byte header, four tx kinds, block id, txid/statement, compact target, work, tx root, topic ids | 640 |
chain/net/frames.rs |
chain-wire §2–3: the tade dialect urn:cyberia:chain:1, frames (*,k) + (#, letter), strict bounded decoders |
400 |
chain/net/chain_view.rs |
the operations the wire asks of the kernel (trait; swap point) | 150 |
chain/net/devchain.rs |
minimal chain behind the trait for the harness: real PoW/MTP/heaviest chain/reorg bound/tx rules; constant target, digest root (dev only); genesis file codec §4.1; blocks.tade persistence | 760 |
chain/net/mempool.rs |
chain-wire §4: admission order, nonce window and per-neuron order, fee floor, 8192/16 MiB eviction by fee per byte, 24 h expiry, revalidation after a tip, templates | 330 |
chain/net/peers.rs |
chain-wire §5: token buckets (64 headers/s, 256 txs/s, 4 MiB/s, 16 streams/s; one second of tokens at start, 4× burst), score with 1/min decay, ban ≥ 100 for 1 h, 8 outbound / 64 inbound, /16 diversity (loopback exempt), bootstrap floor, anchors | 300 |
chain/net/transport.rs |
chain-wire §1: radio endpoint, ALPN cyber/chain/1, radio.key, bounded frame IO, one request per stream with 30 s timeout |
200 |
chain/net/serve.rs |
chain-wire §3: request streams s g r b t k, x codes 1–4, 8 streams per direction |
210 |
chain/net/gossip.rs |
chain-wire §2: headers/txs/checkpoints topics, relay only after validation (neighbor scope, so the relay is explicit), body on gossip +100, 4,096-byte cap | 220 |
chain/net/sync.rs |
chain-wire §3.1–3.2: status exchange, locator g loop (≤ 2,000, 64 rounds), bodies by id along the best header chain in ranges of 128, 4 in flight, 512-block buffer, strict height order, catching-up |
230 |
chain/net/discovery.rs |
chain-wire §6 + deploy/chain peers.toml v1: bootstrap file, anchors, the graph as address book (antenna/socket/follow kind-2 links, wire.rs packing), the dialer |
300 |
chain/net/mod.rs |
Net: one endpoint, three topics, router, the tasks |
290 |
node/chain_node.rs |
ChainNode: chain + mempool + net + CPU miner (sync-quorum gate, --solo), /status cybermark fields of §7 |
220 |
node/chain_routes.rs |
chain-wire §7: every route of the table; /template, /submit, /tip in the reference miner's shapes (soft3#186); legacy unsigned ingress → 410; /dev/* switches on dev chains only |
330 |
bin/soft3.rs |
soft3 node --chain … --radio --peers, soft3 node id, `soft3 chain genesis-dev |
key |
scripts/chain-fleet.nu |
the three-node harness (ops skeleton extended) | 300 |
tests/chain_adversary.rs |
adversarial wire tests against a live node | 320 |
Dependencies added to crate/Cargo.toml: cyber-radio and iroh-gossip
(path, radio stand), tade, cyber-mudra (bridge, shared with the wallet),
tokio, n0-future, rand, toml, bytes; cyber-hemera moved to the
path dependency every sibling uses (one copy).
conformance vectors (chain-conformance.md), in unit tests
Sections 1 (hemera anchors), 2 (compact target, all 13 rows), 3 (work, 4
rows + the 3-block chainwork), 5 (test header bdfbbff9…, mined header
d1eec40d… with nonce 40,165,933 valid at GENESIS_BITS and rejected at
2^228 and at nonce − 1), 6 (coinbase txid d7662bdb…, leaf c2cb6adb…,
node 3685e0a5…, transfer statement c7e4a223…, cyberlink 257 bytes /
valence 2 / p == q, encoding rejections), 7 (topic id 1f003a48…), 11
(status 138 bytes, locator heights for tip 1,000, count = 129 → x 3, body
on gossip +100, 65th header dropped, wrong-chain → x 4 + disconnect,
POST /tx bad signature → 400, legacy → 410) — all green. Section 4
(ASERT), 8 (bbg state), 9 (checkpoints/orphan pool) and 10 (genesis
pipeline) are the kernel's.
cargo test -p soft3 (lib) 133 passed (chain:: 31)
cargo test -p soft3 (bin) 11 passed
cargo test --test chain_adversary 2 passed
Decoder fuzzing (no external fuzzer; xorshift random and mutated inputs,
60,000 cases per run): Header::decode, Tx::decode, decode_body,
decode_bits, Frame::decode, frames::split never panic.
three-node harness — nu scripts/chain-fleet.nu
Dev chain (soft3 chain genesis-dev, target 2^252, 1 s block pacing),
four processes on one machine, separate homes, HTTP 17780–17783, radio UDP
17790–17793, peers.toml v1 with real endpoint ids from soft3 node id.
Only the PIDs the script spawned are killed.
ok binary speaks the phase-1 flags
ok ports free: http 17780,17781,17782,17783 radio 17790,17791,17792,17793
ok homes + genesis + peers under /tmp/chain-fleet.anSylo; wallet aaaeb678a…
ok all 3 nodes answer /status
ok peers connected: node-1=2 node-2=2 node-3=2
ok heights agree at 11 across 3 nodes
ok late joiner synced to height 12 (IBD by ranges)
ok tx 9ec834607… submitted to node-1, mined at 15 by aa6877b76… (node-2 mines, node-1 paused), bob=5 on node-3
ok fork of depth 3 from 15: main 18 reorged to the heavier chain 20, tip 9efb629c9… on every node
ok node-3 recovered from disk: height 20 before the kill, 20 after restart, conserved
chain-fleet: GREEN
Scenarios: peers connect (bootstrap dial + inbound), headers gossip and
bodies are fetched by id from the announcer (never pushed), a late-joining
node downloads the bodies by ranges and reports catching-up: false, a
transaction signed with the harness wallet crosses node 1 → mempool gossip →
mined by node 2 → indexed on node 3 (/tx/{txid}, /balance), an isolated
node mines a 5-block fork against the fleet's 3 and the fleet reorgs (depth 3)
to it on reconnection, a SIGKILLed node replays blocks.tade and rejoins with
conserved: true.
Bugs the harness found and fixed on the way: the s reply rejected as a
request frame; the responder's dialect frame unexpected; gossip-only peer
entries (kept for scoring) blocking chain-level dials on both sides;
loopback peers counted against the /16 diversity cap; body fetch tied to the
announcer's connection; peer chainwork going stale between status exchanges;
a missing parent during a concurrent fork download marked the fork invalid.
adversarial — cargo test --test chain_adversary
Against a live soft3 node --chain dev --mine --solo, from a radio endpoint
and an iroh-gossip instance in the test:
| case | result |
|---|---|
well-formed s |
answered with the node's status, score 0 |
r with count = 129 |
x code 3 |
stream whose first frame is e (no dialect) |
closed, +10 |
malformed request frame (truncated r) |
+10 (score 20) |
| frame declaring a 10 MiB payload | +100, banned, connection closed; a reconnect is refused |
s with another chain id |
x code 4 delivered, then +100 and disconnect |
| PoW-invalid header on the headers topic | +100, banned; the malicious server saw zero body requests (header-first held) |
valid-PoW header announced, garbage body served on b |
body requested, +10, peer kept (one bad block is not a ban) |
| 400 junk transactions in a burst | rate limit drops the excess (+1 each), decode/signature failures +10 → banned; node healthy |
| a body frame pushed on the headers topic | +100, banned |
| after all of it | /health ok, conserved: true, still mining |
open integration points
- Kernel swap.
feat/chain-kernelexposeschain::api::ChainApi(&mut self:accept_header,accept_block(&Block, now),accept_checkpoint,validate_tx(bytes, pending),header_template(to, time, mempool),take_events,status(),locator,headers(from, count),blocks(from, count),balance,nonce,supply,finalized_height). The wire codes againstnet::chain_view::ChainView(&self); the swap is one adapterMutex<impl ChainApi> → ChainViewplus:missing_bodies(walk the best header chain back to the first held body),headers_from_locator(locator →ancestor_at+headers),spendable/balances/tx_location/counts/conserved(kernel state queries or an index kept by the node),events()(polltake_eventsinto a broadcast). Mempool admission can then callvalidate_txinstead of its own balance checks.devchainstays as the harness chain until then. /v2/historyentries carryheight,block,txidand the raw tx aschain-tx/v1-hex;CGOP-v1-hexfor cyberlink entries needs the kernel's cybergraph apply.- The compiled bootstrap set (three cybernode endpoint ids) is empty until
soft3 node idruns on the machines (deploy/chain README step A7); the peers file is the floor meanwhile. Entries with an emptyidare skipped (QUIC needs the id). - Address announcement:
discovery::address_linksbuilds the two cyberlinks; publishing them from the node's neuron (fee 514 units) is not automatic yet — it needs a funded neuron key on the node (--minermines to it, maturity 100 blocks). - Checkpoints: verified against
genesis.checkpoint_key, stored, relayed, served; the kernel'saccept_checkpoint(finality veto) is the consumer. - Relay-after-validation is implemented with iroh-gossip's neighbor scope
(a validated message is re-broadcast to neighbors; echoes are
Knownand not charged for 120 s). Swarm-scope plumtree would forward before validation, which chain-wire §2 forbids. - Spec notes for chain-wire.md: the responder's side of a request stream
also opens with
(*, k);sis both a request and a reply frame; chain frames are(#, letter)since tade frames carry a sigil; therget-range is served but IBD usesbby the validated header ids (exact, no header-mismatch reassignments); loopback addresses are exempt from the /16 rule so a fleet on one machine can form. soft3 mine --backend cpu(soft3#186) is not merged; the harness mines with the node's built-in CPU loop (--mine), which serves the same/template+/submitshapes the reference miner speaks.