Skip to content

docs(vendor): the chia fork rebase is a cross-repo cascade; no fork can be deleted - #62

Closed
MichaelTaylor3d wants to merge 3 commits into
mainfrom
chore/rebase-vendor-chia-036
Closed

docs(vendor): the chia fork rebase is a cross-repo cascade; no fork can be deleted#62
MichaelTaylor3d wants to merge 3 commits into
mainfrom
chore/rebase-vendor-chia-036

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

DO NOT MERGE — investigation result. The rebase this branch was opened for cannot be done in this repo.

Headline

Nothing was rebased and nothing was deleted, because measurement refuted both options. The valuable output is the evidence, now recorded in DEVELOPMENT_LOG.md and the three vendor READMEs so the next attempt does not re-derive it.

The rebase is a cross-repo cascade, not a local bump

Setting vendor/chia-protocol to 0.36.1 produced the most dangerous shape a build failure can take: none at all.

$ cargo metadata --offline
warning: patch `chia-protocol v0.36.1 (vendor/chia-protocol)` was not used in the crate graph
     Locking 1 package to latest compatible version
      Adding chia-protocol v0.26.0 (available: v0.47.0)
EXIT=0

[patch.crates-io] substitutes a package only where the patched version SATISFIES the existing requirement. dig-gossip reaches chia-protocol through dig-peer-protocol, which pins chia-protocol = "0.26" and chia-sdk-client = "0.28" — still true at the latest published 0.3.0. A 0.36.1 patch satisfies neither, so Cargo drops the patch and resolves pristine upstream, with a warning and exit 0.

Ordering a real rebase requires: dig-peer-protocol moves and republishes (release-first, §4.1) → dig-gossip's own direct chia-protocol / chia_streamable_macro pins → the vendored trees. That is at minimum two repos.

The only thing that makes the dropped patch loud is that ProtocolMessageTypes::RegisterPeer stops existing and the tree stops compiling — including tests/wire_golden_vectors.rs. Treat that compile break as the guard it is.

Can each fork die instead? Three noes, each measured

Fork Verdict Evidence
chia-protocol stays Enum is a closed #[repr(u8)], not #[non_exhaustive], and Message.msg_type is typed as it, so from_bytes rejects a DIG opcode. dig_peer_protocol::DigMessage is a raw-u8 escape hatch, but adopting it de-types the inbound decode, the broadcast classifier and the rate-limit keying — a redesign that only moves the delta into the chia-sdk-client fork we keep anyway.
chia-sdk-client stays Upstream 0.34.0 has none of the three fork items: no send_protocol_message, no from_server_websocket (still concrete SplitSink/SplitStream), no inbound-RequestPeers routing.
native-tls stays, security-load-bearing native-tls is the DEFAULT feature and native_tls_acceptor compiles under all(feature = "native-tls", not(feature = "rustls")). Upstream TlsAcceptorBuilder exposes only min_protocol_version / max_protocol_version / accept_alpn / build — no way to require a client cert. Dropping it would silently accept inbound peers with no client certificate, defeating CON-009.

The RateLimits::dig_wire item sometimes listed as a fourth delta no longer exists — removed in dig_ecosystem#2228, now connection::dig_rate_limiter.

Upstream does not collide with 200–222

ProtocolMessageTypes stops at RespondCostInfo = 107 at 0.26.0, 0.36.1 and 0.47.0 alike. No collision, no forced renumber, no wire break. The vendored version can stay where it is indefinitely without risking the live wire.

Blast radius

git diff --stat origin/main...HEAD touches zero files under src/ and zero vendored .rs. One version bump, one new test file, four docs files. No symbol was edited, so no wire, no consumer and no dig-relay vendored copy is affected.

Golden vectors

6/6 green (register_peer, register_ack, dig_message with and without an id, the zero-length prefix case, and the round trip through the vendored decoder). Stated honestly: they are a baseline, not a proof of a rebase — no vendored code moved, so they measure only that the pre-rebase wire is recorded. Their value is to the next attempt.

Gates

  • cargo fmt --all --check — exit 0 (this branch also formats the test file the prior commit left unformatted).
  • cargo clippy --all-targets --all-features -- -D warnings — exit 0.
  • cargo test --test wire_golden_vectors — exit 0, 6 passed.
  • Full suite on this Windows host has pre-existing flakiness in the loopback-socket integration targets (con_1691, str_001, str_002); they are untouched by this branch and CI runs them on ubuntu with --test-threads=1 --retries 2, green on main. CI on this PR is the authoritative gate.

Version: 0.22.2 → 0.22.3 (docs + a test, no behaviour change).

Records the encoded `Message` envelope for RegisterPeer (218), RegisterAck (219) and
DigMessage (220), the raw discriminant of every DIG opcode, and a decode round trip through
the vendored enum.

Committed as its own change, against the PRE-rebase tree, so the vectors measure the rebase
rather than re-record whatever it produces.
Measured, rather than assumed, before touching a vendored tree:

- Bumping `vendor/chia-protocol` to 0.36.1 does not fail. `[patch.crates-io]` substitutes a
  package only where the patched version satisfies the existing requirement, and dig-gossip
  reaches chia-protocol through `dig-peer-protocol`, which pins `^0.26`. Cargo drops the patch
  and resolves pristine upstream, reporting a warning with `cargo metadata` exiting 0.
- Upstream has not claimed 200-222 at 0.26.0, 0.36.1 or 0.47.0 — it stops at 107 — so there is
  no collision and no renumber pressure on the live wire.
- None of the three forks can be deleted. Upstream chia-sdk-client 0.34.0 has none of the three
  fork items, and upstream `TlsAcceptorBuilder` still cannot require a client certificate, which
  makes the native-tls patch security-load-bearing on the default feature set.

Also formats the golden-vector test committed in aead743.
@MichaelTaylor3d
MichaelTaylor3d force-pushed the chore/rebase-vendor-chia-036 branch from 5acb9f6 to 1f3ae0f Compare August 10, 2026 23:18
@MichaelTaylor3d MichaelTaylor3d changed the title chore(vendor): rebase vendored chia fork onto chia-protocol 0.36.1 docs(vendor): the chia fork rebase is a cross-repo cascade; no fork can be deleted Aug 10, 2026
MichaelTaylor3d added a commit that referenced this pull request Aug 11, 2026
… 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>
MichaelTaylor3d added a commit that referenced this pull request Aug 11, 2026
… 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>
MichaelTaylor3d added a commit that referenced this pull request Aug 11, 2026
… 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>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Closing unmerged — this branch's central finding has been falsified by PR #63

This PR concluded that no vendored fork could be deleted ("three noes, each measured") and recorded the
rebase as a cross-repo cascade. Two of the three noes are now wrong, and the branch cannot be merged
either before or after #63. Closing it is not discarding the work — the durable half was already carried
forward, itemised below.

What changed

#63 deletes vendor/chia-protocol and vendor/chia-sdk-client. Measured at that head, not inferred from
a green build:

  • [patch.crates-io] is native-tls only
  • every chia-* in Cargo.lock resolves source = "registry+...", zero path/vendor sources
  • git ls-tree on vendor/ returns fork-delta.sh and native-tls

This PR's verdict was correct about the constraint and wrong about the conclusion: it treated "this
needs a redesign" as a stopping condition rather than a cost estimate. The redesign — moving the wire onto
dig_peer_protocol::DigMessage, whose msg_type is a raw u8 — was then done, and it retired both
crates. #63's DEVELOPMENT_LOG.md records that lesson explicitly so it is not re-learned.

Why this branch cannot merge in either order

What was salvaged, and where it now lives

From this PR Where it lives now
The [patch.crates-io]-drops-silently-with-exit-0 trap, with the cargo metadata transcript #63 DEVELOPMENT_LOG.md, under "An unused [patch.crates-io] entry is a WARNING, not an error"
ProtocolMessageTypes stops at RespondCostInfo = 107 at 0.26.0, 0.36.1 and 0.47.0 — so 200-222 collides with nothing upstream same section
native-tls stays and is security-load-bearing (CON-009) #63 Cargo.toml [patch.crates-io] comment + DEVELOPMENT_LOG.md
Golden wire vectors superseded by #63's tests/wire_golden_vectors_tests.rs9 literals instead of 6, and both directions: the decode tests read the same const the encode side asserts, so a wrong decoder cannot self-agree with a wrong encoder

One fragment is genuinely unique and still true, and is being ported into #63 rather than lost: the
"Why upstream cannot replace this" section of vendor/native-tls/README.dig-gossip.md, specifically the
consumer-visible half — dig-node takes dig-gossip with default-features = false, features = ["rustls", "relay"] and therefore reaches the rustls inbound acceptor instead of the patched OpenSSL one. That is
the only path on which the native-tls patch is not load-bearing, and it is worth stating where the patch
is documented.

The branch is not deleted, so every byte above remains recoverable at 1f3ae0f2.

Follow-ups this PR correctly identified, which stay open

  • The native-tls patch blocks the crates.io publish for a reason publish.yml does not yet state:
    cargo publish strips [patch.crates-io], so a published dig-gossip would build against upstream
    native-tls and silently accept inbound peers presenting no client certificate. Tracked as
    dig_ecosystem#2647; the guard must stay.

MichaelTaylor3d added a commit that referenced this pull request Aug 12, 2026
`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 added a commit that referenced this pull request Aug 12, 2026
…rks (#63)

* test(wire): pin golden frame vectors for opcodes 200/217/218/219/220

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>

* refactor(deps)!: bump dig-peer-protocol to 0.4 and import chia types 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>

* refactor(wire): frame DIG opcodes as DigMessage instead of the forked 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>

* refactor(transport): migrate the connection layer to DigLink

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.

* refactor(transport): carry both bands over DigLink, retiring the Peer 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>

* refactor(discovery): take introducer 218/219 bodies from dig-peer-protocol

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>

* feat(deps)!: delete the vendored chia-protocol and chia-sdk-client forks

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>

* test(transport): port integration tests to the DigLink transport

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>

* test(connection): key the in-crate unit tests by raw DIG opcode

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>

* style(types): drop the unused Streamable import in peer.rs

`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>

* style(limits): drop five no-op u8 casts left by the opcode port

`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>

* build(deps): re-pin dig-peer-protocol to 0.5

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>

* test(con-005): port the rate-limit tests onto OpcodeRateLimiter

`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>

* test(wire): pin the golden vectors in the decode direction too

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>

* fix(con-001): a refused dial is a LinkError now the dial is DigLink's

`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>

* docs(ci): state the real crates.io publish blocker after the chia fork 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>

* docs(spec): correct the Section 1 crate-reuse claims to the DigLink transport

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>

* docs(spec): retire the vendored-fork claims and the Peer/connect_peer 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>

* docs(requirements): retarget the crate-structure and prompt specs at 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>

* docs(requirements): retarget the domain specs at DigLink and the DIG 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>

* docs(devlog): record the fork deletion and salvage the patch-not-used 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>

* docs(manifest): drop the last vendored-fork references from the comment 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>

* docs(connection): describe the DigLink transport, not the deleted forks

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>

* docs(discovery): fix references to deleted vendor directories

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>

* test(dsc-004): handle LinkError in connect_fail test

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>

* docs(manifest): the DIG extension band runs to 222, not 219

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>

* docs(tests): update stale references to vendor paths and legacy transport 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>

* docs(handshake): cite the real outbound dial, not the retired connect_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>

* fix(connection): report a handshake-policy rejection as ClientError on 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>

* ci: run the suite with --no-fail-fast so a count is a total, not a floor

`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>

* docs(spec): correct the dependency, transport-type and rate-limiter claims

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>

* test(dsc-005): a refused dial is a LinkError, matching the dsc-004 twin

`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>

* fix: return MissingHandshake typed client errors

Co-authored-by: MichaelTaylor3d <5665004+MichaelTaylor3d@users.noreply.github.com>

* fix(keepalive): send the liveness probe uncorrelated (#2767)

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>

* test(keepalive): pin the uncorrelated probe against a parked correlation waiter (#2767)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(keepalive): regression + fail-open coverage for the probe collision (#2767)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(dial): classify a non-Handshake first frame as policy, not transport (#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>

* docs(con-004): record that the keepalive probe must be uncorrelated (#2767)

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant