Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d8f8f33
test(wire): pin golden frame vectors for opcodes 200/217/218/219/220
MichaelTaylor3d Aug 11, 2026
9db6c8a
refactor(deps)!: bump dig-peer-protocol to 0.4 and import chia types …
MichaelTaylor3d Aug 11, 2026
546c7e7
refactor(wire): frame DIG opcodes as DigMessage instead of the forked…
MichaelTaylor3d Aug 11, 2026
e5379dc
refactor(transport): migrate the connection layer to DigLink
MichaelTaylor3d Aug 11, 2026
9967518
refactor(transport): carry both bands over DigLink, retiring the Peer…
MichaelTaylor3d Aug 11, 2026
5742a4f
refactor(discovery): take introducer 218/219 bodies from dig-peer-pro…
MichaelTaylor3d Aug 11, 2026
6e6b27a
feat(deps)!: delete the vendored chia-protocol and chia-sdk-client forks
MichaelTaylor3d Aug 11, 2026
5bb04db
test(transport): port integration tests to the DigLink transport
MichaelTaylor3d Aug 11, 2026
e8bdede
test(connection): key the in-crate unit tests by raw DIG opcode
MichaelTaylor3d Aug 11, 2026
6378f3f
style(types): drop the unused Streamable import in peer.rs
MichaelTaylor3d Aug 11, 2026
6071ec3
style(limits): drop five no-op u8 casts left by the opcode port
MichaelTaylor3d Aug 11, 2026
3f048a0
build(deps): re-pin dig-peer-protocol to 0.5
MichaelTaylor3d Aug 11, 2026
1985806
test(con-005): port the rate-limit tests onto OpcodeRateLimiter
MichaelTaylor3d Aug 11, 2026
82b9039
test(wire): pin the golden vectors in the decode direction too
MichaelTaylor3d Aug 11, 2026
94fcd53
fix(con-001): a refused dial is a LinkError now the dial is DigLink's
MichaelTaylor3d Aug 11, 2026
174d224
docs(ci): state the real crates.io publish blocker after the chia for…
MichaelTaylor3d Aug 11, 2026
90db40e
docs(spec): correct the Section 1 crate-reuse claims to the DigLink t…
MichaelTaylor3d Aug 11, 2026
5788a23
docs(spec): retire the vendored-fork claims and the Peer/connect_peer…
MichaelTaylor3d Aug 11, 2026
0cb8c6e
docs(requirements): retarget the crate-structure and prompt specs at …
MichaelTaylor3d Aug 11, 2026
2539aab
docs(requirements): retarget the domain specs at DigLink and the DIG …
MichaelTaylor3d Aug 11, 2026
94403ff
docs(devlog): record the fork deletion and salvage the patch-not-used…
MichaelTaylor3d Aug 11, 2026
d879f33
docs(manifest): drop the last vendored-fork references from the comme…
MichaelTaylor3d Aug 11, 2026
7a84bcb
docs(connection): describe the DigLink transport, not the deleted forks
MichaelTaylor3d Aug 11, 2026
ff48301
docs(discovery): fix references to deleted vendor directories
MichaelTaylor3d Aug 11, 2026
1e29f2e
test(dsc-004): handle LinkError in connect_fail test
MichaelTaylor3d Aug 11, 2026
a033518
docs(manifest): the DIG extension band runs to 222, not 219
MichaelTaylor3d Aug 11, 2026
1e3dd11
docs(tests): update stale references to vendor paths and legacy trans…
MichaelTaylor3d Aug 11, 2026
661133e
docs(handshake): cite the real outbound dial, not the retired connect…
MichaelTaylor3d Aug 11, 2026
2ce46c3
fix(connection): report a handshake-policy rejection as ClientError o…
MichaelTaylor3d Aug 11, 2026
7b2a171
ci: run the suite with --no-fail-fast so a count is a total, not a floor
MichaelTaylor3d Aug 12, 2026
4ed6f14
docs(spec): correct the dependency, transport-type and rate-limiter c…
MichaelTaylor3d Aug 12, 2026
da4c23e
test(dsc-005): a refused dial is a LinkError, matching the dsc-004 twin
MichaelTaylor3d Aug 12, 2026
ce4c08e
fix: return MissingHandshake typed client errors
Copilot Aug 12, 2026
91bca94
fix(keepalive): send the liveness probe uncorrelated (#2767)
MichaelTaylor3d Aug 12, 2026
3392ce8
test(keepalive): pin the uncorrelated probe against a parked correlat…
MichaelTaylor3d Aug 12, 2026
3880a29
test(keepalive): regression + fail-open coverage for the probe collis…
MichaelTaylor3d Aug 12, 2026
5a37486
fix(dial): classify a non-Handshake first frame as policy, not transp…
MichaelTaylor3d Aug 12, 2026
7db09b9
docs(con-004): record that the keepalive probe must be uncorrelated (…
MichaelTaylor3d Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ name: CI
# features are opt-in and pull heavy / alternate-TLS dependencies, so they are
# not part of the coverage gate (their cfg-gated code is excluded from the
# default-feature report rather than counted as "uncovered").
# - `vendor/` (the patched chia-protocol / chia-sdk-client / native-tls crates)
# is excluded via `--ignore-filename-regex` — we only gate on our own `src/`.
# - `vendor/` (the patched `native-tls` crate — the only remaining fork) is
# excluded via `--ignore-filename-regex` — we only gate on our own `src/`.
# - Integration tests bind loopback TLS listeners; they run single-threaded
# (`--test-threads=1`) to match `publish.yml` and avoid port/handshake races.

Expand Down
39 changes: 23 additions & 16 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ name: Publish to crates.io
# Tag-driven release. Pushing a version tag `vX.Y.Z` (cut by release.yml on merge) runs this
# workflow, which cuts a GitHub Release for the tag.
#
# crates.io publish is GUARDED OFF for now (#2228). dig-gossip has three vendored fork dependencies
# in [patch.crates-io] (chia-protocol, chia-sdk-client, native-tls) that `cargo publish` strips
# from the published metadata. This causes `cargo package --locked` to fail with 19 compile errors
# against the bare crates.io versions, making it impossible to publish the crate.
# crates.io publish is GUARDED OFF (dig_ecosystem#2647). dig-gossip carries ONE vendored fork in
# [patch.crates-io] — `native-tls`, patched so the OpenSSL server acceptor sets CERT_REQUIRED plus
# Chia CA trust for inbound mTLS (CON-009); upstream `TlsAcceptorBuilder` offers no way to require a
# client certificate. `cargo publish` STRIPS [patch.crates-io] from the published metadata, so a
# published dig-gossip would build against upstream `native-tls`, compile cleanly, and silently
# accept inbound peers presenting no client certificate at all. That is a silent security
# regression, not a build break — which is exactly why the guard is a workflow step and not a
# reliance on the compiler.
# So the crates.io steps run ONLY on a manual `workflow_dispatch`; a tag push just cuts the GitHub
# Release. Flip this to per-tag once #2228 has resolved the vendored-dependency blocker.
# Release. Flip this to per-tag once dig_ecosystem#2647 removes the need for the `native-tls` patch.

on:
push:
Expand All @@ -28,27 +32,30 @@ env:
CARGO_TERM_COLOR: always

jobs:
# crates.io publish. GUARDED to `workflow_dispatch` only (#2228): a tag push does NOT publish (the
# crate has three vendored fork dependencies in [patch.crates-io] that cargo publish strips,
# causing cargo package --locked to fail with 19 compile errors — see the header note). The job
# crates.io publish. GUARDED to `workflow_dispatch` only (dig_ecosystem#2647): a tag push does NOT
# publish, because `cargo publish` strips the [patch.crates-io] `native-tls` fork and the published
# crate would then accept inbound peers with no client certificate — see the header note. The job
# stays present and explicitly FAILs on tag push (exit 1) so the `create-release` job below can
# `needs: publish` with `if: always()` and still cut a GitHub Release on every tag.
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
steps:
- name: Fail crates.io publish on tag push (#2228)
- name: Fail crates.io publish on tag push (dig_ecosystem#2647)
if: github.event_name != 'workflow_dispatch'
run: |
echo "Cannot publish to crates.io: dig-gossip carries [patch.crates-io] vendored fork"
echo "dependencies (chia-protocol, chia-sdk-client, native-tls) that cargo publish"
echo "strips from the published metadata. A published dig-gossip would silently resolve"
echo "to the bare upstream crates, which lack the necessary opcodes 218/219 (peer"
echo "registration), full-duplex RPC application channel, and inbound mTLS acceptor."
echo "External consumers would obtain a broken crate."
echo "Cannot publish to crates.io: dig-gossip carries one [patch.crates-io] vendored fork,"
echo "native-tls, whose OpenSSL server acceptor sets CERT_REQUIRED plus Chia CA trust for"
echo "inbound mTLS (CON-009). Upstream TlsAcceptorBuilder cannot require a client"
echo "certificate at all."
echo ""
echo "cargo publish STRIPS [patch.crates-io] from the published metadata, so a published"
echo "dig-gossip would build against upstream native-tls, compile cleanly, and silently"
echo "accept inbound peers presenting no client certificate. The failure mode is a silent"
echo "security regression, not a build error."
echo ""
echo "Flipping this job to manual dispatch does NOT make it safe; the publish blocker"
echo "is structural. The forks must be resolved first (dig_ecosystem#2228)."
echo "is structural. The native-tls patch must be removable first (dig_ecosystem#2647)."
echo ""
echo "The git + Cargo.lock consumer path (dig-node) is unaffected."
exit 1
Expand Down
16 changes: 13 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 25 additions & 17 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
# - Specs: docs/requirements/domains/crate_structure/specs/STR-001.md, STR-004.md
# - Master spec: docs/resources/SPEC.md (Sections 1.2, 10.3, 10.4)
#
# Rationale for `chia-sdk-client` shape:
# Rationale for the `dig-peer-protocol` shape:
# STR-001 requires both `native-tls` and `rustls` feature forwards to
# `chia-sdk-client/*`. If we hard-coded `features = ["native-tls"]` on the
# dependency itself, `cargo check --no-default-features --features rustls` could
# not cleanly select the rustls backend. We therefore set `default-features = false`
# on the dependency and enable TLS exclusively via our feature flags.
# `dig-peer-protocol/*` (which forwards them on to `chia-sdk-client`). If we
# hard-coded `features = ["native-tls"]` on the dependency itself,
# `cargo check --no-default-features --features rustls` could not cleanly select
# the rustls backend. We therefore set `default-features = false` on the
# dependency and enable TLS exclusively via our feature flags.
#
# Rationale for optional `siphasher` / `minisketch-rs`:
# STR-001's implementation notes state these are gated behind `compact-blocks`
Expand All @@ -20,7 +21,7 @@

[package]
name = "dig-gossip"
version = "0.22.2"
version = "0.23.0"
edition = "2021"
license = "Apache-2.0"
description = "Peer-to-peer networking and gossip for the DIG Network L2 blockchain"
Expand All @@ -33,13 +34,16 @@ categories = ["network-programming", "cryptography::cryptocurrencies"]

[dependencies]
# DIG peer protocol — superset of chia-protocol, re-exports all chia-* crate types plus
# DIG extension opcodes (200-219), DigMessage, DigMessageType, introducer wire types.
# DIG extension opcodes (200-222), DigMessage, DigMessageType, introducer wire types.
# The 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 here is
# the kind of number that gets copied into a second implementation.
# Single import replaces: chia-protocol, chia-sdk-client, chia-ssl, chia-traits, chia_streamable_macro.
# `dig-peer-protocol` is the rename of the former `dig-protocol` crate (#1383); the crate name in
# code is `dig_peer_protocol`. TLS backends are forwarded through it to chia-sdk-client, so we keep
# `default-features = false` and select the backend exclusively via our own `native-tls` / `rustls`
# feature flags (see the `chia-sdk-client` shape rationale at the top of this manifest).
dig-peer-protocol = { version = "0.2", default-features = false }
# feature flags (see the `dig-peer-protocol` shape rationale at the top of this manifest).
dig-peer-protocol = { version = "0.5", default-features = false }
# Unified DIG Node peer transport (L7 peer-network spec): mTLS `connect(peer)` over the NAT-traversal
# ladder (direct → UPnP → NAT-PMP → PCP → hole-punch → relay-last) yielding a yamux-multiplexed
# `PeerConnection`, plus `peer_id = SHA256(TLS SPKI DER)` verification, the relay client, and the
Expand Down Expand Up @@ -144,7 +148,7 @@ tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
# Inbound TLS termination (CON-002) — paired with `native-tls` / `rustls` features (STR-004).
# Inbound listener (CON-002) uses `native_tls::TlsAcceptor` even when outbound uses rustls — `tokio_tungstenite::MaybeTlsStream::Rustls`
# only wraps **client** `tokio_rustls` streams; [`chia_sdk_client::Peer::from_websocket`] matches `NativeTls` / client `Rustls` (STR-004 note).
# only wraps **client** `tokio_rustls` streams; [`dig_peer_protocol::DigLink::from_websocket`] matches `NativeTls` / client `Rustls` (STR-004 note).
native-tls = { version = "0.2", optional = true }
tokio-native-tls = { version = "0.3", optional = true }

Expand All @@ -153,7 +157,7 @@ tokio-native-tls = { version = "0.3", optional = true }
# vendored `native-tls` [patch] (which does not propagate through a git dependency — the root cause of
# the "peer cert never requested → peer_id underivable → inbound dropped on Linux" bug). rustls
# requests + captures the client cert directly, sidestepping the [patch] propagation entirely. Pinned
# to the SAME rustls 0.23 + aws_lc_rs backend the vendored `chia-sdk-client` outbound connector uses,
# to the SAME rustls 0.23 + aws_lc_rs backend the upstream `chia-sdk-client` outbound connector uses,
# so ONE rustls resolves across the peer stack.
rustls = { version = "0.23", optional = true, default-features = false, features = ["aws_lc_rs", "std"] }
tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["aws_lc_rs"] }
Expand All @@ -164,7 +168,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
bincode = "1"

# `StreamExt` / `SinkExt` for raw WebSocket framing in CON-002 (before [`chia_sdk_client::Peer`]).
# `StreamExt` / `SinkExt` for raw WebSocket framing in CON-002 (before [`dig_peer_protocol::DigLink`]).
futures-util = "0.3"

# Cancellation tokens for task lifecycle (DSC-006 discovery loop, CNC-004 shutdown).
Expand Down Expand Up @@ -337,12 +341,16 @@ name = "dsc_005_tests"
path = "tests/dsc_005_tests.rs"
required-features = ["native-tls"]

# Patched `chia-sdk-client`: forwards inbound `RequestPeers` with remote-issued ids to the
# application channel (full-duplex RPC). See `vendor/chia-sdk-client/src/peer.rs`.
[patch.crates-io]
# DSC-005: `RegisterPeer` / `RegisterAck` opcodes 218/219 on `ProtocolMessageTypes` (see vendor/chia-protocol/README.dig-gossip.md).
chia-protocol = { path = "vendor/chia-protocol" }
chia-sdk-client = { path = "vendor/chia-sdk-client" }
# Patched `native-tls`: OpenSSL server acceptor sets CERT_REQUIRED + Chia CA trust for inbound mTLS
# (CON-009). See `vendor/native-tls/README.dig-gossip.md`.
#
# This is the ONLY remaining vendored fork. It is NOT removable: upstream
# `TlsAcceptorBuilder` exposes only `min_protocol_version` / `max_protocol_version` /
# `accept_alpn` / `build`, with no way to require a client certificate. Dropping it would
# still compile and would silently accept inbound peers presenting no client cert at all.
#
# The `chia-protocol` and `chia-sdk-client` forks were deleted in dig_ecosystem#2228: both
# existed because Chia's `ProtocolMessageTypes` cannot name a DIG opcode (200-222), which
# `dig_peer_protocol::DigLink` solves by framing a raw opcode byte instead.
native-tls = { path = "vendor/native-tls" }
76 changes: 73 additions & 3 deletions DEVELOPMENT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,11 @@ check the accept-loop admission gates first.
proof-of-possession is still enforced via the TLS CertificateVerify signature. `peer_id` reuses the
shared `spki_der_from_leaf_cert_der` + `peer_id_from_tls_spki_der` helpers → byte-identical.
- **`MaybeTlsStream` is `#[non_exhaustive]` and only types the CLIENT rustls stream.** A server-side
`tokio_rustls::server::TlsStream` cannot inhabit it, so `Peer::from_websocket` is unusable inbound.
The vendored `chia-sdk-client` boxes `PeerInner`'s split sink/stream and exposes
`Peer::from_server_websocket(ws, addr, opts)` (generic over the transport, `Peer` stays non-generic).
`tokio_rustls::server::TlsStream` cannot inhabit it, so `from_websocket` is unusable inbound.
The fix boxes the split sink/stream and exposes `from_server_websocket(ws, addr, opts)` (generic
over the transport, while the link handle itself stays non-generic). That escape hatch originally
lived in the vendored `chia-sdk-client`; it now lives in `dig_peer_protocol::DigLink`, which is one
of the reasons the fork could be deleted.
- **aws-lc-sys on Windows.** The rustls `aws_lc_rs` backend fails to C-compile in a deep worktree
(CMake `tlog` path exceeds Windows MAX_PATH). Build/test the rustls features with a short
`CARGO_TARGET_DIR` (e.g. `/c/t/...`); CI (Linux) is unaffected.
Expand Down Expand Up @@ -584,3 +586,71 @@ a hand-written README.** Both vendored READMEs understated their fork, and a han
wrong twice in one investigation. `vendor/fork-delta.sh <crate>` regenerates it; the vendored trees
are unpacked tarballs of a known version, so the same-version registry source is an exact baseline
and everything the diff reports is DIG's by construction.

## An unused `[patch.crates-io]` entry is a WARNING, not an error (dig_ecosystem#2228)

`[patch.crates-io]` substitutes a package only where the patched version SATISFIES the existing
requirement. When it does not, Cargo drops the patch, resolves the pristine upstream crate, and
exits **zero**:

```
$ cargo metadata --offline # after setting vendor/chia-protocol version = "0.36.1"
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
```

The failure had no shape at all. dig-gossip does not depend on `chia-protocol` alone — it depends on
`dig-peer-protocol`, which pins its own `chia-protocol` / `chia-sdk-client` requirements, and a
0.36.1 patch satisfied neither. The only thing that made it loud was that the code stopped compiling
for an unrelated reason.

Two durable consequences, both still true now that only `native-tls` is patched:

- **A patch-not-used warning must never be "fixed" by deleting the reference that surfaces it.** The
compile break IS the guard. Treat it as one.
- **The remaining `native-tls` patch has NO such compile guard, which is exactly why the crates.io
publish is blocked in CI rather than left to the build.** `cargo publish` strips
`[patch.crates-io]`, so a published dig-gossip would build cleanly against upstream `native-tls`
and silently accept inbound peers presenting no client certificate — upstream `TlsAcceptorBuilder`
cannot request one. A guard step in `publish.yml` is the only thing standing between that and a
release (dig_ecosystem#2647).

Also measured and still worth keeping: upstream's `ProtocolMessageTypes` stops at
`RespondCostInfo = 107` at **0.26.0, 0.36.1 and 0.47.0**, so DIG's 200-222 band collides with nothing
upstream and will not force a renumber on any future rebase.

## Deleting the vendored Chia forks: the raw `u8` opcode was the whole mechanism (dig_ecosystem#2228)

An earlier investigation concluded that `chia-protocol` could not be deleted "without moving the wire
off the typed `Message`", and filed that move as a prerequisite redesign rather than a deletion. The
redesign was then done, and it worked. Recording what actually made it possible, because the shape of
the answer generalises:

- **The blocker was one field's TYPE, not the crate.** Chia's `ProtocolMessageTypes` is a closed
`#[repr(u8)]` enum (not `#[non_exhaustive]`), and `Message.msg_type` is typed as it, so
`Message::from_bytes` rejects a DIG opcode. The fork existed solely to add variants 200-222 to that
enum. `dig_peer_protocol::DigMessage` keeps the identical layout and leaves `msg_type` a raw `u8`,
so the same bytes decode with no enum to extend.
- **The cost was that three call sites stopped being enum-typed** — the inbound decode, the broadcast
classifier, and the rate-limit keying. That is what made it a redesign. It is also what made it
cheap in the end: the rate limiter was ALREADY keyed by the raw opcode byte (`HashMap<u8, _>`), so
it needed relocating, not rewriting.
- **The wire was pinned BEFORE the refactor and proven identical after.** All nine golden hex vectors
are byte-for-byte unchanged. A transport swap with no wire-level regression test is a rewrite you
cannot audit; with one, "did the bytes move?" is a question the suite answers rather than a claim
the author makes.
- **`chia-sdk-client` fell out for free once `DigLink` existed.** Its three fork items —
`send_protocol_message`, `from_server_websocket`, and inbound `RequestPeers` routing — are
properties of the link, and the new link has them natively.

What survives: `native-tls` is the **only** remaining `[patch.crates-io]` entry and is
security-load-bearing (CERT_REQUIRED + Chia CA trust on the OpenSSL server acceptor, CON-009). Every
`chia-*` crate now resolves from crates.io with no path or vendor source. `chia-protocol` and
`chia-sdk-client` remain legitimate TRANSITIVE dependencies via `dig-peer-protocol`, so a mention of
either crate is not by itself stale — only a claim about a *vendored fork* of one is.

And the general lesson: **a "cannot be deleted" verdict is only as good as the alternative that was
priced.** The earlier verdict was correct about the constraint and wrong about the conclusion,
because it treated "this needs a redesign" as a stopping condition instead of a cost estimate.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ pub struct PeerInfo {

// Full peer metadata for a live connection
pub struct PeerConnection {
pub peer: Peer, // chia-sdk-client handle
pub peer: DigLink, // dig-peer-protocol peer link
pub peer_id: PeerId,
pub address: SocketAddr,
pub is_outbound: bool,
Expand Down Expand Up @@ -519,7 +519,7 @@ Starvation prevention: one bulk message is allowed through per `PRIORITY_STARVAT

```rust
pub enum GossipError {
ClientError(ClientError), // chia-sdk-client transport error
ClientError(ClientError), // transport error from the re-exported client
ServiceNotStarted, // handle used before start() or after stop()
AlreadyStarted, // start() called twice
PeerBanned(PeerId), // connection rejected — peer is banned
Expand Down
Loading
Loading