feat(deps)!: delete the vendored chia-protocol and chia-sdk-client forks - #63
Conversation
dig-gossip's wire is vendored byte-identical into dig-relay (GPL-2.0) and runs on a live peer network, so the migration off the vendored chia-protocol fork (dig_ecosystem#2228) must not move a single byte. Pin the encoded frames BEFORE any refactor so the claim is measurable rather than asserted: a vector added afterwards would only record whatever the new code produces. Each vector distinguishes the real encoding from the nearest wrong one -- both Option<u16> correlation-id states, a payload with distinguishable ends so a reversed body or wrong length-prefix width is visible, a non-default NodeType discriminant, and both RegisterAck outcomes. Refs: dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
…directly dig-peer-protocol 0.4.0 replaced its blanket `pub use chia_protocol::*` with three named re-exports, so the ~20 genuine chia full-node types dig-gossip used through that glob (Bytes32, Handshake, RequestPeers, SpendBundle, ...) now come from a direct chia-protocol import. That is the intended shape -- chia types for chia peers -- not a workaround. Handshake.node_type is chia_protocol::NodeType while every DIG-side surface speaks dig_peer_protocol::NodeType. Bridge them explicitly rather than casting: both are closed enums over the same seven roles, so the conversion is total in each direction and an exhaustive match makes a future divergence in either crate a build failure instead of a silently wrong role. The transport type (Message -> DigMessage) is deliberately left for the next commit; it is the transport migration proper, not an import fix. Refs: dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
… enum DigMessage.msg_type is a raw wire byte, so a DIG opcode no longer needs a ProtocolMessageTypes variant to name it -- which is the entire reason the vendored chia-protocol fork existed on this path. frame_envelope, frame_dig_message, frame_holdings_announce and frame_store_melted now build DigMessage directly, and frame_dig_message loses its from_bytes round-trip through the enum. message_wire_len stops returning Result: DigMessage::to_bytes is infallible because msg_type is already a byte and data is already serialized. The golden vectors pinned in 5119249 must still pass byte-identically; that is what makes this a refactor rather than a wire change. Refs: dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
DOES NOT COMPILE (2 errors) -- unfinished plumbing, not a flawed approach. Salvaged from a lane killed by a session limit so the work is not lost. Swaps chia_sdk_client::Peer for dig_peer_protocol::DigLink, which is what retires BOTH vendored crates: DigLink carries chia typed traffic via opcode_of::<T>() AND the DIG band via send_dig/send_message, and supplies the from_server_websocket the vendored fork exists to provide. Refs #2619.
… coupling chia_sdk_client::Peer decodes every inbound frame with chia_protocol::Message:: from_bytes, whose ProtocolMessageTypes stops at RespondCostInfo = 107 -- so it rejects DIG opcodes 200-222 before dig-gossip's own code runs. That runtime decode, not any compile-time type, is what kept the vendored chia-protocol fork alive. DigLink frames a raw opcode byte and so carries the DIG band natively, while still sending Chia-typed bodies via opcode_of::<T>(); one link per connection serves both bands. Inbound rate limiting moves from chia_sdk_client::RateLimiter, which is keyed by ProtocolMessageTypes and therefore cannot express a DIG opcode at all, to dig_peer_protocol::OpcodeRateLimiter, which re-keys Chia's own V2_RATE_LIMITS onto the wire byte and so preserves every Chia bound unchanged. Chia-band opcodes are derived from ProtocolMessageTypes in one small module rather than written as literals, keeping chia-protocol the single authority for Chia opcode numbering. Refs: dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
…tocol RegisterPeer and RegisterAck were declared here with #[streamable(message)], which makes the proc-macro emit a ProtocolMessageTypes::RegisterPeer path -- a variant that exists only in the forked chia-protocol. That one attribute was the second thing anchoring the fork. dig-peer-protocol declares the same bodies with plain #[streamable] plus to_dig_message/from_dig_message, carrying the opcode as a raw byte. No enum variant is needed, and the bodies now have one definition shared across repos instead of one per repo. Registration correspondingly moves from the Chia-typed request_infallible to request_dig. Adds the CON-005 guard battery for the rate-limit re-key: an entry that fails to carry from ProtocolMessageTypes onto the wire byte does not error, it silently falls through to the far looser default_settings, which is a DoS surface no compiler can see. Bounds are pinned as absolute literals from both sides, with a control proving an untabled opcode really is looser -- without it the battery would pass even if every opcode were capped identically. All 9 golden wire vectors pass byte-identically. The wire has not moved. Refs: dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
Both forks existed for one reason: Chia's ProtocolMessageTypes stops at RespondCostInfo = 107 and cannot name a DIG opcode (200-222). chia-protocol was forked to add the 23 variants; chia-sdk-client was forked because its Peer reader decodes with chia_protocol::Message::from_bytes, which rejects any frame whose opcode has no variant -- and because upstream has no server-side constructor for an accepted websocket. dig_peer_protocol::DigLink frames a raw opcode byte and ships from_server_websocket, so neither reason survives. Both vendored trees are deleted and both [patch.crates-io] entries removed. native-tls STAYS patched. It is unrelated to this fork and is security load-bearing: upstream TlsAcceptorBuilder exposes no way to require a client certificate, so dropping it would still compile while silently accepting inbound peers with no client cert at all (CON-009). Verified from the resolved graph rather than from a green build, because a mismatched [patch] disappears with a warning and exit 0: cargo metadata reports no unused-patch warnings, no vendored chia-* package, and exactly one version each of chia-protocol (0.26.0) and chia-sdk-client (0.28.0), both from the registry. native-tls remains the sole vendored entry. BREAKING CHANGE: the peer transport is dig_peer_protocol::DigLink rather than chia_sdk_client::Peer; inbound channels carry DigMessage rather than chia_protocol::Message; classify_broadcast takes a wire byte; message_wire_len is infallible; GossipError gains a LinkError variant. Refs: dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
The chia-protocol/chia-sdk-client fork deletion changed the inbound and outbound transport types the integration harness builds against. Port the test suite to DigLink's surface and bump the version to 0.23.0 (0.x breaking bump for the transport change). Co-Authored-By: Claude <noreply@anthropic.com>
Lane resumed — orchestrator status @
|
| Check | Result |
|---|---|
[patch.crates-io] stanza |
native-tls only. No chia entry. |
Every chia-* in Cargo.lock |
source = "registry+https://github.com/rust-lang/crates.io-index" — zero path/vendor sources |
vendor/ tree |
vendor/native-tls only |
git = deps in Cargo.toml |
none |
CI independently corroborates: the last run downloaded chia-protocol v0.26.0 / chia-sdk-client v0.28.0 from crates.io and compiled only vendor/native-tls locally.
The wire did NOT move — this is the dig-relay contract
dig-relay vendors this wire byte-identically, so this needed proof rather than reasoning. The golden vectors were pinned in 5119249 before any refactor, then edited in ac9fdb8 — which is exactly how a wire break ships green. So every expected literal was extracted from both revisions and compared:
BEFORE: 9 literals AFTER: 9 literals diff: (empty)
All nine unchanged (dc01123400000008deadbeef0102037f, c80000000008deadbeef0102037f, da010007000000110000000a3139322e302e322e3838255301, …). Only the construction changed — Message { .. } → DigMessage::new, and to_bytes() became infallible so the .expect() calls were dropped. No coordinated network change and no dig-relay update is implied.
OpcodeRateLimits — the stall point, decided
OpcodeRateLimits's fields and its from_chia are private; Default (derived from V2_RATE_LIMITS) is the only public constructor, so tests/con_005_tests.rs cannot build a cheap table to exhaust.
Decision: add the public conversion to dig-peer-protocol (release-first) rather than rewrite or drop the tests.
- Rewriting against
Default+limit_factorwould pin each test to upstream Chia's exact numbers, so an upstream table change would silently alter what they measure — and thefrequency = 1/2cases become unreachable entirely. - Dropping them loses five live properties of the Chia bound that this crate's
InboundRateLimitercomposes and depends on: per-connection counter independence, the frequency cap, the per-messagemax_sizecap,limit_factorscaling, and window reset. None of them is fork-specific. from_chia(&RateLimits)already exists and does the work; making it a publicFromis additive and preserves the "derive, never copy" property the module's own lockstep note depends on. dig-peer-protocol has zero open PRs, so single-writer is clean.
Also found
- A vacuous-green trap at
inbound_limits.rs:385,public_flood_opcode_set_is_exactly_221_and_222. UpstreamProtocolMessageTypes::from_bytesnow fails for 221/222, so the loop'scontinueskips precisely the two opcodes the test is named after — it would assert an empty set and pass while proving nothing, and it is the only guard that the public-flood classification does not silently widen. Being ported to rawu8with an explicit falsifiability check. - The crates.io publish stays blocked, and not for the reason the workflow states.
publish.ymlis guarded off citing three vendored forks; two are now gone, butcargo publishstrips[patch.crates-io], so a published dig-gossip would build against upstreamnative-tls— which has no way to require a client certificate — and would silently accept inbound peers presenting none (CON-009). Ticketed separately; the guard must stay.
NEXT ACTION
- dig-peer-protocol: public
From<&RateLimits> for OpcodeRateLimits→ 0.5.0 → publish. - dig-gossip: port the
src/cfg(test)units to u8 opcodes and clear the five unused-import clippy failures. - Re-pin dpp
0.5, portcon_005_tests.rs, run the docs/SPEC sweep, reword thewip(...)commit for commitlint, then the triple gate.
This PR stays DRAFT until the gate round returns.
Gate finding (orchestrator, pre-review): the golden vectors are ENCODE-onlyAll nine vectors assert That is the wrong half of the wire to pin for this change, for three reasons that compound:
Worth noting: PR #62's earlier vector set explicitly included "the round trip through the vendored decoder". That coverage existed and was not carried forward into this PR's nine — so this is a regression on the one axis the fork was ever about, not merely a gap that was always there. Required before mergeAdd decode-side vectors that feed the exact same nine byte strings back in and assert the recovered
Anchor the expectations to the literals already in this file rather than to whatever the decoder currently returns — an expectation captured from a run cannot fail when the run is wrong. |
Upstream `chia_protocol::ProtocolMessageTypes` is a closed enum ending at `RespondCostInfo = 107`, so the fork-only `HoldingsAnnounce` (222) and `StoreMelted` (221) variants no longer exist. Port every `#[cfg(test)]` module in `src/` onto the raw `u8` opcode constants and the `dig-peer-protocol` 0.4.0 `OpcodeRateLimiter` API. Two guards were silently defused by the enum's disappearance and are repaired, not merely recompiled: - `public_flood_opcode_set_is_exactly_221_and_222` filtered each opcode through `ProtocolMessageTypes::from_bytes`, which now FAILS for 221 and 222 — so the loop skipped exactly the two opcodes the test is named after and asserted the empty set. It now walks all 256 raw opcodes with no decode filter, and additionally names the resulting set in full. - `every_220_band_opcode_is_classified` had the same filter over the 220-band, which now matches nothing at all. It enumerates `dig_peer_protocol::ALL_DIG_OPCODES` — the canonical namespace list — and asserts the band it checks is non-empty. The staggered-construction fixture in `dig_rate_limiter` is preserved: the Chia half is now an `OpcodeRateLimiter` built at the window start against the published `Handshake` row (5/window), the DIG half ~1 s later, so an elapsed-since-construction window still fails observably. Co-Authored-By: Claude <noreply@anthropic.com>
`DigMessage::to_bytes` is an inherent, infallible method, so the trait import is dead and fails the `-D warnings` gate. Co-Authored-By: Claude <noreply@anthropic.com>
`DigMessage::msg_type` is already a raw `u8` since the port off `ProtocolMessageTypes`, so these `as u8` casts are no-ops that trip `clippy::unnecessary_cast` under `-D warnings`. Co-Authored-By: Claude <noreply@anthropic.com>
0.5.0 adds `impl From<&RateLimits> for OpcodeRateLimits`, which the CON-005 tests need. A 0.x minor is semver-incompatible, so the old "0.4" requirement could never resolve it. Co-Authored-By: Claude <noreply@anthropic.com>
`dig-peer-protocol` 0.5.0 replaces chia's `ProtocolMessageTypes`-keyed `RateLimiter` with `OpcodeRateLimiter`, keyed by the raw wire opcode, so the CON-005 tests move onto it: `OpcodeRateLimits::from(&limits)` re-keys the same chia table the old fixtures used, and `allow` replaces `handle_message`. The one semantic difference — `admit` does not charge a refused message, where chia's inbound limiter did — changes no assertion here: every test exhausts its budget with ADMITTED frames and then asserts the next one is refused, so nothing depends on a rejection consuming budget. The charge-on-reject property still lives in dig-gossip's own `DigRateLimiter`, which keeps the `incoming` flag. `test_rate_limit_blocks_oversized_message` gets stronger rather than merely ported: the bound is pinned from both sides (at `max_size` admits, one byte over refuses) and the refusal is asserted as `Unsendable`, not just "not admitted" — a size refusal survives every window roll, so reporting it as `Deferred` would send a retrying caller to wait on a budget that can never clear. `Admission` is re-exported to make that assertable. Also clears the warnings the previously-failing target masked: unused imports across seven test files and two more no-op `as u8` casts. Co-Authored-By: Claude <noreply@anthropic.com>
The nine vectors were encode-only, which is the wrong half of the contract. The vendored `chia-protocol` fork existed *because* `Message::from_bytes` rejected DIG opcodes; this PR replaces that decode path, and dig-relay vendors the wire byte-identically and sends frames dig-gossip must accept. Proving we still emit the same bytes says nothing about whether we still read theirs. Each hex literal is now a `const` consumed by BOTH an encode and a decode test, so the two directions cannot drift apart — an expectation captured from a decoder run could not fail when the run is wrong. Adds a negative vector: a frame whose opcode has no `ProtocolMessageTypes` variant must decode successfully, which is the entire reason `DigMessage` keys on a raw `u8`. It is paired with an assertion that `ProtocolMessageTypes::from_bytes` genuinely rejects that opcode — without it the vector would pass just as happily on an opcode the forked enum accepts, and so could not distinguish "decodes DIG opcodes" from "has not happened to reject one yet". Also applies rustfmt, which had drifted on this branch and in several test files the fmt gate would have failed on. Co-Authored-By: Claude <noreply@anthropic.com>
`test_outbound_connect_failure` was the only failing test left in the suite: since the outbound dial moved off `chia-sdk-client` onto `DigLink`, a refused TCP connection arrives as `GossipError::LinkError(WebSocket(Io(ConnectionRefused)))`, not `ClientError`. The truthful contract is the new one, so the CON-001 spec row moves with it rather than the mapping being papered over: `ClientError` still carries handshake-POLICY failures (network-id mismatch, incompatible protocol version), and keeping the two variants distinct is what lets a caller tell "never reached the peer" from "the peer refused us". The test therefore asserts the specific variant — `is_err()` would pass on a handshake rejection that never touched the wire. Co-Authored-By: Claude <noreply@anthropic.com>
…k deletion The publish guard's comments and echo text listed three vendored forks in [patch.crates-io] (chia-protocol, chia-sdk-client, native-tls) and attributed the block to 19 compile errors from cargo publish stripping them. Two of those forks were deleted in dig_ecosystem#2228; only native-tls remains. The publish is still correctly blocked, but for a different and more dangerous reason: cargo publish strips [patch.crates-io], so a published dig-gossip would build against upstream native-tls, compile cleanly, and silently accept inbound peers presenting no client certificate (CON-009). The failure mode is a silent security regression, not a build break. Comments and echo text only — the guard step, its condition and its exit 1 are unchanged. Refs dig_ecosystem#2228, dig_ecosystem#2647 Co-Authored-By: Claude <noreply@anthropic.com>
…ransport Sections 1 through 1.7 described the peer connection as chia-sdk-client's `Peer`, reached by `connect_peer()`, with rate limiting by `RateLimiter` + `V2_RATE_LIMITS` and payloads delivered as `chia-protocol::Message`. None of that is the contract any more: dig-gossip depends on `dig-peer-protocol`, whose `DigLink` frames a raw u8 opcode, and delivers `DigMessage`. The Chia types are still used, but as re-exports rather than direct dependencies. SPEC.md is normative, so an independent reimplementation built against these sentences would have named types that dig-gossip does not use. Refs dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
… API surface Sections 2, 5, 8 and 10 still described a vendored `chia-protocol`, a `vendor/` thin-wrapper policy, and an API built on `chia-sdk-client`'s `Peer`, `PeerOptions`, `RateLimiter` and `connect_peer()`. The vendored Chia forks were deleted in dig_ecosystem#2228; the peer link is `dig_peer_protocol::DigLink`, inbound uses `DigLink::from_server_websocket()`, and the framing encoder writes a raw u8 opcode instead of mirroring a discriminant onto `ProtocolMessageTypes`. Corrected in this commit: - 2.1 / 10.2 re-export blocks, which named types the crate does not re-export - 2.4 PeerConnection, whose `peer` field is a `DigLink` - 2.3 / 8.x frame and payload signatures, which take and return `DigMessage` - 5.1 / 5.2 connection flows, incl. why outbound mirrors rather than calls upstream's `connect.rs` (it must capture the remote SPKI DER) - 5.3's `Peer::from_server_websocket` and the native-tls patch rationale - 8.1's "vendored `chia-protocol`" clone note and `vendor/` policy reference - the dispatch-authority paragraph's "mirrors onto the vendored `ProtocolMessageTypes`" claim - 10.3 / 10.4 feature-flag and dependency snippets The wire is unchanged: all nine golden hex vectors are byte-identical across the refactor, so no on-wire statement in this document moved. Refs dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
…dig-peer-protocol STR-001, STR-003, STR-004, IMPLEMENTATION_ORDER and prompt/start.md still required a direct chia-sdk-client dependency, feature forwards to chia-sdk-client/*, and re-exports of Peer/PeerOptions/RateLimiter. The manifest and the str_001 tests already assert the opposite: dig-peer-protocol at 0.4 with default-features = false, and both TLS features forwarding through it. STR-001 gains an acceptance criterion that [patch.crates-io] contains native-tls and nothing else, so a re-vendored Chia crate is a spec violation rather than a silent regression. Refs dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
…opcode band The connection, crate_api, discovery, relay, privacy and erlay domain specs still specified chia-sdk-client's Peer, PeerOptions, connect_peer(), Peer::from_websocket() and RateLimiter, and two TRACKING.yaml files cited vendor/chia-protocol and vendor/chia-sdk-client paths that no longer exist. Corrections of substance, beyond the renames: - CON-001 no longer specifies calling connect_peer(). The crate deliberately mirrors upstream's connect.rs instead, because upstream discards the parsed Handshake and never exposes the remote TLS SPKI DER, and API-005 needs both. - CON-002 states why inbound needs from_server_websocket: from_websocket types the stream as the client-oriented MaybeTlsStream. - CON-005 keys both rate-limit bounds by the raw wire opcode; the previous text had the Chia bound keyed by ProtocolMessageTypes. - DSC-005 tracking no longer claims a vendored chia-protocol supplies the 218/219 enum variants. They travel as raw DigMessage msg_type bytes. - CON-009 tracking names vendor/native-tls as the only remaining vendored crate. All TRACKING.yaml and registry files re-validated as YAML. Refs dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
… trap Carries forward the one durable lesson from the abandoned rebase investigation (PR #62): [patch.crates-io] substitutes only where the patched version SATISFIES the requirement, so a mismatched patch is dropped with a warning and EXIT=0 — the failure has no shape. Rewritten for the post-deletion world, where it now explains why the remaining native-tls patch is guarded in publish.yml rather than by the compiler: nothing stops compiling if it is stripped, the acceptor just stops requiring a client certificate. Also kept from #62: upstream ProtocolMessageTypes stops at RespondCostInfo = 107 at 0.26.0, 0.36.1 and 0.47.0, so the DIG 200-222 band collides with nothing. Not carried forward: #62's verdicts that chia-protocol and chia-sdk-client could not be deleted. The redesign it named as a prerequisite is exactly what was done. The new entry records the mechanism — DigMessage's raw u8 msg_type, which makes the opcode band expressible with no enum to extend — and that the wire was pinned by golden vectors before the refactor and is byte-identical after. Also corrects the #1371 entry, which credited from_server_websocket to the vendored chia-sdk-client; it now lives on DigLink, and is one of the reasons the fork could go. README's PeerConnection field and Cargo.toml's dependency-shape rationale follow the same rename (Cargo.toml comments only — no dependency, version or patch line is touched). Refs dig_ecosystem#2228, dig_ecosystem#2647 Co-Authored-By: Claude <noreply@anthropic.com>
…nt blocks Three dependency comments still pointed at `chia_sdk_client::Peer` and one called the rustls backend 'the vendored chia-sdk-client outbound connector'. The type is `dig_peer_protocol::DigLink` and chia-sdk-client is now an ordinary transitive crates.io dependency. Comments only: the diff contains no dependency, version, feature or [patch.crates-io] line. Refs dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
listener.rs and outbound.rs still documented `chia-sdk-client` and `vendor/chia-sdk-client` as the transport, and outbound.rs linked a docs.rs page for a function neither crate exposes here. Port the reasoning to what the code now does. Co-Authored-By: Claude <noreply@anthropic.com>
Four doc comments referenced vendored chia-protocol and chia-sdk-client directories that were deleted during the DPP 0.4.0 adoption. Update them to describe what the code actually does now: - introducer_client.rs:19 — point to the real dial reference (src/connection/outbound.rs) instead of vendor path; note that chia-sdk-client remains a legitimate crates.io transitive dependency - introducer_register_wire.rs:13 — replace "fork keeps it alive" with "DIG opcodes now travel as raw msg_type bytes, making the fork removable" - discovery/mod.rs:13 — drop vendor-path reference; explain that opcodes 218/219 re-export from dig-peer-protocol - gossip_handle.rs:988 — replace "(see vendor/chia-sdk-client patch)" with "a property of the DigLink receiver" Code behavior unchanged; comment text only. Co-Authored-By: Claude <noreply@anthropic.com>
The introducer client now uses DigLink for the dial, so a refused TCP connect surfaces as GossipError::LinkError, not ClientError. This matches the CON-001 pattern where the dial's typed connection-level error is LinkError; ClientError remains reserved for handshake-policy failures (dig_ecosystem#2228). Add LinkError to the match in test_query_introducer_connect_fail and update DSC-004 acceptance criteria and test plan to clarify the expected error variant. Co-Authored-By: Claude <noreply@anthropic.com>
220 is `DIG_MESSAGE`, 221 `STORE_MELTED`, 222 `HOLDINGS_ANNOUNCE`, and all three are live. dig-relay vendors this wire byte-identically, so an understated band in a dependency comment is exactly the kind of number that gets copied into a second implementation. Comment only — the dependency line is unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
…port APIs Replace stale doc/comment text referencing deleted `vendor/chia-protocol` and `vendor/chia-sdk-client` paths; update references to the old `connect_peer()` function (which does not exist) with descriptions of the actual transport establishment via TLS + handshake in `dig_peer_protocol::DigLink`. Update `send_protocol_message` reference to the actual `send_message` method. Facts: - chia_sdk_client::Peer → dig_peer_protocol::DigLink (the connection type) - connect_peer() does not exist (outbound dial is in src/connection/outbound.rs) - send_protocol_message → send_message - vendor/chia-protocol/, vendor/chia-sdk-client/ paths no longer exist Files changed: 8 test files with doc/comment-only updates. No test logic, assertions, fixtures, or use statements changed. Co-Authored-By: Claude <noreply@anthropic.com>
…_peer() The SPEC traceability block quoted `connect_peer()`, a function no crate in this tree exposes. SPEC.md no longer uses that name either, so the citations pointed at language that had been rewritten. Co-Authored-By: Claude <noreply@anthropic.com>
22f27bd to
661133e
Compare
Adversarial gate: REFUTED — this PR silently drops the inbound rate limiter's anti-flood ratchetMerge is blocked. The fix is in flight as dig-peer-protocol 0.6.0. The break
chia: OpcodeRateLimiter::new(RESET_SECONDS, rate_limit_factor, OpcodeRateLimits::default()),On RateLimiter::new(true, RESET_SECONDS, rate_limit_factor, (*V2_RATE_LIMITS).clone())The leading if self.incoming || passed {
// commit message_counts, message_cumulative_sizes, non_tx_count, non_tx_size
}With
Worked example — no DIG opcode required
Why this is a gate failure and not a defensible design change
Why no existing gate could catch itThe 167 new lines in Stated without overclaiming: FixRoot-caused into pub enum Direction { Inbound, Outbound }
OpcodeRateLimiter::new(direction, reset_seconds, limit_factor, limits)Deliberately not upstream's bare Then here: re-pin Attacks that FAILED (recorded so they are not re-run)
|
Second blocker: handshake-POLICY failures are being wrapped in
|
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
CHANGES-REQUIRED - independent correctness gate, head 661133e
The fork deletion itself is clean and well-evidenced. Verified independently:
- (a) The fork is provably gone from the lock. Every
chia-*entry inCargo.lockcarriessource = "registry+https://github.com/rust-lang/crates.io-index"; the only two packages in the lock with nosourcearedig-gossipitself and the patchednative-tls.[patch.crates-io]containsnative-tlsonly, andvendor/holds onlynative-tls+fork-delta.sh.git diff base..head -- vendor/native-tls/is empty - the security-load-bearing CERT_REQUIRED fork is untouched and its patch entry survives. - (b) The wire did not move. All nine hex literals pinned in
5119249are byte-identical at head; exactly one literal is added (fe0000000008deadbeef0102037f, the negative vector). Encode and decode share oneconstper vector, so the two directions cannot drift into agreeing with each other. - (c) The item-5 vacuity check bites - measured, not read. I widened
is_public_flood_opcode'smatches!arm with| 220u8in my own worktree:public_flood_opcode_set_is_exactly_221_and_222went RED (opcode 220 public-flood classification, left: true, right: false,inbound_limits.rs:408). Restored; the shared checkout was never touched. - (d) No con_005 property was lost. All five survive (per-connection independence, frequency cap,
max_size,limit_factor, window reset), and themax_sizecase is strengthened - it now pins both sides of the bound and assertsAdmission::Unsendablerather than a bare refusal. None of the five depended on the old chia limiter charging rejected frames. - No test was deleted. Test-function names: 943 at base, 972 at head, zero removed.
- The
0xfenegative vector is paired with an explicitProtocolMessageTypes::from_bytes(&[0xfe]).is_err()assertion, so it genuinely distinguishes "decodes DIG opcodes" from "has not happened to reject one yet".
What blocks it: the settled LinkError-vs-ClientError design was applied to only one of the two handshake legs, which leaves the crate red. Test Suite + Coverage (>=80%) is FAILING at this head (run 31501745084) and aborted at 531/977 tests, so the 80% coverage floor is not merely unmet - it is unmeasured. Three findings below; details inline.
Doc-only findings 2 and 3 are handed to @copilot. Finding 1 (and the CON-002/CON-003 sweep that depends on its resolution) is an error-contract decision plus a test, and is NOT for Copilot - route it to an implementer.
RESUME CHECKPOINT — machine restart. Written for an agent with zero context.Everything is pushed. No uncommitted work anywhere.
THE FORK IN THE ROAD —
|
…n both dial legs Recovered WIP from a lane that stopped before pushing. Introduces `DialError`, the two-armed union a dial returns so a policy verdict is not downgraded into a transport error on the way out. Before this, the outbound leg and the introducer client had only `LinkError` available in their return type, so `ClientError::WrongNetwork` / `WrongNodeType` were rendered as `LinkError::Io(Custom(<formatted string>))`. That put "never reached the peer" and "the peer failed our policy" in one variant distinguishable only by matching the error string, which is not a contract, and it contradicted the settled decision on this PR that the two stay distinct because they carry opposite retry semantics. The inbound listener already returned `ClientError` for a policy rejection, so this brings the outbound leg into line rather than inventing a new shape. WIP: not yet compiled or tested on this branch; pushed first so the work survives. Co-Authored-By: Claude <noreply@anthropic.com>
`cargo nextest` stops at the first failing target by default, and the run then reports a pass count that is indistinguishable from a complete one. On this branch a red head stopped at 531 of 977: 446 tests never executed, and because the run aborted before finishing, `--fail-under-lines 80` never measured the coverage floor either. That is worse than a plain failure. "Unmeasured" and "measured and under" are different verdicts, and the job was rendering the first as though it were a count you could reason about. Every suite total quoted from such a run is a lower bound with no upper bound attached. `--no-fail-fast` makes the job finish counting before it fails. It does not weaken the gate: nextest still exits non-zero on any real failure, `--retries 2` still classifies transient loopback flakes, and the coverage floor now gets a complete measurement to apply itself to. Added to the rustls `--lib` job for the same reason. Refs dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
Lane status —
|
| # | Blocker | Where the fix lives | State |
|---|---|---|---|
| B1 | Inbound rate limiter lost incoming = true, so refused frames are no longer charged and a flood is free |
dig-peer-protocol | NOT in this repo |
| B2 | Handshake-POLICY failures wrapped as LinkError |
dig-gossip | fixed at 2ce46c3, compiles; doc/spec sweep in flight |
| B3 | RequestPeers correlation-id carve-out — read_inbound does requests.remove(id) for every opcode |
dig-peer-protocol | NOT in this repo |
B1 and B3 have no dig-gossip-local fix, and this was measured rather than assumed.
OpcodeRateLimits's five fields (default_settings, non_tx_frequency, non_tx_max_total_size, tx,
other) are all private with no accessors, so dig-gossip cannot re-key the table itself without
duplicating the crate's From<&RateLimits> — and that crate's own module doc explains why a
locally-assembled table is dangerous: keys re-keyed from a different chia_protocol version shift bytes,
every Chia opcode misses its entry and falls to default_settings, a silent loosening with no compile
error. RequestMap and read_inbound are likewise pub(crate) inside that crate. Re-implementing
either here would be exactly the second-implementation drift this ecosystem's crate rules forbid.
The one thing standing between this PR and merge
dig-peer-protocol PR #8 (fix/rate-limit-direction, head 786aa55c) fixes both B1 and B3, and
its state is better than this PR's notes claimed — the earlier checkpoint recorded it as "WIP,
mid-implementation, does not necessarily compile" at c53544b. It has since advanced and is now
mergeable with all 8 checks green: Format/Clippy/Test/Docs, Check version increment, Lint commit
messages, Coverage (>=80% lines), CodeQL ×3.
- B1 →
pub enum Direction { Inbound, Outbound }as the first parameter ofOpcodeRateLimiter::new.
Deliberately an enum rather than upstream's positionalbool, because a baretruereading as nothing
at a call site is precisely how this defect vanished unnoticed in the first place. - B3 →
Requestrecords the opcodes that may complete it, andremove(id)becomes
take_matching(id, msg_type). No hardcoded opcode list, so it does not recreate the closed-allow-list
problem the fork existed to escape.
Its body says DO NOT MERGE — gate round pending, and that must be respected. It is a !-breaking
change to a published crate on the peer wire; merging it publishes an immutable 0.6.0 to crates.io. It
needs its gate round, not a fast merge — a gate verdict that arrives after publication is a post-mortem.
Once 0.6.0 is live, the delta here is small and mechanical:
Cargo.toml:dig-peer-protocol = { version = "0.6", default-features = false }, then re-locksrc/connection/inbound_limits.rs:83: passDirection::Inbound- restore the four doc sites to assert the ratchet again (
inbound_limits.rs:78,112,
service/state.rs:137,tests/con_005_tests.rs:32) - add the B1 regression test: drive frames past refusal, then assert a subsequently legal frame is
still refused, including bothnon_txaggregates - version bump last
Settled, do not relitigate
LinkErrorvsClientErrorstay distinct. Do not "fix" a red test by accepting either variant, and
never match on the error string.OpcodeRateLimitswas solved by adding the publicFrom<&RateLimits>to the owning crate (shipped as
0.5.0).con_005_tests.rsports 15/15 with one test strengthened. No coverage was dropped.- The wire did not move: 9/9 golden literals byte-identical, and the decode tests read the same
constthe encode side asserts, so a wrong decoder cannot self-agree with a wrong encoder.
dig-relay needs no change.
Sibling PRs
- docs(vendor): the chia fork rebase is a cross-repo cascade; no fork can be deleted #62 — CLOSED unmerged. Its central finding ("no fork can be deleted") is falsified by this PR, four
of its seven files live in directories this PR deletes, and its test file is typed on the closed
ProtocolMessageTypesenum so it could not compile here anyway. Everything durable in it was already
salvaged into this PR'sDEVELOPMENT_LOG.md; the close comment itemises what went where. One unique
fragment is still owed: the "Why upstream cannot replace this" section of
vendor/native-tls/README.dig-gossip.md, whose consumer-visible half (dig-node takes dig-gossip with
default-features = false, features = ["rustls", "relay"]and so reaches the rustls acceptor, the one
path where thenative-tlspatch is not load-bearing) is worth keeping. - fix(connection): adopt DigLink so one bad frame cannot tear down a peer link #60 — waits for this PR. Its
src/connection/link_adapter.rstranslatedDigMessage↔
chia_protocol::Messageat one seam so the rest of the crate could stay onMessage. This PR migrates
the whole crate toDigMessage, so that adapter is entirely subsumed — there is noMessageleft
to translate. What survives and matters istests/sec_2391_frame_tolerance_tests.rsplus the
hostile-frame harness, which become the acceptance test for the new transport and for B3's fix. If
tolerance does not hold onDigLink, that is a finding, not a rebase conflict.
…laims
SPEC.md is the contract an independent reimplementation is built against, so
each of these sentences was a real defect rather than a cosmetic one.
* Dependency routing. SPEC.md claimed dig-peer-protocol was "the single
dependency" reaching the Chia ecosystem, and that the Chia crates were
"reached through the re-exports above", while its own table eight lines
later marked chia-protocol and chia-traits as direct reuse. Cargo.toml
declares both directly and lib.rs re-exports the wire types straight from
chia_protocol, so the prose was the wrong half. Also corrects chia-ssl,
which the table called direct reuse but which is not in Cargo.toml at all,
and the Cargo.toml comment making the same over-claim.
* Superseded transport type. The crate transports dig_peer_protocol::DigMessage;
chia_protocol::Message appears zero times under src/. A reimplementer reading
the old sentence would build on Chia's closed #[repr(u8)] discriminant, which
is precisely the type this branch exists to stop using, and would hit the
decode rejection the vendored fork was created to work around.
* Rate limiter. Four code sites and three CON-005 rows asserted the inbound
gate charges refused frames on both halves. Only DigRateLimiter is
inbound-shaped today; OpcodeRateLimiter exposes no such flag in
dig-peer-protocol 0.5, and OpcodeRateLimits' fields are private so there is
no local substitute. Each site now states what is true and marks the gap
with TODO(dig_ecosystem#2228) rather than asserting a security property the
code does not have.
* Rename collateral. A Peer -> DigLink rename ran through prose it should not
have, mangling quoted SPEC headings ("Peer identity from mTLS", "Peer
sharing via gossip", "Message cache") into names those sections do not have.
Co-Authored-By: Claude <noreply@anthropic.com>
First complete measurement of this branch —
|
`test_register_introducer_connect_fail` dials `wss://127.0.0.1:7/ws` with nothing listening and accepted only `ClientError | IntroducerError`. A refused TCP connect is unambiguously "never reached the peer", which is what the settled design assigns to `LinkError` — so the code was right and the expectation was stale. This is the same defect `1e29f2e` fixed in `dsc_004_tests::test_query_introducer_connect_fail`, and it takes that commit's shape exactly, including the DSC-005 acceptance row. Two things are worth recording about why it survived that commit: - The fail-fast nextest run stopped inside `dsc_004` and never reached `dsc_005`, so the twin sat in the 446 tests that never executed. It surfaced on the first run after `--no-fail-fast` landed. - A local `--all-features` run reports it PASSING. That configuration enables `native-tls` and `rustls` together, which takes a different connector path than CI's `--no-default-features --features native-tls,...`. An `--all-features` green is not evidence for this crate. This is not a relaxation. The prohibition on this branch is against accepting either variant for a handshake-POLICY rejection, where `ClientError` is the only correct answer; `test_query_introducer_handshake_wrong_network` still demands it specifically and still passes. Verified in CI's exact feature set: `13 tests run: 13 passed, 0 skipped`. Also salvages the one non-duplicated section of PR #62 before it is discarded: why upstream `native-tls` cannot replace the vendored patch, and the single consumer configuration (dig-node, `default-features = false, features = ["rustls", "relay"]`) in which the patch is not load-bearing — verified against dig-node's manifest. Refs dig_ecosystem#2228 Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
CHANGES-REQUIRED — correctness gate (head da4c23e2c1c27608ad896af249f9e7a44c839063)
Most of this PR verifies clean. What blocks it is one thing: the RequestPeers correlation-id carve-out is NOT "structurally moot", and the code being deleted in this very PR contains the comment explaining why.
What I verified GREEN (independently)
- Wire did not move. Diffed every hex literal in
tests/wire_golden_vectors_tests.rsatd8f8f33vs HEAD: identical, sole addition"fe0000000008deadbeef0102037f". The test also got stronger — every vector is now asserted in both directions off one literal. The negative-vector guardthe_unassigned_opcode_really_is_unrepresentable_as_a_protocol_message_typeis real (ProtocolMessageTypes::from_bytes(&[0xfe]).is_err()), so the 9th vector is falsifiable. dig-relay's byte-identical vendoring is safe. - Defect (a), the lost
incomingflag: genuinely fixed.dig_rate_limiter.rs:113charges onself.incoming || passed, and it is pinned from BOTH sides —inbound_charges_rejected_frames_so_junk_buys_no_retriesfails if the flag is dropped (charge-only-admitted),outbound_charges_only_admitted_framesfails if it is hardcoded true.check()still returns the verdict. The window test staggers construction by ~1 s of a 2 s window specifically to kill an elapsed-since-construction implementation, with a control assertion that the boundary was not crossed early. This is not a vacuous suite. - Defect (b), the WIP salvage: completed, not merely compiling.
tests/dsc_004_tests.rs:229stands up a real WSS introducer serving a wrongnetwork_idand assertsmatches!(err, GossipError::ClientError(_))— the settled variant, not a string, not a tolerant either-arm match.validate_remote_handshake(...)?atoutbound.rs:265routes throughFrom<HandshakeValidationError> for DialError→Client, preservingWrongNetwork. con_005_tests.rs: 15/15 port is real, not degraded toDefault. All five claimed properties still measured against the realV2_RATE_LIMITS-derived table. Zero#[test]functions deleted anywhere intests/orsrc/(+808/-266intests/, plus a newcon_005_rate_limit_rekey_tests.rs). No coverage was lost by deletion.
Findings below, ranked.
Co-authored-by: MichaelTaylor3d <5665004+MichaelTaylor3d@users.noreply.github.com>
Gate round returned — BLOCKED on a gating regressionVerdicts at The blocker (R1) — near-deterministic mutual teardownDeleting the fork removed an opcode-based carve-out that was load-bearing. Two independent gates reached the same mechanism:
The deleted fork stated this in its own comment ( What is confirmed GOOD (do not re-derive)
Remaining items
Note on
|
Two dig-gossip peers tore each other's link down. Both allocate correlation ids from a counter starting at zero and both keepalive loops start at handshake on a shared interval, so the first probes carried the same id. DigLink matches inbound frames on correlation id before forwarding, so each side's waiter received the peer's RequestPeers, the peer's request never reached the forwarder, its auto-reply never fired, and both sides tore down at the staleness check while logging a timeout that named the wrong cause. The probe now goes out with id: None and liveness is observed on the application inbound broadcast, which skips the id-match arm entirely. An unavailable broadcast is liveness-neutral: the round is skipped and the peer is kept, the correct direction for a probe whose only action is to disconnect. Co-Authored-By: Claude <noreply@anthropic.com>
…ion waiter (#2767) Co-Authored-By: Claude <noreply@anthropic.com>
…ion (#2767) Co-Authored-By: Claude <noreply@anthropic.com>
…ort (#2228) An opcode outside ProtocolMessageTypes cannot be expressed as a typed ClientError, so it takes a dig-gossip-owned DialError::UnknownOpcode carrying the raw byte rather than a formatted string. Also removes the false --no-default-features coherence claim in introducer_client and the stubs that existed only to serve it: no TLS backend does not build today (dig_ecosystem#2225), and makes the crates.io publish guard unconditional so dispatch is not an escape hatch around the stripped native-tls patch (dig_ecosystem#2647). Co-Authored-By: Claude <noreply@anthropic.com>
…#2767) Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Correctness gate (fresh context) — PASS
Head reviewed: 7db09b9333fbdfd6cab868a660e44b0dbe901aec (resolved from the remote, not from the dispatch).
Scope: the combined delta da4c23e2..7db09b9 (six commits, two authors) plus re-verification of the three round-1 findings.
The three round-1 findings are genuinely closed
- RequestPeers correlation-id carve-out — closed at
91bca94.send_probeusesDigLink::send(id: None), the reply is observed on the application inbound broadcast, and the id-match arm is structurally unreachable for the probe. - Typed variant downgraded to a formatted string — closed at
ce4c08e. Bothoutbound.rs:242andintroducer_client.rs:131,219now returnClientError::MissingHandshake; no formatted-string handshake error remains. - Wrong-opcode-after-connect classified as transport — closed at
5a37486.non_handshake_first_framemaps a Chia-band opcode to the typedClientError::InvalidResponseand an unmappable one toDialError::UnknownOpcode(u8)->GossipError::UnknownHandshakeOpcode(u8). No error-string matching anywhere; both arms pinned from both sides.
Anti-vacuity: three mutations RE-RUN, not taken on report
All run in my own throwaway worktree (C:/tmp/worktrees/dg-gate63, since removed). No shared checkout was mutated.
| Mutation | Result |
|---|---|
A. revert src/connection/keepalive.rs to ce4c08e (pre-fix) |
RED — colliding_correlation_ids_do_not_tear_the_link_down panics at tests/con_2767_keepalive_correlation_tests.rs:107, "A must still hold B after 6s of probing". Exactly the claimed message. |
B. delete the fail-open last_success = Instant::now() reset in the subscribe_inbound else-branch |
RED — an_unobservable_probe_does_not_disconnect_the_peer panics at :161. The reset is genuinely load-bearing; a bare continue would have been torn down by the staleness check, as the round-1 note predicted. |
C. (the important one) pre-fix keepalive AND remove connect_and_aligns b.request_peers_from(...) |
GREEN — the buggy build passes. This proves the id-alignment step is what makes the regression test discriminate at all, and independently explains why the pre-existing test_keepalive_bidirectional was green with the defect present. The collision is forced, not hoped for. |
Baseline green re-run for all of: con_2767_keepalive_correlation_tests (2), con_004_tests (4), dsc_004_tests (6), dsc_005_tests (7), con_006_tests (9), plus the new connection::dial_error (2) and connection::keepalive (4) unit tests. All pass.
CON-006 metering — verified metered exactly once
Dropping record_live_peer_inbound_bytes from keepalive.rs is correct, and I checked the mechanism rather than the summary. An uncorrelated RespondPeers is no longer claimed by a correlation waiter, so it reaches the forwarder, and both forwarders (listener.rs:872 and gossip_handle.rs:1018) charge record_live_peer_inbound_bytes on every allowed inbound frame before the auto-reply and the broadcast. Keeping the manual call would have double-counted; removing it leaves exactly one charge. Not twice, not zero.
Task-3 deletions — nothing live removed
I verified the deletion rationale empirically rather than accepting it. cargo check --no-default-features already fails on origin/main (5 errors) — src/service/state.rs:67 and lib.rs:177 import/re-export ClientState unconditionally. So the cfg(not(any(...))) stubs at the old :283-310 were unreachable dead code serving a doc claim that was false, and replacing that claim with an honest note plus the #2225 tracking reference is the right resolution.
The wire did not move
git diff origin/main -- src/relay/ is empty at this head. Re-confirmed. dig-relay's vendored byte-identical copy is unaffected.
Craft
The new surfaces read well: send_probe / await_respond_peers / subscribe_inbound / non_handshake_first_frame are intent-revealing, every non-obvious branch carries a WHY comment (the Lagged-is-liveness note and the fail-open rationale are both exemplary), and every new public item is doc-commented. The #2767 module doc states the property, not the outcome.
Blast radius
gitnexus was not used (no index in the worktree) — the sanctioned ripgrep + direct-read fallback, and the PR body says so. I re-did it independently: all call sites of the changed symbols enumerated, and no downstream code matches on GossipError at all (checked dig-node and dig-relay), so the two new public enum variants break nothing today. The 0.22.2 -> 0.23.0 bump is the correct breaking level for a 0.x crate.
Four non-gating notes
Posted inline below. None blocks merge; I am resolving all four myself so they cannot trip required_conversation_resolution.
Nothing handed to Copilot — there is no gating finding to hand over.
Note for the orchestrator: Test Suite + Coverage (>=80%) was still IN_PROGRESS when I read the rollup. Assert required checks by name with check-merge-preconditions.sh before merging; the PR is also still a draft.
#64) * test(sec-2391): stub — re-derive frame-tolerance acceptance test on post-#63 main Co-Authored-By: Claude <noreply@anthropic.com> * test(sec-2391): prove one anomalous frame cannot tear down a peer link DigLink's inbound loop removes three former one-frame kill switches (unmatched correlation id, unknown opcode, undecodable frame), but nothing in this crate proved it, so a future change could silently restore one. Drives each anomaly over a real TLS websocket and asserts a subsequent correlated request is still served, plus a negative control proving that assertion fails against a genuinely torn-down link. Refs DIG-Network/dig_ecosystem#2391 Co-Authored-By: Claude <noreply@anthropic.com> * test(sec-2391): guard the hostile fixtures and bump to 0.23.1 Pins each hostile frame to the decoder so a fixture that quietly stopped being hostile cannot leave the tolerance tests green while exercising nothing. Test-only change, so patch. Co-Authored-By: Claude <noreply@anthropic.com> * test(sec-2391): tighten the hostile opcode guard Co-authored-by: MichaelTaylor3d <5665004+MichaelTaylor3d@users.noreply.github.com> * chore: report validated review fix Co-authored-by: MichaelTaylor3d <5665004+MichaelTaylor3d@users.noreply.github.com> * chore: drop incidental index count updates Co-authored-by: MichaelTaylor3d <5665004+MichaelTaylor3d@users.noreply.github.com> * test(sec-2391): key the hostile opcode guard to ALL_DIG_OPCODES The guard listed three DIG opcodes by hand, so it covered 3 of the 23 assigned and could not catch its own drift. ALL_DIG_OPCODES is the canonical set already used for the same purpose in src/connection/inbound_limits.rs. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
DO NOT MERGE — BLOCKED on a gating regression. The triple gate returned CHANGES-REQUIRED (correctness) and REFUTED (adversarial): deleting the fork removes an opcode carve-out whose absence makes two peers tear each other's link down on the first keepalive. Fix shape is being settled; this PR stays a draft until it lands and the gates re-run.
Closes DIG-Network/dig_ecosystem#2228.
Closes DIG-Network/dig_ecosystem#2767.
Does not close DIG-Network/dig_ecosystem#2225: that ticket is that a TLS-disabled feature configuration does not compile, which remains true (it already failed on
main). This PR only deletes the false claim that it stayed coherent, plus the unreachable stubs serving that claim.What this does
Deletes the vendored
chia-protocolandchia-sdk-clientforks and moves dig-gossip's transport ontodig_peer_protocol::DigLink. 89 files, +1031/−7757.An earlier revision of this PR reported the deletion as blocked. That finding has been superseded: the blocker it identified was real, and the transport migration it prescribed is what this PR now contains.
Why the forks existed
Not a compile error — a runtime decode. Every inbound frame went through
vendor/chia-sdk-client/src/peer.rs:446,Message::from_bytes(&binary)?. Upstreamchia_protocol::ProtocolMessageTypesis a closed#[repr(u8)]enum (not#[non_exhaustive]) that stops atRespondCostInfo = 107, so without the fork that call rejects every DIG opcode 200–222 before dig-gossip's own code is reached.Message's fields are private under#[streamable(no_serde)], so no local wrapper could paper over it.DigLinkis the upstream-intended replacement and retires both vendored crates rather than one: it framesDigMessage, whosemsg_typeis a rawu8, and suppliesfrom_server_websocketandsend_message— two of the three reasons thechia-sdk-clientfork existed. The third (inboundRequestPeersrouted to the application channel rather than the outboundRequestMap) was originally described here as "structurally moot". That was wrong, and two independent gates have now falsified it.DigLink::read_inboundmatches on id FIRST (dig-peer-protocol-0.5.0/src/link.rs:470-478), so a colliding id is delivered to our waiter and never reaches the receiver. BothRequestMaps startnext_idat 0 (request_map.rs:45) and both peers fire the first keepalive probe simultaneously, so the first probes collide on id = 0 and the two peers disconnect each other. See the blocking issue linked below.Evidence the fork is actually gone
Not inferred from the build succeeding —
[patch.crates-io]substitutes only where the patched version satisfies the requirement, and a mismatch drops the patch silently with exit 0. So this is read from the lock file and the vendor tree directly:[patch.crates-io]native-tlsonly — no chia entrychia-*inCargo.locksource = "registry+https://github.com/rust-lang/crates.io-index"; zero path/vendor sourcesvendor/treevendor/native-tlsonlygit =depsCI corroborates independently: it downloads
chia-protocol v0.26.0andchia-sdk-client v0.28.0from crates.io and compiles onlyvendor/native-tlslocally.vendor/native-tlsstays and is untouched. It is security-load-bearing: it setsCERT_REQUIRED+ Chia CA trust for inbound mTLS (CON-009), and upstreamTlsAcceptorBuilderexposes no way to require a client certificate.The wire did NOT move
dig-relay vendors this wire byte-identically (GPL-2.0), so this is a cross-repo contract and needed proof rather than reasoning.
The golden vectors were pinned in
5119249before any refactor, then edited inac9fdb8— which is exactly the shape in which a wire break ships green. Every expected literal was therefore extracted from both revisions and compared:All nine unchanged. Only the construction changed:
Message { .. }→DigMessage::new, andto_bytes()became infallible so the.expect()calls were dropped.No coordinated network change and no dig-relay update is implied by this PR.
OpcodeRateLimits— the decision this PR was stalled onOpcodeRateLimits's fields and itsfrom_chiaare private, andDefault(derived fromV2_RATE_LIMITS) is its only public constructor — sotests/con_005_tests.rscannot build a small table to exhaust cheaply.Settled by adding the public conversion to dig-peer-protocol (release-first: DIG-Network/dig-peer-protocol#7 → 0.5.0 → publish → re-pin here), rather than rewriting the tests against
Defaultor deleting them.Default+limit_factorwould pin each test to upstream Chia's exact numbers, so an upstream table change would silently alter what they measure — and thefrequency = 1/2cases become unreachable.InboundRateLimitercomposes and depends on: per-connection counter independence, the frequency cap, the per-messagemax_sizecap,limit_factorscaling, and window reset. None is fork-specific, so none is dead surface.from_chia(&RateLimits)already exists and does the work; exposing it as aFromis additive and preserves the "derive, never copy" property the module's own lockstep note depends on.Known follow-ups
publish.ymlis guarded off citing three vendored forks; two are now gone, butcargo publishstrips[patch.crates-io], so a published dig-gossip would build against upstreamnative-tlsand silently accept inbound peers presenting no client certificate. Tracked as dig_ecosystem#2647; the guard must stay.Bump
0.22.2→0.23.0. Breaking (the transport type changes on the public surface); on a0.xcrate the breaking bump is the MINOR, so consumers must re-pin.Round 3 — #2767 keepalive collision, #2228 arm classification, #2225 false claim
1. The keepalive probe no longer parks a correlation waiter (dig_ecosystem#2767) — GATING
Withdrawal: the earlier claim that the fork's
RequestPeerscarve-out was "structurally moot" was false.DigLink::read_inbound(dig-peer-protocol-0.5.0/src/link.rs:470-478) matches on correlation id before forwarding. Both peers startnext_idat 0 (request_map.rs:45) and both keepalive loops start at handshake on a shared interval, so both first probes carried id 0, each waiter received the peer'sRequestPeers, the peer's request never reached the auto-reply atlistener.rs:875, and both sides tore the link down at the staleness check while logging "no successful probe within PEER_TIMEOUT_SECS" — which names the wrong cause.Fix: the probe goes out uncorrelated (
DigLink::send,id: None) and liveness is observed on the application inbound broadcast. Anid: Noneframe skips the id-match arm entirely. Fails loose: an unobservable round is skipped and the staleness window is reset, so the peer is kept — the correct direction for a probe whose only action is to disconnect.2. CON-006 metering — the keepalive-side call was DROPPED (asked, measured, not assumed)
Today the correlated
RespondPeersis consumed by the request map and never reachesinbound_rx, sokeepalive.rs:232was the only meter for it. Once the reply arrives uncorrelated it flows through the forwarder (listener.rs:872/gossip_handle.rs:1018), which already callsrecord_live_peer_inbound_bytes. Keeping the keepalive-side call would double-count every probe, so it is gone.tests/con_006_tests.rswas read first: every case drivesrecord_live_peer_inbound_bytes/PeerConnectionWireMetricsdirectly with synthetic frames and none assumes keepalive-side metering.con_006_tests: 9 passed.3. A non-
Handshakefirst frame is POLICY, not transport (dig_ecosystem#2228)dial_error::non_handshake_first_frameclassifies all three sites.ClientError::{InvalidResponse,UnexpectedMessage}takeProtocolMessageTypes, so where the opcode maps it is the typedClientError; where it does not, a dig-gossip-ownedDialError::UnknownOpcode(u8)->GossipError::UnknownHandshakeOpcode(u8)carries the raw byte. No formatted string stands in for a type.ProtocolMessageTypesis#[repr(u8)] Streamablewith noTryFrom<u8>, so the single-byte decode is the total mapping.4. A false claim about a build that does not exist (dig_ecosystem#2225)
introducer_client.rsclaimed--no-default-featuresbuilds "remain coherent". They do not compile (dig_peer_protocol::{Client, ClientState}imported unconditionally atservice/state.rs,service/gossip_service.rs,lib.rs;gossip_handle.rs:795,801), and no CI job builds without a TLS backend. The claim and the#[cfg(not(any(...)))]stubs that existed only to serve it are deleted, with the real state recorded in the module docs. Pre-existing, not a regression from this PR.5. The crates.io publish guard is now UNCONDITIONAL (dig_ecosystem#2647)
.github/workflows/publish.ymlblocked publish only on tag push;workflow_dispatchstill published. Its own header admitted dispatch "does NOT make it safe". Anyone with dispatch permission could have shipped a dig-gossip built against upstreamnative-tlsthat silently accepts inbound peers presenting no client certificate. The guard now fails on every event, and the surrounding comments no longer imply dispatch is a sanctioned escape hatch.Blast radius checked
gitnexus was NOT used: this worktree carries no
.gitnexusindex, and the per-worktreeanalyzewas skipped under the 10-minute/disk bound. Blast radius was established by ripgrep + direct reads of every call site, per the sanctioned fallback.keepalive_loopspawn_keepalive_taskonly; spawned atlistener.rs:798(inbound) andgossip_handle.rs(outbound)record_live_peer_inbound_byteskeepalive.rs:232(removed),listener.rs:872,gossip_handle.rs:1018DialErroroutbound.rs,introducer_client.rs,From<DialError> for GossipErrorGossipErrorLinkErrorcon_001,dsc_004,dsc_005— all on the refused-connect path, not the wrong-opcode pathsrc/relay/has a ZERO-byte diff vsorigin/main(git diff origin/main -- src/relay/is empty) — dig-relay's byte-identical vendoring is intact. No wire change.Follow-ups — recorded, NOT fixed here
Two further instances of the same correlation-id steal exist and belong to the dig-peer-protocol cascade, not to a dig-gossip carve-out:
src/service/gossip_handle.rs:539-552 send_stream_framesends DIG-band opcode-220 frames withid: Some(stream_id & 0xFFFF)from an independent id space, so a collision delivers a stream frame to a keepalive waiter and it silently never reaches the stream reassembler.request_infallible(RequestPeers)(listener.rs:869,gossip_handle.rs:842).Verification
cargo test --lib111 passed; 0 failedcargo test --test con_2767_keepalive_correlation_tests2 passed; 0 failed(13.59s)cargo test --test con_004_tests4 passedcargo test --test con_006_tests9 passedcargo test --test con_001_tests8 passedcargo test --test dsc_004_tests6 passedcargo test --test dsc_005_tests7 passedcargo clippy --all-targets --all-featurescargo fmt --all -- --checkLoad-bearing proof (each test observed RED for the right reason):
colliding_correlation_ids_do_not_tear_the_link_down— withkeepalive.rsrestored toce4c08e:FAILED — A must still hold B after 6s of probing. With the fix: passes.probe_reaches_the_peer_application_despite_an_outstanding_correlated_waiter— withsend_probereverted torequest_raw:FAILED — probe sends: RequestTimeout(43), i.e. the mutual deadlock itself. With the fix: passes.an_unobservable_probe_does_not_disconnect_the_peer— failed until the skip path also reset the staleness window; without that reset the peer was still torn down a few rounds later.Version stays
0.23.0(already bumped, still breaking).