diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c5c6ca..8d8f99e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,14 +13,23 @@ name: CI # the merge on transient flakiness. Coverage instrumentation still runs via # `cargo llvm-cov nextest` so the 80% floor is measured the same way. # +# `--no-fail-fast` (dig_ecosystem#2228) is what makes the numbers this job prints +# TOTALS rather than floors. Without it nextest stops at the first failing target, +# and the run reports a pass count that reads exactly like a complete one: a red +# head here stopped at 531 of 977, so 446 tests never executed AND the 80% line +# floor below was never measured at all — the job failed for the one broken test +# while silently declining to answer the coverage question it exists to answer. +# "Unmeasured" and "measured and under" are different verdicts and must not look +# alike. The job still fails on any real failure; it just finishes counting first. +# # Coverage notes: # - Measured against the production-shaped **default** feature set # (`native-tls,relay,erlay,compact-blocks,dandelion`). The `tor` and `rustls` # 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. @@ -112,6 +121,7 @@ jobs: --no-default-features --features rustls,relay,erlay,compact-blocks,dandelion --lib + --no-fail-fast -- --test-threads=1 @@ -146,6 +156,7 @@ jobs: --ignore-filename-regex 'vendor/' --fail-under-lines 80 --lcov --output-path lcov.info + --no-fail-fast --test-threads=1 --retries 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 58cee51..08853ab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,12 +3,19 @@ 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. -# 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. +# 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. +# The guard is therefore UNCONDITIONAL: neither a tag push nor a manual `workflow_dispatch` +# publishes. Dispatch is not a sanctioned escape hatch — the blocker is structural, and anyone with +# dispatch permission could otherwise ship a dig-gossip that accepts uncertificated peers. A tag push +# still cuts the GitHub Release. Re-enable publishing only once dig_ecosystem#2647 removes the need +# for the `native-tls` patch. on: push: @@ -28,57 +35,60 @@ 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 - # 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. + # crates.io publish. GUARDED OFF ENTIRELY (dig_ecosystem#2647): NO event publishes, 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 + # always FAILs (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) - if: github.event_name != 'workflow_dispatch' + - name: Fail crates.io publish on every event (dig_ecosystem#2647) 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 "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 "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 "Manual dispatch does NOT make this safe; the publish blocker is structural, so this" + echo "guard refuses EVERY event. The native-tls patch must be removable first" + echo "(dig_ecosystem#2647)." echo "" echo "The git + Cargo.lock consumer path (dig-node) is unaffected." exit 1 - name: Checkout code - if: github.event_name == 'workflow_dispatch' + if: false # unreachable: the guard above fails the job on every event (dig_ecosystem#2647) uses: actions/checkout@v4 - name: Install Rust toolchain - if: github.event_name == 'workflow_dispatch' + if: false # unreachable: the guard above fails the job on every event (dig_ecosystem#2647) uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Cache dependencies - if: github.event_name == 'workflow_dispatch' + if: false # unreachable: the guard above fails the job on every event (dig_ecosystem#2647) uses: Swatinem/rust-cache@v2 # `--locked` (not `--allow-dirty`): verify against the committed Cargo.lock so deps resolve to # their pinned revisions instead of re-resolving bare-git deps to a drifted `main` tip. - name: Verify package can be built - if: github.event_name == 'workflow_dispatch' + if: false # unreachable: the guard above fails the job on every event (dig_ecosystem#2647) run: cargo build --release --locked - name: Verify package can be packaged - if: github.event_name == 'workflow_dispatch' + if: false # unreachable: the guard above fails the job on every event (dig_ecosystem#2647) run: cargo package --locked - name: Check if CARGO_REGISTRY_TOKEN is available - if: github.event_name == 'workflow_dispatch' + if: false # unreachable: the guard above fails the job on every event (dig_ecosystem#2647) run: | if [ -z "${{ secrets.CARGO_REGISTRY_TOKEN }}" ]; then echo "CARGO_REGISTRY_TOKEN secret is not set in repository settings" @@ -86,7 +96,7 @@ jobs: fi - name: Publish to crates.io - if: github.event_name == 'workflow_dispatch' + if: false # unreachable: the guard above fails the job on every event (dig_ecosystem#2647) run: cargo publish --locked --token ${{ secrets.CARGO_REGISTRY_TOKEN }} env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index d1b6c51..7aeab39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,6 +665,8 @@ dependencies = [ [[package]] name = "chia-protocol" version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9256179e4c912313532d7a47a50f67f6128313ee86a3798e54050afb73d6042" dependencies = [ "chia-bls 0.26.0", "chia-sha2 0.26.0", @@ -706,6 +708,8 @@ dependencies = [ [[package]] name = "chia-sdk-client" version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc22c29cc7562e9b65bf4d02bd0881657f1628dca84ad5b40d87a912e8a0f5d" dependencies = [ "aws-lc-rs", "chia-protocol", @@ -1437,7 +1441,7 @@ dependencies = [ [[package]] name = "dig-gossip" -version = "0.22.2" +version = "0.23.0" dependencies = [ "arti-client", "bincode 1.3.3", @@ -1538,9 +1542,9 @@ dependencies = [ [[package]] name = "dig-peer-protocol" -version = "0.2.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "114832cad5ae6b8adfd319e8480d1eed0ffb8bc77e41b9d3f7fb4ce9ef6219ee" +checksum = "05ae7bfaf7d9535ef214ba53b019be888560d410b7472b3ad76a0ef07b62654b" dependencies = [ "chia-protocol", "chia-sdk-client", @@ -1548,7 +1552,13 @@ dependencies = [ "chia-ssl", "chia-traits 0.26.0", "chia_streamable_macro 0.26.0", + "futures-util", "serde", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "tracing", + "tungstenite", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1f0cd6c..e4d00e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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` @@ -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" @@ -33,13 +34,19 @@ 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. -# Single import replaces: chia-protocol, chia-sdk-client, chia-ssl, chia-traits, chia_streamable_macro. +# 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-sdk-client and chia-ssl outright. chia-protocol, chia-traits and +# chia_streamable_macro stay declared below: the streamable proc-macro reads this manifest and +# generates `chia_protocol::` paths, so they must be nameable here even though the code prefers +# the re-exports. # `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 @@ -144,7 +151,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 } @@ -153,7 +160,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"] } @@ -164,7 +171,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). @@ -337,12 +344,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" } diff --git a/DEVELOPMENT_LOG.md b/DEVELOPMENT_LOG.md index 218ff2c..3e18b77 100644 --- a/DEVELOPMENT_LOG.md +++ b/DEVELOPMENT_LOG.md @@ -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. @@ -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 ` 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`), 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. diff --git a/README.md b/README.md index 88e1c47..c958698 100644 --- a/README.md +++ b/README.md @@ -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, @@ -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 diff --git a/docs/prompt/start.md b/docs/prompt/start.md index 4140631..55f7720 100644 --- a/docs/prompt/start.md +++ b/docs/prompt/start.md @@ -45,12 +45,12 @@ ## Hard Requirements -1. **Use chia crate ecosystem first** — never reimplement what `chia-protocol`, `chia-sdk-client`, `chia-ssl`, `chia-traits` provide. The SPEC Section 1.4 lists every type reused from Chia crates. +1. **Use `dig-peer-protocol` first** — it owns the peer link (`DigLink`) and the `DigMessage` envelope, and re-exports everything `chia-protocol`, `chia-sdk-client`, `chia-ssl` and `chia-traits` provide. Never depend on `chia-sdk-client` directly, and never vendor or patch a Chia crate. The SPEC Section 1.4 lists every reused type and where it comes from. 2. **No custom handshake** — use `chia-protocol::Handshake` with DIG values. 3. **No custom message framing** — use `chia-protocol::Message` and `chia-traits::Streamable`. -4. **No custom rate limiting** — use `chia-sdk-client::RateLimiter` with `V2_RATE_LIMITS`. -5. **No custom TLS** — use `chia-ssl::ChiaCertificate` and `chia-sdk-client` TLS utilities. -6. **No custom DNS resolution** — use `chia-sdk-client::Network::lookup_all()`. +4. **No custom rate limiting** — use `dig_peer_protocol::OpcodeRateLimiter`, which enforces Chia's `V2_RATE_LIMITS` keyed by raw wire opcode. +5. **No custom TLS** — use the re-exported `ChiaCertificate` and TLS utilities. +6. **No custom DNS resolution** — use the re-exported `Network::lookup_all()`. 7. **Re-export, don't redefine** — `Peer`, `Message`, `Handshake`, `NodeType`, `ProtocolMessageTypes` from upstream. 8. **No block validation** — this crate transports messages; it never validates block/transaction content. 9. **No CLVM execution** — this crate is payload-agnostic. @@ -66,9 +66,8 @@ | Component | Crate | Version | |-----------|-------|---------| +| Peer wire (link, envelope, opcodes, re-exports) | `dig-peer-protocol` | 0.4 | | Protocol types | `chia-protocol` | 0.26 | -| Peer connections | `chia-sdk-client` | 0.28 | -| TLS certificates | `chia-ssl` | 0.26 | | Serialization traits | `chia-traits` | 0.26 | | Async runtime | `tokio` | 1.x | | WebSocket | `tokio-tungstenite` | 0.24 | diff --git a/docs/requirements/IMPLEMENTATION_ORDER.md b/docs/requirements/IMPLEMENTATION_ORDER.md index 69d2109..e37f6da 100644 --- a/docs/requirements/IMPLEMENTATION_ORDER.md +++ b/docs/requirements/IMPLEMENTATION_ORDER.md @@ -11,7 +11,7 @@ After completing a requirement: write tests, verify they pass, update TRACKING.y - [x] STR-001 — Cargo.toml with chia crate dependencies, feature gates, and metadata - [x] STR-002 — Module hierarchy (`src/lib.rs` root, submodule layout matching SPEC Section 10.1) -- [x] STR-003 — Re-export strategy (chia-protocol, chia-sdk-client, chia-ssl types) +- [x] STR-003 — Re-export strategy (dig-peer-protocol, and the chia-* types it re-exports) - [x] STR-004 — Feature flags (native-tls, rustls, relay, erlay, compact-blocks) - [x] STR-005 — Test infrastructure (`tests/` layout, helpers, mock peer harness) @@ -21,7 +21,7 @@ After completing a requirement: write tests, verify they pass, update TRACKING.y - [x] API-002 — GossipHandle type (broadcast, send_to, request, inbound_receiver, stats) - [x] API-003 — GossipConfig struct (listen_addr, peer_id, network_id, network, targets, bootstrap) - [x] API-004 — GossipError enum (wraps ClientError, peer errors, discovery errors, relay errors) -- [x] API-005 — PeerConnection struct (wraps chia-sdk-client::Peer with gossip metadata) +- [x] API-005 — PeerConnection struct (wraps dig_peer_protocol::DigLink with gossip metadata) - [x] API-006 — PeerReputation and PenaltyReason (penalty accumulation, ban threshold, auto-unban) - [x] API-007 — PeerId type alias and PeerInfo with get_group()/get_key() - [x] API-008 — GossipStats and RelayStats structs @@ -31,7 +31,7 @@ After completing a requirement: write tests, verify they pass, update TRACKING.y ## Phase 2: Connection Lifecycle -- [x] CON-001 — Outbound connection via chia-sdk-client connect_peer() +- [x] CON-001 — Outbound connection: handshake over the raw WebSocket, then DigLink - [x] CON-002 — Inbound connection listener (TcpListener + TLS accept + Peer::from_websocket) - [x] CON-003 — Handshake validation (network_id match, protocol_version compat) - [x] CON-004 — Keepalive (Ping/Pong, timeout detection at PEER_TIMEOUT_SECS) @@ -45,7 +45,7 @@ After completing a requirement: write tests, verify they pass, update TRACKING.y - [x] DSC-001 — AddressManager with tried/new tables (Rust port of address_manager.py) - [x] DSC-002 — Address manager persistent serialization (save/load to peers file) -- [x] DSC-003 — DNS seeding via chia-sdk-client Network::lookup_all() +- [x] DSC-003 — DNS seeding via the re-exported Network::lookup_all() - [x] DSC-004 — Introducer query (RequestPeersIntroducer flow) - [x] DSC-005 — Introducer registration (DIG extension: register_peer) - [x] DSC-006 — Discovery loop with DNS-first then introducer with exponential backoff diff --git a/docs/requirements/REQUIREMENTS_REGISTRY.yaml b/docs/requirements/REQUIREMENTS_REGISTRY.yaml index 0b746c4..11ecbc1 100644 --- a/docs/requirements/REQUIREMENTS_REGISTRY.yaml +++ b/docs/requirements/REQUIREMENTS_REGISTRY.yaml @@ -27,7 +27,7 @@ domains: - id: connection title: "Connection" - summary: "Connection lifecycle: outbound via chia-sdk-client connect_peer(), inbound via TcpListener + Peer::from_websocket(), handshake validation, keepalive, rate limiting, peer banning, version sanitization." + summary: "Connection lifecycle: outbound handshake then DigLink::from_websocket(), inbound via TcpListener + DigLink::from_server_websocket(), handshake validation, keepalive, rate limiting, peer banning, version sanitization." status: planned paths: normative: domains/connection/NORMATIVE.md @@ -37,7 +37,7 @@ domains: - id: discovery title: "Discovery" - summary: "Peer discovery: AddressManager (tried/new tables, Rust port), persistent serialization, DNS seeding via chia-sdk-client Network, introducer query/registration, discovery loop with exponential backoff, peer exchange, feeler connections, parallel connect, AS-level diversity." + summary: "Peer discovery: AddressManager (tried/new tables, Rust port), persistent serialization, DNS seeding via the re-exported Network, introducer query/registration, discovery loop with exponential backoff, peer exchange, feeler connections, parallel connect, AS-level diversity." status: planned paths: normative: domains/discovery/NORMATIVE.md diff --git a/docs/requirements/domains/connection/NORMATIVE.md b/docs/requirements/domains/connection/NORMATIVE.md index 06904f0..f0e7d86 100644 --- a/docs/requirements/domains/connection/NORMATIVE.md +++ b/docs/requirements/domains/connection/NORMATIVE.md @@ -6,15 +6,15 @@ ## Requirements -### CON-001: Outbound Connection via connect_peer() +### CON-001: Outbound Connection -Outbound connections MUST use `chia-sdk-client::connect_peer()`. TLS MUST be loaded via `load_ssl_cert()` or generated via `ChiaCertificate::generate()`. A TLS connector MUST be created. The `Handshake` MUST include the DIG `network_id`. The resulting `Peer` MUST be wrapped in `PeerConnection` with gossip metadata. `RequestPeers` MUST be sent after a successful outbound connection. +Outbound connections MUST use the outbound connect flow (handshake over the raw WebSocket, then `DigLink`). TLS MUST be loaded via `load_ssl_cert()` or generated via `ChiaCertificate::generate()`. A TLS connector MUST be created. The `Handshake` MUST include the DIG `network_id`. The resulting `DigLink` MUST be wrapped in `PeerConnection` with gossip metadata. `RequestPeers` MUST be sent after a successful outbound connection. **Spec reference:** SPEC Section 5.1 (Outbound Connection) ### CON-002: Inbound Connection Listener -Inbound connections MUST be accepted via `TcpListener`, TLS handshake, `tokio_tungstenite::accept_async()`, and `Peer::from_websocket()`. The server MUST receive and validate the inbound `Handshake`, send a `Handshake` response, wrap in `PeerConnection`, add the peer to the address manager "new" table, and relay peer info to other connected peers. +Inbound connections MUST be accepted via `TcpListener`, TLS handshake, `tokio_tungstenite::accept_async()`, and `DigLink::from_server_websocket()`. The server MUST receive and validate the inbound `Handshake`, send a `Handshake` response, wrap in `PeerConnection`, add the peer to the address manager "new" table, and relay peer info to other connected peers. **Spec reference:** SPEC Section 5.2 (Inbound Connection) @@ -34,7 +34,7 @@ A `Ping` message MUST be sent at `PING_INTERVAL_SECS` (30-second) intervals. If ### CON-005: Per-Connection Rate Limiting -Inbound connections MUST each have a separate `InboundRateLimiter` instance, composing a `chia-sdk-client::RateLimiter` initialized with `V2_RATE_LIMITS` with dig-gossip's own `DigRateLimiter` over the DIG extension table. DIG extension message types (200+ range) MUST be bounded by that table, keyed by the raw opcode byte. Outbound rate limiting is handled internally by `Peer::send_raw()`. +Inbound connections MUST each have a separate `InboundRateLimiter` instance, composing a `dig_peer_protocol::OpcodeRateLimiter` over Chia's `V2_RATE_LIMITS` rows with dig-gossip's own `DigRateLimiter` over the DIG extension table. DIG extension message types (200+ range) MUST be bounded by that table, keyed by the raw opcode byte. Outbound rate limiting is handled internally by `DigLink`'s send path. **Spec reference:** SPEC Section 5.3 (Rate Limiting) @@ -58,6 +58,6 @@ The `software_version` field from the `Handshake` MUST have Unicode Cc (control) ### CON-009: Mandatory Mutual TLS (mTLS) via chia-ssl on All Peer Connections -ALL peer-to-peer connections (both inbound and outbound) MUST use mutual TLS (mTLS) where both sides present `chia-ssl` certificates. TLS certificates MUST be managed exclusively via the `chia-ssl` crate (`ChiaCertificate::generate()` for new nodes, `load_ssl_cert()` for existing). Outbound connections MUST use `create_native_tls_connector()` or `create_rustls_connector()` from `chia-sdk-client`, which include the node's own certificate as a client cert for mutual authentication. Inbound connections MUST use a TLS acceptor configured with `verify_mode = CERT_REQUIRED` (matching Chia's [`server.py:67`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/server.py#L67)) so the connecting peer MUST present its certificate. Connections where the peer does not present a certificate MUST be rejected. Unencrypted WebSocket connections (plain `ws://`) MUST be rejected. Server-only TLS (where only the listener has a cert) MUST NOT be accepted for P2P — both sides MUST present certificates. Peer identity (`PeerId`) MUST be derived from SHA256 of the remote peer's TLS certificate public key, extracted during the mTLS handshake. Relay connections are exempt from mTLS (they use standard `wss://` server-only TLS). +ALL peer-to-peer connections (both inbound and outbound) MUST use mutual TLS (mTLS) where both sides present `chia-ssl` certificates. TLS certificates MUST be managed exclusively via the `chia-ssl` crate (`ChiaCertificate::generate()` for new nodes, `load_ssl_cert()` for existing). Outbound connections MUST use `create_native_tls_connector()` or `create_rustls_connector()` from `dig-peer-protocol`, which include the node's own certificate as a client cert for mutual authentication. Inbound connections MUST use a TLS acceptor configured with `verify_mode = CERT_REQUIRED` (matching Chia's [`server.py:67`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/server.py#L67)) so the connecting peer MUST present its certificate. Connections where the peer does not present a certificate MUST be rejected. Unencrypted WebSocket connections (plain `ws://`) MUST be rejected. Server-only TLS (where only the listener has a cert) MUST NOT be accepted for P2P — both sides MUST present certificates. Peer identity (`PeerId`) MUST be derived from SHA256 of the remote peer's TLS certificate public key, extracted during the mTLS handshake. Relay connections are exempt from mTLS (they use standard `wss://` server-only TLS). **Spec reference:** SPEC Section 5.3 (Mandatory Mutual TLS), Section 1.2, Section 1.3 Design Decision 4, Section 1.5 Behavior 3, Section 2.2 (PeerId from TLS key) diff --git a/docs/requirements/domains/connection/TRACKING.yaml b/docs/requirements/domains/connection/TRACKING.yaml index 69e2119..a9e8cbd 100644 --- a/docs/requirements/domains/connection/TRACKING.yaml +++ b/docs/requirements/domains/connection/TRACKING.yaml @@ -4,13 +4,13 @@ verification: ./VERIFICATION.md items: - id: CON-001 section: "5.1" - summary: "Outbound connection via connect_peer()" + summary: "Outbound connection: handshake over the raw WebSocket, then DigLink" status: verified - spec_ref: "SPEC.md#51-outbound-connection-reuses-chia-sdk-client" + spec_ref: "SPEC.md#51-outbound-connection" tests: - tests/con_001_tests.rs notes: > - connection/outbound.rs mirrors chia-sdk connect_peer + returns Handshake + remote SPKI for PeerId + PeerConnection fields. + connection/outbound.rs mirrors upstream's connect.rs + returns Handshake + remote SPKI for PeerId + PeerConnection fields. GossipHandle::connect_to uses TLS connector, RequestPeers, AddressManager::add_to_new_table; PeerSlot::Live stores Peer; inbound_rx forwarded to broadcast hub. API-002/API-008 offline tests use __connect_stub_peer_with_direction. lib.rs re-exports create_native_tls_connector when feature native-tls (create_rustls_connector when rustls-only). @@ -30,7 +30,7 @@ items: section: "5.1, 5.2" summary: "Handshake validation" status: verified - spec_ref: "SPEC.md#51-outbound-connection-reuses-chia-sdk-client" + spec_ref: "SPEC.md#51-outbound-connection" tests: - tests/con_003_tests.rs notes: > @@ -64,7 +64,7 @@ items: section: "2.4" summary: "Connection metrics tracking" status: verified - spec_ref: "SPEC.md#24-peerconnection-dig-extension-of-chia-sdk-clientpeer" + spec_ref: "SPEC.md#24-peerconnection-dig-extension-of-dig_peer_protocoldiglink" tests: - tests/con_006_tests.rs notes: > @@ -87,7 +87,7 @@ items: section: "5.1, 5.2" summary: "Version string sanitization" status: verified - spec_ref: "SPEC.md#51-outbound-connection-reuses-chia-sdk-client" + spec_ref: "SPEC.md#51-outbound-connection" tests: - tests/con_008_tests.rs notes: > @@ -104,8 +104,8 @@ items: tests: - tests/con_009_tests.rs notes: > - Outbound: `chia-sdk-client` `create_native_tls_connector` / `create_rustls_connector` attach `ChiaCertificate` - as client identity (`vendor/chia-sdk-client/src/tls.rs`). Inbound: `vendor/native-tls` OpenSSL fork sets + Outbound: the re-exported `create_native_tls_connector` / `create_rustls_connector` attach `ChiaCertificate` + as client identity. Inbound: the `vendor/native-tls` OpenSSL fork - the only remaining vendored crate - sets CERT_REQUIRED + Chia CA (`chia_ca.crt`) per `README.dig-gossip.md`; Windows/macOS retain `peer_id_for_addr` fallback when `peer_certificate()` is unavailable (listener module docs). `tests/con_009_tests.rs` covers connector smoke, cert load round-trip, OpenSSL negative probe, and live `connect_to` mTLS. Relay exempt. diff --git a/docs/requirements/domains/connection/VERIFICATION.md b/docs/requirements/domains/connection/VERIFICATION.md index 00eab04..718bd1d 100644 --- a/docs/requirements/domains/connection/VERIFICATION.md +++ b/docs/requirements/domains/connection/VERIFICATION.md @@ -7,7 +7,7 @@ | ID | Status | Summary | Verification Approach | |---------|--------|--------------------------------------------------|--------------------------------------------------------------------------------------------| -| CON-001 | verified | Outbound connection via connect_peer() | `tests/con_001_tests.rs`: TLS load/generate/connector; WSS harness handshake + RequestPeers; `GossipHandle::connect_to` + `AddressManager` batch; `ClientError` → `GossipError`; peer field wiring / creation_time | +| CON-001 | verified | Outbound connection: handshake, then DigLink | `tests/con_001_tests.rs`: TLS load/generate/connector; WSS harness handshake + RequestPeers; `GossipHandle::connect_to` + `AddressManager` batch; `ClientError` → `GossipError`; peer field wiring / creation_time | | CON-002 | verified | Inbound connection listener | `tests/con_002_tests.rs`: bind `:0`, TLS+WSS+Handshake, wrong network_id reject, PeerConnection inbound metadata, AddressManager batch, RespondPeers relay to live peer, max_connections cap; self-connection reject on non-Windows (cert-based PeerId) | | CON-003 | verified | Handshake validation | `tests/con_003_tests.rs`: sanitize Cc/Cf; protocol floor; network_id; 128-byte limit; empty id fields; integration two-node `__con003_peer_versions_for_tests` (inbound + outbound) | | CON-004 | verified | Keepalive via Ping/Pong | `tests/con_004_tests.rs`: RequestPeers keepalive probe, RTT samples, bidirectional, remote stop → disconnect + ConnectionIssue penalty; config overrides for timing | diff --git a/docs/requirements/domains/connection/specs/CON-001.md b/docs/requirements/domains/connection/specs/CON-001.md index e947ca4..ae51ea7 100644 --- a/docs/requirements/domains/connection/specs/CON-001.md +++ b/docs/requirements/domains/connection/specs/CON-001.md @@ -1,4 +1,4 @@ -# CON-001: Outbound Connection via connect_peer() +# CON-001: Outbound Connection > **Authoritative requirement:** [NORMATIVE.md](../NORMATIVE.md) > **Verification:** [VERIFICATION.md](../VERIFICATION.md) @@ -7,7 +7,7 @@ ## Summary -Outbound peer connections must use `chia-sdk-client::connect_peer()` to establish TLS-secured WebSocket connections with the DIG network handshake. TLS certificates are loaded from disk via `load_ssl_cert()` or generated on first run via `ChiaCertificate::generate()`. The resulting `Peer` is wrapped in a `PeerConnection` with gossip metadata, and a `RequestPeers` message is sent immediately after connection for peer discovery. +Outbound peer connections must use the outbound connect flow (handshake over the raw WebSocket, then `DigLink`) to establish TLS-secured WebSocket connections with the DIG network handshake. TLS certificates are loaded from disk via `load_ssl_cert()` or generated on first run via `ChiaCertificate::generate()`. The resulting `DigLink` is wrapped in a `PeerConnection` with gossip metadata, and a `RequestPeers` message is sent immediately after connection for peer discovery. ## Specification @@ -16,7 +16,7 @@ Outbound peer connections must use `chia-sdk-client::connect_peer()` to establis TLS credentials MUST be obtained in one of two ways: ```rust -use chia_sdk_client::load_ssl_cert; +use dig_peer_protocol::load_ssl_cert; use chia_ssl::ChiaCertificate; // Option 1: Load existing certificate from disk @@ -32,7 +32,7 @@ let chia_cert = ChiaCertificate::generate()?; A TLS connector MUST be created from the certificate: ```rust -use chia_sdk_client::create_native_tls_connector; +use dig_peer_protocol::create_native_tls_connector; // With native-tls feature: let connector = create_native_tls_connector(&cert, &key)?; @@ -41,33 +41,38 @@ let connector = create_native_tls_connector(&cert, &key)?; // let connector = create_rustls_connector(&cert, &key)?; ``` -### Connection via connect_peer() +### Connection -The outbound connection MUST be established using `connect_peer()`: +The outbound connection MUST be established by the crate's own connect flow, which mirrors +upstream's `connect.rs` rather than calling it: upstream discards the parsed `Handshake` and +never exposes the remote TLS SubjectPublicKeyInfo bytes, and both are required to build a +`PeerConnection` and derive a `PeerId` (API-005). ```rust -use chia_sdk_client::connect_peer; +use dig_peer_protocol::{DigLink, LinkOptions}; use chia_protocol::NodeType; -let (peer, receiver) = connect_peer( +let result = connect_outbound( &config.network_id, // DIG network_id (e.g., SHA256("dig_mainnet")) connector, socket_addr, // Target peer's SocketAddr - PeerOptions { + LinkOptions { rate_limit_factor: config.peer_options.rate_limit_factor, ..Default::default() }, ).await?; -// connect_peer() internally: -// 1. Peer::connect() -> WebSocket TLS connection -// 2. Sends chia-protocol::Handshake with DIG network_id -// 3. Receives and validates Handshake response -// 4. Returns (Peer, mpsc::Receiver) +// The flow: +// 1. wss:// dial -> WebSocket TLS connection +// 2. Capture remote_spki_der before the stream is consumed +// 3. Send chia-protocol::Handshake with DIG network_id +// 4. Receive and validate the Handshake response +// 5. Upgrade via DigLink::from_websocket(ws, options) +// 6. Yields (DigLink, mpsc::Receiver, Handshake, remote_spki_der) ``` ### Wrap in PeerConnection -The `Peer` MUST be wrapped in a `PeerConnection` with gossip metadata: +The `DigLink` MUST be wrapped in a `PeerConnection` with gossip metadata: ```rust let peer_connection = PeerConnection { @@ -105,21 +110,22 @@ address_manager.add_to_new_table(&respond.peer_list, &peer_info, 0).await; - [ ] TLS certificate is loaded via `load_ssl_cert()` when cert files exist on disk - [ ] TLS certificate is generated via `ChiaCertificate::generate()` when no cert files exist - [ ] A TLS connector is created via `create_native_tls_connector()` (or `create_rustls_connector()`) -- [ ] `connect_peer()` is called with the DIG `network_id`, connector, target address, and options -- [ ] The returned `Peer` is wrapped in a `PeerConnection` with `is_outbound: true` +- [ ] The connect flow is driven with the DIG `network_id`, connector, target address, and options +- [ ] The returned `DigLink` is wrapped in a `PeerConnection` with `is_outbound: true` - [ ] `PeerConnection` fields are populated from the handshake response - [ ] `creation_time` is set to the current Unix timestamp - [ ] `RequestPeers` is sent after successful connection - [ ] `RespondPeers` response is added to the address manager via `add_to_new_table()` -- [ ] Connection failures propagate as `GossipError::ClientError` +- [ ] Connection failures propagate as `GossipError::LinkError` (the dial is `DigLink`'s, so the + typed connection-level error is `LinkError`; `ClientError` remains the handshake-policy error) ## Implementation Notes - Primary files: `src/connection/mod.rs`, `src/service/gossip_service.rs` -- Dependencies: `chia-sdk-client` (connect_peer, load_ssl_cert, create_native_tls_connector, Peer, PeerOptions), `chia-ssl` (ChiaCertificate), `chia-protocol` (Handshake, RequestPeers, RespondPeers) +- Dependencies: `dig-peer-protocol` (DigLink, LinkOptions, load_ssl_cert, create_native_tls_connector), `chia-ssl` (ChiaCertificate), `chia-protocol` (Handshake, RequestPeers, RespondPeers) - Edge cases: - Certificate files may not exist on first run; fall back to `ChiaCertificate::generate()` and persist - - `connect_peer()` will reject peers with mismatched `network_id` (see CON-003) + - Handshake validation rejects peers with a mismatched `network_id` (see CON-003) - The `receiver` channel must be spawned into a per-connection message loop task - Multiple outbound connections may be initiated in parallel (see discovery domain) @@ -129,13 +135,13 @@ address_manager.add_to_new_table(&respond.peer_list, &peer_info, 0).await; | Test | Type | Description | Expected Result | |------|------|-------------|-----------------| -| test_outbound_connect_handshake | Integration | Connect two peers via connect_peer() with DIG network_id | Connection succeeds; both peers have valid PeerConnection | +| test_outbound_connect_handshake | Integration | Connect two peers via the outbound flow with DIG network_id | Connection succeeds; both peers have valid PeerConnection | | test_tls_cert_load | Unit | Call load_ssl_cert() with valid cert/key paths | Returns certificate and key without error | | test_tls_cert_generate | Unit | Call ChiaCertificate::generate() | Returns valid self-signed certificate | | test_connector_creation | Unit | Create TLS connector from certificate | Connector created without error | | test_peer_connection_wrapping | Unit | Wrap Peer in PeerConnection with metadata | All fields correctly populated, is_outbound is true | | test_request_peers_after_connect | Integration | Connect and verify RequestPeers is sent | RespondPeers received; peer_list added to address manager | -| test_outbound_connect_failure | Integration | Attempt connect_peer() to unreachable address | Returns GossipError::ClientError | +| test_outbound_connect_failure | Integration | Attempt an outbound connect to an unreachable address | Returns GossipError::LinkError | | test_creation_time_set | Unit | Create PeerConnection and check creation_time | creation_time is within 1 second of current time | ### Expected Test Files diff --git a/docs/requirements/domains/connection/specs/CON-002.md b/docs/requirements/domains/connection/specs/CON-002.md index 3772b43..7c61b43 100644 --- a/docs/requirements/domains/connection/specs/CON-002.md +++ b/docs/requirements/domains/connection/specs/CON-002.md @@ -7,10 +7,16 @@ ## Summary -`chia-sdk-client`'s `Peer` only supports outbound connections. For inbound connections, the gossip service must accept TCP connections via `TcpListener`, perform TLS handshake, upgrade to WebSocket via `tokio_tungstenite::accept_async()`, and create a `Peer` via `Peer::from_websocket()`. The inbound handshake must be received, validated, and responded to. The new peer is added to the address manager "new" table and its info is relayed to other connected peers. +`DigLink::from_websocket()` types the stream as the client-oriented `MaybeTlsStream`, so it cannot take a server-side TLS stream. For inbound connections, the gossip service must accept TCP connections via `TcpListener`, perform TLS handshake, upgrade to WebSocket via `tokio_tungstenite::accept_async()`, and create a `DigLink` via `DigLink::from_server_websocket()`. The inbound handshake must be received, validated, and responded to. The new peer is added to the address manager "new" table and its info is relayed to other connected peers. ## Specification +> **Implementation:** the `ClientError`-vs-`LinkError` choice is made by +> [`connection::dial_error::DialError`](../../../../../src/connection/dial_error.rs), which keeps a +> handshake-policy rejection (`ClientError`, never retryable) apart from a transport failure +> (`LinkError`, retryable). See API-004 for the full statement. + + ### Listener Setup The inbound listener MUST bind to the configured listen address: @@ -36,7 +42,7 @@ loop { let ws_stream = tokio_tungstenite::accept_async(tls_stream).await?; // Create Peer from WebSocket - let (peer, receiver) = Peer::from_websocket(ws_stream, peer_options.clone()); + let (peer, receiver) = DigLink::from_server_websocket(ws_stream, remote_addr, peer_options.clone()); // Spawn per-connection handler tokio::spawn(handle_inbound_connection(peer, receiver, remote_addr)); @@ -131,7 +137,7 @@ async fn relay_peer_info(new_peer: &PeerConnection) { - [ ] `TcpListener` binds to `config.listen_addr` - [ ] Incoming TCP connections go through TLS handshake - [ ] WebSocket upgrade via `tokio_tungstenite::accept_async()` -- [ ] `Peer::from_websocket()` creates a Peer from the WebSocket stream +- [ ] `DigLink::from_server_websocket()` creates a DigLink from the server-side WebSocket stream - [ ] Inbound `Handshake` is received and decoded via `Streamable` - [ ] `network_id` is validated against the local config (rejects mismatch) - [ ] A `Handshake` response is sent with local node info @@ -144,7 +150,7 @@ async fn relay_peer_info(new_peer: &PeerConnection) { ## Implementation Notes - Primary files: `src/connection/listener.rs`, `src/connection/mod.rs` -- Dependencies: `tokio` (TcpListener, spawn), `tokio-tungstenite` (accept_async), `chia-sdk-client` (Peer::from_websocket, PeerOptions), `chia-protocol` (Handshake, Streamable, TimestampedPeerInfo) +- Dependencies: `tokio` (TcpListener, spawn), `tokio-tungstenite` (accept_async), `dig-peer-protocol` (DigLink::from_server_websocket, LinkOptions), `chia-protocol` (Handshake, Streamable, TimestampedPeerInfo) - Edge cases: - TLS handshake failure should log and skip (not crash the listener) - WebSocket upgrade failure should log and skip diff --git a/docs/requirements/domains/connection/specs/CON-003.md b/docs/requirements/domains/connection/specs/CON-003.md index 5e41d6a..3a4ff6e 100644 --- a/docs/requirements/domains/connection/specs/CON-003.md +++ b/docs/requirements/domains/connection/specs/CON-003.md @@ -11,6 +11,12 @@ All inbound and outbound handshakes must be validated before the connection is a ## Specification +> **Implementation:** the `ClientError`-vs-`LinkError` choice is made by +> [`connection::dial_error::DialError`](../../../../../src/connection/dial_error.rs), which keeps a +> handshake-policy rejection (`ClientError`, never retryable) apart from a transport failure +> (`LinkError`, retryable). See API-004 for the full statement. + + ### Network ID Validation The `network_id` from the remote `Handshake` MUST match the local `config.network_id`. Mismatches MUST cause immediate disconnection: @@ -110,12 +116,12 @@ if sanitized.len() > MAX_SOFTWARE_VERSION_LEN { ## Implementation Notes - Primary files: `src/connection/mod.rs` (validation functions) -- Dependencies: `chia-protocol` (Handshake), `chia-sdk-client` (ClientError) +- Dependencies: `chia-protocol` (Handshake), `dig-peer-protocol` (ClientError) - Edge cases: - `software_version` consisting entirely of Cc/Cf characters results in an empty string (valid) - `software_version` at exactly 128 bytes is accepted; 129 bytes is rejected - Multi-byte UTF-8 characters: length check is on byte length, not character count - - `connect_peer()` already validates `network_id` for outbound; inbound must validate independently + - The outbound flow already validates `network_id`; inbound must validate independently - Empty `network_id` or `protocol_version` should be rejected ## Verification diff --git a/docs/requirements/domains/connection/specs/CON-004.md b/docs/requirements/domains/connection/specs/CON-004.md index e3fe10b..a05d8ae 100644 --- a/docs/requirements/domains/connection/specs/CON-004.md +++ b/docs/requirements/domains/connection/specs/CON-004.md @@ -109,7 +109,24 @@ When the keepalive loop detects a timeout (no `Pong` within `PEER_TIMEOUT_SECS`) ## Implementation Notes -- Primary files: `src/connection/mod.rs` (keepalive loop), `src/types/reputation.rs` (RTT tracking) +### The probe MUST be uncorrelated + +`chia-protocol` defines no application-level `Ping`/`Pong`, so the observable probe is a +`RequestPeers` -> `RespondPeers` round-trip. That probe MUST be sent with **no correlation id**, and +its reply MUST be observed on the application inbound stream rather than on a correlation waiter. + +A correlated probe collides with the peer's identically-allocated id. Both peers allocate +correlation ids from a counter starting at zero and both keepalive loops start at handshake on the +same interval, so two probes can carry the same id. The link matches an inbound frame on correlation +id *before* forwarding it, so each side's waiter receives the peer's **request** instead of a +response: the peer's request never reaches the auto-reply path, neither side records a success, and +both disconnect at the staleness check while logging a timeout that names the wrong cause. + +This fails **loose** — a peer that is alive but silent on the application stream is kept, and an +inbound stream that is unavailable (service starting or stopping) skips the round without charging +the staleness window. That is the correct direction for a probe whose only action is to disconnect. + +- Primary files: `src/connection/keepalive.rs` (keepalive loop), `src/types/reputation.rs` (RTT tracking) - Dependencies: `tokio` (interval, Duration, Instant), `chia-protocol` (Ping/Pong messages via Peer) - Edge cases: - Ping/Pong is bidirectional: we must also respond to incoming Pings with Pongs @@ -132,7 +149,10 @@ When the keepalive loop detects a timeout (no `Pong` within `PEER_TIMEOUT_SECS`) | test_avg_rtt_calculation | Unit | Add known RTT values, check avg_rtt_ms | Average computed correctly | | test_timeout_penalty | Integration | Trigger timeout, check peer reputation | ConnectionIssue penalty applied | | test_bidirectional_keepalive | Integration | Verify both sides send Ping and respond with Pong | Both directions functional | +| colliding_correlation_ids_do_not_tear_the_link_down | Integration | Two peers whose keepalive loops probe from the SAME correlation id, observed across six probe intervals | Both peers still Live, no reconnect, RTT samples recorded | +| an_unobservable_probe_does_not_disconnect_the_peer | Integration | Inbound broadcast removed while a peer is live | Peer kept, no keepalive penalty | ### Expected Test Files - tests/con_004_tests.rs +- tests/con_2767_keepalive_correlation_tests.rs diff --git a/docs/requirements/domains/connection/specs/CON-005.md b/docs/requirements/domains/connection/specs/CON-005.md index 3a8ba58..e572a2e 100644 --- a/docs/requirements/domains/connection/specs/CON-005.md +++ b/docs/requirements/domains/connection/specs/CON-005.md @@ -7,18 +7,18 @@ ## Summary -Rate limiting uses `chia-sdk-client::RateLimiter` with `V2_RATE_LIMITS`. Outbound rate limiting is built into `Peer::send_raw()` (it loops with 1-second sleep until the rate limit clears). Each inbound connection must have its own separate `RateLimiter` instance to enforce per-connection message frequency and size limits. DIG extension message types (200+ range) must be added to the rate limit configuration. +Rate limiting uses `dig_peer_protocol::OpcodeRateLimiter`, which carries Chia's `V2_RATE_LIMITS` rows re-keyed by raw wire opcode. Outbound rate limiting is built into `DigLink::send_message()`, which waits for rate-limit budget up to `LinkOptions::budget_timeout`. Each inbound connection must have its own separate `OpcodeRateLimiter` instance to enforce per-connection message frequency and size limits. DIG extension message types (200+ range) must be added to the rate limit configuration. ## Specification ### Outbound Rate Limiting (Built-in) -Outbound rate limiting is handled internally by `chia-sdk-client::Peer::send_raw()`. No additional implementation is needed for outbound: +Outbound rate limiting is handled internally by `DigLink::send_message()`. No additional implementation is needed for outbound: ```rust -// Peer::send_raw() internally enforces rate limits. +// DigLink's send path internally enforces rate limits. // It loops with a 1-second sleep until the rate limit window clears. -// This is provided by chia-sdk-client — we do NOT reimplement. +// This is provided by DigLink — we do NOT reimplement. ``` ### Inbound Rate Limiting (Per-Connection) @@ -29,9 +29,11 @@ Each inbound connection MUST have its own admission gate, holding both bounds be use dig_gossip::InboundRateLimiter; // Create a separate gate for each inbound connection. It composes: -// - chia_sdk_client::RateLimiter over V2_RATE_LIMITS, keyed by ProtocolMessageTypes +// - dig_peer_protocol::OpcodeRateLimiter over V2_RATE_LIMITS, keyed by the raw opcode byte // - DigRateLimiter over dig_extension_rate_limits_map(), keyed by the raw opcode byte -// Both use incoming = true and the same 60-second absolute window. +// Both share the same 60-second absolute window. Only DigRateLimiter is inbound-shaped +// (incoming = true, so a refused frame still charges); OpcodeRateLimiter exposes no such flag +// in dig-peer-protocol 0.5 — see the note under Acceptance Criteria. let inbound_limiter = InboundRateLimiter::new(config.peer_options.rate_limit_factor); ``` @@ -97,7 +99,7 @@ fn create_dig_rate_limits() -> RateLimits { max_size: 4096, }); - // 220-band public broadcasts (ProtocolMessageTypes variants in the vendored fork, keyed + // 220-band public broadcasts (DIG extension opcodes, keyed // by their raw opcode in the same u8 -> RateLimit table as every other DIG row). // StoreMelted = 221 — fixed-size, infrequent public broadcast (#1316). limits.insert(221, RateLimit { @@ -168,22 +170,27 @@ async fn handle_inbound_message( ## Acceptance Criteria - [ ] Each inbound connection has its own `InboundRateLimiter` instance -- [ ] Both halves are initialized with `incoming = true` for inbound connections +- [ ] The `DigRateLimiter` half is initialized with `incoming = true` for inbound connections, so a + refused frame still charges its counter +- [ ] **Known gap (dig_ecosystem#2228):** the `OpcodeRateLimiter` half carries no `incoming` flag in + `dig-peer-protocol` 0.5, so it charges only admitted frames. `OpcodeRateLimits`' fields are + private, so `dig-gossip` cannot restore the ratchet locally; it returns with 0.6.0's + `Direction::Inbound`. - [ ] `V2_RATE_LIMITS` is used as the base rate limit configuration - [ ] DIG extension message types (200-208) are added with appropriate frequency and size limits - [ ] Every inbound message is checked against the rate limiter before processing - [ ] Rate limit violations apply a `RateLimitExceeded` penalty to the peer's reputation -- [ ] Outbound rate limiting is delegated to `Peer::send_raw()` (no custom implementation) -- [ ] Rate limiter uses the configured `rate_limit_factor` from `PeerOptions` +- [ ] Outbound rate limiting is delegated to `DigLink`'s send path (no custom implementation) +- [ ] Rate limiter uses the configured `rate_limit_factor` from `LinkOptions` - [ ] Rate limit window is 60 seconds (reset_seconds) ## Implementation Notes - Primary files: `src/connection/mod.rs` (rate limiter creation and checking) -- Dependencies: `chia-sdk-client` (RateLimiter, RateLimits, RateLimit, V2_RATE_LIMITS); the DIG per-opcode bound is dig-gossip's own `DigRateLimiter` (dig_ecosystem#2228) +- Dependencies: `dig-peer-protocol` (OpcodeRateLimiter, OpcodeRateLimits, RateLimits, RateLimit, V2_RATE_LIMITS); the DIG per-opcode bound is dig-gossip's own `DigRateLimiter` (dig_ecosystem#2228) - Edge cases: - `rate_limit_factor` of 0 effectively disables rate limiting (useful for testing) - - DIG message types must not collide with existing Chia `ProtocolMessageTypes` (200+ range is safe) + - DIG opcodes must not collide with Chia's own discriminants; Chia stops at `RespondCostInfo = 107`, so the 200-222 band is safe - Rate limiter state is per-connection and not shared between connections - If a peer repeatedly exceeds rate limits, the accumulated `RateLimitExceeded` penalties will trigger a ban (see CON-007) - `InboundRateLimiter::allows()` returns false if either bound is exceeded; it does not block @@ -194,7 +201,7 @@ async fn handle_inbound_message( | Test | Type | Description | Expected Result | |------|------|-------------|-----------------| -| test_inbound_rate_limiter_creation | Unit | Create InboundRateLimiter | Gate created with incoming=true on both halves | +| test_inbound_rate_limiter_creation | Unit | Create InboundRateLimiter | Gate created; DIG half incoming=true (Chia half per dig_ecosystem#2228) | | test_separate_limiter_per_connection | Unit | Create two connections, verify independent limiters | Each has its own limiter state | | test_dig_message_types_added | Unit | Check rate limits contain DIG types 200-208 | All DIG types present with correct limits | | test_rate_limit_allows_normal_traffic | Unit | Send messages within rate limit | All messages accepted | diff --git a/docs/requirements/domains/connection/specs/CON-007.md b/docs/requirements/domains/connection/specs/CON-007.md index bc40696..4b287cc 100644 --- a/docs/requirements/domains/connection/specs/CON-007.md +++ b/docs/requirements/domains/connection/specs/CON-007.md @@ -7,7 +7,7 @@ ## Summary -Peer banning extends `chia-sdk-client::ClientState`'s binary ban/trust model with numeric penalty accumulation via `PeerReputation`. Penalties are assigned for various misbehaviors (invalid blocks, spam, protocol violations, etc.). When a peer's cumulative `penalty_points` reaches `PENALTY_BAN_THRESHOLD` (100 points), the peer is banned via `ClientState::ban()` and the `PeerReputation` is flagged. Bans automatically expire after `BAN_DURATION_SECS` (3600 seconds). +Peer banning extends the re-exported `ClientState`'s binary ban/trust model with numeric penalty accumulation via `PeerReputation`. Penalties are assigned for various misbehaviors (invalid blocks, spam, protocol violations, etc.). When a peer's cumulative `penalty_points` reaches `PENALTY_BAN_THRESHOLD` (100 points), the peer is banned via `ClientState::ban()` and the `PeerReputation` is flagged. Bans automatically expire after `BAN_DURATION_SECS` (3600 seconds). ## Specification @@ -90,7 +90,7 @@ async fn ban_peer( let should_ban = peer_connection.reputation.apply_penalty(reason); if should_ban { - // Delegate to chia-sdk-client's ClientState + // Delegate to the re-exported ClientState client_state.ban(peer_connection.address.ip()).await; tracing::warn!( @@ -168,7 +168,7 @@ async fn check_unbans( ## Implementation Notes - Primary files: `src/types/reputation.rs` (PeerReputation, PenaltyReason), `src/connection/mod.rs` (ban execution) -- Dependencies: `chia-sdk-client` (ClientState::ban, ClientState::unban) +- Dependencies: `dig-peer-protocol` (ClientState::ban, ClientState::unban) - Edge cases: - A single `InvalidBlock` or `ConsensusError` penalty (100 points) triggers an immediate ban - Penalty points do not decay over time (only reset on unban) diff --git a/docs/requirements/domains/connection/specs/CON-009.md b/docs/requirements/domains/connection/specs/CON-009.md index 369ded7..3ed5a43 100644 --- a/docs/requirements/domains/connection/specs/CON-009.md +++ b/docs/requirements/domains/connection/specs/CON-009.md @@ -16,7 +16,7 @@ This matches Chia's mTLS design where all peer connections use mutual certificat - The client presents its certificate: via `ssl_context.load_cert_chain()` ([`server.py:69`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/server.py#L69)) - Peer node ID is derived from the certificate: `peer_node_id` ([`ws_connection.py:95`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/ws_connection.py#L95)) -The `chia-ssl` crate provides `ChiaCertificate::generate()` for creating node certificates, and `chia-sdk-client` provides `load_ssl_cert()`, `create_native_tls_connector()`, and `create_rustls_connector()` which configure the TLS connector with the node's certificate as a **client certificate** for mutual auth. +The `chia-ssl` crate provides `ChiaCertificate::generate()` for creating node certificates, and `dig-peer-protocol` re-exports `load_ssl_cert()`, `create_native_tls_connector()`, and `create_rustls_connector()` which configure the TLS connector with the node's certificate as a **client certificate** for mutual auth. --- @@ -26,7 +26,7 @@ The `chia-ssl` crate provides `ChiaCertificate::generate()` for creating node ce ```rust use chia_ssl::ChiaCertificate; -use chia_sdk_client::{load_ssl_cert, create_native_tls_connector}; +use dig_peer_protocol::{load_ssl_cert, create_native_tls_connector}; // On first run: generate a new certificate let cert = ChiaCertificate::generate()?; @@ -47,8 +47,8 @@ let connector = create_native_tls_connector(&cert)?; All outbound connections MUST use the TLS connector: ```rust -// connect_peer() internally uses Peer::connect() which requires a Connector -let (peer, receiver) = connect_peer( +// The wss:// dial requires a Connector carrying the node's client certificate +let result = connect_outbound( network_id, connector, // TLS connector from chia-ssl certificate socket_addr, @@ -82,7 +82,7 @@ let peer_cert = extract_peer_certificate(&tls_stream); let peer_id: PeerId = sha256(&peer_cert.public_key_der()); let ws_stream = tokio_tungstenite::accept_async(tls_stream).await?; -let (peer, receiver) = Peer::from_websocket(ws_stream, options)?; +let (peer, receiver) = DigLink::from_server_websocket(ws_stream, remote_addr, options); ``` ### Peer Identity from mTLS @@ -99,7 +99,7 @@ pub type PeerId = Bytes32; // SHA256(remote_tls_public_key) ### Rejection of Non-mTLS Connections -- Outbound: `connect_peer()` requires a `Connector` parameter that includes the node's client certificate — there is no code path for plain WebSocket or server-only TLS. +- Outbound: the connect flow requires a `Connector` parameter that includes the node's client certificate — there is no code path for plain WebSocket or server-only TLS. - Inbound: The listener MUST NOT call `tokio_tungstenite::accept_async()` on a raw TCP stream. The mTLS acceptor MUST be applied first. If the peer does not present a certificate, the TLS handshake fails and the connection MUST be dropped. - Server-only TLS (where only the listener has a cert but the client does not) MUST NOT be accepted for P2P connections. Both sides MUST present certificates. @@ -127,7 +127,7 @@ pub type PeerId = Bytes32; // SHA256(remote_tls_public_key) ## Implementation Notes - **Primary files:** `src/connection/listener.rs` (inbound TLS), `src/service/gossip_service.rs` (cert loading) -- **Dependencies:** CON-001 (outbound uses connect_peer with connector), CON-002 (inbound uses TLS acceptor) +- **Dependencies:** CON-001 (outbound dials with a client-cert connector), CON-002 (inbound uses TLS acceptor) - **Edge cases:** - Certificate file missing on startup → generate new via `ChiaCertificate::generate()` - Certificate file corrupt → error on load, do not fall back to unencrypted @@ -172,4 +172,4 @@ pub type PeerId = Bytes32; // SHA256(remote_tls_public_key) - **SPEC.md Section 5.1 Steps 1-2**: Load TLS cert, create connector - **SPEC.md Section 5.2 Step 2**: TLS handshake on inbound - **Chia L1 reference — `server.py:54-71`:** [`server.py`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/server.py#L54) — TLS context setup for all peer connections -- **chia-sdk-client TLS:** [`tls.rs`](https://github.com/Chia-Network/chia-wallet-sdk) — `load_ssl_cert()`, `create_native_tls_connector()`, `create_rustls_connector()` +- **Upstream TLS helpers (re-exported by `dig-peer-protocol`):** [`tls.rs`](https://github.com/Chia-Network/chia-wallet-sdk) — `load_ssl_cert()`, `create_native_tls_connector()`, `create_rustls_connector()` diff --git a/docs/requirements/domains/crate_api/NORMATIVE.md b/docs/requirements/domains/crate_api/NORMATIVE.md index d5be5f7..ffd3e24 100644 --- a/docs/requirements/domains/crate_api/NORMATIVE.md +++ b/docs/requirements/domains/crate_api/NORMATIVE.md @@ -26,19 +26,19 @@ GossipConfig MUST contain fields: `listen_addr`, `peer_id`, `network_id`, `netwo ### API-004: GossipError Enum Variants -GossipError MUST be a `#[derive(Debug, Clone, thiserror::Error)]` enum wrapping `chia-sdk-client::ClientError` via `#[from]` and providing variants: `ClientError`, `PeerNotConnected`, `PeerBanned`, `MaxConnectionsReached`, `DuplicateConnection`, `SelfConnection`, `RequestTimeout`, `IntroducerNotConfigured`, `IntroducerError`, `RelayNotConfigured`, `RelayError`, `ServiceNotStarted`, `ChannelClosed`, `IoError`, `SketchError(String)`, `SketchDecodeFailed`. The `SketchError` variant covers minisketch encoding/decoding errors during ERLAY reconciliation. `SketchDecodeFailed` is a unit variant for when sketch decoding produces no result (symmetric difference too large for sketch capacity). +GossipError MUST be a `#[derive(Debug, Clone, thiserror::Error)]` enum wrapping the re-exported `ClientError` via `#[from]` and providing variants: `ClientError`, `PeerNotConnected`, `PeerBanned`, `MaxConnectionsReached`, `DuplicateConnection`, `SelfConnection`, `RequestTimeout`, `IntroducerNotConfigured`, `IntroducerError`, `RelayNotConfigured`, `RelayError`, `ServiceNotStarted`, `ChannelClosed`, `IoError`, `SketchError(String)`, `SketchDecodeFailed`. The `SketchError` variant covers minisketch encoding/decoding errors during ERLAY reconciliation. `SketchDecodeFailed` is a unit variant for when sketch decoding produces no result (symmetric difference too large for sketch capacity). **Spec reference:** SPEC Section 4 (Error Types), Section 8.3 (ERLAY reconciliation error paths) ### API-005: PeerConnection Struct -PeerConnection MUST wrap `chia-sdk-client::Peer` with gossip-specific metadata fields: `peer`, `peer_id`, `address`, `is_outbound`, `node_type`, `protocol_version`, `software_version`, `peer_server_port`, `capabilities`, `creation_time`, `bytes_read`, `bytes_written`, `last_message_time`, `reputation`, `inbound_rx`. +PeerConnection MUST wrap `dig_peer_protocol::DigLink` with gossip-specific metadata fields: `peer`, `peer_id`, `address`, `is_outbound`, `node_type`, `protocol_version`, `software_version`, `peer_server_port`, `capabilities`, `creation_time`, `bytes_read`, `bytes_written`, `last_message_time`, `reputation`, `inbound_rx`. **Spec reference:** SPEC Section 2.4 (PeerConnection) ### API-006: PeerReputation and PenaltyReason -PeerReputation MUST track `penalty_points`, `is_banned`, `ban_until`, `last_penalty_reason`, `avg_rtt_ms`, `rtt_history`, `score`, `as_number`. PenaltyReason MUST enumerate: `InvalidBlock`, `InvalidAttestation`, `MalformedMessage`, `Spam`, `ConnectionIssue`, `ProtocolViolation`, `RateLimitExceeded`, `ConsensusError`. PeerReputation extends `chia-sdk-client::ClientState`'s binary ban/trust with numeric penalties. +PeerReputation MUST track `penalty_points`, `is_banned`, `ban_until`, `last_penalty_reason`, `avg_rtt_ms`, `rtt_history`, `score`, `as_number`. PenaltyReason MUST enumerate: `InvalidBlock`, `InvalidAttestation`, `MalformedMessage`, `Spam`, `ConnectionIssue`, `ProtocolViolation`, `RateLimitExceeded`, `ConsensusError`. PeerReputation extends the re-exported `ClientState`'s binary ban/trust with numeric penalties. **Spec reference:** SPEC Section 2.5 (PeerReputation) diff --git a/docs/requirements/domains/crate_api/TRACKING.yaml b/docs/requirements/domains/crate_api/TRACKING.yaml index ac95e09..395c958 100644 --- a/docs/requirements/domains/crate_api/TRACKING.yaml +++ b/docs/requirements/domains/crate_api/TRACKING.yaml @@ -54,7 +54,7 @@ items: ERLAY variants SketchError / SketchDecodeFailed added. API-001 variants InvalidConfig / AlreadyStarted retained. - id: API-005 section: "2.4" - summary: "PeerConnection wraps chia-sdk-client::Peer with metadata" + summary: "PeerConnection wraps dig_peer_protocol::DigLink with metadata" status: verified spec_ref: "SPEC.md#24-peerconnection" tests: diff --git a/docs/requirements/domains/crate_api/VERIFICATION.md b/docs/requirements/domains/crate_api/VERIFICATION.md index 5c3e804..b2bb764 100644 --- a/docs/requirements/domains/crate_api/VERIFICATION.md +++ b/docs/requirements/domains/crate_api/VERIFICATION.md @@ -9,7 +9,7 @@ |---------|--------|----------------------------------------------|---------------------------------------------------------------------------------------| | API-001 | verified | GossipService constructor | `tests/api_001_tests.rs`: new/start/stop lifecycle, TLS load/generate, IoError path, config validation; `GossipHandle::health_check` after stop | | API-002 | verified | GossipHandle methods | `tests/api_002_tests.rs`: clone/inbound subscription, broadcast (exclude), broadcast_typed, send_to (known/unknown/banned), request peers + timeout path, stats, peer_count, connected_peers/get_connections (empty until CON-001), connect_to (success/duplicate/self/max), disconnect, ban/penalize/auto-ban, `discover_from_introducer` empty-endpoint → `InvalidConfig` (DSC-004 real dial in `dsc_004_tests`), relay stubs, post-stop errors | -| API-003 | verified | GossipConfig struct fields | `tests/api_003_tests.rs`: full struct literal + field reads; defaults (listen_addr, targets, max_connections, intervals, fanout, max_seen, **dns_seed_timeout / dns_seed_batch_size**); optional introducer/relay/subsystems; `Network` / `Bytes32` / `PeerOptions` bindings; `#[cfg(feature="tor")]` tor slot with `--all-features` | +| API-003 | verified | GossipConfig struct fields | `tests/api_003_tests.rs`: full struct literal + field reads; defaults (listen_addr, targets, max_connections, intervals, fanout, max_seen, **dns_seed_timeout / dns_seed_batch_size**); optional introducer/relay/subsystems; `Network` / `Bytes32` / `LinkOptions` bindings; `#[cfg(feature="tor")]` tor slot with `--all-features` | | API-004 | verified | GossipError enum variants | `tests/api_004_tests.rs`: Display strings per API-004 table (incl. `AddressManagerStore` / DSC-002); `From` / `?`; `Clone` (incl. ClientError via Arc); `Debug`; Sketch* variants | | API-005 | verified | PeerConnection wraps Peer with metadata | `tests/api_005_tests.rs`: all fields + initial bytes/reputation; inbound/outbound; TLS peer_id derivation (ChiaCertificate + x509-parser); loopback WS RequestPeers → inbound_rx | | API-006 | verified | PeerReputation and PenaltyReason | `tests/api_006_tests.rs`: defaults; penalty accumulation + saturating add; auto-ban at threshold + ban_until; `refresh_ban_status` expiry; RTT window/mean/score; zero-RTT score; `as_number`; all `PenaltyReason` variants + CON-007 weight regression; `last_penalty_reason` | diff --git a/docs/requirements/domains/crate_api/specs/API-001.md b/docs/requirements/domains/crate_api/specs/API-001.md index 0ec8941..8b03f7c 100644 --- a/docs/requirements/domains/crate_api/specs/API-001.md +++ b/docs/requirements/domains/crate_api/specs/API-001.md @@ -23,7 +23,7 @@ impl GossipService { ### Construction Behavior -1. **TLS initialization**: Load TLS certificate from `config.cert_path` and `config.key_path` using `chia-sdk-client::load_ssl_cert()`. If the files do not exist, generate a new certificate pair using `chia-ssl::ChiaCertificate::generate()`. +1. **TLS initialization**: Load TLS certificate from `config.cert_path` and `config.key_path` using the re-exported `load_ssl_cert()`. If the files do not exist, generate a new certificate pair using `chia-ssl::ChiaCertificate::generate()`. 2. **Configuration validation**: Validate that required fields are populated (e.g., `network_id` is non-zero, `listen_addr` is parseable, `target_outbound_count <= max_connections`). 3. **Internal state preparation**: Initialize address manager, seen message set, peer connection map, and channel infrastructure. No tasks are spawned and no network I/O is performed. 4. **Error propagation**: Return `GossipError::IoError` for TLS file errors, `GossipError::ClientError` for TLS creation failures. @@ -77,7 +77,7 @@ service.stop().await?; ## Implementation Notes - Primary file: `src/service/gossip_service.rs` -- Dependencies: `chia-ssl::ChiaCertificate`, `chia-sdk-client::load_ssl_cert`, `GossipConfig`, `GossipError`, `GossipHandle` +- Dependencies: `chia-ssl::ChiaCertificate`, `dig_peer_protocol::load_ssl_cert`, `GossipConfig`, `GossipError`, `GossipHandle` - Edge cases: - TLS cert paths that point to unreadable files should produce `GossipError::IoError` - Generating a new cert should persist it to the configured paths for future reuse diff --git a/docs/requirements/domains/crate_api/specs/API-002.md b/docs/requirements/domains/crate_api/specs/API-002.md index 8e40401..20e81c2 100644 --- a/docs/requirements/domains/crate_api/specs/API-002.md +++ b/docs/requirements/domains/crate_api/specs/API-002.md @@ -40,7 +40,7 @@ impl GossipHandle { exclude: Option, ) -> Result; - /// Send a message to a specific peer (via their chia-sdk-client::Peer). + /// Send a message to a specific peer (via their DigLink). pub async fn send_to( &self, peer_id: PeerId, @@ -85,7 +85,7 @@ impl GossipHandle { outbound_only: bool, ) -> Vec; - /// Connect to a peer (uses chia-sdk-client::connect_peer internally). + /// Connect to a peer (uses the outbound connect flow internally). pub async fn connect_to(&self, addr: SocketAddr) -> Result; /// Disconnect a peer. @@ -186,7 +186,7 @@ async fn use_handle(handle: GossipHandle) -> Result<(), dig_gossip::GossipError> - [ ] `GossipHandle::connected_peers` returns all `PeerConnection`s - [ ] `GossipHandle::peer_count` returns the current connection count - [ ] `GossipHandle::get_connections` filters by `NodeType` and outbound direction -- [ ] `GossipHandle::connect_to` establishes a connection via `connect_peer()` and returns the `PeerId` +- [ ] `GossipHandle::connect_to` establishes a connection via the outbound flow and returns the `PeerId` - [ ] `GossipHandle::connect_to` returns `MaxConnectionsReached` when at limit - [ ] `GossipHandle::connect_to` returns `DuplicateConnection` for already-connected peers - [ ] `GossipHandle::connect_to` returns `SelfConnection` when connecting to own address @@ -202,7 +202,7 @@ async fn use_handle(handle: GossipHandle) -> Result<(), dig_gossip::GossipError> ## Implementation Notes - Primary file: `src/service/gossip_handle.rs` -- Dependencies: `chia-sdk-client::Peer`, `chia-sdk-client::connect_peer`, `chia-protocol::Message`, `chia-traits::Streamable`, `chia-protocol::ChiaProtocolMessage`, `tokio::sync::mpsc`, `PeerConnection`, `GossipError`, `GossipStats`, `RelayStats` +- Dependencies: `dig_peer_protocol::DigLink`, `dig_peer_protocol::DigMessage`, `chia-traits::Streamable`, `chia-protocol::ChiaProtocolMessage`, `tokio::sync::mpsc`, `PeerConnection`, `GossipError`, `GossipStats`, `RelayStats` - Edge cases: - `broadcast` with zero connected peers should return `Ok(0)`, not an error - `send_to` and `request` must check peer is still connected before sending diff --git a/docs/requirements/domains/crate_api/specs/API-003.md b/docs/requirements/domains/crate_api/specs/API-003.md index 749de8a..9af106e 100644 --- a/docs/requirements/domains/crate_api/specs/API-003.md +++ b/docs/requirements/domains/crate_api/specs/API-003.md @@ -23,7 +23,7 @@ pub struct GossipConfig { pub peer_id: PeerId, /// Network ID (e.g., SHA256("dig_mainnet")). pub network_id: Bytes32, - /// Network config for DNS lookup (uses chia-sdk-client::Network). + /// Network config for DNS lookup (uses the re-exported Network). pub network: Network, /// DNS seed timeout per introducer batch (DSC-003 → `Network::lookup_all`). pub dns_seed_timeout: std::time::Duration, @@ -53,7 +53,7 @@ pub struct GossipConfig { /// Path to persist address manager state. pub peers_file_path: PathBuf, /// Peer connection options (rate_limit_factor). - pub peer_options: PeerOptions, + pub peer_options: LinkOptions, /// Dandelion++ configuration (optional). SPEC Section 1.9.1. /// Feature-gated behind `dandelion`. pub dandelion: Option, @@ -95,8 +95,8 @@ pub struct GossipConfig { | `SocketAddr` | `std::net::SocketAddr` | | `PeerId` | `chia-protocol::Bytes32` (type alias) | | `Bytes32` | `chia-protocol` | -| `Network` | `chia-sdk-client` | -| `PeerOptions` | `chia-sdk-client` | +| `Network` | `dig-peer-protocol` (re-exported) | +| `LinkOptions` | `dig-peer-protocol` | | `IntroducerConfig` | DIG-specific (this crate) | | `RelayConfig` | DIG-specific (this crate) | | `DandelionConfig` | DIG-specific (this crate, SPEC Section 1.9.1) | @@ -126,7 +126,7 @@ pub struct GossipConfig { - [ ] `GossipConfig` has field `gossip_fanout: usize` with default `8` - [ ] `GossipConfig` has field `max_seen_messages: usize` with default `100_000` - [ ] `GossipConfig` has field `peers_file_path: PathBuf` -- [ ] `GossipConfig` has field `peer_options: PeerOptions` +- [ ] `GossipConfig` has field `peer_options: LinkOptions` - [ ] `GossipConfig` has field `dandelion: Option` (feature-gated behind `dandelion`) - [ ] `GossipConfig` has field `peer_id_rotation: Option` - [ ] `GossipConfig` has field `tor: Option` (feature-gated behind `tor`) @@ -137,7 +137,7 @@ pub struct GossipConfig { ## Implementation Notes - Primary file: `src/types/config.rs` -- Dependencies: `chia-protocol::Bytes32`, `chia-sdk-client::Network`, `chia-sdk-client::PeerOptions`, `IntroducerConfig`, `RelayConfig`, `PeerId` +- Dependencies: `chia-protocol::Bytes32`, the re-exported `Network`, `dig_peer_protocol::DigLinkOptions`, `IntroducerConfig`, `RelayConfig`, `PeerId` - Edge cases: - `peer_id` and `network_id` have no meaningful default (they are identity/network specific); `Default` impl may use zero-filled `Bytes32` as a sentinel - `network` depends on DNS configuration and has no simple default; users must configure it @@ -161,7 +161,7 @@ pub struct GossipConfig { | test_config_default_dns_seed_batch_size | Unit | Default DNS batch size | 2 (`DEFAULT_DNS_SEED_BATCH_SIZE`) | | test_config_optional_introducer | Unit | Config with introducer = None | Valid config, no introducer | | test_config_optional_relay | Unit | Config with relay = None | Valid config, no relay | -| test_config_peer_options_type | Unit | Verify peer_options is PeerOptions from chia-sdk-client | Correct type | +| test_config_peer_options_type | Unit | Verify peer_options is LinkOptions from dig-peer-protocol | Correct type | | test_config_network_id_type | Unit | Verify network_id is Bytes32 from chia-protocol | Correct type | ### Expected Test Files diff --git a/docs/requirements/domains/crate_api/specs/API-004.md b/docs/requirements/domains/crate_api/specs/API-004.md index 9610d2e..bf298aa 100644 --- a/docs/requirements/domains/crate_api/specs/API-004.md +++ b/docs/requirements/domains/crate_api/specs/API-004.md @@ -7,7 +7,7 @@ ## Summary -GossipError is the unified error type for the gossip crate. It wraps `chia-sdk-client::ClientError` via the `From` trait and adds gossip-specific error variants covering peer management, connection limits, discovery, relay, and service lifecycle errors. It derives `Debug`, `Clone`, and uses `thiserror::Error` for display formatting. +GossipError is the unified error type for the gossip crate. It wraps both re-exported peer-layer error types via the `From` trait — `ClientError` and `LinkError` — and adds gossip-specific error variants covering peer management, connection limits, discovery, relay, and service lifecycle errors. It derives `Debug`, `Clone`, and uses `thiserror::Error` for display formatting. ## Specification @@ -16,9 +16,14 @@ GossipError is the unified error type for the gossip crate. It wraps `chia-sdk-c ```rust #[derive(Debug, Clone, thiserror::Error)] pub enum GossipError { - /// Wraps chia-sdk-client::ClientError for connection-level errors. + /// The peer was reached and rejected on policy (handshake validation, TLS material). + /// Never worth retrying the same address. #[error("client error: {0}")] - ClientError(#[from] ClientError), + ClientError(Arc), + + /// The peer was never reached, or the pipe broke. Retrying the same address may succeed. + #[error("link error: {0}")] + LinkError(Arc), #[error("peer not connected: {0}")] PeerNotConnected(PeerId), @@ -78,7 +83,8 @@ pub enum GossipError { | Variant | When Raised | |---------|-------------| -| `ClientError` | Any error from `chia-sdk-client` operations (connect, send, TLS) | +| `ClientError` | The peer was reached and rejected on **policy**: handshake validation failed (`WrongNetwork`, `WrongNodeType`, incompatible protocol version), or TLS material failed to load. The typed inner variant survives to the caller — a caller MUST NOT have to match on an error string | +| `LinkError` | The **transport** failed: the dial is `DigLink`'s, so a refused connection, TLS failure, timeout or framing error surfaces here | | `PeerNotConnected` | `send_to`, `request`, `disconnect`, `ban_peer`, `penalize_peer` on unknown peer | | `PeerBanned` | Attempting to connect to or send to a banned peer | | `MaxConnectionsReached` | `connect_to` when `max_connections` limit hit | @@ -98,20 +104,51 @@ pub enum GossipError { ### From Implementations +Both are hand-written rather than derived, because each variant stores an `Arc` (neither upstream +error type is `Clone`, but `GossipError` is): + ```rust -// Automatic via #[from] on ClientError variant impl From for GossipError { fn from(err: ClientError) -> Self { - GossipError::ClientError(err) + GossipError::ClientError(Arc::new(err)) + } +} + +impl From for GossipError { + fn from(err: LinkError) -> Self { + GossipError::LinkError(Arc::new(err)) } } ``` +### Choosing between them: `DialError` + +A dial can fail either way, so the dial functions return +[`connection::dial_error::DialError`](../../../../../src/connection/dial_error.rs) — a two-armed +union of `ClientError` and `LinkError` that converts into the matching `GossipError` variant. It +exists so a policy rejection is never downgraded into `LinkError::Io(_)` carrying a formatted +string, which is what erased the typed `ClientError::WrongNetwork` a caller needs in order to tell +"not our network" from "host is down". + +The two arms carry **opposite retry semantics**, and that is the whole reason they are kept apart: + +| Arm | Meaning | Retry the same address? | +|-----|---------|-------------------------| +| `DialError::Link` → `GossipError::LinkError` | Never reached the peer | Yes — it may be transient | +| `DialError::Client` → `GossipError::ClientError` | Reached the peer, policy rejected it | No — the verdict will not change | + +Both handshake legs agree: `connection::listener` (inbound) and `connection::outbound` (outbound) +raise the same `GossipError` variant for an identical rejection, whoever dialled. See also +CON-002 and CON-003, which specify `ClientError` for a handshake rejection. + ## Acceptance Criteria - [ ] `GossipError` derives `Debug` and `Clone` - [ ] `GossipError` derives `thiserror::Error` -- [ ] `GossipError::ClientError` wraps `chia-sdk-client::ClientError` with `#[from]` +- [ ] `GossipError::ClientError` wraps the re-exported `ClientError`, convertible via `From` +- [ ] `GossipError::LinkError` wraps the re-exported `LinkError`, convertible via `From` +- [ ] A handshake-policy rejection surfaces as `ClientError` on **both** dial legs, with the typed + inner variant intact; a transport failure surfaces as `LinkError` - [ ] `GossipError::PeerNotConnected` contains a `PeerId` - [ ] `GossipError::PeerBanned` contains a `PeerId` - [ ] `GossipError::MaxConnectionsReached` contains a `usize` (the limit) @@ -134,7 +171,7 @@ impl From for GossipError { ## Implementation Notes - Primary file: `src/error.rs` -- Dependencies: `chia-sdk-client::ClientError`, `PeerId` (`chia-protocol::Bytes32`), `thiserror` +- Dependencies: the re-exported `ClientError`, `PeerId` (`chia-protocol::Bytes32`), `thiserror` - Edge cases: - `ClientError` must implement `Clone` for `GossipError` to derive `Clone`; if it does not, the `ClientError` variant may need to store a `String` representation instead - `PeerId` display in error messages uses `Bytes32`'s `Display` impl (hex string) diff --git a/docs/requirements/domains/crate_api/specs/API-005.md b/docs/requirements/domains/crate_api/specs/API-005.md index 2c36edd..3664812 100644 --- a/docs/requirements/domains/crate_api/specs/API-005.md +++ b/docs/requirements/domains/crate_api/specs/API-005.md @@ -7,7 +7,7 @@ ## Summary -PeerConnection wraps `chia-sdk-client::Peer` with gossip-specific metadata. While `Peer` handles the raw WebSocket connection and message I/O, PeerConnection adds identity, direction, handshake data, traffic counters, reputation tracking, and a per-connection inbound message receiver. It is the primary struct used throughout the gossip layer to represent an active peer connection. +PeerConnection wraps `dig_peer_protocol::DigLink` with gossip-specific metadata. While `DigLink` handles the raw WebSocket connection and message I/O, PeerConnection adds identity, direction, handshake data, traffic counters, reputation tracking, and a per-connection inbound message receiver. It is the primary struct used throughout the gossip layer to represent an active peer connection. ## Specification @@ -15,10 +15,10 @@ PeerConnection wraps `chia-sdk-client::Peer` with gossip-specific metadata. Whil ```rust /// Extended peer connection state for the gossip layer. -/// Wraps `chia-sdk-client::Peer` with gossip-specific metadata. +/// Wraps `dig_peer_protocol::DigLink` with gossip-specific metadata. pub struct PeerConnection { - /// The underlying chia-sdk-client Peer connection. - pub peer: Peer, + /// The underlying DigLink connection. + pub peer: DigLink, /// Unique peer identifier (SHA256 of TLS public key). pub peer_id: PeerId, /// Remote socket address. @@ -54,7 +54,7 @@ pub struct PeerConnection { | Field | Source | |-------|--------| -| `peer` | Returned by `connect_peer()` or `Peer::from_websocket()` | +| `peer` | Returned by the outbound connect flow or `DigLink::from_server_websocket()` | | `peer_id` | Derived from SHA256 of the peer's TLS public key | | `address` | Remote socket address from the TCP connection | | `is_outbound` | `true` for connections we initiated, `false` for inbound | @@ -67,13 +67,13 @@ pub struct PeerConnection { | `bytes_read` / `bytes_written` | Accumulated from message I/O | | `last_message_time` | Updated on each received message | | `reputation` | Initialized as `PeerReputation::default()` | -| `inbound_rx` | Receiver half of the per-connection message channel from `connect_peer()` | +| `inbound_rx` | Receiver half of the per-connection `DigMessage` channel from the connect flow | ### Type Dependencies | Type | Source | |------|--------| -| `Peer` | `chia-sdk-client` | +| `DigLink` | `dig-peer-protocol` | | `PeerId` | `chia-protocol::Bytes32` (type alias) | | `SocketAddr` | `std::net::SocketAddr` | | `NodeType` | `chia-protocol` | @@ -83,7 +83,7 @@ pub struct PeerConnection { ## Acceptance Criteria -- [ ] `PeerConnection` has field `peer: Peer` from `chia-sdk-client` +- [ ] `PeerConnection` has field `peer: DigLink` from `dig-peer-protocol` - [ ] `PeerConnection` has field `peer_id: PeerId` - [ ] `PeerConnection` has field `address: SocketAddr` - [ ] `PeerConnection` has field `is_outbound: bool` @@ -105,9 +105,9 @@ pub struct PeerConnection { ## Implementation Notes - Primary file: `src/types/peer.rs` -- Dependencies: `chia-sdk-client::Peer`, `chia-protocol::Bytes32`, `chia-protocol::NodeType`, `chia-protocol::Message`, `tokio::sync::mpsc`, `PeerReputation` +- Dependencies: `dig_peer_protocol::DigLink`, `chia-protocol::Bytes32`, `chia-protocol::NodeType`, `dig_peer_protocol::DigMessage`, `tokio::sync::mpsc`, `PeerReputation` - Edge cases: - - `PeerConnection` cannot implement `Clone` because `mpsc::Receiver` is not cloneable and `Peer` may not be cloneable + - `PeerConnection` cannot implement `Clone` because `mpsc::Receiver` is not cloneable and `DigLink` may not be cloneable - `bytes_read`/`bytes_written` should be updated atomically or behind a lock if accessed from multiple tasks - `last_message_time` should be updated in the per-connection message loop, not by the caller - `capabilities` format is `Vec<(u16, String)>` matching `chia-protocol::Handshake::capabilities` diff --git a/docs/requirements/domains/crate_api/specs/API-006.md b/docs/requirements/domains/crate_api/specs/API-006.md index 57d02d0..b8c27fe 100644 --- a/docs/requirements/domains/crate_api/specs/API-006.md +++ b/docs/requirements/domains/crate_api/specs/API-006.md @@ -7,7 +7,7 @@ ## Summary -PeerReputation extends `chia-sdk-client::ClientState`'s binary ban/trust model with numeric penalty tracking, RTT-based latency scoring, and AS-level grouping. PenaltyReason enumerates the reasons a peer can be penalized. Together they provide latency-aware peer selection and graduated penalty enforcement for the gossip layer. +PeerReputation extends the re-exported `ClientState`'s binary ban/trust model with numeric penalty tracking, RTT-based latency scoring, and AS-level grouping. PenaltyReason enumerates the reasons a peer can be penalized. Together they provide latency-aware peer selection and graduated penalty enforcement for the gossip layer. ## Specification diff --git a/docs/requirements/domains/crate_structure/NORMATIVE.md b/docs/requirements/domains/crate_structure/NORMATIVE.md index c3f6f09..d0f3545 100644 --- a/docs/requirements/domains/crate_structure/NORMATIVE.md +++ b/docs/requirements/domains/crate_structure/NORMATIVE.md @@ -8,7 +8,7 @@ ### STR-001: Cargo.toml Dependencies and Feature Gates -Cargo.toml MUST include chia-protocol 0.26, chia-sdk-client 0.28, chia-ssl 0.26, chia-traits 0.26, tokio, serde, bincode, serde_json, tracing, thiserror, rand, lru, siphasher, minisketch-rs. Feature gates: native-tls, rustls, relay, erlay, compact-blocks. +Cargo.toml MUST include dig-peer-protocol 0.4 (default-features = false), chia-protocol 0.26, chia-traits 0.26, chia-sha2 0.26, chia_streamable_macro 0.26, tokio, serde, bincode, serde_json, tracing, thiserror, rand, lru, siphasher, minisketch-rs. Feature gates: native-tls, rustls, relay, erlay, compact-blocks. **Spec reference:** SPEC Section 1.2 (Crate Dependencies), Section 10.3 (Feature Flags), Section 10.4 (Cargo.toml Dependencies) diff --git a/docs/requirements/domains/crate_structure/TRACKING.yaml b/docs/requirements/domains/crate_structure/TRACKING.yaml index c80590a..6a54948 100644 --- a/docs/requirements/domains/crate_structure/TRACKING.yaml +++ b/docs/requirements/domains/crate_structure/TRACKING.yaml @@ -11,7 +11,7 @@ items: - tests/str_001_tests.rs notes: > `minisketch-rs` is not declared: its build depends on `bindgen` (`links = "clang"`), - which collides in Cargo’s resolver with `chia-sdk-client`’s optional `rustls` → `aws-lc-rs (bindgen)` edge even when only `native-tls` is enabled. The `erlay` + which collides in Cargo’s resolver with the transitive `chia-sdk-client`’s optional `rustls` → `aws-lc-rs (bindgen)` edge even when only `native-tls` is enabled. The `erlay` feature remains an empty gate for cfg(feature = "erlay") until a pure-Rust sketch or upstream graph fix lands. - id: STR-002 diff --git a/docs/requirements/domains/crate_structure/specs/STR-001.md b/docs/requirements/domains/crate_structure/specs/STR-001.md index 6b0a3ef..8433948 100644 --- a/docs/requirements/domains/crate_structure/specs/STR-001.md +++ b/docs/requirements/domains/crate_structure/specs/STR-001.md @@ -7,7 +7,7 @@ ## Summary -The crate's `Cargo.toml` must declare all required dependencies at the correct versions and define all feature gates specified by the SPEC. This ensures the crate builds against the correct Chia ecosystem versions and exposes the proper conditional compilation flags for optional functionality (TLS backend selection, relay support, ERLAY transaction relay, compact block relay). +The crate's `Cargo.toml` must declare all required dependencies at the correct versions and define all feature gates specified by the SPEC. This ensures the crate builds against the correct `dig-peer-protocol` and Chia ecosystem versions and exposes the proper conditional compilation flags for optional functionality (TLS backend selection, relay support, ERLAY transaction relay, compact block relay). ## Specification @@ -17,11 +17,18 @@ The `[dependencies]` section of `Cargo.toml` MUST include: ```toml [dependencies] -# Chia crates (direct reuse) +# The DIG peer wire — the single dependency through which the Chia crates are reached. +# `default-features = false` so the TLS backend is selected exclusively by our own +# `native-tls` / `rustls` features. +dig-peer-protocol = { version = "0.4", default-features = false } + +# Named directly ONLY because `chia_streamable_macro` reads Cargo.toml for them and +# generates `chia_protocol::` paths at compile time. Code imports these types through +# `dig-peer-protocol`, never from these entries. chia-protocol = "0.26" -chia-sdk-client = { version = "0.28", features = ["native-tls"] } -chia-ssl = "0.26" chia-traits = "0.26" +chia-sha2 = "0.26" +chia_streamable_macro = "0.26" # Async runtime tokio = { version = "1", features = ["full"] } @@ -50,8 +57,8 @@ The `[features]` section MUST include: ```toml [features] default = ["native-tls", "relay", "erlay", "compact-blocks"] -native-tls = ["chia-sdk-client/native-tls"] -rustls = ["chia-sdk-client/rustls"] +native-tls = ["dig-peer-protocol/native-tls", "dep:native-tls", "dep:tokio-native-tls"] +rustls = ["dig-peer-protocol/rustls", "dep:rustls", "dep:tokio-rustls", "dep:rustls-pemfile"] relay = [] erlay = ["minisketch-rs"] compact-blocks = ["siphasher"] @@ -61,10 +68,11 @@ compact-blocks = ["siphasher"] | Dependency | Purpose | |-----------|---------| -| `chia-protocol` | Wire protocol types (Handshake, Message, NodeType, etc.) | -| `chia-sdk-client` | Peer connections, rate limiting, TLS, DNS lookup | -| `chia-ssl` | TLS certificate generation and loading | -| `chia-traits` | Streamable serialization trait | +| `dig-peer-protocol` | The peer link (`DigLink`), the `DigMessage` envelope, the DIG opcodes, opcode-keyed rate limiting, the introducer wire types, and the re-exported `chia-protocol` / `chia-sdk-client` / `chia-ssl` / `chia-traits` surface | +| `chia-protocol` | Full-node wire structs not re-exported by `dig-peer-protocol`, and a manifest entry `chia_streamable_macro` requires | +| `chia-traits` | A manifest entry `chia_streamable_macro` requires | +| `chia-sha2` | A manifest entry `chia_streamable_macro` requires | +| `chia_streamable_macro` | Derives the wire encoding for this crate's own `#[streamable]` structs | | `tokio` | Async runtime, timers, tasks, channels | | `serde` / `bincode` | Serialization for relay protocol and address manager persistence | | `serde_json` | JSON serialization for relay and introducer messages | @@ -77,16 +85,16 @@ compact-blocks = ["siphasher"] ## Acceptance Criteria -- [ ] `Cargo.toml` declares `chia-protocol = "0.26"` -- [ ] `Cargo.toml` declares `chia-sdk-client` at version `"0.28"` with `native-tls` feature -- [ ] `Cargo.toml` declares `chia-ssl = "0.26"` -- [ ] `Cargo.toml` declares `chia-traits = "0.26"` +- [ ] `Cargo.toml` declares `dig-peer-protocol` at version `"0.4"` with `default-features = false` +- [ ] `Cargo.toml` does NOT declare `chia-sdk-client` or `chia-ssl` as direct dependencies — both are reached through `dig-peer-protocol` +- [ ] `Cargo.toml` declares `chia-protocol = "0.26"`, `chia-traits = "0.26"`, `chia-sha2 = "0.26"` and `chia_streamable_macro = "0.26"` +- [ ] `[patch.crates-io]` contains `native-tls` and nothing else — no Chia crate is vendored or patched - [ ] `Cargo.toml` declares `tokio` with `features = ["full"]` - [ ] `Cargo.toml` declares `serde` with `features = ["derive"]` - [ ] `Cargo.toml` declares `bincode`, `serde_json`, `tracing`, `thiserror`, `rand`, `lru` - [ ] `Cargo.toml` declares `siphasher` and `minisketch-rs` -- [ ] Feature `native-tls` forwards to `chia-sdk-client/native-tls` -- [ ] Feature `rustls` forwards to `chia-sdk-client/rustls` +- [ ] Feature `native-tls` forwards to `dig-peer-protocol/native-tls` +- [ ] Feature `rustls` forwards to `dig-peer-protocol/rustls` - [ ] Feature `relay` is defined (no dependencies) - [ ] Feature `erlay` depends on `minisketch-rs` - [ ] Feature `compact-blocks` depends on `siphasher` @@ -109,16 +117,15 @@ compact-blocks = ["siphasher"] | Test | Type | Description | Expected Result | |------|------|-------------|-----------------| +| test_cargo_toml_has_dig_peer_protocol | Unit | Parse Cargo.toml and check dig-peer-protocol version and default-features | Version is "0.4" with default-features = false | | test_cargo_toml_has_chia_protocol | Unit | Parse Cargo.toml and check chia-protocol version | Version is "0.26" | -| test_cargo_toml_has_chia_sdk_client | Unit | Parse Cargo.toml and check chia-sdk-client version and features | Version is "0.28" with native-tls | -| test_cargo_toml_has_chia_ssl | Unit | Parse Cargo.toml and check chia-ssl version | Version is "0.26" | | test_cargo_toml_has_chia_traits | Unit | Parse Cargo.toml and check chia-traits version | Version is "0.26" | | test_cargo_toml_has_tokio | Unit | Parse Cargo.toml and check tokio with full features | tokio present with features = ["full"] | | test_cargo_toml_has_serde_deps | Unit | Parse Cargo.toml and check serde, bincode, serde_json | All three present with correct features | | test_cargo_toml_has_utility_deps | Unit | Parse Cargo.toml and check tracing, thiserror, rand, lru | All four present | | test_cargo_toml_has_optional_deps | Unit | Parse Cargo.toml and check siphasher, minisketch-rs | Both present | -| test_feature_native_tls | Unit | Verify native-tls feature forwards to chia-sdk-client | Feature defined correctly | -| test_feature_rustls | Unit | Verify rustls feature forwards to chia-sdk-client | Feature defined correctly | +| test_feature_native_tls | Unit | Verify native-tls feature forwards to dig-peer-protocol | Feature defined correctly | +| test_feature_rustls | Unit | Verify rustls feature forwards to dig-peer-protocol | Feature defined correctly | | test_feature_relay | Unit | Verify relay feature is defined | Feature defined with no deps | | test_feature_erlay | Unit | Verify erlay feature depends on minisketch-rs | Feature defined with minisketch-rs dep | | test_feature_compact_blocks | Unit | Verify compact-blocks feature depends on siphasher | Feature defined with siphasher dep | diff --git a/docs/requirements/domains/crate_structure/specs/STR-002.md b/docs/requirements/domains/crate_structure/specs/STR-002.md index 3c0009b..257fb1a 100644 --- a/docs/requirements/domains/crate_structure/specs/STR-002.md +++ b/docs/requirements/domains/crate_structure/specs/STR-002.md @@ -79,7 +79,7 @@ Each module directory MUST contain a `mod.rs` that declares and re-exports its s | `constants.rs` | DIG-specific constants and ported Chia Python constants | | `error.rs` | Unified error type wrapping ClientError with gossip-specific variants | | `service/` | GossipService construction/lifecycle and GossipHandle for runtime interaction | -| `connection/` | Inbound TCP/TLS listener (outbound uses chia-sdk-client directly) | +| `connection/` | Inbound TCP/TLS listener (outbound drives the handshake itself, then upgrades to DigLink) | | `discovery/` | Address manager, node discovery loop, introducer client, vetted peers | | `relay/` | Relay WebSocket client, relay service lifecycle, relay-specific types | | `gossip/` | Plumtree, compact blocks, ERLAY, priority lanes, backpressure, dedup | diff --git a/docs/requirements/domains/crate_structure/specs/STR-003.md b/docs/requirements/domains/crate_structure/specs/STR-003.md index a4ef39e..de9a0b7 100644 --- a/docs/requirements/domains/crate_structure/specs/STR-003.md +++ b/docs/requirements/domains/crate_structure/specs/STR-003.md @@ -33,13 +33,16 @@ pub use chia_protocol::{ // Introducer query 63/64 + registration 218/219 — wire structs live in-tree (crates.io `chia-protocol` lacks 218/219 bodies). pub use discovery::introducer_wire::{RequestPeersIntroducer, RespondPeersIntroducer}; pub use discovery::introducer_register_wire::{RegisterPeer, RegisterAck}; -pub use chia_sdk_client::{ - Peer, PeerOptions, Client, ClientState, Network, - RateLimiter, RateLimits, RateLimit, V2_RATE_LIMITS, +pub use dig_peer_protocol::{ + Bytes, DigLink, DigMessage, LinkError, LinkOptions, + NodeType, ProtocolMessageTypes, + OpcodeRateLimiter, OpcodeRateLimits, + // The Chia surface, re-exported rather than depended on directly. + Client, ClientState, Network, + RateLimits, RateLimit, V2_RATE_LIMITS, ClientError, load_ssl_cert, + ChiaCertificate, Streamable, }; -pub use chia_ssl::ChiaCertificate; -pub use chia_traits::Streamable; ``` ### DIG-specific Type Re-exports @@ -101,12 +104,13 @@ use dig_gossip::{ - [ ] `lib.rs` re-exports all full node protocol messages (`NewPeak`, `NewTransaction`, `RequestTransaction`, `RespondTransaction`, `RequestBlock`, `RespondBlock`, `RejectBlock`, `RequestBlocks`, `RespondBlocks`, `RejectBlocks`, `NewUnfinishedBlock`, `RequestUnfinishedBlock`, `RespondUnfinishedBlock`, `RequestMempoolTransactions`) - [ ] `lib.rs` re-exports `RequestPeers`, `RespondPeers` from `chia-protocol` - [ ] `lib.rs` re-exports `RequestPeersIntroducer`, `RespondPeersIntroducer` from `discovery::introducer_wire` (DSC-004 wire bodies; enum variants remain on `ProtocolMessageTypes`) -- [ ] `lib.rs` re-exports `RegisterPeer`, `RegisterAck` from `discovery::introducer_register_wire` (DSC-005; vendored `chia-protocol` adds matching `ProtocolMessageTypes` discriminants) +- [ ] `lib.rs` re-exports `RegisterPeer`, `RegisterAck` from `discovery::introducer_register_wire` (DSC-005; the 218/219 opcodes travel as raw `DigMessage::msg_type` bytes, so no Chia enum variant is needed) - [ ] `lib.rs` re-exports `PeerRegistration` alongside `IntroducerClient` from `discovery::introducer_client` - [ ] `lib.rs` re-exports `SpendBundle`, `FullBlock`, `TimestampedPeerInfo`, `ChiaProtocolMessage` -- [ ] `lib.rs` re-exports `Peer`, `PeerOptions`, `Client`, `ClientState`, `Network` from `chia-sdk-client` -- [ ] `lib.rs` re-exports `RateLimiter`, `RateLimits`, `RateLimit`, `V2_RATE_LIMITS` from `chia-sdk-client` -- [ ] `lib.rs` re-exports `ClientError`, `load_ssl_cert` from `chia-sdk-client` +- [ ] `lib.rs` re-exports `DigLink`, `DigMessage`, `LinkOptions`, `LinkError`, `Bytes`, `NodeType`, `ProtocolMessageTypes` from `dig-peer-protocol` +- [ ] `lib.rs` re-exports `Client`, `ClientState`, `Network` from `dig-peer-protocol` +- [ ] `lib.rs` re-exports `OpcodeRateLimiter`, `OpcodeRateLimits`, `RateLimits`, `RateLimit`, `V2_RATE_LIMITS` from `dig-peer-protocol` +- [ ] `lib.rs` re-exports `ClientError`, `load_ssl_cert`, `ChiaCertificate`, `Streamable` from `dig-peer-protocol` - [ ] `lib.rs` re-exports `ChiaCertificate` from `chia-ssl` - [ ] `lib.rs` re-exports `Streamable` from `chia-traits` - [ ] `lib.rs` re-exports `PeerId`, `PeerInfo`, `PeerConnection` from `types::peer` @@ -128,7 +132,7 @@ use dig_gossip::{ - Edge cases: - Re-exports from `relay` module should be gated with `#[cfg(feature = "relay")]` - The `constants::*` glob export must not conflict with re-exported names from Chia crates - - If `chia-sdk-client` changes its public API between versions, re-exports must be updated accordingly + - If `dig-peer-protocol` changes its public API between versions, re-exports must be updated accordingly ## Verification diff --git a/docs/requirements/domains/crate_structure/specs/STR-004.md b/docs/requirements/domains/crate_structure/specs/STR-004.md index 0ef6f54..06071bc 100644 --- a/docs/requirements/domains/crate_structure/specs/STR-004.md +++ b/docs/requirements/domains/crate_structure/specs/STR-004.md @@ -16,8 +16,8 @@ Feature flags must match the SPEC Section 10.3 definitions exactly. Each feature ```toml [features] default = ["native-tls", "relay", "erlay", "compact-blocks", "dandelion"] -native-tls = ["chia-sdk-client/native-tls"] -rustls = ["chia-sdk-client/rustls"] +native-tls = ["dig-peer-protocol/native-tls", "dep:native-tls", "dep:tokio-native-tls"] +rustls = ["dig-peer-protocol/rustls", "dep:rustls", "dep:tokio-rustls", "dep:rustls-pemfile"] relay = [] erlay = ["minisketch-rs"] compact-blocks = ["siphasher"] @@ -29,8 +29,8 @@ tor = ["arti-client", "tokio-socks"] | Feature | Dependency | Conditional Compilation Effect | |---------|-----------|-------------------------------| -| `native-tls` | Forwards to `chia-sdk-client/native-tls` | Enables native TLS backend via `create_native_tls_connector()` | -| `rustls` | Forwards to `chia-sdk-client/rustls` | Enables rustls TLS backend via `create_rustls_connector()` | +| `native-tls` | Forwards to `dig-peer-protocol/native-tls`, plus `native-tls` / `tokio-native-tls` | Enables the native TLS backend via `create_native_tls_connector()` for outbound, and the OpenSSL inbound acceptor | +| `rustls` | Forwards to `dig-peer-protocol/rustls`, plus `rustls` / `tokio-rustls` / `rustls-pemfile` | Enables the rustls TLS backend via `create_rustls_connector()` for outbound, and the rustls inbound acceptor (CON-009) | | `relay` | None | Enables `relay/` module compilation and relay-related code paths | | `erlay` | `minisketch-rs` | Enables `gossip/erlay.rs` and ERLAY reconciliation code paths | | `compact-blocks` | `siphasher` | Enables `gossip/compact_block.rs` and compact block code paths | @@ -68,19 +68,19 @@ pub mod tor; ```rust #[cfg(feature = "native-tls")] fn create_tls_connector(cert: &[u8], key: &[u8]) -> Result<...> { - chia_sdk_client::create_native_tls_connector(cert, key) + dig_peer_protocol::create_native_tls_connector(cert) } #[cfg(feature = "rustls")] fn create_tls_connector(cert: &[u8], key: &[u8]) -> Result<...> { - chia_sdk_client::create_rustls_connector(cert, key) + dig_peer_protocol::create_rustls_connector(cert) } ``` ## Acceptance Criteria -- [ ] Feature `native-tls` is defined and forwards to `chia-sdk-client/native-tls` -- [ ] Feature `rustls` is defined and forwards to `chia-sdk-client/rustls` +- [ ] Feature `native-tls` is defined and forwards to `dig-peer-protocol/native-tls` +- [ ] Feature `rustls` is defined and forwards to `dig-peer-protocol/rustls` - [ ] Feature `relay` is defined with no additional dependencies - [ ] Feature `erlay` is defined and depends on `minisketch-rs` - [ ] Feature `compact-blocks` is defined and depends on `siphasher` diff --git a/docs/requirements/domains/crate_structure/specs/STR-005.md b/docs/requirements/domains/crate_structure/specs/STR-005.md index 191fe8d..7dbe8b2 100644 --- a/docs/requirements/domains/crate_structure/specs/STR-005.md +++ b/docs/requirements/domains/crate_structure/specs/STR-005.md @@ -81,7 +81,7 @@ pub fn test_gossip_config(temp_dir: &Path) -> GossipConfig { gossip_fanout: 3, max_seen_messages: 1000, peers_file_path: temp_dir.join("peers.dat").to_path_buf(), - peer_options: PeerOptions::default(), + peer_options: LinkOptions::default(), } } diff --git a/docs/requirements/domains/discovery/NORMATIVE.md b/docs/requirements/domains/discovery/NORMATIVE.md index 53c7596..d5135d3 100644 --- a/docs/requirements/domains/discovery/NORMATIVE.md +++ b/docs/requirements/domains/discovery/NORMATIVE.md @@ -20,7 +20,7 @@ AddressManager MUST support save()/load() to a peers file path. Binary serializa ### DSC-003: DNS Seeding -DNS seeding MUST use chia-sdk-client::Network::lookup_all() with configurable timeout and batching. DNS introducers MUST be configurable via GossipConfig. +DNS seeding MUST use the re-exported Network::lookup_all() with configurable timeout and batching. DNS introducers MUST be configurable via GossipConfig. **Spec reference:** SPEC Section 6.2 (DNS Seeding) diff --git a/docs/requirements/domains/discovery/TRACKING.yaml b/docs/requirements/domains/discovery/TRACKING.yaml index 753e078..966caed 100644 --- a/docs/requirements/domains/discovery/TRACKING.yaml +++ b/docs/requirements/domains/discovery/TRACKING.yaml @@ -27,7 +27,7 @@ items: section: "6.2" summary: "DNS seeding via Network::lookup_all()" status: verified - spec_ref: "SPEC.md#62-dns-seeding-reuses-chia-sdk-clientnetwork" + spec_ref: "SPEC.md#62-dns-seeding-reuses-the-re-exported-network" tests: - tests/dsc_003_tests.rs notes: > @@ -52,7 +52,7 @@ items: tests: - tests/dsc_005_tests.rs notes: > - vendor/chia-protocol: ProtocolMessageTypes RegisterPeer=218 / RegisterAck=219 so Message decode works; + opcodes 218/219 travel as raw DigMessage msg_type bytes, so no Chia enum variant is needed; introducer_register_wire.rs: streamable bodies; IntroducerClient::register_with_introducer; GossipHandle::register_with_introducer -> RegisterAck; empty introducer.endpoint -> InvalidConfig; STR-005 wss_full_node::spawn_one_shot_introducer_register; DigMessageType 218/219 + inbound dig_wire limits. diff --git a/docs/requirements/domains/discovery/specs/DSC-003.md b/docs/requirements/domains/discovery/specs/DSC-003.md index 8eb577d..a04e9de 100644 --- a/docs/requirements/domains/discovery/specs/DSC-003.md +++ b/docs/requirements/domains/discovery/specs/DSC-003.md @@ -7,14 +7,14 @@ ## Summary -DNS seeding is the first method used to discover initial peers when the address manager is empty. It reuses `chia-sdk-client::Network::lookup_all()` which handles DNS resolution with timeout and batching. The DIG network configures its own DNS introducer hostnames rather than Chia's defaults. +DNS seeding is the first method used to discover initial peers when the address manager is empty. It reuses the re-exported `Network::lookup_all()` which handles DNS resolution with timeout and batching. The DIG network configures its own DNS introducer hostnames rather than Chia's defaults. ## Specification ### Network Configuration ```rust -use chia_sdk_client::Network; +use dig_peer_protocol::Network; use std::time::Duration; /// Create a Network configured for DIG DNS seeding. @@ -38,7 +38,7 @@ pub async fn dns_seed( timeout: Duration, batch_size: usize, ) -> Vec { - // Delegates to chia-sdk-client::Network::lookup_all() + // Delegates to the re-exported Network::lookup_all() network.lookup_all(timeout, batch_size).await } ``` @@ -79,7 +79,7 @@ address_manager.add_to_new_table(×tamped, &self_peer_info, 0).await; ## Acceptance Criteria -- [ ] DNS seeding uses `chia-sdk-client::Network::lookup_all()` -- no custom DNS implementation +- [ ] DNS seeding uses the re-exported `Network::lookup_all()` -- no custom DNS implementation - [ ] DNS introducer hostnames are configurable via `GossipConfig` - [ ] DNS timeout is configurable (default 30 seconds) - [ ] DNS batch size is configurable (default 2) @@ -90,7 +90,7 @@ address_manager.add_to_new_table(×tamped, &self_peer_info, 0).await; ## Implementation Notes - Primary file: `src/discovery/node_discovery.rs` (DNS seeding is part of the discovery loop) -- Dependencies: `chia-sdk-client` (`Network`, `lookup_all()`), `chia-protocol` (`TimestampedPeerInfo`) +- Dependencies: `dig-peer-protocol` (`Network`, `lookup_all()`), `chia-protocol` (`TimestampedPeerInfo`) - Edge cases: - All DNS introducers may be unreachable: log warning, return empty list - DNS may return duplicate addresses: the address manager handles deduplication via bucket assignment diff --git a/docs/requirements/domains/discovery/specs/DSC-004.md b/docs/requirements/domains/discovery/specs/DSC-004.md index e5922fb..5f7667d 100644 --- a/docs/requirements/domains/discovery/specs/DSC-004.md +++ b/docs/requirements/domains/discovery/specs/DSC-004.md @@ -15,7 +15,7 @@ The introducer query allows a node to obtain initial peers from a known introduc ```rust use chia_protocol::{RequestPeersIntroducer, RespondPeersIntroducer, TimestampedPeerInfo}; -use chia_sdk_client::Peer; +use dig_peer_protocol::Peer; /// Query an introducer for peers. /// @@ -75,7 +75,7 @@ pub struct HandshakeConfig { ```rust /// Connect to an introducer server and perform handshake. -/// Uses chia-sdk-client's connect_peer() or Peer::connect() with +/// Drives the DIG handshake over a wss:// dial, then upgrades to DigLink, with /// appropriate TLS configuration. async fn connect_to_introducer( addr: &str, @@ -98,13 +98,14 @@ Uses `chia-protocol` types directly: - [ ] `RespondPeersIntroducer` is received and peer_list extracted - [ ] Connection is closed after receiving response - [ ] Timeout is enforced on the entire query operation -- [ ] Connection failures return `GossipError` (do not panic) +- [ ] Connection failures propagate as `GossipError::LinkError` (the dial is `DigLink`'s, so the + typed connection-level error is `LinkError`; `ClientError` remains the handshake-policy error) - [ ] Uses `chia-protocol::RequestPeersIntroducer` and `RespondPeersIntroducer` directly ## Implementation Notes - Primary file: `src/discovery/introducer_client.rs` -- Dependencies: `chia-protocol` (`RequestPeersIntroducer`, `RespondPeersIntroducer`, `TimestampedPeerInfo`, `NodeType`, `Handshake`), `chia-sdk-client` (`Peer`, `connect_peer`, TLS utilities) +- Dependencies: `chia-protocol` (`RequestPeersIntroducer`, `RespondPeersIntroducer`, `TimestampedPeerInfo`, `NodeType`, `Handshake`), `dig-peer-protocol` (`DigLink`, TLS utilities) - Edge cases: - Introducer may be unreachable: return error, discovery loop handles retry with backoff - Introducer may return empty peer list: return Ok(empty vec), not an error @@ -119,7 +120,7 @@ Uses `chia-protocol` types directly: | test_query_introducer_success | Integration | Mock introducer returns peer list | Peers returned correctly | | test_query_introducer_empty | Integration | Mock introducer returns empty list | Returns Ok(empty vec) | | test_query_introducer_timeout | Integration | Mock introducer never responds | Returns GossipError after timeout | -| test_query_introducer_connect_fail | Integration | Invalid introducer address | Returns GossipError | +| test_query_introducer_connect_fail | Integration | Invalid introducer address | Returns GossipError::LinkError | | test_query_introducer_handshake_fail | Integration | Mock rejects handshake | Returns GossipError | | test_query_uses_chia_protocol_types | Unit | Verify RequestPeersIntroducer sent | Correct message type on wire | | test_query_closes_connection | Integration | Query completes, verify connection closed | Peer dropped after response | diff --git a/docs/requirements/domains/discovery/specs/DSC-005.md b/docs/requirements/domains/discovery/specs/DSC-005.md index 21b4566..dbf1946 100644 --- a/docs/requirements/domains/discovery/specs/DSC-005.md +++ b/docs/requirements/domains/discovery/specs/DSC-005.md @@ -102,7 +102,7 @@ pub struct PeerRegistration { ## Implementation Notes - Primary file: `src/discovery/introducer_client.rs` -- Dependencies: `chia-protocol` (`NodeType`, `Handshake`), `chia-sdk-client` (`Peer`, TLS), `chia-traits` (`Streamable`) +- Dependencies: `chia-protocol` (`NodeType`, `Handshake`), `dig-peer-protocol` (`DigLink`, TLS), `chia-traits` (`Streamable`) - Edge cases: - Introducer may reject registration (success=false): return the ack, let caller decide - Introducer may not support registration (old version): timeout or error @@ -117,7 +117,7 @@ pub struct PeerRegistration { | test_register_success | Integration | Mock introducer accepts registration | Returns RegisterAck { success: true } | | test_register_rejected | Integration | Mock introducer rejects registration | Returns RegisterAck { success: false } | | test_register_timeout | Integration | Mock introducer never responds | Returns GossipError after timeout | -| test_register_connect_fail | Integration | Invalid introducer address | Returns GossipError | +| test_register_introducer_connect_fail | Integration | Invalid introducer address | Returns GossipError::LinkError | | test_register_sends_correct_fields | Unit | Verify RegisterPeer has ip, port, node_type | All fields serialized correctly | | test_register_closes_connection | Integration | Registration completes, verify closed | Peer dropped after ack | | test_register_message_type_range | Unit | Verify message type ID is >= 200 | DIG extension range | diff --git a/docs/requirements/domains/discovery/specs/DSC-007.md b/docs/requirements/domains/discovery/specs/DSC-007.md index 6f71461..4e700f7 100644 --- a/docs/requirements/domains/discovery/specs/DSC-007.md +++ b/docs/requirements/domains/discovery/specs/DSC-007.md @@ -15,7 +15,7 @@ Peer exchange enables connected nodes to share known peers with each other via ` ```rust use chia_protocol::{RequestPeers, RespondPeers, TimestampedPeerInfo}; -use chia_sdk_client::Peer; +use dig_peer_protocol::Peer; /// Request peers from a newly connected outbound peer. /// Called immediately after a successful outbound connection. @@ -87,13 +87,13 @@ pub async fn handle_request_peers( ## Implementation Notes - Primary file: `src/discovery/node_discovery.rs` -- Dependencies: `chia-protocol` (`RequestPeers`, `RespondPeers`, `TimestampedPeerInfo`), `chia-sdk-client` (`Peer`), AddressManager (DSC-001) +- Dependencies: `chia-protocol` (`RequestPeers`, `RespondPeers`, `TimestampedPeerInfo`), `dig-peer-protocol` (`DigLink`), AddressManager (DSC-001) - Edge cases: - Peer may respond with an empty peer list: handle gracefully, do not treat as error - Peer may disconnect before responding: `request_infallible` returns error, logged and ignored - Peer list may contain duplicates or already-known peers: address manager handles dedup - Should not request peers from inbound connections (only outbound) to prevent information leakage about our address manager state - - Rate limiting applies: `Peer` already enforces `V2_RATE_LIMITS` on RequestPeers/RespondPeers + - Rate limiting applies: `DigLink` already enforces `V2_RATE_LIMITS` on RequestPeers/RespondPeers ## Test Plan diff --git a/docs/requirements/domains/erlay/specs/ERL-007.md b/docs/requirements/domains/erlay/specs/ERL-007.md index ec94b26..fbe2d11 100644 --- a/docs/requirements/domains/erlay/specs/ERL-007.md +++ b/docs/requirements/domains/erlay/specs/ERL-007.md @@ -18,8 +18,8 @@ The `PeerConnection.is_outbound` field determines whether a peer is outbound (we ```rust /// Extended peer connection state for the gossip layer. pub struct PeerConnection { - /// The underlying chia-sdk-client Peer connection. - pub peer: Peer, + /// The underlying DigLink connection. + pub peer: DigLink, /// Unique peer identifier. pub peer_id: PeerId, /// Whether we initiated this connection (outbound) or they connected to us (inbound). diff --git a/docs/requirements/domains/privacy/specs/PRV-007.md b/docs/requirements/domains/privacy/specs/PRV-007.md index 13728de..90fcb42 100644 --- a/docs/requirements/domains/privacy/specs/PRV-007.md +++ b/docs/requirements/domains/privacy/specs/PRV-007.md @@ -123,7 +123,7 @@ The address manager MUST track peers by `IP:port` (socket address), NOT by PeerI - Primary file: `src/privacy/peer_id_rotation.rs` - Dependencies: - `chia-ssl` (`ChiaCertificate::generate()`) - - `chia-sdk-client` (`create_native_tls_connector()` / `create_rustls_connector()` for rebuilding TLS with new cert) + - `dig-peer-protocol` (`create_native_tls_connector()` / `create_rustls_connector()` for rebuilding TLS with new cert) - `tokio` (async sleep, task spawning) - `tracing` (structured logging) - Edge cases: diff --git a/docs/requirements/domains/privacy/specs/PRV-008.md b/docs/requirements/domains/privacy/specs/PRV-008.md index c895010..d3c6643 100644 --- a/docs/requirements/domains/privacy/specs/PRV-008.md +++ b/docs/requirements/domains/privacy/specs/PRV-008.md @@ -39,7 +39,7 @@ When rotation is disabled, the node MUST: ```rust use chia_ssl::ChiaCertificate; -use chia_sdk_client::load_ssl_cert; +use dig_peer_protocol::load_ssl_cert; /// Initialize TLS identity based on rotation configuration. /// @@ -109,7 +109,7 @@ When rotation is disabled, the certificate MUST be persisted to disk: - Primary file: `src/privacy/peer_id_rotation.rs` - Dependencies: - `chia-ssl` (`ChiaCertificate::generate()`, certificate save/load) - - `chia-sdk-client` (`load_ssl_cert()`) + - `dig-peer-protocol` (`load_ssl_cert()`) - Edge cases: - First run with no existing certificate: must generate and persist one, even with rotation disabled - Certificate file corruption: should fail loudly at startup rather than silently generating a new identity diff --git a/docs/requirements/domains/privacy/specs/PRV-010.md b/docs/requirements/domains/privacy/specs/PRV-010.md index 4fd32b0..3fa0e67 100644 --- a/docs/requirements/domains/privacy/specs/PRV-010.md +++ b/docs/requirements/domains/privacy/specs/PRV-010.md @@ -220,7 +220,7 @@ Mutual TLS (chia-ssl certificates) MUST still be required for connections over T - `tokio-socks` (SOCKS5 proxy client, behind `tor` feature gate) - `tokio-tungstenite` (WebSocket upgrade over TLS over Tor) - `chia-ssl` (mTLS certificates presented through Tor tunnel) - - `chia-sdk-client` (TLS connector creation) + - `dig-peer-protocol` (TLS connector creation) - `tracing` (structured logging) - Edge cases: - Tor daemon not running: SOCKS5 connection fails with a clear error; fall back to direct/relay if `prefer_tor = false` diff --git a/docs/requirements/domains/relay/specs/RLY-007.md b/docs/requirements/domains/relay/specs/RLY-007.md index 8e919ff..a3eaa98 100644 --- a/docs/requirements/domains/relay/specs/RLY-007.md +++ b/docs/requirements/domains/relay/specs/RLY-007.md @@ -195,11 +195,11 @@ impl RelayClient { peer_id: PeerId, external_addr: SocketAddr, ) -> Result { - // Use chia-sdk-client's connect_peer() for TLS handshake + // Drive the DIG handshake over the wss:// dial, then upgrade to DigLink let timeout_duration = Duration::from_secs(10); let peer = tokio::time::timeout( timeout_duration, - connect_peer(external_addr, self.tls_connector.clone()), + connect_outbound(external_addr, self.tls_connector.clone()), ) .await .map_err(|_| GossipError::RelayError("hole punch connect timeout".into()))? @@ -249,13 +249,13 @@ impl GossipService { - [ ] On failure: relay path is kept (no change to message delivery) - [ ] On failure: retry is scheduled after `HOLE_PUNCH_RETRY_SECS` (300 seconds) - [ ] Hole punch state is tracked per peer pair (`HolePunchState` enum) -- [ ] Direct connection uses full TLS handshake via `chia-sdk-client::connect_peer()` +- [ ] Direct connection uses full TLS handshake via the outbound connect flow (handshake over the raw WebSocket, then `DigLink`) - [ ] Connection attempt has a timeout (does not block indefinitely) ## Implementation Notes - Primary files: `src/relay/relay_client.rs`, `src/relay/relay_service.rs`, `src/relay/relay_types.rs`, `src/service/gossip_service.rs` -- Dependencies: `chia-sdk-client::connect_peer`, `tokio::time`, `tokio::net::TcpStream` +- Dependencies: `the outbound connect flow`, `tokio::time`, `tokio::net::TcpStream` - Edge cases: - Both peers may initiate hole punch simultaneously for the same pair; the relay should deduplicate - The external address observed by the relay may differ from the peer's actual externally reachable address (e.g., behind symmetric NAT); hole punch will fail in this case diff --git a/docs/requirements/domains/relay/specs/RLY-008.md b/docs/requirements/domains/relay/specs/RLY-008.md index 1f7425b..2cf775c 100644 --- a/docs/requirements/domains/relay/specs/RLY-008.md +++ b/docs/requirements/domains/relay/specs/RLY-008.md @@ -17,7 +17,7 @@ The gossip layer must implement transport selection logic that determines whethe /// Transport used for communicating with a specific peer. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PeerTransport { - /// Direct P2P via TCP+TLS+WebSocket (chia-sdk-client::Peer). + /// Direct P2P via TCP+TLS+WebSocket (dig_peer_protocol::DigLink). Direct, /// Via relay server WebSocket (RelayClient). Relay, @@ -124,7 +124,7 @@ impl TransportSelector { peer_id: &PeerId, addr: SocketAddr, ) -> Result { - // Uses chia-sdk-client connect_peer() under the hood + // Drives the DIG handshake, then upgrades to DigLink // Connection attempt with timeout handled by the connection layer self.peer_transports.insert(*peer_id, PeerTransport::Direct); Ok(PeerTransport::Direct) @@ -239,7 +239,7 @@ impl TransportSelector { ## Implementation Notes - Primary files: `src/relay/relay_service.rs`, `src/service/gossip_service.rs`, `src/relay/relay_types.rs` -- Dependencies: `chia-sdk-client::connect_peer`, `tokio::time` +- Dependencies: `the outbound connect flow`, `tokio::time` - Edge cases: - A peer may be reachable via both direct and relay simultaneously (during migration); only one should be active - If `prefer_relay = true` but the relay goes down, the system should attempt direct connections for all relay peers diff --git a/docs/resources/SPEC.md b/docs/resources/SPEC.md index 36082cf..ae5c228 100644 --- a/docs/resources/SPEC.md +++ b/docs/resources/SPEC.md @@ -8,19 +8,19 @@ `dig-gossip` is a self-contained Rust crate that manages **peer-to-peer networking and gossip** for the DIG Network L2 blockchain. It handles peer discovery, connection management, message routing, and protocol-level communication between full nodes. The crate accepts application-level payloads (blocks, transactions, attestations) as opaque typed inputs and delivers them to connected peers via a Chia-compatible gossip protocol. -**This crate maximally reuses the Chia Rust ecosystem** rather than reimplementing functionality. The wire protocol types (`Handshake`, `Message`, `NodeType`, `ProtocolMessageTypes`), peer connection management (`Peer`, `Client`), rate limiting (`RateLimiter`, `RateLimits`), TLS handling, and DNS resolution are all provided by `chia-protocol` and `chia-sdk-client`. `dig-gossip` builds on top of these, adding: relay fallback, introducer registration, address manager persistence, gossip fanout, and message deduplication. +**This crate maximally reuses the Chia Rust ecosystem** rather than reimplementing functionality, and it reaches that ecosystem through exactly one dependency: **`dig-peer-protocol`**. That crate re-exports the Chia wire types (`Handshake`, `NodeType`, `ProtocolMessageTypes`, `Streamable`, `ChiaCertificate`), the Chia peer-manager and TLS surface (`Client`/`ClientState`, `Connector`, `load_ssl_cert`, `create_native_tls_connector`/`create_rustls_connector`, `Network`), and supplies the DIG peer link itself — `DigLink`, framing a raw `u8` opcode so DIG's 200-222 extension band is expressible on the wire. `dig-gossip` builds on top of these, adding: relay fallback, introducer registration, address manager persistence, gossip fanout, and message deduplication. The gossip layer **does** perform: -- **Peer discovery** via introducer registration and querying, DNS seeding (using `chia-sdk-client`'s `Network::lookup_all()`), and peer exchange between connected nodes. -- **Connection management** — establishing connections via `chia-sdk-client`'s `Peer::connect()` and `connect_peer()`, maintaining connections with keepalive, and tearing down on timeout. +- **Peer discovery** via introducer registration and querying, DNS seeding (using `dig-peer-protocol`'s re-exported `Network::lookup_all()`), and peer exchange between connected nodes. +- **Connection management** — establishing WebSocket-over-mTLS peer links as `dig_peer_protocol::DigLink`, maintaining connections with keepalive, and tearing down on timeout. - **Relay fallback** — when direct P2P connections cannot be established (NAT, firewall), messages are routed through a relay server as a transparent fallback. - **Structured gossip (Plumtree)** — eager/lazy push protocol that maintains a spanning tree for full-message push and uses lazy push (hash-only announcements) for redundancy, reducing bandwidth by 60-80% over Chia's naive flood-to-all approach. - **Compact block relay** — blocks are propagated as header + short transaction IDs; receivers reconstruct from mempool, requesting only missing transactions. Reduces block propagation bandwidth by 90%+. - **ERLAY-style transaction relay** — low-fanout flooding (announce to ~8 peers) combined with periodic set reconciliation (minisketch/IBLT) with remaining peers, reducing per-transaction bandwidth from O(connections) to O(1). - **Message priority lanes** — consensus-critical messages (NewPeak, attestations, blocks) are sent ahead of bulk data (mempool sync, peer exchange, historical block requests), preventing head-of-line blocking. - **Peer sharing** — exchanging known peer lists between connected nodes via `chia-protocol`'s `RequestPeers`/`RespondPeers`. -- **Rate limiting with adaptive backpressure** — using `chia-sdk-client`'s `RateLimiter` with `V2_RATE_LIMITS` for per-connection message rate enforcement, extended with adaptive backpressure that monitors outbound queue depth and selectively throttles non-critical messages under load. -- **Peer reputation with latency-aware scoring** — tracking peer behavior (valid/invalid messages, timeouts, protocol violations) with penalty-based banning, extending `chia-sdk-client`'s `ClientState` ban/trust model. Peers are scored by RTT (from Ping/Pong) and low-latency peers are preferred for outbound connections. +- **Rate limiting with adaptive backpressure** — using `dig-peer-protocol`'s `OpcodeRateLimiter`, which enforces Chia's published `V2_RATE_LIMITS` table re-keyed by raw wire opcode, for per-connection message rate enforcement, extended with adaptive backpressure that monitors outbound queue depth and selectively throttles non-critical messages under load. +- **Peer reputation with latency-aware scoring** — tracking peer behavior (valid/invalid messages, timeouts, protocol violations) with penalty-based banning, extending the re-exported `ClientState` ban/trust model. Peers are scored by RTT (from Ping/Pong) and low-latency peers are preferred for outbound connections. - **Address management with AS-level diversity** — maintaining tried/new peer address tables with bucket-based eviction, matching Chia's `AddressManager` (ported from Bitcoin's `CAddrMan`), enhanced with AS-level diversity (one outbound per autonomous system) for stronger eclipse attack resistance than Chia's /16 grouping. - **Parallel connection establishment** — bootstrap connects to multiple peers concurrently rather than Chia's sequential one-at-a-time approach. - **NAT traversal upgrade** — relay connections can be upgraded to direct P2P via STUN-style hole punching coordinated through the relay server. @@ -32,13 +32,25 @@ The gossip layer does **not** perform: - **Coinstate management** (coin record storage, state root computation) — handled by `dig-coinstore`. - **Consensus** (fork choice, finality, validator set management, checkpoint aggregation). -The design is derived from Chia's production networking stack, primarily consumed through the **Chia Rust crates** rather than ported from the Python source: - -**Chia Rust crates used directly (not reimplemented):** -- **`chia-protocol`** ([crates.io](https://crates.io/crates/chia-protocol)): Wire protocol types — `Handshake`, `Message`, `NodeType`, `ProtocolMessageTypes`, `RequestPeers`, `RespondPeers`, `RequestPeersIntroducer`, `RespondPeersIntroducer`, `NewPeak`, `NewTransaction`, `RequestTransaction`, `RespondTransaction`, `RequestBlock`, `RespondBlock`, `RequestBlocks`, `RespondBlocks`, `NewUnfinishedBlock`, `RequestUnfinishedBlock`, `RespondUnfinishedBlock`, `RequestMempoolTransactions`, `SpendBundle`, `FullBlock`, `Bytes32`, `ChiaProtocolMessage` trait. -- **`chia-sdk-client`** ([crates.io](https://crates.io/crates/chia-sdk-client)): Peer connection — `Peer` (WebSocket connection wrapper with `send()`, `request_raw()`, `request_infallible()`, `request_fallible()`), `Client`/`ClientState` (peer manager with ban/trust), `PeerOptions`, `Network` (DNS introducer lookup), `RateLimiter` (per-connection rate enforcement), `RateLimits`/`RateLimit` (rate limit tables), `V1_RATE_LIMITS`/`V2_RATE_LIMITS` (pre-configured Chia rate limits), `connect_peer()` (full handshake flow), `load_ssl_cert()`, `create_native_tls_connector()`/`create_rustls_connector()` (TLS setup), `ClientError`. -- **`chia-ssl`** ([crates.io](https://crates.io/crates/chia-ssl)): TLS certificates — `ChiaCertificate` (generate/load), `CHIA_CA_CRT` (Chia CA certificate). -- **`chia-traits`** ([crates.io](https://crates.io/crates/chia-traits)): Serialization — `Streamable` trait for wire format encoding/decoding. +The design is derived from Chia's production networking stack, primarily consumed through the **Chia Rust crates** rather than ported from the Python source. Those crates are reached through `dig-peer-protocol`, which re-exports them and adds the DIG extension band: + +**`dig-peer-protocol`** ([crates.io](https://crates.io/crates/dig-peer-protocol)) — the sole owner of the peer link, and the path through which the client, TLS and DIG-extension surfaces are consumed: +- **DIG peer link** — `DigLink` (WebSocket peer link with `send_message()`, `send_protocol_message()`, `request_infallible()`, `request_fallible()`, `from_websocket()`, `from_server_websocket()`), `LinkOptions`, `LinkError`. +- **DIG wire envelope** — `DigMessage` (a `msg_type: u8` / `id: Option` / `data: Bytes` envelope, layout-identical to Chia's `Message` but with the discriminant left as a raw byte), `DigMessageType`, `Bytes`, and the opcode constants (`DIG_BAND_START`, `DIG_MESSAGE`, `HOLDINGS_ANNOUNCE`, `STORE_MELTED`, `ALL_DIG_OPCODES`, `is_dig_opcode`). +- **Introducer wire types** — `RegisterPeer`, `RegisterAck`, `RequestPeersIntroducer`, `RespondPeersIntroducer`. +- **Opcode-keyed rate limiting** — `OpcodeRateLimiter`, `OpcodeRateLimits`, `Admission`. +- **Re-exported Chia surface** — `ProtocolMessageTypes`, `ChiaProtocolMessage`, `TimestampedPeerInfo`, `Streamable`, `ChiaCertificate`, `NodeType`, `Network`, `Client`/`ClientState`, `Connector`, `RateLimit`, `load_ssl_cert`, `create_native_tls_connector`/`create_rustls_connector`, `ClientError`. + +**Chia Rust crates used directly (not reimplemented).** They arrive by two different routes, and the +distinction is normative because it decides what a reimplementation must declare in its own manifest: +`chia-protocol` and `chia-traits` are **direct dependencies** of this crate (`Cargo.toml`; the wire +types are re-exported straight from `chia_protocol` — see `src/lib.rs`), while `chia-sdk-client` and +`chia-ssl` are reached **transitively**, through `dig-peer-protocol`'s re-exports above, and are not +declared here at all. +- **`chia-protocol`** ([crates.io](https://crates.io/crates/chia-protocol)): Wire protocol types — `Handshake`, `NodeType`, `ProtocolMessageTypes`, `RequestPeers`, `RespondPeers`, `RequestPeersIntroducer`, `RespondPeersIntroducer`, `NewPeak`, `NewTransaction`, `RequestTransaction`, `RespondTransaction`, `RequestBlock`, `RespondBlock`, `RequestBlocks`, `RespondBlocks`, `NewUnfinishedBlock`, `RequestUnfinishedBlock`, `RespondUnfinishedBlock`, `RequestMempoolTransactions`, `SpendBundle`, `FullBlock`, `Bytes32`, `ChiaProtocolMessage` trait. +- **`chia-sdk-client`** ([crates.io](https://crates.io/crates/chia-sdk-client)): `Client`/`ClientState` (peer manager with ban/trust), `Network` (DNS introducer lookup), `RateLimit` (rate-limit table row), `load_ssl_cert()`, `create_native_tls_connector()`/`create_rustls_connector()` (TLS setup), `ClientError`. Consumed through `dig-peer-protocol`'s re-exports, never as a direct dependency. The peer connection itself is `dig_peer_protocol::DigLink`, not this crate's `Peer`. +- **`chia-ssl`** ([crates.io](https://crates.io/crates/chia-ssl)): TLS certificates — `ChiaCertificate` (generate/load), `CHIA_CA_CRT` (Chia CA certificate). Consumed through `dig-peer-protocol`'s re-exports, never as a direct dependency. +- **`chia-traits`** ([crates.io](https://crates.io/crates/chia-traits)): Serialization — `Streamable` trait for wire format encoding/decoding. A direct dependency. **Chia Python source (reference for address manager and discovery loop logic):** - **Peer discovery**: [`chia/server/node_discovery.py`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/node_discovery.py) @@ -49,15 +61,16 @@ The design is derived from Chia's production networking stack, primarily consume - **Relay client**: `l2_driver_state_channel/src/services/relay/client.rs`, `l2_driver_state_channel/src/services/relay/types.rs` - **Introducer client**: `l2_driver_state_channel/src/services/network/introducer_client.rs` -**Hard boundary:** Inputs = application payloads (`Vec` or typed via `chia-protocol`'s `Streamable + ChiaProtocolMessage`) to broadcast/send. Outputs = received payloads delivered to the caller via async channels as `chia-protocol::Message`. Block validation, CLVM execution, mempool management, coinstate, and consensus are outside this crate. The gossip crate is **payload-agnostic** — it transports `Message`s between peers. The caller defines what those bytes mean. +**Hard boundary:** Inputs = application payloads (`Vec` or typed via `chia-protocol`'s `Streamable + ChiaProtocolMessage`) to broadcast/send. Outputs = received payloads delivered to the caller via async channels as `dig_peer_protocol::DigMessage`. Block validation, CLVM execution, mempool management, coinstate, and consensus are outside this crate. The gossip crate is **payload-agnostic** — it transports `dig_peer_protocol::DigMessage` envelopes between peers. The caller defines what those bytes mean. It does **not** transport `chia_protocol::Message`: that type's discriminant is a closed `#[repr(u8)]` enum which cannot name a DIG opcode, and a reimplementation that builds on it will reject every frame in the DIG 200-222 band at decode time. ### 1.1 Design Principles -- **Chia crate reuse over reimplementation**: Every type and behavior that exists in the Chia Rust crates (`chia-protocol`, `chia-sdk-client`, `chia-ssl`, `chia-traits`) is used directly. We do NOT redefine `Handshake`, `NodeType`, `Message`, `ProtocolMessageTypes`, `Peer`, `RateLimiter`, or TLS handling. We only implement what doesn't exist in the Chia ecosystem: address manager, discovery loop, relay fallback, introducer registration, gossip fanout, and message deduplication. +- **Chia crate reuse over reimplementation**: Every type and behavior that exists in the Chia Rust crates (`chia-protocol`, `chia-sdk-client`, `chia-ssl`, `chia-traits`) is used as-is — the wire types from `chia-protocol`/`chia-traits` directly, the client and TLS surfaces via `dig-peer-protocol`'s re-exports. We do NOT redefine `Handshake`, `NodeType`, `ProtocolMessageTypes`, `ClientState`, or TLS handling. We only implement what doesn't exist upstream: address manager, discovery loop, relay fallback, introducer registration, gossip fanout, and message deduplication. +- **One dependency for the peer wire**: `dig-peer-protocol` is the sole owner of the peer link and the sole path to the client/TLS surfaces. `dig-gossip` MUST NOT depend on `chia-sdk-client` or `chia-ssl` directly, and MUST NOT vendor or patch a Chia crate. It MAY depend on `chia-protocol`/`chia-traits` directly, and does — they carry only wire *types*, not a transport, so a direct dependency cannot reintroduce a second peer link. Chia's `ProtocolMessageTypes` is a closed `#[repr(u8)]` enum that cannot name a DIG opcode; `DigLink` frames a raw `u8` instead, so the DIG 200-222 band needs no fork of the Chia types. - **Chia protocol parity**: The handshake, message framing, peer exchange, and discovery protocols match Chia's networking protocol. `chia-protocol`'s `Handshake` struct is used directly with DIG-specific `network_id` and `capabilities` values. - **Relay as transparent fallback**: When direct P2P fails (NAT, firewall), the relay server acts as a message proxy. The caller sees no difference — messages arrive through the same channel regardless of transport. Matches `l2_driver_state_channel/src/services/relay/service.rs`. - **Introducer for bootstrap**: New nodes register with an introducer and query it for initial peers, matching Chia's `FullNodeDiscovery._introducer_client()` ([`node_discovery.py:173-184`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/node_discovery.py#L173)) and `l2_driver_state_channel/src/services/network/introducer_client.rs`. -- **Payload-agnostic transport**: The gossip layer does not inspect or validate message payloads. It transports `chia-protocol::Message` envelopes between peers. The caller registers handlers for specific `ProtocolMessageTypes`. +- **Payload-agnostic transport**: The gossip layer does not inspect or validate message payloads. It transports `dig_peer_protocol::DigMessage` envelopes between peers. The caller registers handlers keyed by the envelope's raw `msg_type` opcode. - **Peer sharing via gossip**: Connected peers exchange peer lists periodically via `chia-protocol`'s `RequestPeers`/`RespondPeers` ([`full_node_protocol.py:207-216`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/protocols/full_node_protocol.py#L207)). - **Address manager with tried/new tables**: Peer addresses are managed using the Bitcoin/Chia bucket-based address manager ([`address_manager.py`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/address_manager.py)), providing resistance to eclipse attacks. This is the one major component that must be ported to Rust — no Chia Rust crate provides it. @@ -65,12 +78,13 @@ The design is derived from Chia's production networking stack, primarily consume | Crate | Purpose | Reuse vs New | |-------|---------|-------------| -| `chia-protocol` | Wire protocol types: `Handshake`, `Message`, `NodeType`, `ProtocolMessageTypes`, `Bytes32`, `RequestPeers`, `RespondPeers`, `NewPeak`, `NewTransaction`, `SpendBundle`, `FullBlock`, all request/respond/reject types. `ChiaProtocolMessage` trait. | **Direct reuse** | -| `chia-sdk-client` | `Peer` (WebSocket connection), `Client`/`ClientState` (peer manager), `PeerOptions`, `Network` (DNS lookup), `RateLimiter`/`RateLimits` (rate limiting), `V2_RATE_LIMITS`, `connect_peer()` (handshake), TLS utilities. | **Direct reuse** | -| `chia-ssl` | `ChiaCertificate`, `CHIA_CA_CRT`. TLS certificate generation and loading. | **Direct reuse** | +| `chia-protocol` | Wire protocol types: `Handshake`, `NodeType`, `ProtocolMessageTypes`, `Bytes32`, `RequestPeers`, `RespondPeers`, `NewPeak`, `NewTransaction`, `SpendBundle`, `FullBlock`, all request/respond/reject types. `ChiaProtocolMessage` trait. | **Direct reuse** | +| `dig-peer-protocol` | `DigLink` (WebSocket peer link), `LinkOptions`, `DigMessage`/`DigMessageType`, the DIG opcode constants, the introducer wire types, `OpcodeRateLimiter`/`OpcodeRateLimits`, and the re-exported Chia surface below. | **Direct dependency** | +| `chia-sdk-client` | `Client`/`ClientState` (peer manager), `Network` (DNS lookup), `RateLimit`, `ClientError`, TLS utilities. | **Transitive**, via `dig-peer-protocol` re-exports | +| `chia-ssl` | `ChiaCertificate`, `CHIA_CA_CRT`. TLS certificate generation and loading. | **Transitive**, via `dig-peer-protocol` re-exports | | `chia-traits` | `Streamable` trait for wire serialization/deserialization. | **Direct reuse** | | `tokio` | Async runtime. Timers, tasks, channels, TCP listeners. | Dependency | -| `tokio-tungstenite` | WebSocket (already a dependency of `chia-sdk-client`). | Transitive | +| `tokio-tungstenite` | WebSocket (also a dependency of `dig-peer-protocol`). | Dependency | | `serde` / `bincode` | Serialization for relay protocol and address manager persistence. | Dependency | | `serde_json` | JSON serialization for relay and introducer messages. | Dependency | | `tracing` | Structured logging. | Dependency | @@ -84,16 +98,16 @@ The design is derived from Chia's production networking stack, primarily consume | # | Decision | Rationale | |---|----------|-----------| -| 1 | Reuse `chia-sdk-client::Peer` for connections | `Peer` already handles WebSocket TLS connections, message framing (4-byte length prefix), `Streamable` serialization, request/response correlation via message IDs, and outbound rate limiting. No reason to reimplement. | -| 2 | Reuse `chia-sdk-client::RateLimiter` + `V2_RATE_LIMITS` | Complete Chia-compatible rate limiting with V1/V2 limit tables. The DIG per-opcode table is keyed by the raw wire byte and is dig-gossip's own (`DigRateLimiter`), composed with the Chia bound rather than merged into it. | -| 3 | Reuse `chia-protocol::Handshake` for connection setup | The handshake struct has `network_id`, `protocol_version`, `software_version`, `server_port`, `node_type`, `capabilities`. We pass DIG-specific values, not a new struct. `connect_peer()` handles the full handshake flow. | +| 1 | Reuse `dig_peer_protocol::DigLink` for connections | `DigLink` handles WebSocket TLS connections, message framing (4-byte length prefix), `Streamable` serialization, request/response correlation via message IDs, and outbound rate limiting — and, unlike Chia's `Peer`, frames the discriminant as a raw `u8`, so the DIG 200-222 band travels on a stock envelope. No reason to reimplement. | +| 2 | Reuse `dig_peer_protocol::OpcodeRateLimiter` | Chia-compatible rate limiting: `OpcodeRateLimits` carries Chia's published `V2_RATE_LIMITS` rows re-keyed by raw wire opcode. The DIG per-opcode table is keyed by the same raw byte and is dig-gossip's own (`DigRateLimiter`), composed with the Chia bound rather than merged into it. | +| 3 | Reuse `chia-protocol::Handshake` for connection setup | The handshake struct has `network_id`, `protocol_version`, `software_version`, `server_port`, `node_type`, `capabilities`. We pass DIG-specific values, not a new struct. The outbound module drives the handshake exchange over the raw WebSocket before upgrading to `DigLink`. | | 4 | Reuse `chia-ssl` for TLS | `ChiaCertificate::generate()`, `load_ssl_cert()`, and `create_native_tls_connector()` / `create_rustls_connector()` already exist. | -| 5 | Reuse `chia-sdk-client::Network` for DNS seeding | `Network::lookup_all()` handles DNS resolution with timeout and batching. We configure with DIG DNS servers. | +| 5 | Reuse the re-exported `Network` for DNS seeding | `Network::lookup_all()` handles DNS resolution with timeout and batching. We configure with DIG DNS servers. | | 6 | Port `AddressManager` from Python (no Rust crate exists) | Chia's `address_manager.py` is a Python port of Bitcoin's `CAddrMan`. No Rust equivalent exists in the Chia crate ecosystem. This must be ported. | | 7 | Port discovery loop from Python (no Rust crate exists) | Chia's `node_discovery.py` discovery loop (introducer backoff, feeler connections, peer connect logic) has no Rust equivalent. This must be ported. | -| 8 | Relay as fallback, not primary | Direct P2P via `chia-sdk-client::Peer` is attempted first. Relay is used only when direct connection fails. Matches `l2_driver_state_channel/src/services/relay/types.rs` `RelayConfig::prefer_relay` default `false`. | -| 9 | DIG-specific `ProtocolMessageTypes` for extensions | Chia's `ProtocolMessageTypes` enum doesn't include DIG L2 messages (attestations, checkpoints). We define DIG extension types in a separate enum and map them to unused Chia message type IDs (200+). | -| 10 | `chia-sdk-client::ClientState` extended for reputation | `ClientState` provides basic ban/trust per IP. We extend with penalty-based reputation tracking per `PeerId`. | +| 8 | Relay as fallback, not primary | Direct P2P via `DigLink` is attempted first. Relay is used only when direct connection fails. Matches `l2_driver_state_channel/src/services/relay/types.rs` `RelayConfig::prefer_relay` default `false`. | +| 9 | DIG opcodes travel as raw bytes, never as `ProtocolMessageTypes` | Chia's `ProtocolMessageTypes` enum doesn't include DIG L2 messages (attestations, checkpoints), and it is a closed `#[repr(u8)]` enum, so a DIG discriminant is not representable in it. `dig_peer_protocol::DigMessageType` names the DIG extension opcodes (200-222, an unused band upstream — Chia's highest is `RespondCostInfo = 107`) and `DigMessage` carries the discriminant as a raw `u8`. No Chia type is forked, extended, or renumbered. | +| 10 | The re-exported `ClientState` extended for reputation | `ClientState` provides basic ban/trust per IP. We extend with penalty-based reputation tracking per `PeerId`. | | 11 | `std` only | Full-node networking infrastructure. No `no_std` support needed. | | 12 | Plumtree structured gossip over naive flooding | Chia broadcasts to ALL connected peers. This is O(peers × messages). Plumtree maintains a spanning tree for eager push and uses lazy push (hash-only) for redundancy. Reduces bandwidth 60-80%. Critical for DIG L2's faster block times and higher attestation volume. | | 13 | Compact block relay (BIP 152 equivalent) | Chia sends full `RespondBlock` (up to 2MB+). Most transactions are already in the receiver's mempool. Compact blocks send header + 6-byte short tx IDs; receiver reconstructs from mempool. Reduces block propagation bandwidth 90%+ and latency significantly. | @@ -113,7 +127,7 @@ Types used **directly** from Chia crates (NOT redefined in dig-gossip): |------|-------------|-------------------| | `Bytes32` | `chia-protocol` | Peer IDs, network IDs, message hashes | | `Handshake` | `chia-protocol` | Connection handshake (populated with DIG values) | -| `Message` | `chia-protocol` | Wire-level message envelope (`msg_type`, `id`, `data`) | +| `DigMessage` | `dig-peer-protocol` | Wire-level message envelope (`msg_type: u8`, `id`, `data`) — layout-identical to Chia's `Message`, with the discriminant left as a raw byte so DIG opcodes 200-222 are expressible | | `NodeType` | `chia-protocol` | Node type discrimination (FullNode, Wallet, Introducer) | | `ProtocolMessageTypes` | `chia-protocol` | Message type discriminant | | `RequestPeers` / `RespondPeers` | `chia-protocol` | Peer exchange between full nodes | @@ -127,31 +141,31 @@ Types used **directly** from Chia crates (NOT redefined in dig-gossip): | `SpendBundle` | `chia-protocol` | Transaction payload | | `FullBlock` | `chia-protocol` | Block payload | | `TimestampedPeerInfo` | `chia-protocol` | Peer info in `RespondPeers` | -| `Peer` | `chia-sdk-client` | WebSocket connection wrapper | -| `PeerOptions` | `chia-sdk-client` | Connection options (rate_limit_factor) | -| `Client` / `ClientState` | `chia-sdk-client` | Peer connection manager with ban/trust | -| `Network` | `chia-sdk-client` | DNS introducer lookup | -| `RateLimiter` | `chia-sdk-client` | Per-connection rate limiting | -| `RateLimits` / `RateLimit` | `chia-sdk-client` | Rate limit configuration | -| `V2_RATE_LIMITS` | `chia-sdk-client` | Pre-configured Chia V2 rate limits | -| `connect_peer()` | `chia-sdk-client` | Full handshake + connect flow | -| `load_ssl_cert()` | `chia-sdk-client` | TLS certificate loading | -| `create_native_tls_connector()` | `chia-sdk-client` | TLS connector creation | -| `ClientError` | `chia-sdk-client` | Connection error types | -| `ChiaCertificate` | `chia-ssl` | TLS certificate generation | -| `Streamable` | `chia-traits` | Wire serialization trait | +| `DigLink` | `dig-peer-protocol` | WebSocket peer link (client and server side) | +| `LinkOptions` | `dig-peer-protocol` | Link options (rate-limit factor, budget timeout) | +| `DigMessageType` | `dig-peer-protocol` | DIG extension opcode names (200-222) | +| `OpcodeRateLimiter` / `OpcodeRateLimits` | `dig-peer-protocol` | Chia's rate-limit table re-keyed by raw wire opcode | +| `RegisterPeer` / `RegisterAck` / `RequestPeersIntroducer` / `RespondPeersIntroducer` | `dig-peer-protocol` | Introducer wire types | +| `Client` / `ClientState` | `chia-sdk-client`, re-exported by `dig-peer-protocol` | Peer connection manager with ban/trust | +| `Network` | `chia-sdk-client`, re-exported by `dig-peer-protocol` | DNS introducer lookup | +| `RateLimits` / `RateLimit` / `V2_RATE_LIMITS` | `chia-sdk-client`, re-exported by `dig-peer-protocol` | Chia's published rate-limit configuration | +| `load_ssl_cert()` | `chia-sdk-client`, re-exported by `dig-peer-protocol` | TLS certificate loading | +| `create_native_tls_connector()` / `create_rustls_connector()` | `chia-sdk-client`, re-exported by `dig-peer-protocol` | TLS connector creation | +| `ClientError` | `chia-sdk-client`, re-exported by `dig-peer-protocol` | Connection error types | +| `ChiaCertificate` | `chia-ssl`, re-exported by `dig-peer-protocol` | TLS certificate generation | +| `Streamable` | `chia-traits`, re-exported by `dig-peer-protocol` | Wire serialization trait | ### 1.5 Chia Behaviors Adopted (via crate reuse) | # | Behavior | How Adopted | Reference | |---|----------|-------------|-----------| -| 1 | Handshake with capabilities | `connect_peer()` sends `chia-protocol::Handshake` with capabilities list. | [`chia-sdk-client/src/connect.rs:20-32`](https://github.com/Chia-Network/chia-wallet-sdk) | -| 2 | V2 rate limiting | `chia-sdk-client::RateLimiter` with `V2_RATE_LIMITS` handles per-message-type frequency and size limits. | [`chia-sdk-client/src/rate_limits.rs`](https://github.com/Chia-Network/chia-wallet-sdk) | +| 1 | Handshake with capabilities | The outbound module sends `chia-protocol::Handshake` with the capabilities list over the raw WebSocket, mirroring upstream's `connect.rs` flow, before upgrading to `DigLink`. | [`chia-sdk-client/src/connect.rs:20-32`](https://github.com/Chia-Network/chia-wallet-sdk) | +| 2 | V2 rate limiting | `dig_peer_protocol::OpcodeRateLimiter` enforces Chia's `V2_RATE_LIMITS` frequency and size limits, keyed by raw wire opcode. | [`chia-sdk-client/src/rate_limits.rs`](https://github.com/Chia-Network/chia-wallet-sdk) | | 3 | TLS mutual authentication | `chia-ssl::ChiaCertificate::generate()` + `create_native_tls_connector()` or `create_rustls_connector()`. | [`chia-sdk-client/src/tls.rs`](https://github.com/Chia-Network/chia-wallet-sdk) | -| 4 | Message framing | `chia-protocol::Message` uses `Streamable` for binary encoding. `Peer` handles WebSocket binary frames. | [`chia-protocol`](https://crates.io/crates/chia-protocol) | -| 5 | Request/response correlation | `Peer::request_raw()` assigns message IDs and waits for correlated responses via `RequestMap`. | [`chia-sdk-client/src/peer.rs:302-316`](https://github.com/Chia-Network/chia-wallet-sdk) | +| 4 | Message framing | `dig_peer_protocol::DigMessage` uses `Streamable` for binary encoding, byte-identical to Chia's `Message`. `DigLink` handles WebSocket binary frames. | [`chia-protocol`](https://crates.io/crates/chia-protocol) | +| 5 | Request/response correlation | `DigLink`'s request methods assign message IDs and wait for correlated responses via its request map. | [`chia-sdk-client/src/peer.rs:302-316`](https://github.com/Chia-Network/chia-wallet-sdk) | | 6 | DNS seeding | `Network::lookup_all()` with timeout and batching. | [`chia-sdk-client/src/network.rs:40-68`](https://github.com/Chia-Network/chia-wallet-sdk) | -| 7 | Network ID validation | `connect_peer()` rejects peers with mismatched `network_id`. | [`chia-sdk-client/src/connect.rs:54-58`](https://github.com/Chia-Network/chia-wallet-sdk) | +| 7 | Network ID validation | Handshake validation rejects peers with a mismatched `network_id`. | [`chia-sdk-client/src/connect.rs:54-58`](https://github.com/Chia-Network/chia-wallet-sdk) | | 8 | Peer ban/trust | `ClientState::ban()`, `ClientState::unban()`, `ClientState::trust()`, `ClientState::is_banned()`. | [`chia-sdk-client/src/client.rs:93-133`](https://github.com/Chia-Network/chia-wallet-sdk) | ### 1.6 Chia Behaviors Ported from Python (no Rust crate) @@ -176,8 +190,8 @@ Types used **directly** from Chia crates (NOT redefined in dig-gossip): |---|-----------|-------------| | 1 | Relay server fallback | Nodes behind NAT/firewall can participate in gossip through a relay server. Chia has no relay. From `l2_driver_state_channel/src/services/relay/`. | | 2 | Introducer registration | Nodes actively register with the introducer (IP, port, node_type), not just query it. Chia's introducer is query-only. From `l2_driver_state_channel/src/services/network/introducer_client.rs`. | -| 3 | DIG protocol message types | Attestation, checkpoint, and status messages (types 200+). | -| 4 | Inbound connection listener | `chia-sdk-client`'s `Peer` only does outbound connections. We add a `TcpListener` accepting inbound. | +| 3 | DIG protocol message types | Attestation, checkpoint, and status messages (opcodes 200-222), carried as raw `msg_type` bytes in a `DigMessage`. | +| 4 | Inbound connection listener | `DigLink` is built for outbound `wss://` dials. We add a `TcpListener` accepting inbound and upgrade the accepted server stream via `DigLink::from_server_websocket()`. | ### 1.8 Improvements Over Chia L1 @@ -406,16 +420,15 @@ transport-layer rejection from an app-layer one because both surface to the clie ### 2.1 Types Reused from Chia Crates -The following types are **re-exported** from Chia crates, not redefined: +The following types are **re-exported**, not redefined. The Chia types are reached through +`dig-peer-protocol`; `chia-protocol` remains a direct dependency only for the full-node wire +structs it does not re-export. ```rust // From chia-protocol pub use chia_protocol::{ Bytes32, Handshake, - Message, - NodeType, - ProtocolMessageTypes, // Full node protocol messages NewPeak, NewTransaction, RequestTransaction, RespondTransaction, RequestBlock, RespondBlock, RejectBlock, @@ -423,29 +436,28 @@ pub use chia_protocol::{ NewUnfinishedBlock, RequestUnfinishedBlock, RespondUnfinishedBlock, RequestMempoolTransactions, RequestPeers, RespondPeers, - RequestPeersIntroducer, RespondPeersIntroducer, // Payload types SpendBundle, FullBlock, // Peer info TimestampedPeerInfo, }; -// From chia-sdk-client -pub use chia_sdk_client::{ - Peer, PeerOptions, - Client, ClientState, +// The DIG peer wire, owned by dig-peer-protocol. +pub use dig_peer_protocol::{ + Bytes, ChiaProtocolMessage, DigLink, DigMessage, LinkError, LinkOptions, + NodeType, ProtocolMessageTypes, + OpcodeRateLimiter, OpcodeRateLimits, +}; + +// The Chia surface, re-exported by dig-peer-protocol rather than depended on directly. +pub use dig_peer_protocol::{ + Client, ClientState, ClientError, Network, - RateLimiter, RateLimits, RateLimit, - V2_RATE_LIMITS, - ClientError, + RateLimits, RateLimit, V2_RATE_LIMITS, load_ssl_cert, + ChiaCertificate, // chia-ssl + Streamable, // chia-traits }; - -// From chia-ssl -pub use chia_ssl::ChiaCertificate; - -// From chia-traits -pub use chia_traits::Streamable; ``` ### 2.2 PeerId (type alias) @@ -497,17 +509,17 @@ application protocols — directed (`DIG_MESSAGE = 220`) or broadcast Opcode **220** (`DIG_MESSAGE`) carries a `dig-message` **directed envelope** between two peers. It is a first-class `ProtocolMessageTypes::DigMessage` variant so it rides -the ordinary [`Message`](chia_protocol::Message) transport (send / inbound), and the +the ordinary [`DigMessage`](dig_peer_protocol::DigMessage) transport (send / inbound), and the canonical constant is exported as `dig_gossip::DIG_MESSAGE` (mirrored by `dig_peer_protocol::DIG_MESSAGE` for non-gossip consumers). - **Envelope is OPAQUE.** dig-gossip is the transport only — the sealed envelope rides - verbatim in `Message.data` (bytes in equal bytes out). dig-gossip never seals, opens, + verbatim in `DigMessage.data` (bytes in equal bytes out). dig-gossip never seals, opens, or parses it, and has no BLS / recipient-key dependency (Wave A, envelope-only). The end-to-end sealing to the recipient's DID key is `dig-message`'s (CLAUDE.md §5.4). - **Directed, never broadcast.** `classify_broadcast(DigMessage) = Unicast`; a directed message is delivered 1:1 via `send_dig_message`, never Plumtree-flooded. -- **Correlation.** `Message.id` pairs the frames of one exchange (e.g. a stream). +- **Correlation.** `DigMessage.id` pairs the frames of one exchange (e.g. a stream). **Send/route API** (on `GossipHandle`, plus free functions in `service::dig_message`): @@ -516,7 +528,7 @@ canonical constant is exported as `dig_gossip::DIG_MESSAGE` (mirrored by | `send_dig_message(peer, envelope, correlation_id)` | Send a directed envelope over opcode 220. | | `dig_message_payload(&Message) -> Option<&[u8]>` | Inbound routing: lift the opaque envelope from an opcode-220 frame (else `None`). | | `is_dig_message(u8) -> bool` | Recognise opcode 220. | -| `frame_envelope(&[u8], Option) -> Message` | Build the outbound opcode-220 frame. | +| `frame_envelope(&[u8], Option) -> DigMessage` | Build the outbound opcode-220 frame. | **Opcode 220 (`DigMessage`, directed envelope) — base-bounded by design (accepted).** Unlike the 221/222 public-flood broadcasts, opcode 220 carries a *directed* (unicast) dig-message envelope as opaque bytes; dig-gossip is pure transport and never opens, decodes, or verifies the envelope. Opcode 220 therefore has NO dedicated DIG rate-limit row and is deliberately bounded only by the Chia `default_settings` base limit — 100 frames/min, 1 MiB/frame, 100 MiB cumulative per connection — applied FIRST and unconditionally by `RateLimiter::handle_message` inside `InboundRateLimiter::allows`. This bound is REAL and non-fail-open: the fail-open `DigRateLimiter::check` runs only afterward and can add, never loosen, a restriction. @@ -587,8 +599,8 @@ key**, supplied by the caller from the peer's mTLS cert binding (the message car | `StoreMeltedAnnounce::verify(&self, signer_pk_g1: &[u8; 48]) -> bool` | Verify the signature against the signer's BLS G1 key (receiver). | | `StoreMeltedAnnounce::{encode,decode}` | Fixed-length big-endian wire round-trip. | | `sign_store_melted(sk, store_id, melt_height) -> [u8; 96]` / `store_melted_sig_preimage(store_id, melt_height) -> [u8; 32]` | Signature helpers. | -| `frame_store_melted(&StoreMeltedAnnounce) -> Message` | Build the outbound opcode-221 broadcast frame (`id = None`). | -| `store_melted_payload(&Message) -> Option` | Inbound routing: lift + decode an opcode-221 frame (else `None`). | +| `frame_store_melted(&StoreMeltedAnnounce) -> DigMessage` | Build the outbound opcode-221 broadcast frame (`id = None`). | +| `store_melted_payload(&DigMessage) -> Option` | Inbound routing: lift + decode an opcode-221 frame (else `None`). | | `is_store_melted(u8) -> bool` | Recognise opcode 221. | #### 2.3.4 `HOLDINGS_ANNOUNCE = 222` — holdings-announce broadcast (#1428, spec #1394) @@ -703,8 +715,8 @@ dig-dht's ingest recompute it byte-identically. | `HoldingsAnnounce::{encode,decode}` | Variable-length big-endian wire round-trip. | | `canonical_encode(&[HoldingsDelta]) -> Vec` / `holdings_signing_message(&peer_id, seq, announced_at, &changes) -> Vec` | Signed-bytes + signing-message helpers. | | `signing_message_digest(&peer_id, seq, announced_at, &changes) -> [u8;32]` | SHA-256 fingerprint of the signing message (KAT/layout helper; NOT what is signed). | -| `frame_holdings_announce(&HoldingsAnnounce) -> Message` | Build the outbound opcode-222 broadcast frame (`id = None`). | -| `holdings_announce_payload(&Message) -> Option` | Inbound routing: lift + decode an opcode-222 frame (else `None`). | +| `frame_holdings_announce(&HoldingsAnnounce) -> DigMessage` | Build the outbound opcode-222 broadcast frame (`id = None`). | +| `holdings_announce_payload(&DigMessage) -> Option` | Inbound routing: lift + decode an opcode-222 frame (else `None`). | | `is_holdings_announce(u8) -> bool` | Recognise opcode 222. | **KAT golden vector.** The ECDSA-P256 signature is randomized, so it is NOT hex-pinnable; @@ -714,16 +726,16 @@ domain tag / `canonical_encode` / field order of this cross-repo wire contract. SPKI→peer_id binding and the sign/verify behaviour (including the "sign with a foreign key, present the victim's SPKI" forgery rejection) are covered by behavioural tests. -### 2.4 PeerConnection (DIG extension of `chia-sdk-client::Peer`) +### 2.4 PeerConnection (DIG extension of `dig_peer_protocol::DigLink`) -`chia-sdk-client::Peer` handles the WebSocket connection and message I/O. `PeerConnection` wraps it with additional metadata for the gossip layer. +`DigLink` handles the WebSocket connection and message I/O. `PeerConnection` wraps it with additional metadata for the gossip layer. ```rust /// Extended peer connection state for the gossip layer. -/// Wraps `chia-sdk-client::Peer` with gossip-specific metadata. +/// Wraps `dig_peer_protocol::DigLink` with gossip-specific metadata. pub struct PeerConnection { - /// The underlying chia-sdk-client Peer connection. - pub peer: Peer, + /// The underlying DigLink connection. + pub peer: DigLink, /// Unique peer identifier (SHA256 of TLS public key). pub peer_id: PeerId, /// Remote socket address. @@ -751,13 +763,13 @@ pub struct PeerConnection { /// Peer reputation tracker (DIG extension). pub reputation: PeerReputation, /// Inbound message receiver for this connection. - pub inbound_rx: mpsc::Receiver, + pub inbound_rx: mpsc::Receiver, } ``` ### 2.5 PeerReputation (DIG extension) -Extends `chia-sdk-client::ClientState`'s binary ban/trust with numeric penalties. +Extends `ClientState`'s binary ban/trust with numeric penalties. ```rust /// Reasons a peer can be penalized. @@ -903,7 +915,7 @@ pub struct GossipConfig { pub peer_id: PeerId, /// Network ID (e.g., SHA256("dig_mainnet")). pub network_id: Bytes32, - /// Network config for DNS lookup (uses chia-sdk-client::Network). + /// Network config for DNS lookup (uses the re-exported `Network`). pub network: Network, /// Target number of outbound connections. /// Chia: node_discovery.py:49. Default: 8. @@ -1184,7 +1196,7 @@ pub struct GossipHandle { /* ... */ } impl GossipHandle { // -- Message sending -- - /// Broadcast a chia-protocol::Message to connected peers via gossip fanout. + /// Broadcast a DigMessage to connected peers via gossip fanout. pub async fn broadcast( &self, message: Message, @@ -1192,14 +1204,14 @@ impl GossipHandle { ) -> Result; /// Broadcast a typed Streamable + ChiaProtocolMessage. - /// Serializes to Message internally using chia-traits::Streamable. + /// Serializes to DigMessage internally using chia-traits::Streamable. pub async fn broadcast_typed( &self, body: T, exclude: Option, ) -> Result; - /// Send a message to a specific peer (via their chia-sdk-client::Peer). + /// Send a message to a specific peer (via their `DigLink`). pub async fn send_to( &self, peer_id: PeerId, @@ -1219,7 +1231,7 @@ impl GossipHandle { // -- Message receiving -- /// Inbound message receiver. Each item is (sender_peer_id, chia-protocol::Message). - pub fn inbound_receiver(&self) -> &mpsc::Receiver<(PeerId, Message)>; + pub fn inbound_receiver(&self) -> Result, GossipError>; // -- Peer management -- @@ -1236,7 +1248,7 @@ impl GossipHandle { outbound_only: bool, ) -> Vec; - /// Connect to a peer (uses chia-sdk-client::connect_peer internally). + /// Connect to a peer (drives the handshake, then upgrades to `DigLink`). pub async fn connect_to(&self, addr: SocketAddr) -> Result; /// Disconnect a peer. @@ -1308,7 +1320,7 @@ pub struct RelayStats { ```rust #[derive(Debug, Clone, thiserror::Error)] pub enum GossipError { - /// Wraps chia-sdk-client::ClientError for connection-level errors. + /// Wraps the re-exported `ClientError` for connection-level errors. #[error("client error: {0}")] ClientError(#[from] ClientError), @@ -1360,23 +1372,43 @@ pub enum GossipError { ## 5. Connection Lifecycle -### 5.1 Outbound Connection (reuses `chia-sdk-client`) +### 5.1 Outbound Connection + +The outbound module mirrors upstream's `connect.rs` flow rather than calling it, because +upstream discards the parsed `Handshake` and never exposes the remote TLS SubjectPublicKeyInfo +bytes — both of which `PeerConnection` and `PeerId` (§5.3, API-005) require. ``` -Outbound connection (uses connect_peer() from chia-sdk-client): +Outbound connection: │ ├─ 1. Load TLS cert via load_ssl_cert() / ChiaCertificate::generate() ├─ 2. Create connector via create_native_tls_connector() or create_rustls_connector() - ├─ 3. Call connect_peer(network_id, connector, socket_addr, options) - │ → Internally: Peer::connect() → WebSocket TLS connect - │ → Sends chia-protocol::Handshake with DIG network_id - │ → Receives and validates Handshake response - │ → Returns (Peer, mpsc::Receiver) + ├─ 3. Dial wss:// with that connector + │ → Capture remote_spki_der from the WebSocketStream before it is consumed + │ → Send chia-protocol::Handshake with DIG network_id + │ → Receive and validate the Handshake response + │ → Upgrade via DigLink::from_websocket(ws, options) + │ → Yields (DigLink, mpsc::Receiver, Handshake, remote_spki_der) ├─ 4. Wrap in PeerConnection with gossip metadata ├─ 5. Add peer to address manager ├─ 6. Send RequestPeers for discovery (node_discovery.py:135-136) └─ 7. Spawn per-connection message loop task +Step 7 includes the CON-004 keepalive. Every `PING_INTERVAL_SECS` the loop sends a `RequestPeers` +probe **with no correlation id** and waits up to `PEER_TIMEOUT_SECS` for the peer's `RespondPeers` +on the application inbound stream. + +The probe MUST NOT be correlated. Both peers allocate correlation ids from a counter starting at +zero and both keepalive loops start at handshake on the same interval, so two correlated probes can +carry the same id — and because a link matches an inbound frame on correlation id before forwarding +it, each side's waiter would receive the peer's **request** rather than a response. The peer's +request would never reach the auto-reply path, neither side would record a success, and both would +disconnect at the staleness check while logging a timeout that names the wrong cause. + +The design fails loose: an alive-but-silent peer is kept, and a round whose reply cannot be observed +at all (the inbound stream is absent while the service starts or stops) is skipped without charging +the staleness window. + Relay fallback (when direct P2P fails): │ ├─ 1. Connect to relay via WebSocket @@ -1387,7 +1419,9 @@ Relay fallback (when direct P2P fails): ### 5.2 Inbound Connection -`chia-sdk-client`'s `Peer` only supports outbound connections. For inbound, we accept TCP/TLS connections and use `Peer::from_websocket()`: +`DigLink::from_websocket()` types the stream as the client-oriented `MaybeTlsStream`, so it cannot +take a server-side TLS stream. For inbound, we accept TCP/TLS connections and use +`DigLink::from_server_websocket()`: ``` Listener bind (GossipService::start, once at startup): @@ -1401,8 +1435,8 @@ Inbound connection (per accepted socket): ├─ 1. TcpListener::accept() ├─ 2. TLS handshake (using chia-ssl certificate) ├─ 3. tokio_tungstenite::accept_async() - ├─ 4. Peer::from_websocket(ws, options) - │ → Returns (Peer, mpsc::Receiver) + ├─ 4. DigLink::from_server_websocket(ws, remote_addr, options) + │ → Returns (DigLink, mpsc::Receiver) ├─ 5. Receive Handshake, validate network_id ├─ 6. Send Handshake response ├─ 7. Wrap in PeerConnection @@ -1640,8 +1674,8 @@ check→insert stays atomic. - **Mutual authentication**: Both sides of every P2P connection present a `chia-ssl` certificate. The connecting peer presents its certificate to the listener, and the listener presents its certificate to the connecting peer. Both sides extract `PeerId = SHA256(remote_certificate_public_key)` from the peer's presented certificate. - **Certificate management**: Exclusively via `chia-ssl`. `ChiaCertificate::generate()` creates new node certificates on first run. `load_ssl_cert()` loads existing certificates on subsequent runs. -- **Outbound mTLS**: `create_native_tls_connector()` or `create_rustls_connector()` from `chia-sdk-client` creates a TLS connector that includes the node's own certificate (client cert) for mutual authentication. This connector is passed to `connect_peer()`. -- **Inbound mTLS**: The TLS acceptor is configured to **request + require** the peer client certificate (matching Chia's [`server.py:67`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/server.py#L67) `ssl_context.verify_mode = ssl.CERT_REQUIRED`). The listener requires the connecting peer to present a certificate; if none is presented, or if the TLS handshake fails, the connection is dropped. Under the `rustls` feature (the production `dig-node` build) the acceptor is a **rustls `ServerConfig`** presenting the node's `chia-ssl` certificate with a **CA-agnostic `ClientCertVerifier`** that requests, requires, and captures the peer certificate but does not validate it against any CA (self-signed peers are expected — see below); proof-of-possession of the peer's private key is still enforced via the TLS CertificateVerify signature. This replaces the `native-tls` acceptor for `rustls` builds because a `[patch.crates-io]` `native-tls` fork does not propagate through a git dependency, which left the stock acceptor **not requesting** the client certificate on OpenSSL/Linux (peer certificate absent → `PeerId` underivable → inbound dropped). The `native-tls` acceptor is retained for `native-tls`-only builds. The captured server-side stream is handed to `Peer::from_server_websocket()` (the server counterpart to `Peer::from_websocket()`, which only types the client `MaybeTlsStream`). +- **Outbound mTLS**: `create_native_tls_connector()` or `create_rustls_connector()` creates a TLS connector that includes the node's own certificate (client cert) for mutual authentication. This connector is used for the `wss://` dial that the `DigLink` is built on. +- **Inbound mTLS**: The TLS acceptor is configured to **request + require** the peer client certificate (matching Chia's [`server.py:67`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/server.py#L67) `ssl_context.verify_mode = ssl.CERT_REQUIRED`). The listener requires the connecting peer to present a certificate; if none is presented, or if the TLS handshake fails, the connection is dropped. Under the `rustls` feature (the production `dig-node` build) the acceptor is a **rustls `ServerConfig`** presenting the node's `chia-ssl` certificate with a **CA-agnostic `ClientCertVerifier`** that requests, requires, and captures the peer certificate but does not validate it against any CA (self-signed peers are expected — see below); proof-of-possession of the peer's private key is still enforced via the TLS CertificateVerify signature. This replaces the `native-tls` acceptor for `rustls` builds because a `[patch.crates-io]` `native-tls` fork does not propagate through a git dependency, which left the stock acceptor **not requesting** the client certificate on OpenSSL/Linux (peer certificate absent → `PeerId` underivable → inbound dropped). The `native-tls` acceptor is retained for `native-tls`-only builds; its `[patch.crates-io]` fork sets `CERT_REQUIRED` plus Chia CA trust on the OpenSSL server acceptor, which upstream `TlsAcceptorBuilder` offers no way to request. The captured server-side stream is handed to `DigLink::from_server_websocket()` (the server counterpart to `DigLink::from_websocket()`, which only types the client `MaybeTlsStream`). - **Peer identity from mTLS**: `PeerId = SHA256(remote_TLS_certificate_public_key)`. Because mTLS guarantees both sides present certificates, each side can derive the other's `PeerId` from the certificate exchanged during the TLS handshake. This binds peer identity to cryptographic key material — impersonation requires possessing the private key. Matches Chia's `peer_node_id` derivation from certificate hash ([`ws_connection.py:95`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/server/ws_connection.py#L95)). - **Self-signed certificates**: Expected (Chia model). Both connector and acceptor use `danger_accept_invalid_certs(true)` / skip CA chain validation — peer identity is verified by `PeerId` hash, not by a certificate authority. The Chia CA cert (`CHIA_CA_CRT` from `chia-ssl`) is used as a root but verification is relaxed for self-signed node certs. - **No fallback**: If mTLS handshake fails for any reason (missing cert, expired cert, corrupt cert), the connection MUST be dropped. There is no fallback to plain WebSocket or server-only TLS. @@ -1651,16 +1685,18 @@ This matches Chia's mTLS design where both client and server present certificate ### 5.4 Rate Limiting -Uses `chia-sdk-client::RateLimiter` for the Chia bound, composed with dig-gossip's own -`DigRateLimiter` for the DIG per-opcode bound (dig_ecosystem#2228): +Uses `dig_peer_protocol::OpcodeRateLimiter` for the Chia bound, composed with dig-gossip's own +`DigRateLimiter` for the DIG per-opcode bound (dig_ecosystem#2228). Both are keyed by the raw +wire opcode, so no rate-limit decision names `ProtocolMessageTypes`: ```rust -// Outbound: RateLimiter is built into Peer::send_raw() -// (it loops with 1s sleep until rate limit clears) +// Outbound: rate limiting is built into DigLink's send path +// (it waits for budget, up to LinkOptions::budget_timeout) // Inbound: create a separate admission gate for each connection. It composes the Chia -// RateLimiter (V2_RATE_LIMITS, keyed by ProtocolMessageTypes) with DigRateLimiter -// (dig_extension_rate_limits_map(), keyed by the raw opcode byte) behind one lock. +// bound (OpcodeRateLimiter over OpcodeRateLimits, i.e. V2_RATE_LIMITS re-keyed by opcode) +// with DigRateLimiter (dig_extension_rate_limits_map(), keyed by the raw opcode byte) +// behind one lock. let inbound_limiter = InboundRateLimiter::new(config.peer_options.rate_limit_factor); // For DIG extension messages, extend V2_RATE_LIMITS with additional entries @@ -1682,9 +1718,9 @@ When a received frame is rejected by the per-connection inbound cap, the forward ### 6.1 Overview -Uses `chia-sdk-client::Network::lookup_all()` for DNS resolution. The discovery loop and address manager are ported from Chia Python. +Uses the re-exported `Network::lookup_all()` for DNS resolution. The discovery loop and address manager are ported from Chia Python. -### 6.2 DNS Seeding (reuses `chia-sdk-client::Network`) +### 6.2 DNS Seeding (reuses the re-exported `Network`) ```rust let network = Network { @@ -2026,12 +2062,11 @@ map and `PlumtreeState`) MUST release both locks before step 5's per-peer send l Neither lock may be held across a `send_raw`/`send_protocol_message(...).await` point — a `std::sync::MutexGuard` held across an await is `!Send`, so `GossipHandle::broadcast`'s future would itself become non-`Send`, breaking `tokio::spawn`-ability. `dig-gossip`'s implementation -satisfies this today. Each eager send clones the outbound `Message` body (a `Vec`-backed -type from the vendored `chia-protocol` crate, not reference-counted) — this is an accepted O(N) -per-broadcast cost proportional to the eager fan-out (bounded by `GossipConfig::gossip_fanout`, -default 8), not a growth-over-time or attacker-amplifiable vector; eliminating it would require -changing the vendored wire `Message` type to a refcounted buffer, which is out of scope for this -crate (see `vendor/` policy — thin wrapper, never fork upstream types). +satisfies this today. Each eager send clones the outbound `DigMessage` body (a `Vec`-backed +`dig_peer_protocol::Bytes`, not reference-counted) — this is an accepted O(N) per-broadcast cost +proportional to the eager fan-out (bounded by `GossipConfig::gossip_fanout`, default 8), not a +growth-over-time or attacker-amplifiable vector; eliminating it would require changing the wire +envelope to a refcounted buffer in `dig-peer-protocol`, which is out of scope for this crate. **On receiving a message via eager push:** @@ -2219,9 +2254,9 @@ pub enum MessagePriority { ```rust struct PriorityOutbound { - critical: VecDeque, // Drained first, always - normal: VecDeque, // Drained when critical is empty - bulk: VecDeque, // Drained when both above are empty + critical: VecDeque, // Drained first, always + normal: VecDeque, // Drained when critical is empty + bulk: VecDeque, // Drained when both above are empty } // Drain order: exhaust critical → exhaust normal → one bulk message → check critical again @@ -2312,9 +2347,11 @@ broadcast. INT-016 tests assert every opcode routes by its declared strategy. `route_dig_message` is not only the routing map; it is the **live per-opcode dispatch authority**. The two `GossipHandle` entry points below are the ONLY sanctioned way to put a `200..=219` opcode on the wire — a caller MUST NOT hand-frame a DIG opcode and call `broadcast` / `send_directed_message` -directly. Both frame the opcode through the single encoder `frame_dig_message` (which mirrors each -`DigMessageType` discriminant onto the vendored `ProtocolMessageTypes`, so a stock `Message` carries -the DIG opcode) and then dispatch by strategy: +directly. Both frame the opcode through the single encoder `frame_dig_message`, which writes the +`DigMessageType` discriminant directly into `DigMessage::msg_type` as a raw `u8`. No Chia enum is +consulted or extended: `ProtocolMessageTypes` is a closed `#[repr(u8)]` enum that cannot name a DIG +opcode, and the raw-byte envelope is what makes the 200-222 band expressible without forking it. +Dispatch then proceeds by strategy: | Strategy (opcodes) | Entry point | Behaviour | Wrong entry point | |--------------------|-------------|-----------|-------------------| @@ -2332,27 +2369,27 @@ opcode's dispatch outcome-class equals its `route_dig_message` classification (t ### 9.1 Crate Boundary -`dig-gossip` is a **library crate** (`lib`). It wraps `chia-sdk-client` and `chia-protocol` to provide a gossip layer. It does **not** include block validation, CLVM, mempool, coinstate, or consensus. +`dig-gossip` is a **library crate** (`lib`). It wraps `dig-peer-protocol` (and, through it, the Chia crates) to provide a gossip layer. It does **not** include block validation, CLVM, mempool, coinstate, or consensus. **Input**: `chia-protocol::Message` (or typed `T: Streamable + ChiaProtocolMessage`) via `broadcast()` / `send_to()`. -**Output**: `(PeerId, chia-protocol::Message)` via inbound channel receiver. +**Output**: `(PeerId, dig_peer_protocol::DigMessage)` via inbound channel receiver. ### 9.2 What dig-gossip Implements vs Reuses | Component | Source | dig-gossip Role | |-----------|--------|----------------| | Wire protocol types | `chia-protocol` | **Reuse** (re-export) | -| Peer connection (WebSocket + TLS) | `chia-sdk-client::Peer` | **Reuse** | -| Handshake flow | `chia-sdk-client::connect_peer()` | **Reuse** | -| Rate limiting | `chia-sdk-client::RateLimiter` (Chia bound) + `DigRateLimiter` (DIG per-opcode bound) | **Reuse** the Chia table; the DIG table is dig-gossip's own | -| TLS certificates | `chia-ssl` + `chia-sdk-client` TLS utils | **Reuse** | -| DNS resolution | `chia-sdk-client::Network` | **Reuse** | -| Ban/trust management | `chia-sdk-client::ClientState` | **Reuse** + extend with reputation | +| Peer connection (WebSocket + TLS) | `dig_peer_protocol::DigLink` | **Reuse** | +| Handshake flow | `chia-protocol::Handshake` over the raw WebSocket, then `DigLink` | **Reuse** the wire struct; the flow is dig-gossip's own (it must capture the SPKI DER) | +| Rate limiting | `dig_peer_protocol::OpcodeRateLimiter` (Chia bound) + `DigRateLimiter` (DIG per-opcode bound) | **Reuse** the Chia table; the DIG table is dig-gossip's own | +| TLS certificates | `chia-ssl` + the re-exported TLS utils | **Reuse** | +| DNS resolution | the re-exported `Network` | **Reuse** | +| Ban/trust management | the re-exported `ClientState` | **Reuse** + extend with reputation | | Serialization | `chia-traits::Streamable` | **Reuse** | | Address manager | Chia Python `address_manager.py` | **Port to Rust** (no crate exists) | | Discovery loop | Chia Python `node_discovery.py` | **Port to Rust** (no crate exists) | | Introducer peers | Chia Python `introducer_peers.py` | **Port to Rust** (no crate exists) | -| Inbound connection listener | New | **Implement** (`Peer::from_websocket` exists) | +| Inbound connection listener | New | **Implement** (`DigLink::from_server_websocket` exists) | | Relay fallback | `l2_driver_state_channel` | **Port/adapt** | | Introducer registration | `l2_driver_state_channel` | **Port/adapt** | | Plumtree structured gossip | New (based on Leitão et al., 2007) | **Implement** | @@ -2388,7 +2425,7 @@ dig-gossip/ │ ├── types/ │ │ ├── mod.rs # Re-exports │ │ ├── peer.rs # PeerId (alias), PeerInfo (with get_group/get_key), -│ │ │ # PeerConnection (wraps chia-sdk-client::Peer) +│ │ │ # PeerConnection (wraps dig_peer_protocol::DigLink) │ │ ├── config.rs # GossipConfig, IntroducerConfig, RelayConfig │ │ ├── stats.rs # GossipStats, RelayStats │ │ ├── reputation.rs # PeerReputation, PenaltyReason @@ -2404,8 +2441,8 @@ dig-gossip/ │ │ │ ├── connection/ │ │ ├── mod.rs -│ │ └── listener.rs # TcpListener + TLS accept + Peer::from_websocket() -│ │ # (chia-sdk-client::Peer handles the rest) +│ │ └── listener.rs # TcpListener + TLS accept + DigLink::from_server_websocket() +│ │ # (DigLink handles the rest) │ │ │ ├── discovery/ │ │ ├── mod.rs @@ -2445,7 +2482,7 @@ dig-gossip/ │ └── latency.rs # RTT tracker, peer scoring │ └── tests/ - ├── connection_tests.rs # Handshake via connect_peer(), lifecycle + ├── connection_tests.rs # Handshake + DigLink upgrade, lifecycle ├── discovery_tests.rs # Address manager, AS diversity, introducer, DNS ├── plumtree_tests.rs # Eager/lazy push, tree formation, self-healing ├── compact_block_tests.rs # Encoding, decoding, mempool reconstruction @@ -2468,24 +2505,25 @@ dig-gossip/ // Re-exports from Chia crates (NOT reimplemented) // ========================================================================= pub use chia_protocol::{ - Bytes32, Handshake, Message, NodeType, ProtocolMessageTypes, + Bytes32, Handshake, NewPeak, NewTransaction, RequestTransaction, RespondTransaction, RequestBlock, RespondBlock, RejectBlock, RequestBlocks, RespondBlocks, RejectBlocks, NewUnfinishedBlock, RequestUnfinishedBlock, RespondUnfinishedBlock, RequestMempoolTransactions, RequestPeers, RespondPeers, - RequestPeersIntroducer, RespondPeersIntroducer, SpendBundle, FullBlock, TimestampedPeerInfo, - ChiaProtocolMessage, }; -pub use chia_sdk_client::{ - Peer, PeerOptions, Client, ClientState, Network, - RateLimiter, RateLimits, RateLimit, V2_RATE_LIMITS, +pub use dig_peer_protocol::{ + Bytes, DigLink, DigMessage, LinkError, LinkOptions, + NodeType, ProtocolMessageTypes, + OpcodeRateLimiter, OpcodeRateLimits, + // Re-exported Chia surface + Client, ClientState, Network, + RateLimits, RateLimit, V2_RATE_LIMITS, ClientError, load_ssl_cert, + ChiaCertificate, Streamable, }; -pub use chia_ssl::ChiaCertificate; -pub use chia_traits::Streamable; // ========================================================================= // DIG-specific types (implemented in this crate) @@ -2495,6 +2533,8 @@ pub use types::config::{GossipConfig, IntroducerConfig, RelayConfig}; pub use types::stats::{GossipStats, RelayStats}; pub use types::reputation::{PeerReputation, PenaltyReason}; pub use types::dig_messages::DigMessageType; +pub use discovery::introducer_register_wire::{RegisterPeer, RegisterAck}; +pub use discovery::introducer_wire::{RequestPeersIntroducer, RespondPeersIntroducer}; pub use service::gossip_service::GossipService; pub use service::gossip_handle::GossipHandle; @@ -2514,8 +2554,8 @@ pub use constants::*; ```toml [features] default = ["native-tls", "relay", "erlay", "compact-blocks", "dandelion"] -native-tls = ["chia-sdk-client/native-tls"] # native-tls outbound + inbound acceptor -rustls = ["chia-sdk-client/rustls", "dep:rustls", "dep:tokio-rustls", "dep:rustls-pemfile"] # rustls outbound + inbound acceptor (#1371) +native-tls = ["dig-peer-protocol/native-tls", "dep:native-tls", "dep:tokio-native-tls"] # native-tls outbound + inbound acceptor +rustls = ["dig-peer-protocol/rustls", "dep:rustls", "dep:tokio-rustls", "dep:rustls-pemfile"] # rustls outbound + inbound acceptor (#1371) relay = [] # Relay fallback + NAT traversal support erlay = ["minisketch-rs"] # ERLAY-style transaction relay with set reconciliation compact-blocks = ["siphasher"] # Compact block relay (BIP 152 equivalent) @@ -2527,11 +2567,16 @@ tor = ["arti-client", "tokio-socks"] # Tor/SOCKS5 proxy transport (o ```toml [dependencies] -# Chia crates (direct reuse) +# The DIG peer wire — the single path to the Chia crates. +dig-peer-protocol = { version = "0.4", default-features = false } + +# Chia crates named directly only because `chia_streamable_macro` reads Cargo.toml +# for them and generates `chia_protocol::` paths at compile time. Code imports these +# types through `dig-peer-protocol`, never from these entries. chia-protocol = "0.26" -chia-sdk-client = { version = "0.28", features = ["native-tls"] } -chia-ssl = "0.26" chia-traits = "0.26" +chia-sha2 = "0.26" +chia_streamable_macro = "0.26" # Async runtime tokio = { version = "1", features = ["full"] } @@ -2577,7 +2622,7 @@ minisketch-rs = "0.2" ### 11.2 Integration Tests -- **connect_peer() integration**: connect two nodes using `chia-sdk-client::connect_peer()`, verify handshake with DIG `network_id`. +- **Outbound connect integration**: connect two nodes through the outbound module, verify handshake with DIG `network_id`. - **Peer::request_infallible() for RequestPeers**: verify `RespondPeers` round-trip. - **Plumtree three-node gossip**: broadcast from A, B receives via eager, C receives via lazy→pull. Verify tree forms and self-heals. - **Plumtree tree optimization**: verify that after initial convergence, eager peers are low-latency and redundant paths are pruned. @@ -2595,7 +2640,7 @@ minisketch-rs = "0.2" ### 11.3 Benchmark Tests -- **Message throughput**: messages/second through `chia-sdk-client::Peer` (baseline from Chia crate). +- **Message throughput**: messages/second through `DigLink`. - **Plumtree vs flood bandwidth**: measure total bytes transferred across 50-node network for 1000 messages. Target: Plumtree < 40% of naive flood. - **Compact block vs full block**: measure bytes and latency for block propagation across 10 hops. Target: compact block < 10% bandwidth of full block. - **ERLAY vs flood tx relay**: measure bytes per transaction across 50-connection node. Target: ERLAY < 20% of flood. diff --git a/src/connection/chia_opcodes.rs b/src/connection/chia_opcodes.rs new file mode 100644 index 0000000..42671e7 --- /dev/null +++ b/src/connection/chia_opcodes.rs @@ -0,0 +1,56 @@ +//! Wire opcode bytes for the Chia full-node messages dig-gossip exchanges. +//! +//! # Why these exist +//! +//! [`DigMessage::msg_type`](dig_peer_protocol::DigMessage) is a raw wire byte rather +//! than an enum, because the DIG opcode band (200-222) has no `ProtocolMessageTypes` +//! variant to name it — that mismatch is exactly what the vendored `chia-protocol` +//! fork used to paper over (dig_ecosystem#2228). +//! +//! Chia-band traffic still needs its opcodes, so they are derived here from +//! `chia_protocol::ProtocolMessageTypes` rather than written as literals. That keeps +//! `chia-protocol` the single authority for Chia opcode numbering: if upstream ever +//! renumbers one, these follow automatically instead of silently disagreeing. + +use chia_protocol::ProtocolMessageTypes; + +/// Wire opcode of a `Handshake` frame — the first message of every session. +pub(crate) const HANDSHAKE: u8 = ProtocolMessageTypes::Handshake as u8; + +/// Wire opcode of a `RequestPeers` frame. +pub(crate) const REQUEST_PEERS: u8 = ProtocolMessageTypes::RequestPeers as u8; + +/// Wire opcode of a `RespondPeers` frame. +pub(crate) const RESPOND_PEERS: u8 = ProtocolMessageTypes::RespondPeers as u8; + +#[cfg(test)] +mod tests { + use super::{HANDSHAKE, REQUEST_PEERS, RESPOND_PEERS}; + + /// Each constant equals the single byte `Streamable` puts on the wire for its + /// enum variant. + /// + /// The `as u8` cast and the `Streamable` encoding are two independent paths to + /// the same number; pinning them against each other is what makes these + /// constants trustworthy, since `DigLink` frames Chia bodies via the + /// `Streamable` path while dig-gossip's raw pre-link phase compares against the + /// cast. A divergence would desynchronise the two halves of one handshake. + #[test] + fn constants_match_the_streamable_encoding() { + use chia_protocol::ProtocolMessageTypes as P; + use chia_traits::Streamable; + + for (name, variant, constant) in [ + ("Handshake", P::Handshake, HANDSHAKE), + ("RequestPeers", P::RequestPeers, REQUEST_PEERS), + ("RespondPeers", P::RespondPeers, RESPOND_PEERS), + ] { + let encoded = variant.to_bytes().expect("opcode encodes"); + assert_eq!(encoded.len(), 1, "{name} encodes to exactly one byte"); + assert_eq!( + encoded[0], constant, + "{name} constant matches the wire byte" + ); + } + } +} diff --git a/src/connection/dial_error.rs b/src/connection/dial_error.rs new file mode 100644 index 0000000..b83cb51 --- /dev/null +++ b/src/connection/dial_error.rs @@ -0,0 +1,148 @@ +//! [`DialError`] — the two-armed error a dial can fail with, kept apart because the +//! two arms carry **opposite retry semantics**. +//! +//! A dial has two failure kinds and they are not interchangeable: +//! +//! * **Transport** — the peer was never reached, or the pipe broke: connection refused, +//! TLS failure, a timeout, a framing error. Retrying the same address is reasonable. +//! * **Policy** — the peer *was* reached, it sent a [`Handshake`](chia_protocol::Handshake), +//! and we rejected it: wrong `network_id`, wrong `node_type`, an incompatible protocol +//! version. Retrying the same address changes nothing; the peer is simply not one of ours. +//! +//! [`dig_peer_protocol`] already models that split as two types — +//! [`LinkError`] for the transport and [`ClientError`] for the client-side policy verdict — +//! and [`GossipError`](crate::GossipError) keeps them as two variants. `DialError` is the +//! union a dial function returns so it can report either one **without downgrading**: before +//! it existed, the outbound leg had only `LinkError` available and rendered every policy +//! rejection as `LinkError::Io(_)` carrying a formatted string, which erased the typed +//! [`ClientError::WrongNetwork`] a caller needs to tell "not our network" from "host is down". +//! +//! Both legs of the handshake now agree: the inbound listener returns [`ClientError`] for a +//! policy rejection (`listener::negotiate_inbound_over_ws`) and so does the outbound dial, so +//! an identical rejection surfaces as the same [`GossipError`] variant whoever dialled. +//! +//! Specified by **CON-002** (inbound handshake) and **CON-003** (handshake validation), both of +//! which require `GossipError::ClientError` for a rejection, and by **API-004**, which records the +//! opposite retry semantics of the two arms. + +use chia_protocol::ProtocolMessageTypes; +use dig_peer_protocol::{ClientError, LinkError, Streamable}; +use thiserror::Error; + +use crate::error::GossipError; + +/// A dial failed either in the transport or on handshake policy — see the module docs for +/// why the two are kept apart. +#[derive(Debug, Error)] +pub enum DialError { + /// The peer was reached and rejected on policy (or a client-side TLS/certificate step + /// failed). Surfaces as [`GossipError::ClientError`]; **not** worth retrying. + #[error("client error: {0}")] + Client(#[from] ClientError), + + /// The transport itself failed: refused, timed out, broken framing. Surfaces as + /// [`GossipError::LinkError`]; retrying the same address may succeed. + #[error("link error: {0}")] + Link(#[from] LinkError), + + /// The peer answered the dial with an opcode that has **no** `ProtocolMessageTypes` variant, + /// so the rejection cannot be expressed as a typed [`ClientError`]. + /// + /// This is the [`Client`](DialError::Client) arm in every respect but the type: the peer was + /// reached and rejected on content, so it is **policy**, not transport. It exists because + /// [`ClientError::InvalidResponse`] takes `ProtocolMessageTypes` and this crate must not + /// launder an unmappable opcode through a formatted string — the exact downgrade the module + /// docs above condemn. + #[error("expected a Handshake, found unknown opcode {0}")] + UnknownOpcode(u8), +} + +/// Classify "the first frame after connect was not a `Handshake`" as the **policy** rejection it +/// is: the peer was reached and rejected on content, so re-dialling the same address meets the +/// same behaviour. +/// +/// The typed [`ClientError::InvalidResponse`] is preferred whenever `opcode` names a known +/// [`ProtocolMessageTypes`]; anything outside that enum — a DIG-band opcode, a garbage byte — +/// takes [`DialError::UnknownOpcode`], which carries the raw byte rather than a rendered string. +/// +/// `ProtocolMessageTypes` is a `#[repr(u8)]` `Streamable` enum with no `TryFrom`, so the +/// single-byte decode IS the total mapping from opcode to variant. +#[must_use] +pub(crate) fn non_handshake_first_frame(opcode: u8) -> DialError { + match ProtocolMessageTypes::from_bytes(&[opcode]) { + Ok(found) => DialError::Client(ClientError::InvalidResponse( + vec![ProtocolMessageTypes::Handshake], + found, + )), + Err(_) => DialError::UnknownOpcode(opcode), + } +} + +impl From for GossipError { + fn from(error: DialError) -> Self { + match error { + DialError::Client(e) => Self::from(e), + DialError::Link(e) => Self::from(e), + // Policy, and the raw byte is the whole diagnostic — so it keeps its own variant + // rather than being flattened into a `ClientError` that cannot name the opcode. + DialError::UnknownOpcode(op) => Self::UnknownHandshakeOpcode(op), + } + } +} + +impl From for DialError { + /// A handshake-validation verdict is policy by definition, so it always takes the + /// [`Client`](DialError::Client) arm — preserving typed variants such as + /// [`ClientError::WrongNetwork`]. + fn from(error: crate::connection::handshake::HandshakeValidationError) -> Self { + Self::Client(ClientError::from(error)) + } +} + +#[cfg(test)] +mod tests { + //! #2228 — a wrong first frame is POLICY. The two cases are pinned from BOTH sides so the + //! classification cannot silently collapse into one arm. + + use super::*; + use crate::connection::chia_opcodes; + use crate::types::dig_messages::DigMessageType; + + /// A Chia-band opcode maps to a `ProtocolMessageTypes`, so the rejection is expressible as the + /// typed `ClientError` — never a `LinkError`, which would tell a caller to retry the address. + #[test] + fn a_known_opcode_is_a_typed_client_rejection() { + let err = non_handshake_first_frame(chia_opcodes::REQUEST_PEERS); + match err { + DialError::Client(ClientError::InvalidResponse(expected, found)) => { + assert_eq!(expected, vec![ProtocolMessageTypes::Handshake]); + assert_eq!(found, ProtocolMessageTypes::RequestPeers); + } + other => panic!("expected a typed client rejection, got {other:?}"), + } + } + + /// A DIG-band opcode has no `ProtocolMessageTypes` variant. It must still be policy, and it + /// must carry the raw byte — the reason this arm exists instead of a formatted string. + #[test] + fn an_unmappable_opcode_keeps_the_raw_byte_and_stays_policy() { + let opcode = DigMessageType::NewAttestation as u8; + assert!( + ProtocolMessageTypes::from_bytes(&[opcode]).is_err(), + "the fixture must be OUTSIDE ProtocolMessageTypes or it proves nothing" + ); + + let err = non_handshake_first_frame(opcode); + assert!( + matches!(err, DialError::UnknownOpcode(op) if op == opcode), + "expected the raw opcode to survive, got {err:?}" + ); + assert!( + matches!( + GossipError::from(err), + GossipError::UnknownHandshakeOpcode(op) if op == opcode + ), + "an unmappable opcode must not be laundered into a transport error" + ); + } +} diff --git a/src/connection/dig_rate_limiter.rs b/src/connection/dig_rate_limiter.rs index c5ee781..f4bdb6d 100644 --- a/src/connection/dig_rate_limiter.rs +++ b/src/connection/dig_rate_limiter.rs @@ -18,9 +18,9 @@ //! ## Relationship to the Chia bound //! //! This limiter never *replaces* Chia's; it only ever adds a restriction. The composed inbound -//! gate ([`InboundRateLimiter`](super::inbound_limits::InboundRateLimiter)) applies -//! [`RateLimiter::handle_message`](dig_peer_protocol::RateLimiter::handle_message) first and -//! unconditionally, so a DIG opcode with no row is still bounded by Chia's `default_settings`. +//! gate ([`InboundRateLimiter`](super::inbound_limits::InboundRateLimiter)) applies the Chia base +//! bound first and unconditionally, so a DIG opcode with no row is still bounded by Chia's +//! `default_settings`. //! //! ## Normative trace //! @@ -149,7 +149,9 @@ mod tests { use std::{thread::sleep, time::Duration}; - use dig_peer_protocol::{Bytes, Message, ProtocolMessageTypes, RateLimiter, V2_RATE_LIMITS}; + use dig_peer_protocol::{ + Bytes, DigMessage, OpcodeRateLimiter, OpcodeRateLimits, ProtocolMessageTypes, Streamable, + }; use super::*; @@ -186,14 +188,16 @@ mod tests { #[test] fn window_boundary_is_absolute_and_shared_with_the_chia_limiter() { const RESET: u64 = 2; - - let mut chia_limits = (*V2_RATE_LIMITS).clone(); - chia_limits.other.insert( - ProtocolMessageTypes::Handshake, - RateLimit::new(1.0, 1_000_000.0, None), - ); - let handshake = || Message { - msg_type: ProtocolMessageTypes::Handshake, + /// The `Handshake` row in Chia's published `V2_RATE_LIMITS`: five frames per window. + const HANDSHAKE_FREQ: u32 = 5; + + let handshake_opcode = *ProtocolMessageTypes::Handshake + .to_bytes() + .expect("ProtocolMessageTypes is a single-byte streamable enum") + .first() + .expect("its encoding is exactly one byte"); + let handshake = || DigMessage { + msg_type: handshake_opcode, id: None, data: Bytes::new(vec![0u8; 10]), }; @@ -202,14 +206,16 @@ mod tests { let opening_period = current_period(RESET); // Constructed at the START of the window. - let mut chia = RateLimiter::new(true, RESET, 1.0, chia_limits); + let mut chia = OpcodeRateLimiter::new(RESET, 1.0, OpcodeRateLimits::default()); // Constructed ~1 s LATER, in the SAME window — the stagger is the discriminator. sleep(Duration::from_secs(1)); let mut dig = limiter_of(true, RESET, 1.0); // Exhaust both within this window. - assert!(chia.handle_message(&handshake())); - assert!(!chia.handle_message(&handshake()), "chia half exhausted"); + for i in 0..HANDSHAKE_FREQ { + assert!(chia.allow(&handshake()), "chia frame {i} is within its row"); + } + assert!(!chia.allow(&handshake()), "chia half exhausted"); for _ in 0..FREQ as u32 { assert!(dig.check(221, 1)); } @@ -222,13 +228,13 @@ mod tests { opening_period, "test lost its race with the window boundary; the bounds below would be vacuous" ); - assert!(!chia.handle_message(&handshake())); + assert!(!chia.allow(&handshake())); assert!(!dig.check(221, 1)); // Cross the shared absolute boundary. wait_for_window_start(RESET); assert!( - chia.handle_message(&handshake()), + chia.allow(&handshake()), "chia tallies must clear on the absolute boundary" ); assert!( diff --git a/src/connection/handshake.rs b/src/connection/handshake.rs index 30ac167..95f593f 100644 --- a/src/connection/handshake.rs +++ b/src/connection/handshake.rs @@ -9,11 +9,13 @@ //! //! ## SPEC traceability //! -//! - **SPEC §5.1 step 3** — outbound: `connect_peer()` “receives and validates Handshake response”. +//! - **SPEC §5.1 step 3** — outbound: the dial +//! ([`connect_outbound_peer`](crate::connection::outbound::connect_outbound_peer)) “receives and +//! validates Handshake response”. //! - **SPEC §5.2 step 5** — inbound: “Receive Handshake, validate `network_id`.” -//! - **SPEC §1.5 #1** — capabilities negotiated via `chia-protocol::Handshake` (`connect_peer()` -//! sends capabilities list). Validation here ensures the remote meets DIG compatibility. -//! - **SPEC §1.5 #7** — `connect_peer()` rejects peers with mismatched `network_id`. +//! - **SPEC §1.5 #1** — capabilities negotiated via `chia-protocol::Handshake` (the outbound dial +//! sends the capabilities list). Validation here ensures the remote meets DIG compatibility. +//! - **SPEC §1.5 #7** — the outbound dial rejects peers with mismatched `network_id`. //! - **SPEC §1.4** — `Handshake` type used directly from `chia-protocol` (not redefined). //! //! ## Normative trace @@ -40,8 +42,8 @@ #![allow(clippy::result_large_err)] +use chia_protocol::Handshake; use dig_peer_protocol::ClientError; -use dig_peer_protocol::Handshake; use thiserror::Error; use unicode_general_category::{get_general_category, GeneralCategory}; @@ -172,6 +174,18 @@ impl From for ClientError { } } +// There is deliberately NO `From for LinkError`. +// +// `LinkError` has no wrong-network variant, so such an impl could only render the verdict as +// `LinkError::Io(String)` — and that lossy conversion is exactly the defect this crate already +// shipped once: the outbound dial reached for it via `?`, a caller lost the typed +// `ClientError::WrongNetwork`, and telling "not our network" (never retry) from "host is down" +// (retry) came down to matching on an error string. +// +// A handshake verdict is policy by definition, so it has exactly one home: +// `From for DialError` takes the `Client` arm. Keeping the `LinkError` +// route absent means the compiler, not review, is what stops the downgrade coming back. + /// Validate `their_handshake` against our expected network id string (hex genesis id from /// [`crate::connection::outbound::network_id_handshake_string`]). /// @@ -215,3 +229,119 @@ pub fn validate_remote_handshake( } Ok(sanitized) } + +// ============================================================================ +// NodeType bridge — the chia/DIG boundary +// ============================================================================ + +/// Translate the `node_type` carried on a Chia [`Handshake`] into the DIG role enum. +/// +/// # Why a bridge and not a cast +/// +/// `Handshake` is a Chia full-node message, so its `node_type` is +/// `chia_protocol::NodeType`; every DIG-side surface (peer records, introducer +/// registration, SPEC §6.5) speaks [`dig_peer_protocol::NodeType`]. The two enums +/// enumerate the same seven roles with the same wire discriminants `1..=7`, but they +/// are distinct Rust types, and an `as`-cast between them would silently paper over +/// any future divergence in either crate. +/// +/// # Why this is total +/// +/// Both are closed Rust enums, so a value of either type is necessarily one of the +/// seven roles — there is no unknown-discriminant case to handle, and therefore no +/// temptation to default one. (An unparseable byte is rejected earlier, when +/// `Handshake` itself is decoded.) The exhaustive match means adding a role to +/// either crate breaks the build here rather than silently mapping to a wrong role. +#[must_use] +pub fn dig_node_type_of(node_type: chia_protocol::NodeType) -> dig_peer_protocol::NodeType { + match node_type { + chia_protocol::NodeType::FullNode => dig_peer_protocol::NodeType::FullNode, + chia_protocol::NodeType::Harvester => dig_peer_protocol::NodeType::Harvester, + chia_protocol::NodeType::Farmer => dig_peer_protocol::NodeType::Farmer, + chia_protocol::NodeType::Timelord => dig_peer_protocol::NodeType::Timelord, + chia_protocol::NodeType::Introducer => dig_peer_protocol::NodeType::Introducer, + chia_protocol::NodeType::Wallet => dig_peer_protocol::NodeType::Wallet, + chia_protocol::NodeType::DataLayer => dig_peer_protocol::NodeType::DataLayer, + } +} + +/// Translate a DIG role into the `node_type` a Chia [`Handshake`] carries. +/// +/// The exact inverse of [`dig_node_type_of`]; see that function for why the two +/// enums need a bridge at all and why both directions are total. +#[must_use] +pub fn chia_node_type_of(node_type: dig_peer_protocol::NodeType) -> chia_protocol::NodeType { + match node_type { + dig_peer_protocol::NodeType::FullNode => chia_protocol::NodeType::FullNode, + dig_peer_protocol::NodeType::Harvester => chia_protocol::NodeType::Harvester, + dig_peer_protocol::NodeType::Farmer => chia_protocol::NodeType::Farmer, + dig_peer_protocol::NodeType::Timelord => chia_protocol::NodeType::Timelord, + dig_peer_protocol::NodeType::Introducer => chia_protocol::NodeType::Introducer, + dig_peer_protocol::NodeType::Wallet => chia_protocol::NodeType::Wallet, + dig_peer_protocol::NodeType::DataLayer => chia_protocol::NodeType::DataLayer, + } +} + +#[cfg(test)] +mod node_type_bridge_tests { + use super::{chia_node_type_of, dig_node_type_of}; + + /// Every DIG role round-trips through the Chia enum and back to itself, and + /// lands on the same wire byte in both representations. + /// + /// Asserting the byte as well as the round-trip is what makes this test + /// load-bearing: a bridge that mapped two roles onto each other consistently + /// in both directions would round-trip perfectly while putting the wrong + /// discriminant on the wire. + #[test] + fn node_type_bridge_covers_every_role() { + use dig_peer_protocol::NodeType as Dig; + + let roles = [ + Dig::FullNode, + Dig::Harvester, + Dig::Farmer, + Dig::Timelord, + Dig::Introducer, + Dig::Wallet, + Dig::DataLayer, + ]; + assert_eq!(roles.len(), 7, "all seven roles are covered"); + + for role in roles { + let chia = chia_node_type_of(role); + assert_eq!( + chia as u8, + role.to_byte(), + "{role:?} must occupy the same wire byte in both enums" + ); + assert_eq!(dig_node_type_of(chia), role, "{role:?} round-trips"); + } + } + + /// The bridge is a bijection: mapping DIG -> Chia -> DIG returns the original + /// role for all seven, so no two roles collapse onto one. + #[test] + fn node_type_bridge_is_a_bijection() { + use dig_peer_protocol::NodeType as Dig; + + let roles = [ + Dig::FullNode, + Dig::Harvester, + Dig::Farmer, + Dig::Timelord, + Dig::Introducer, + Dig::Wallet, + Dig::DataLayer, + ]; + let mapped: Vec = roles.iter().map(|r| chia_node_type_of(*r) as u8).collect(); + let mut unique = mapped.clone(); + unique.sort_unstable(); + unique.dedup(); + assert_eq!( + unique.len(), + roles.len(), + "no two DIG roles share a Chia role" + ); + } +} diff --git a/src/connection/inbound_limits.rs b/src/connection/inbound_limits.rs index bfd1ead..ed1d1cb 100644 --- a/src/connection/inbound_limits.rs +++ b/src/connection/inbound_limits.rs @@ -1,4 +1,4 @@ -//! CON-005 — per-connection **inbound** rate limits on top of [`V2_RATE_LIMITS`](dig_peer_protocol::V2_RATE_LIMITS). +//! CON-005 — per-connection **inbound** rate limits on top of Chia's `V2_RATE_LIMITS`. //! //! ## Normative trace //! @@ -9,9 +9,9 @@ //! ## Outbound vs inbound //! //! Outbound sends go through [`dig_peer_protocol::Peer::send_raw`] which already applies -//! [`RateLimiter`] with `incoming = false` (CON-005 acceptance: *no custom outbound implementation*). +//! [`OpcodeRateLimiter`] with no inbound flag (CON-005 acceptance: *no custom outbound implementation*). //! Inbound frames are delivered on the per-connection `mpsc` from [`Peer::from_websocket`]; **DIG** -//! enforces [`RateLimiter::handle_message`] here **before** forwarding to the broadcast hub. +//! enforces [`OpcodeRateLimiter::allow`] here **before** forwarding to the broadcast hub. //! //! ## DIG wire types (the `dig_extension_rate_limits_map` table) //! @@ -19,9 +19,9 @@ //! [`ProtocolMessageTypes`] variants in `chia-protocol` 0.26, so they cannot appear in //! [`dig_peer_protocol::RateLimits`] `tx` / `other` maps. But the **220-band** opcodes — //! `StoreMelted` = 221 (#1316), `HoldingsAnnounce` = 222 (#1720) — ARE `ProtocolMessageTypes` -//! variants and DO arrive on the live wire as Chia [`Message`] values. Either way their bound is a +//! variants and DO arrive on the live wire as Chia [`DigMessage`] values. Either way their bound is a //! DIG bound, keyed by the raw opcode byte in [`dig_extension_rate_limits_map`] and enforced by -//! [`DigRateLimiter`] — which [`RateLimiter::handle_message`] knows nothing about. +//! [`DigRateLimiter`] — which [`OpcodeRateLimiter::allow`] knows nothing about. //! //! [`InboundRateLimiter`] closes that gap: the live forwarders admit frames through it (not through //! `handle_message` directly), and for 220-band frames it additionally requires the @@ -35,7 +35,7 @@ use std::collections::HashMap; -use dig_peer_protocol::{Message, ProtocolMessageTypes, RateLimit, RateLimiter, V2_RATE_LIMITS}; +use dig_peer_protocol::{DigMessage, OpcodeRateLimiter, OpcodeRateLimits, RateLimit}; use super::dig_rate_limiter::DigRateLimiter; use crate::types::dig_messages::DigMessageType; @@ -50,8 +50,8 @@ const RESET_SECONDS: u64 = 60; /// The first opcode of the DIG 220..=255 wire band. /// /// Opcodes in this band (e.g. `StoreMelted` = 221 (#1316), `HoldingsAnnounce` = 222 (#1720)) ARE -/// `chia_protocol::ProtocolMessageTypes` variants — so they arrive as real Chia [`Message`] values — -/// but their bound is a DIG bound that [`RateLimiter::handle_message`] never reads. The live +/// `chia_protocol::ProtocolMessageTypes` variants — so they arrive as real Chia [`DigMessage`] values — +/// but their bound is a DIG bound that [`OpcodeRateLimiter::allow`] never reads. The live /// ingress gate therefore has to consult [`DigRateLimiter`] for them explicitly; see /// [`InboundRateLimiter::allows`]. const DIG_WIRE_BAND_START: u8 = 220; @@ -64,23 +64,35 @@ const DIG_WIRE_BAND_START: u8 = 220; /// checks, and no way for a call site to consult one and forget the other. #[derive(Debug, Clone)] pub struct InboundRateLimiter { - /// Chia's bound, keyed by [`ProtocolMessageTypes`]: `default_settings` / `tx` / `other`. - chia: RateLimiter, + /// Chia's bound, keyed by the raw wire byte: `default_settings` / `tx` / `other`. + /// + /// [`OpcodeRateLimiter`] carries no table of its own — it re-keys Chia's `V2_RATE_LIMITS` + /// from `ProtocolMessageTypes` onto the wire byte, so every Chia opcode keeps exactly the + /// bound Chia gives it while a DIG opcode (which has no enum variant) remains expressible. + chia: OpcodeRateLimiter, /// DIG's bound, keyed by the raw opcode byte: [`dig_extension_rate_limits_map`]. dig: DigRateLimiter, } impl InboundRateLimiter { - /// Builds the gate for one inbound connection — `incoming = true`, a [`RESET_SECONDS`] window, - /// every bound scaled by - /// [`rate_limit_factor`](crate::types::config::GossipConfig::peer_options). + /// Builds the gate for one inbound connection — a [`RESET_SECONDS`] window, every bound scaled + /// by [`rate_limit_factor`](crate::types::config::GossipConfig::peer_options). + /// + /// Only the DIG half is inbound-shaped today: [`DigRateLimiter`] takes `incoming = true`, so a + /// frame it refuses still charges its counter and a peer cannot free quota by being refused. + /// [`OpcodeRateLimiter`] as of `dig-peer-protocol` 0.5 exposes no such flag, so the Chia half + /// charges only frames it admits — a peer flooding past the Chia bound is refused each time but + /// does not ratchet itself further into the window. + /// + /// TODO(dig_ecosystem#2228): restore the inbound ratchet on the Chia half once + /// `dig-peer-protocol` 0.6.0 lands `Direction::Inbound`; `OpcodeRateLimits`' fields are private, + /// so there is no local substitute and this cannot be fixed from inside `dig-gossip`. pub fn new(rate_limit_factor: f64) -> Self { Self { - chia: RateLimiter::new( - true, + chia: OpcodeRateLimiter::new( RESET_SECONDS, rate_limit_factor, - (*V2_RATE_LIMITS).clone(), + OpcodeRateLimits::default(), ), dig: DigRateLimiter::new( true, @@ -93,26 +105,27 @@ impl InboundRateLimiter { /// Whether `msg` is admitted. A frame passes only if EVERY applicable check passes. /// - /// 1. [`RateLimiter::handle_message`] — the Chia bound — always, and first, so its counters - /// advance for every frame regardless of opcode. + /// 1. [`OpcodeRateLimiter::allow`] — the Chia bound — always, and first, so it is consulted for + /// every frame regardless of opcode. /// 2. For frames in the DIG wire band (opcode `>= DIG_WIRE_BAND_START`), ALSO /// [`DigRateLimiter::check`] on the raw opcode. /// - /// **Why both for the 220 band:** 221/222 ARE Chia `Message` variants, but `handle_message` has - /// no `tx`/`other` row for them, so it falls through to the loose `default_settings` (100 - /// frames/min, 1 MiB) and their deliberate [`dig_extension_rate_limits_map`] rows (#1316, - /// #1720) would never bind on the live wire. Requiring the DIG pass in addition is what makes + /// **Why both for the 220 band:** the Chia table has no `tx`/`other` row for 220-222, so they + /// fall through to the loose `default_settings` (100 frames/min, 1 MiB) and their deliberate + /// [`dig_extension_rate_limits_map`] rows (#1316, #1720) would never bind on the live wire. Requiring the DIG pass in addition is what makes /// those rows actually enforced. Frames below the band are decided by `handle_message` alone. /// /// The DIG half can only ever ADD a restriction: it is consulted after the Chia bound has /// already been applied, and an opcode with no row fails open. - pub fn allows(&mut self, msg: &Message) -> bool { - // Always apply the Chia base bound first (and unconditionally, so its counters advance). - if !self.chia.handle_message(msg) { + pub fn allows(&mut self, msg: &DigMessage) -> bool { + // Always consult the Chia base bound first, whatever the opcode, so no frame reaches the + // DIG half ungated. Note this charges only ADMITTED frames — see `new` and + // TODO(dig_ecosystem#2228) for the missing inbound ratchet. + if !self.chia.allow(msg) { return false; } - let opcode = msg.msg_type as u8; + let opcode = msg.msg_type; if opcode >= DIG_WIRE_BAND_START { // DIG 220-band frame: its real bound is the DIG row, so require that pass too. self.dig.check(opcode, msg.data.len() as u32) @@ -134,9 +147,9 @@ impl InboundRateLimiter { /// enum to prove it). It is the single source of truth for the SET of flood opcodes the #1626/#1796 /// penalty exemption applies to (the exemption itself is further narrowed to RATE violations — see /// [`rejected_frame_incurs_penalty`]). -pub(crate) fn is_public_flood_opcode(msg_type: ProtocolMessageTypes) -> bool { +pub(crate) fn is_public_flood_opcode(msg_type: u8) -> bool { matches!( - msg_type as u8, + msg_type, crate::service::store_melted::STORE_MELTED | crate::service::holdings_announce::HOLDINGS_ANNOUNCE ) @@ -162,7 +175,7 @@ pub(crate) fn is_public_flood_opcode(msg_type: ProtocolMessageTypes) -> bool { /// Dropping an over-cap (rate) flood frame alone is graceful: the receiver's seen-set, Plumtree /// eager/lazy redundancy, and the periodic re-announce all recover the message without the delivering /// peer being punished. The exemption is thus opcode + violation-kind scoped, not opcode-only. -pub(crate) fn rejected_frame_incurs_penalty(msg: &Message) -> bool { +pub(crate) fn rejected_frame_incurs_penalty(msg: &DigMessage) -> bool { if is_public_flood_opcode(msg.msg_type) { // Flood opcode: exempt for an over-cap RATE rejection, penalised for a SIZE violation. exceeds_dig_wire_max_size(msg) @@ -176,9 +189,9 @@ pub(crate) fn rejected_frame_incurs_penalty(msg: &Message) -> bool { /// rather than a rate/frequency one. The row is the SINGLE SOURCE OF TRUTH for the bound (never a /// hardcoded literal); an opcode with no row cannot exceed a bound it doesn't have, so returns /// `false` (unreachable for 221/222 — the completeness guard pins their rows). -fn exceeds_dig_wire_max_size(msg: &Message) -> bool { +fn exceeds_dig_wire_max_size(msg: &DigMessage) -> bool { dig_extension_rate_limits_map() - .get(&(msg.msg_type as u8)) + .get(&msg.msg_type) .map(|row| (msg.data.len() as f64) > row.max_size) .unwrap_or(false) } @@ -294,19 +307,29 @@ mod tests { //! `default_settings` and these tests go RED (proven by reverting the branch). The external //! mirror in `tests/con_005_tests.rs` cannot detect that regression and is only a secondary check. - use dig_peer_protocol::{Bytes, ProtocolMessageTypes, Streamable}; + use dig_peer_protocol::{Bytes, ProtocolMessageTypes, Streamable, ALL_DIG_OPCODES}; use super::*; + /// Chia's `Handshake` wire opcode, derived from the enum rather than hard-coded so the contrast + /// fixture below tracks upstream if the discriminant ever moves. + fn handshake_opcode() -> u8 { + *ProtocolMessageTypes::Handshake + .to_bytes() + .expect("ProtocolMessageTypes is a single-byte streamable enum") + .first() + .expect("its encoding is exactly one byte") + } + /// #1760 D — completeness guard for the DIG 220-band rate-limit rows. /// /// [`DigRateLimiter::check`] **fails OPEN**: an opcode in the 220 band with no /// [`dig_extension_rate_limits_map`] row silently falls through to the loose Chia /// `default_settings` (100/min, 1 MiB) instead of a deliberate bound (the class of gap #1720 - /// closed for 221/222). This test enumerates every ≥[`DIG_WIRE_BAND_START`] - /// [`ProtocolMessageTypes`] variant that actually exists (probed via the wire discriminant, so - /// it can never go stale against a hand-copied list) and asserts each is CLASSIFIED — either it - /// carries a dedicated rate-limit row, or it is a documented member of + /// closed for 221/222). This test enumerates every ≥[`DIG_WIRE_BAND_START`] opcode DIG has + /// actually assigned — taken from `dig_peer_protocol::ALL_DIG_OPCODES`, the canonical + /// namespace list, so it can never go stale against a hand-copied literal — and asserts each is + /// CLASSIFIED: either it carries a dedicated rate-limit row, or it is a documented member of /// [`BASE_BOUND_ONLY_BAND_OPCODES`]. A newly-added 220-band opcode that is neither fails this /// test, forcing a deliberate rate-limit decision rather than a silent fail-open default. #[test] @@ -319,12 +342,15 @@ mod tests { const BASE_BOUND_ONLY_BAND_OPCODES: &[u8] = &[crate::service::dig_message::DIG_MESSAGE]; let map = dig_extension_rate_limits_map(); - for opcode in DIG_WIRE_BAND_START..=u8::MAX { - // Probe whether this opcode is a real `ProtocolMessageTypes` variant via its wire - // discriminant — the authoritative source, so the guard tracks the enum, not a literal. - if ProtocolMessageTypes::from_bytes(&[opcode]).is_err() { - continue; - } + let band: Vec = ALL_DIG_OPCODES + .into_iter() + .filter(|opcode| *opcode >= DIG_WIRE_BAND_START) + .collect(); + assert!( + !band.is_empty(), + "the assigned 220-band opcode set must be non-empty, or this guard checks nothing" + ); + for opcode in band { let has_row = map.contains_key(&opcode); let base_bound_only = BASE_BOUND_ONLY_BAND_OPCODES.contains(&opcode); assert!( @@ -342,8 +368,8 @@ mod tests { /// this admits the 21st via the 100/min default, so the test pins that branch to production. #[test] fn real_gate_bounds_holdings_announce_222() { - let announce_frame = || Message { - msg_type: ProtocolMessageTypes::HoldingsAnnounce, + let announce_frame = || DigMessage { + msg_type: crate::service::holdings_announce::HOLDINGS_ANNOUNCE, id: None, data: Bytes::new(vec![0u8; 1024]), // well under the 128 KiB max_size }; @@ -376,23 +402,37 @@ mod tests { } /// #1626 — the public-flood exemption set is EXACTLY `StoreMelted` (221) and `HoldingsAnnounce` - /// (222), enumerated over the real wire enum so it can never drift against the canonical - /// [`classify_broadcast`](crate::gossip::broadcaster::classify_broadcast) grouping or a hand-typed - /// list. + /// (222), enumerated over the WHOLE opcode space so the classification can never silently widen + /// away from the canonical + /// [`classify_broadcast`](crate::gossip::broadcaster::classify_broadcast) grouping. + /// + /// Every one of the 256 opcodes is asked directly. There is deliberately no decode filter: 221 + /// and 222 have no `ProtocolMessageTypes` variant, so filtering on a successful decode would + /// skip exactly the two opcodes this test is named after and leave it asserting the empty set. #[test] fn public_flood_opcode_set_is_exactly_221_and_222() { + let mut flood = Vec::new(); for opcode in 0u8..=u8::MAX { - let Ok(msg_type) = ProtocolMessageTypes::from_bytes(&[opcode]) else { - continue; - }; let expected = opcode == crate::service::store_melted::STORE_MELTED || opcode == crate::service::holdings_announce::HOLDINGS_ANNOUNCE; assert_eq!( - is_public_flood_opcode(msg_type), + is_public_flood_opcode(opcode), expected, "opcode {opcode} public-flood classification" ); + if is_public_flood_opcode(opcode) { + flood.push(opcode); + } } + // Belt and braces against a future refactor that makes the loop body vacuous: the set is + // named, in full, not merely agreed with opcode by opcode. + assert_eq!( + flood, + vec![ + crate::service::store_melted::STORE_MELTED, + crate::service::holdings_announce::HOLDINGS_ANNOUNCE + ] + ); } /// #1626 — a 222 (HoldingsAnnounce) frame the REAL gate rejects for exceeding the per-connection @@ -403,8 +443,8 @@ mod tests { /// frame, so the final assertion (`!incurs_penalty`) failed and the delivering peer was charged. #[test] fn over_cap_holdings_announce_222_is_dropped_but_not_penalised() { - let frame = |seed: u32| Message { - msg_type: ProtocolMessageTypes::HoldingsAnnounce, + let frame = |seed: u32| DigMessage { + msg_type: crate::service::holdings_announce::HOLDINGS_ANNOUNCE, id: None, data: Bytes::new({ // Distinct payloads (well under the 128 KiB cap) so each is a real, non-duplicate frame. @@ -436,8 +476,8 @@ mod tests { /// exempt from the penalty. Covers 221 identically to 222 (the false-attribution bug is the same). #[test] fn over_cap_store_melted_221_is_dropped_but_not_penalised() { - let frame = |seed: u32| Message { - msg_type: ProtocolMessageTypes::StoreMelted, + let frame = |seed: u32| DigMessage { + msg_type: crate::service::store_melted::STORE_MELTED, id: None, data: Bytes::new({ let mut v = vec![0u8; 164]; @@ -468,8 +508,8 @@ mod tests { /// penalty. Proves the exemption is opcode-scoped, not a blanket disable of rate-limit attribution. #[test] fn over_cap_non_flood_opcode_is_still_penalised() { - let frame = || Message { - msg_type: ProtocolMessageTypes::Handshake, + let frame = || DigMessage { + msg_type: handshake_opcode(), id: None, data: Bytes::new(vec![0u8; 16]), }; @@ -498,8 +538,8 @@ mod tests { /// WHY, so an oversized flood frame escaped attribution. #[test] fn oversized_holdings_announce_222_is_penalised() { - let over_size = Message { - msg_type: ProtocolMessageTypes::HoldingsAnnounce, + let over_size = DigMessage { + msg_type: crate::service::holdings_announce::HOLDINGS_ANNOUNCE, id: None, data: Bytes::new(vec![ 0u8; @@ -524,8 +564,8 @@ mod tests { /// RED before #1796: opcode-only exemption let it escape the penalty. #[test] fn oversized_store_melted_221_is_penalised() { - let over_size = Message { - msg_type: ProtocolMessageTypes::StoreMelted, + let over_size = DigMessage { + msg_type: crate::service::store_melted::STORE_MELTED, id: None, data: Bytes::new(vec![0u8; crate::service::store_melted::ENCODED_LEN + 1]), }; @@ -559,8 +599,8 @@ mod tests { /// (the DIG row) and rejects the 11th, driven through the REAL [`InboundRateLimiter::allows`]. #[test] fn real_gate_bounds_store_melted_221() { - let melted_frame = || Message { - msg_type: ProtocolMessageTypes::StoreMelted, + let melted_frame = || DigMessage { + msg_type: crate::service::store_melted::STORE_MELTED, id: None, data: Bytes::new(vec![0u8; 164]), // fixed StoreMeltedAnnounce ENCODED_LEN }; diff --git a/src/connection/keepalive.rs b/src/connection/keepalive.rs index 00e5c55..73f3256 100644 --- a/src/connection/keepalive.rs +++ b/src/connection/keepalive.rs @@ -38,8 +38,8 @@ //! The published [`chia_protocol`](https://docs.rs/chia-protocol/0.26.0/chia_protocol/) **0.26** wire //! enum [`ProtocolMessageTypes`](chia_protocol::ProtocolMessageTypes) does **not** define separate //! application-level Ping/Pong message types — Chia’s networking docs describe **WebSocket** library -//! heartbeats for transport liveness. Upstream [`dig_peer_protocol::Peer`](dig_peer_protocol::Peer)’s -//! inbound loop discards raw WS control frames (`Ping`/`Pong`) before they become [`Message`](chia_protocol::Message)s. +//! heartbeats for transport liveness. Upstream [`dig_peer_protocol::DigLink`](dig_peer_protocol::DigLink)’s +//! inbound loop discards raw WS control frames (`Ping`/`Pong`) before they become [`DigMessage`](chia_protocol::DigMessage)s. //! //! **DIG policy:** we treat a successful **`RequestPeers` → `RespondPeers`** round-trip as the //! observable keepalive probe (same Chia types already used right after outbound connect in @@ -58,12 +58,29 @@ //! uses [`crate::constants::PING_INTERVAL_SECS`] / [`crate::constants::PEER_TIMEOUT_SECS`]. Integration //! tests set small values so `con_004_tests` finishes quickly. //! +//! ## The probe is UNCORRELATED (#2767) +//! +//! The probe is sent with [`DigLink::send`] (`id: None`) and the reply is observed on the +//! service-wide application inbound broadcast — **not** on a correlation waiter. +//! +//! A correlated probe collides with the peer's identically-allocated id. Both peers allocate +//! correlation ids from a counter that starts at zero, and both keepalive loops start at handshake +//! on the same interval, so two probes can carry the same id. Each link matches inbound frames on +//! correlation id *before* forwarding, so each side's waiter receives the **peer's `RequestPeers`** +//! instead of a `RespondPeers`. The peer's request never reaches the forwarder, its auto-reply +//! never fires, neither side records a success, and both tear the link down at the staleness check +//! — logging a timeout that names the wrong cause. An `id: None` frame skips the id-match arm +//! entirely, so the lockstep cannot exist. +//! +//! This fails **loose**: a peer that is alive but silent on the broadcast is kept. That is the +//! correct direction for a probe whose only action is to disconnect. +//! //! ## Per-probe deadline //! -//! A dead TCP peer may leave [`Peer::request_infallible`] awaiting forever. Each probe is wrapped in -//! [`tokio::time::timeout`] for `keepalive_peer_timeout_secs` (or [`PEER_TIMEOUT_SECS`]) so we surface -//! failure and disconnect (same path as transport errors) without blocking the keepalive task -//! indefinitely. +//! A dead TCP peer would otherwise leave the reply wait pending forever. Each probe's wait is +//! wrapped in [`tokio::time::timeout`] for `keepalive_peer_timeout_secs` (or [`PEER_TIMEOUT_SECS`]) +//! so we surface failure and disconnect (same path as transport errors) without blocking the +//! keepalive task indefinitely. //! //! ## Design decisions //! @@ -81,14 +98,14 @@ use std::sync::Arc; use std::time::Duration; -use dig_peer_protocol::Peer; -use dig_peer_protocol::Streamable; -use dig_peer_protocol::{RequestPeers, RespondPeers}; +use chia_protocol::RequestPeers; +use dig_peer_protocol::{DigLink, DigMessage}; +use crate::connection::chia_opcodes; // SPEC §2.13 — PING_INTERVAL_SECS (default 30) and PEER_TIMEOUT_SECS (default 90) // are DIG-specific constants not present in Chia crates. use crate::constants::{PEER_TIMEOUT_SECS, PING_INTERVAL_SECS}; -use crate::service::state::{record_live_peer_inbound_bytes, PeerSlot, ServiceState}; +use crate::service::state::{PeerSlot, ServiceState}; use crate::types::peer::PeerId; use crate::types::reputation::PenaltyReason; @@ -105,6 +122,56 @@ fn unix_secs() -> u64 { .as_secs() } +/// Send one liveness probe to `peer`, **uncorrelated** (#2767). +/// +/// [`DigLink::send`] frames the body with `id: None`. That is the load-bearing property, not the +/// choice of `RequestPeers`: both peers allocate correlation ids from a counter that starts at +/// zero, and both keepalive loops start at handshake on a shared interval, so two *correlated* +/// probes can carry the same id. Each link matches inbound frames on correlation id before +/// forwarding, so each side's waiter would receive the peer's **request** — the peer's request +/// would never reach the forwarder, its auto-reply would never fire, and both sides would tear the +/// link down. An `id: None` frame skips the id-match arm entirely. +async fn send_probe(peer: &DigLink) -> Result<(), dig_peer_protocol::LinkError> { + peer.send(RequestPeers::new()).await +} + +/// Subscribe to the service-wide inbound broadcast, or `None` while it is uninitialised. +/// +/// The sender exists only between [`GossipService::start`](crate::service::GossipService::start) +/// and `stop`, so `None` means "the service is not fully up" — never "the peer is unreachable". +/// Callers MUST treat `None` as liveness-neutral (#2767). +fn subscribe_inbound( + state: &ServiceState, +) -> Option> { + let guard = state.inbound_tx.lock().ok()?; + Some(guard.as_ref()?.subscribe()) +} + +/// Wait for a `RespondPeers` frame published by `peer_id` on the application inbound stream. +/// +/// Returns `true` when the peer answered, `false` when the broadcast closed (service shutdown). +/// Frames from other peers are skipped, and a +/// [`Lagged`](tokio::sync::broadcast::error::RecvError::Lagged) is **liveness-neutral** — lag means +/// the connection is carrying more traffic than this receiver drained, which is evidence of life, +/// not of death — so the wait simply continues. The caller bounds this with a `timeout`. +async fn await_respond_peers( + inbound: &mut tokio::sync::broadcast::Receiver<(PeerId, DigMessage)>, + peer_id: PeerId, +) -> bool { + use tokio::sync::broadcast::error::RecvError; + loop { + match inbound.recv().await { + Ok((pid, msg)) => { + if pid == peer_id && msg.msg_type == chia_opcodes::RESPOND_PEERS { + return true; + } + } + Err(RecvError::Lagged(_)) => continue, + Err(RecvError::Closed) => return false, + } + } +} + /// Spawn a detached Tokio task that periodically probes `peer` and disconnects on /// failure or staleness. /// @@ -143,7 +210,7 @@ pub(crate) fn spawn_keepalive_task( state: Arc, peer_id: PeerId, generation: u64, - peer: Peer, + peer: DigLink, ) -> tokio::task::AbortHandle { tokio::spawn(async move { keepalive_loop(state, peer_id, generation, peer).await }) .abort_handle() @@ -159,8 +226,10 @@ pub(crate) fn spawn_keepalive_task( /// 2. If no successful probe has been received within `PEER_TIMEOUT_SECS` (default 90 s), /// disconnect immediately — the 90 s window allows up to 3 missed 30 s intervals /// before giving up, matching CON-004 acceptance criteria. -/// 3. Send a `RequestPeers` probe wrapped in a `tokio::time::timeout` of -/// `PEER_TIMEOUT_SECS` so a half-open TCP socket cannot block this task forever. +/// 3. Send an **uncorrelated** `RequestPeers` probe (#2767) and wait for the peer's +/// `RespondPeers` on the application inbound broadcast, wrapped in a `tokio::time::timeout` of +/// `PEER_TIMEOUT_SECS` so a half-open TCP socket cannot block this task forever. If the +/// broadcast is unavailable the round is skipped **without** taking the failure path. /// 4. On success: record the RTT sample into the peer's /// [`PeerReputation`](crate::types::reputation::PeerReputation) (windowed average, /// feeds PRF-001 score). @@ -172,7 +241,7 @@ pub(crate) fn spawn_keepalive_task( /// The loop checks [`ServiceState::is_running`](crate::service::state::ServiceState::is_running) /// both before sleeping *and* after waking. This ensures prompt exit when the /// service is shutting down even if the sleep was already in flight. -async fn keepalive_loop(state: Arc, peer_id: PeerId, generation: u64, peer: Peer) { +async fn keepalive_loop(state: Arc, peer_id: PeerId, generation: u64, peer: DigLink) { // Resolve config overrides once — they are immutable for the connection lifetime. let ping_secs = state .config @@ -213,23 +282,54 @@ async fn keepalive_loop(state: Arc, peer_id: PeerId, generation: u break; } + // --- subscribe to the application inbound stream BEFORE probing (#2767) --- + // The reply is observed on the service-wide broadcast, not on a correlation waiter, so the + // subscription must exist before the probe goes out or a fast peer's reply races past us. + let Some(mut inbound) = subscribe_inbound(&state) else { + // Fail open: the broadcast is only absent while the service is starting or stopping. + // A probe we cannot observe is not evidence the peer is dead, and the only action this + // loop can take is to disconnect — so skip the round and leave the peer connected. + // + // The staleness window must be reset too, not merely the probe skipped: charging an + // unmeasurable interval against it would tear the link down a few rounds later anyway, + // which is the very outcome this branch exists to prevent. + last_success = std::time::Instant::now(); + tracing::debug!( + target: "dig_gossip::keepalive", + %peer_id, + "keepalive: inbound broadcast unavailable; skipping this round (peer kept)" + ); + continue; + }; + // --- send probe (CON-004 step 3) --- - // `Instant::now()` is taken *before* the request so that the elapsed time - // between `start` and success includes serialization, network, and - // deserialization — giving a realistic end-to-end RTT sample. + // `Instant::now()` is taken *before* the send so that the elapsed time between `start` and + // the observed reply includes serialization, network, and deserialization — giving a + // realistic end-to-end RTT sample. let start = std::time::Instant::now(); - // `request_raw` returns the full wire [`Message`] so CON-006 can meter exact serialized - // inbound bytes (same framing as the forwarder path). `request_infallible` only yields the - // decoded body and would hide the length we need for `bytes_read`. - let probe = peer.request_raw(RequestPeers::new()); - match tokio::time::timeout(Duration::from_secs(timeout_secs), probe).await { - // --- success: record RTT into PeerReputation (CON-004 step 4) --- - Ok(Ok(wire_msg)) => { - if RespondPeers::from_bytes(&wire_msg.data).is_err() { - continue; - } - let wl = wire_msg.to_bytes().map(|b| b.len() as u64).unwrap_or(0); - record_live_peer_inbound_bytes(&state, peer_id, wl); + if let Err(e) = send_probe(&peer).await { + tracing::warn!( + target: "dig_gossip::keepalive", + %peer_id, + error = %e, + "keepalive: RequestPeers probe failed to send; disconnecting" + ); + disconnect_after_keepalive_failure(&state, peer_id, generation).await; + break; + } + + // --- await the peer's RespondPeers on the application stream (CON-004 step 4) --- + // CON-006 metering is intentionally NOT done here: an uncorrelated reply reaches the + // forwarder (`listener.rs` / `gossip_handle.rs`), which already charges `bytes_read`. + // Metering it again would double-count every keepalive round. + let observed = tokio::time::timeout( + Duration::from_secs(timeout_secs), + await_respond_peers(&mut inbound, peer_id), + ) + .await; + + match observed { + Ok(true) => { last_success = std::time::Instant::now(); let rtt_ms = start.elapsed().as_millis() as u64; // Clone `Arc>` while holding `peers`, then drop the @@ -248,17 +348,8 @@ async fn keepalive_loop(state: Arc, peer_id: PeerId, generation: u rep.record_rtt_ms(rtt_ms); }; } - // --- transport error: peer is alive but protocol failed --- - Ok(Err(e)) => { - tracing::warn!( - target: "dig_gossip::keepalive", - %peer_id, - error = %e, - "keepalive: RequestPeers probe failed; disconnecting" - ); - disconnect_after_keepalive_failure(&state, peer_id, generation).await; - break; - } + // The broadcast closed: the service is stopping, not a peer failure. + Ok(false) => break, // --- timeout: peer did not respond within PEER_TIMEOUT_SECS --- // This catches half-open TCP connections where the remote end has // crashed but the local OS has not yet detected the failure. @@ -366,3 +457,139 @@ async fn disconnect_after_keepalive_failure( state.execute_dig_timed_ban(peer_id, remote_ip, now).await; } } + +#[cfg(test)] +mod tests { + //! #2767 — the probe must not park a correlation waiter. + //! + //! The mechanism test runs over a **real loopback WebSocket pair**, because the defect lives in + //! [`DigLink`]'s inbound matcher: a symmetric in-memory double could not express a stolen frame. + + use super::*; + use dig_peer_protocol::{LinkOptions, Streamable}; + use tokio::net::TcpListener; + use tokio_tungstenite::{accept_async, connect_async, MaybeTlsStream}; + + /// Both halves of a live loopback link, each with its application inbound receiver. + async fn link_pair() -> ( + (DigLink, tokio::sync::mpsc::Receiver), + (DigLink, tokio::sync::mpsc::Receiver), + ) { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind loopback"); + let addr = listener.local_addr().expect("local_addr"); + + let server = async { + let (tcp, _) = listener.accept().await.expect("accept"); + let ws = accept_async(MaybeTlsStream::Plain(tcp)) + .await + .expect("ws accept"); + DigLink::from_websocket(ws, LinkOptions::default()).expect("server link") + }; + let client = async { + let url = format!("ws://127.0.0.1:{}/", addr.port()); + let (ws, _) = connect_async(url.as_str()).await.expect("ws connect"); + DigLink::from_websocket(ws, LinkOptions::default()).expect("client link") + }; + tokio::join!(server, client) + } + + /// **#2767 mechanism.** The peer has an outstanding correlated waiter at id 0 — exactly the + /// state a simultaneously-started keepalive loop is in. A correlated probe would carry id 0 + /// too, be swallowed by that waiter, and never reach the peer's application; the auto-reply + /// that keeps the link alive would therefore never fire. The uncorrelated probe must arrive. + /// + /// The peer's own probe is a real `request_raw` rather than a hand-rolled frame so the waiter + /// is registered by the same code path production uses. + #[tokio::test] + async fn probe_reaches_the_peer_application_despite_an_outstanding_correlated_waiter() { + let ((a, _a_rx), (b, mut b_rx)) = link_pair().await; + + // The peer starts ITS probe first, parking a waiter at correlation id 0. + let b_probe = tokio::spawn(async move { b.request_raw(RequestPeers::new()).await }); + tokio::time::sleep(Duration::from_millis(100)).await; + + send_probe(&a).await.expect("probe sends"); + + let seen = tokio::time::timeout(Duration::from_secs(2), b_rx.recv()) + .await + .expect("the peer's application must observe the probe within 2s") + .expect("inbound channel open"); + assert_eq!( + seen.msg_type, + chia_opcodes::REQUEST_PEERS, + "the probe must reach the peer's application, not its correlation waiter" + ); + assert!( + seen.id.is_none(), + "the probe must be uncorrelated so no waiter can claim it" + ); + assert!( + RequestPeers::from_bytes(&seen.data).is_ok(), + "the probe body must still be a RequestPeers the peer can auto-reply to" + ); + b_probe.abort(); + } + + fn respond_peers_frame() -> DigMessage { + DigMessage::new( + chia_opcodes::RESPOND_PEERS, + None, + chia_protocol::RespondPeers::new(vec![]) + .to_bytes() + .expect("encode") + .into(), + ) + } + + fn other_frame() -> DigMessage { + DigMessage::new(chia_opcodes::REQUEST_PEERS, None, Vec::new().into()) + } + + /// A reply from a DIFFERENT peer must not be read as this peer's liveness. The control frame + /// arrives afterwards so the test would fail — not hang — if the peer filter were dropped. + #[tokio::test] + async fn a_reply_from_another_peer_is_not_this_peer_s_liveness() { + let (tx, mut rx) = tokio::sync::broadcast::channel(8); + let me = PeerId::from([1u8; 32]); + let them = PeerId::from([2u8; 32]); + + tx.send((them, respond_peers_frame())).expect("send"); + tx.send((me, other_frame())).expect("send"); + drop(tx); + + assert!( + !await_respond_peers(&mut rx, me).await, + "only a RespondPeers from THIS peer counts; the stream then closed" + ); + } + + /// `Lagged` means the connection carried more traffic than this receiver drained — evidence of + /// life, not of death. The wait must continue and still see the reply queued behind it. + #[tokio::test] + async fn lag_is_liveness_neutral_and_the_wait_continues() { + let (tx, mut rx) = tokio::sync::broadcast::channel(2); + let me = PeerId::from([7u8; 32]); + + // Overflow the buffer so the next `recv` yields `Lagged`, then queue the real reply. + for _ in 0..4 { + tx.send((me, other_frame())).expect("send"); + } + tx.send((me, respond_peers_frame())).expect("send"); + + assert!( + await_respond_peers(&mut rx, me).await, + "a lagged receiver must keep waiting and still observe the reply" + ); + } + + /// A closed broadcast is service shutdown, not a peer failure — the caller breaks the loop + /// rather than tearing the peer down. + #[tokio::test] + async fn a_closed_broadcast_reports_shutdown_rather_than_liveness() { + let (tx, mut rx) = tokio::sync::broadcast::channel(4); + drop(tx); + assert!(!await_respond_peers(&mut rx, PeerId::from([9u8; 32])).await); + } +} diff --git a/src/connection/listener.rs b/src/connection/listener.rs index b0e2b65..bef7325 100644 --- a/src/connection/listener.rs +++ b/src/connection/listener.rs @@ -1,4 +1,4 @@ -//! Inbound P2P acceptance: [`tokio::net::TcpListener`] -> TLS -> WebSocket -> [`dig_peer_protocol::Peer`]. +//! Inbound P2P acceptance: [`tokio::net::TcpListener`] -> TLS -> WebSocket -> [`dig_peer_protocol::DigLink`]. //! //! ## SPEC traceability //! @@ -6,7 +6,7 @@ //! 1. `TcpListener::accept()` //! 2. TLS handshake (using `chia-ssl` certificate) //! 3. `tokio_tungstenite::accept_async()` -//! 4. `Peer::from_websocket(ws, options)` +//! 4. `DigLink::from_websocket(ws, options)` //! 5. Receive Handshake, validate `network_id` //! 6. Send Handshake response //! 7. Wrap in `PeerConnection` @@ -15,8 +15,8 @@ //! - **SPEC §5.3** — mandatory mutual TLS (mTLS) via `chia-ssl`: //! "ALL peer-to-peer connections MUST use mutual TLS. Both client and server present //! certificates." Matches Chia `server.py:54-71`, `server.py:67 verify_mode = ssl.CERT_REQUIRED`. -//! - **SPEC §1.7 #4** — "Inbound connection listener": `chia-sdk-client`'s `Peer` only does -//! outbound connections; we add a `TcpListener` accepting inbound. +//! - **SPEC §1.7 #4** — "Inbound connection listener": `dig-peer-protocol`'s [`DigLink`] dials +//! outbound; we add a `TcpListener` accepting inbound and hand the accepted socket to it. //! - **SPEC §1.6 #2** — "Inbound peer relay": when an inbound connection arrives, add peer //! to address manager and relay to other peers (`node_discovery.py:112-127`). //! - **SPEC §1.5 #8** — peer ban/trust: `ClientState::ban()` / `is_banned()` checked before @@ -27,19 +27,21 @@ //! //! ## Why this is not `dig_peer_protocol::connect_peer` //! -//! Upstream [`Peer`](dig_peer_protocol::Peer) is built for **outbound** `wss://` clients. DIG must +//! Upstream [`DigLink`](dig_peer_protocol::DigLink) is built for **outbound** `wss://` clients. DIG must //! **listen** on [`crate::types::config::GossipConfig::listen_addr`], terminate TLS with the node //! [`dig_peer_protocol::ChiaCertificate`], run [`tokio_tungstenite::accept_async`], then call -//! [`Peer::from_websocket`](dig_peer_protocol::Peer::from_websocket) — mirroring the pseudo-code in +//! [`DigLink::from_websocket`](dig_peer_protocol::DigLink::from_websocket) — mirroring the pseudo-code in //! CON-002 and [`SPEC.md`](../../../docs/resources/SPEC.md) §5.2. //! //! ## TLS backends (STR-004) //! //! - **`native-tls` (default):** [`native_tls::TlsAcceptor`] + [`tokio_native_tls`], matching //! CON-001 integration tests ([`tests/common/wss_full_node.rs`](../../../tests/common/wss_full_node.rs)). -//! - **`rustls` without `native-tls` (outbound):** [`chia_sdk_client`] uses rustls for `wss://` dials. -//! **Inbound** still uses [`native_tls::TlsAcceptor`] so [`MaybeTlsStream::NativeTls`] matches -//! [`Peer::from_websocket`] (upstream only types **client** `MaybeTlsStream::Rustls`). +//! - **`rustls` without `native-tls` (outbound):** rustls backs the `wss://` dial, forwarded through +//! [`dig_peer_protocol`]. **Inbound** still uses [`native_tls::TlsAcceptor`] so +//! [`MaybeTlsStream::NativeTls`] matches [`DigLink::from_websocket`], which types only the +//! **client** side of `MaybeTlsStream::Rustls`; the server stream reaches `DigLink` through +//! [`DigLink::from_server_websocket`](dig_peer_protocol::DigLink::from_server_websocket) instead. //! - **CON-009 (mTLS):** On **Linux / non-Apple Unix** (OpenSSL-backed `native-tls`), we use a //! **vendored** [`native-tls`](../../../vendor/native-tls/README.dig-gossip.md) fork that sets //! `CERT_REQUIRED` + Chia CA trust (Chia `server.py:67`). **Windows (SChannel)** and **macOS @@ -55,21 +57,22 @@ //! `tests/con_008_tests.rs` (matrix + “matches Chia category policy”); **CON-003** adds protocol / //! network gates around the same helper (`tests/con_003_tests.rs`). -// CON-002: Large `ClientError` payloads are intentional — they propagate upstream -// `chia_sdk_client` variants verbatim, matching the API-004 `GossipError::ClientError` wrapper. +// CON-002: Large `ClientError` payloads are intentional — they propagate +// `dig_peer_protocol::ClientError` variants verbatim, matching the API-004 +// `GossipError::ClientError` wrapper. #![allow(clippy::result_large_err)] +use crate::connection::chia_opcodes; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use chia_protocol::{Handshake, RespondPeers, TimestampedPeerInfo}; #[cfg(all(feature = "native-tls", not(feature = "rustls")))] use dig_peer_protocol::ChiaCertificate; +use dig_peer_protocol::DigMessage; use dig_peer_protocol::Streamable; -use dig_peer_protocol::{ClientError, Peer, PeerOptions}; -use dig_peer_protocol::{ - Handshake, Message, NodeType, ProtocolMessageTypes, RespondPeers, TimestampedPeerInfo, -}; +use dig_peer_protocol::{ClientError, DigLink, LinkOptions}; use futures_util::{SinkExt, StreamExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::broadcast; @@ -79,7 +82,21 @@ use tokio_tungstenite::tungstenite::Message as WsMsg; use tokio_tungstenite::MaybeTlsStream; use tokio_tungstenite::{accept_async_with_config, WebSocketStream}; -use crate::connection::handshake::ADVERTISED_PROTOCOL_VERSION; +/// Wire opcode of a Chia [`Handshake`] frame. +/// +/// The pre-`DigLink` phase of an inbound negotiation reads raw websocket frames and +/// compares [`DigMessage::msg_type`], which is a bare wire byte. Deriving these from +/// `ProtocolMessageTypes` keeps `chia-protocol` the single authority for Chia opcode +/// numbering rather than restating a literal here. +const HANDSHAKE_OPCODE: u8 = chia_protocol::ProtocolMessageTypes::Handshake as u8; + +/// Wire opcode of a Chia `RequestPeers` frame. See [`HANDSHAKE_OPCODE`]. +const REQUEST_PEERS_OPCODE: u8 = chia_protocol::ProtocolMessageTypes::RequestPeers as u8; + +/// Wire opcode of a Chia [`RespondPeers`] frame. See [`HANDSHAKE_OPCODE`]. +const RESPOND_PEERS_OPCODE: u8 = chia_protocol::ProtocolMessageTypes::RespondPeers as u8; + +use crate::connection::handshake::{dig_node_type_of, ADVERTISED_PROTOCOL_VERSION}; use crate::connection::outbound::network_id_handshake_string; #[cfg(all(feature = "native-tls", not(feature = "rustls")))] use crate::connection::outbound::spki_der_from_leaf_cert_der; @@ -107,7 +124,7 @@ const INBOUND_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30); // Inbound TLS (`native_tls::TlsAcceptor`) — used for **both** `native-tls` and `rustls` features. // // Why `native_tls` for *inbound* even when `rustls` is enabled: -// Upstream `dig_peer_protocol::Peer::from_websocket` types the stream as +// Upstream `dig_peer_protocol::DigLink::from_websocket` types the stream as // `MaybeTlsStream::NativeTls` on the server side. The `rustls` feature only // affects *outbound* dialing (CON-001). Using `native_tls` here keeps the // type system happy without forking upstream abstractions. See module-level @@ -245,7 +262,7 @@ async fn handle_inbound_native( /// `peer_id` even though its old slot is dead. That was the #1691 defect: a bounced peer could never /// reconnect and every read it attempted 404'd. Instead the freshly-authenticated inbound session is /// admitted and **supersedes** the stale slot at insert time (see [`negotiate_inbound_over_ws`], -/// which aborts the displaced slot's keepalive then closes its [`Peer`]). +/// which aborts the displaced slot's keepalive then closes its [`DigLink`]). /// /// This is safe because `peer_id` here is derived from the **completed, verified** TLS handshake /// (`SHA-256` of the captured client-cert SPKI; see the SPKI capture at each caller). Only the holder @@ -365,7 +382,7 @@ async fn handle_inbound_rustls_inner( // Step 6: WebSocket upgrade over the server rustls stream. The server-side stream cannot inhabit // the `#[non_exhaustive]` client `MaybeTlsStream`, so we hand the raw stream to `accept_async` - // and later build the `Peer` via `Peer::from_server_websocket` (see `negotiate_inbound_over_ws`). + // and later build the `DigLink` via `DigLink::from_server_websocket` (see `negotiate_inbound_over_ws`). let ws = accept_async_with_config(tls, Some(crate::connection::ws_config())) .await .map_err(ws_err)?; @@ -436,7 +453,7 @@ async fn handle_inbound_native_inner( // Step 6: WebSocket upgrade over the now-established TLS stream. // We wrap the `native_tls` stream in `MaybeTlsStream::NativeTls` so the type matches - // what `Peer::from_websocket` expects downstream. + // what `DigLink::from_websocket` expects downstream. let ws = accept_async_with_config( MaybeTlsStream::NativeTls(tls), Some(crate::connection::ws_config()), @@ -553,7 +570,7 @@ async fn relay_new_peer_to_live_peers( state: &ServiceState, new_row: TimestampedPeerInfo, ) -> Result<(), ClientError> { - let peers: Vec = { + let peers: Vec = { let g = state .peers .lock() @@ -561,7 +578,7 @@ async fn relay_new_peer_to_live_peers( g.values() .filter_map(|slot| match slot { PeerSlot::Live(l) => Some(l.peer.clone()), - // Stub + POOL-* `dig-nat` members have no WebSocket `Peer` to gossip the new row to. + // Stub + POOL-* `dig-nat` members have no WebSocket `DigLink` to gossip the new row to. PeerSlot::Stub(_) | PeerSlot::Nat(_) => None, }) .collect() @@ -573,15 +590,17 @@ async fn relay_new_peer_to_live_peers( Ok(()) } -/// Read the next Chia [`Message`] from a raw [`WebSocketStream`] (ping/pong passthrough). +/// Read the next Chia [`DigMessage`] from a raw [`WebSocketStream`] (ping/pong passthrough). /// -/// **Why defer `Peer::from_websocket` until after one `RequestPeers` on the raw socket?** The first +/// **Why defer `DigLink::from_websocket` until after one `RequestPeers` on the raw socket?** The first /// outbound packet from [`GossipHandle::connect_to`](crate::service::gossip_handle::GossipHandle::connect_to) -/// may arrive before our `Peer` reader task exists, so we answer that **initial** probe on the raw -/// WebSocket. Later [`RequestPeers`](chia_protocol::RequestPeers) keepalives use the vendored -/// [`chia_sdk_client`] patch (`vendor/chia-sdk-client`): inbound `RequestPeers` is forwarded to the -/// application and answered with [`Peer::send_protocol_message`](dig_peer_protocol::Peer::send_protocol_message). -async fn read_next_wire_message(ws: &mut WebSocketStream) -> Result +/// may arrive before our `DigLink` reader task exists, so we answer that **initial** probe on the raw +/// WebSocket. Later [`RequestPeers`](chia_protocol::RequestPeers) keepalives need no such special +/// handling: [`DigLink`] forwards every unmatched inbound message on the receiver it returns, and we +/// answer from there with [`DigLink::send`](dig_peer_protocol::DigLink::send). (This is why the +/// `chia-sdk-client` fork could be deleted — upstream's `Peer` swallowed unmatched inbound messages, +/// which is the behaviour that fork existed to patch.) +async fn read_next_wire_message(ws: &mut WebSocketStream) -> Result where S: AsyncRead + AsyncWrite + Unpin, { @@ -589,7 +608,8 @@ where let raw = ws.next().await.ok_or(ClientError::MissingHandshake)??; match raw { WsMsg::Binary(bin) => { - return Message::from_bytes(&bin).map_err(ClientError::Streamable); + return DigMessage::from_bytes(&bin) + .ok_or_else(|| ClientError::Io(std::io::Error::other("malformed DIG frame"))); } WsMsg::Ping(p) => { ws.send(WsMsg::Pong(p)) @@ -614,7 +634,7 @@ where /// 4. **Receive and answer `RequestPeers`** — outbound peers issue this immediately (CON-001). /// 5. **Address manager insert** — add the newcomer to the new-table (DSC-001 bucketing). /// 6. **Relay** — push the newcomer's `TimestampedPeerInfo` to all existing live peers. -/// 7. **Upgrade to `Peer`** — hand off to [`Peer::from_websocket`] for the steady-state reader. +/// 7. **Upgrade to `DigLink`** — hand off to [`DigLink::from_websocket`] for the steady-state reader. /// 8. **Bridge inbound messages** — spawn a task that forwards wire messages into the /// [`ServiceState::inbound_tx`] broadcast channel (API-002 event bus). /// 9. **Insert `LiveSlot`** — the peer is now fully registered and visible to `peer_count`, etc. @@ -640,7 +660,7 @@ async fn negotiate_inbound_over_ws( where S: AsyncRead + AsyncWrite + Unpin + Send + 'static, { - let opts: PeerOptions = state.config.peer_options; + let opts: LinkOptions = state.config.peer_options; // --- Phase 1: Receive the remote's Handshake (with timeout) --- let first = tokio::time::timeout(INBOUND_HANDSHAKE_TIMEOUT, read_next_wire_message(&mut ws)) @@ -653,11 +673,11 @@ where })??; // The first application message MUST be a Handshake; anything else is a protocol violation. - if first.msg_type != ProtocolMessageTypes::Handshake { - return Err(ClientError::InvalidResponse( - vec![ProtocolMessageTypes::Handshake], - first.msg_type, - )); + if first.msg_type != HANDSHAKE_OPCODE { + return Err(ClientError::Io(std::io::Error::other(format!( + "expected a Handshake (opcode {HANDSHAKE_OPCODE}), got opcode {}", + first.msg_type + )))); } let their_handshake = Handshake::from_bytes(&first.data)?; @@ -677,26 +697,24 @@ where // #2215: the ONE configured value, identical to what the dial path sends. software_version: state.config.software_version.clone(), server_port: listen_port_for_handshake(&state), - node_type: NodeType::FullNode, + node_type: chia_protocol::NodeType::FullNode, capabilities: vec![ (1, "1".to_string()), // BASE protocol (2, "1".to_string()), // BLOCK_HEADERS (3, "1".to_string()), // RATE_LIMITS_V2 ], }; - let reply = Message { - msg_type: ProtocolMessageTypes::Handshake, - id: None, // Handshakes have no correlation id in the Chia wire protocol. - data: our_handshake + let reply = DigMessage::new( + HANDSHAKE_OPCODE, + None, // Handshakes have no correlation id in the Chia wire protocol. + our_handshake .to_bytes() .map_err(ClientError::Streamable)? .into(), - }; - ws.send(WsMsg::Binary( - reply.to_bytes().map_err(ClientError::Streamable)?, - )) - .await - .map_err(|e| ClientError::Io(std::io::Error::other(e.to_string())))?; + ); + ws.send(WsMsg::Binary(reply.to_bytes())) + .await + .map_err(|e| ClientError::Io(std::io::Error::other(e.to_string())))?; // CON-003: remote_software_version_sanitized was computed *before* our reply was sent. // This means we validate-then-respond, never the reverse — a malformed remote version @@ -705,7 +723,7 @@ where // --- Phase 4: Handle the expected `RequestPeers` from the outbound peer (CON-001 pattern) --- // The outbound `connect_to` issues `RequestPeers` immediately after the handshake exchange // (see `GossipHandle::connect_to`). We answer on the *raw* WebSocket before handing to - // `Peer::from_websocket` — see `read_next_wire_message` doc for the rationale. + // `DigLink::from_websocket` — see `read_next_wire_message` doc for the rationale. let second = tokio::time::timeout(INBOUND_HANDSHAKE_TIMEOUT, read_next_wire_message(&mut ws)) .await .map_err(|_| { @@ -714,20 +732,18 @@ where "inbound RequestPeers timeout", )) })??; - if second.msg_type == ProtocolMessageTypes::RequestPeers { + if second.msg_type == REQUEST_PEERS_OPCODE { // Reply with an empty peer list for now. Future DSC-* requirements will populate this // from the address manager's tried/new tables. let resp = RespondPeers::new(vec![]); - let out = Message { - msg_type: ProtocolMessageTypes::RespondPeers, - id: second.id, // Preserve correlation id so the outbound Peer reader can match it. - data: resp.to_bytes().map_err(ClientError::Streamable)?.into(), - }; - ws.send(WsMsg::Binary( - out.to_bytes().map_err(ClientError::Streamable)?, - )) - .await - .map_err(|e| ClientError::Io(std::io::Error::other(e.to_string())))?; + let out = DigMessage::new( + RESPOND_PEERS_OPCODE, + second.id, // Preserve correlation id so the requester's link can match it. + resp.to_bytes().map_err(ClientError::Streamable)?.into(), + ); + ws.send(WsMsg::Binary(out.to_bytes())) + .await + .map_err(|e| ClientError::Io(std::io::Error::other(e.to_string())))?; } // --- Phase 5: Register in the address manager (DSC-001 new-table bucketing) --- @@ -749,13 +765,13 @@ where // SPEC §5.2 step 9 — "Relay peer info (node_discovery.py:126-127)." relay_new_peer_to_live_peers(&state, new_row).await?; - // --- Phase 7: Upgrade to `Peer` (chia_sdk_client managed reader/writer) --- + // --- Phase 7: Upgrade to `DigLink` (dig-peer-protocol's managed reader/writer) --- // After this point the WebSocket is consumed; all further communication goes through - // the `Peer` handle (send) and the `inbound_rx` channel (receive). We use + // the `DigLink` handle (send) and the `inbound_rx` channel (receive). We use // `from_server_websocket` (not `from_websocket`) because the server rustls stream cannot inhabit // the client-oriented `MaybeTlsStream`; the peer address is already known so no stream // introspection is needed. Byte-identical behaviour for the native-tls path (#1371). - let (peer, mut inbound_rx) = Peer::from_server_websocket(ws, remote_addr, opts)?; + let (peer, mut inbound_rx) = DigLink::from_server_websocket(ws, remote_addr, opts); // --- Phase 8: Per-connection inbound rate limiter (CON-005) + peer map insert --- // SPEC §5.4 — "Inbound: create a separate rate limiter for each connection" @@ -767,7 +783,7 @@ where )); let meta = StubPeer { remote: remote_addr, - node_type: their_handshake.node_type, + node_type: dig_node_type_of(their_handshake.node_type), is_outbound: false, // This is the *inbound* path; outbound has its own insertion logic. }; let peer_for_keepalive = peer.clone(); @@ -828,10 +844,10 @@ where // --- Phase 9: Bridge inbound wire messages into the service broadcast channel --- // CON-005: [`InboundRateLimiter::allows`] must approve each frame before CON-004 keepalive - // auto-replies and before the `(PeerId, Message)` publish. + // auto-replies and before the `(PeerId, DigMessage)` publish. if let Ok(guard) = state.inbound_tx.lock() { if let Some(tx_b) = guard.as_ref() { - let tx: broadcast::Sender<(PeerId, Message)> = tx_b.clone(); + let tx: broadcast::Sender<(PeerId, DigMessage)> = tx_b.clone(); let pid_task = peer_id; // This session's generation — so a rate-limit trip cannot penalize a later reconnect (#1691). let gen_task = generation; @@ -851,18 +867,16 @@ where } continue; } - if let Ok(wl_in) = message_wire_len(&msg) { + { + let wl_in = message_wire_len(&msg); record_live_peer_inbound_bytes(&state_fwd, pid_task, wl_in); } - if msg.msg_type == ProtocolMessageTypes::RequestPeers { + if msg.msg_type == chia_opcodes::REQUEST_PEERS { if let Ok(body) = RespondPeers::new(vec![]).to_bytes() { - let reply = Message { - msg_type: ProtocolMessageTypes::RespondPeers, - id: msg.id, - data: body.into(), - }; - let wl_out = message_wire_len(&reply).ok(); - let _ = peer_rpc.send_protocol_message(reply).await; + let reply = + DigMessage::new(chia_opcodes::RESPOND_PEERS, msg.id, body.into()); + let wl_out = Some(message_wire_len(&reply)); + let _ = peer_rpc.send_message(reply).await; if let Some(w) = wl_out { record_live_peer_outbound_bytes(&state_fwd, pid_task, w); } diff --git a/src/connection/mod.rs b/src/connection/mod.rs index fab4054..8860efe 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -4,12 +4,16 @@ //! **Outbound** connect uses `chia-sdk-client` TLS + WSS (CON-001) — see [`outbound`]. //! **Related requirements:** `docs/requirements/domains/connection/`. +/// The transport-vs-policy split a dial reports failures through ([`DialError`](dial_error::DialError)). +pub mod dial_error; + pub mod handshake; /// CON-004 keepalive + RTT sampling (application-level `RequestPeers` probe). pub mod keepalive; /// CON-005 — the DIG per-opcode inbound rate limiter, keyed by the raw wire byte. +pub(crate) mod chia_opcodes; pub mod dig_rate_limiter; /// CON-005 inbound admission gate: Chia's `V2_RATE_LIMITS` bound composed with the DIG bound. diff --git a/src/connection/outbound.rs b/src/connection/outbound.rs index 59cc00b..2669823 100644 --- a/src/connection/outbound.rs +++ b/src/connection/outbound.rs @@ -1,4 +1,4 @@ -//! Outbound peer establishment via `chia-sdk-client` TLS + WebSocket + Chia handshake. +//! Outbound peer establishment via `dig-peer-protocol` TLS + WebSocket + Chia handshake. //! //! ## SPEC traceability //! @@ -15,53 +15,56 @@ //! - **SPEC §1.5 #1** — handshake with capabilities via `connect_peer()`. //! - **SPEC §1.6 #1** — peer exchange on outbound connect: after connecting, send //! `RequestPeers` to discover more peers (`node_discovery.py:135-136`). -//! - **SPEC §1.4** — `Handshake`, `Message`, `NodeType` used directly from `chia-protocol`. +//! - **SPEC §1.4** — `Handshake`, `DigMessage`, `NodeType` used directly from `chia-protocol`. //! //! **Normative:** [CON-001](../../../docs/requirements/domains/connection/specs/CON-001.md) / //! [NORMATIVE.md](../../../docs/requirements/domains/connection/NORMATIVE.md) — outbound MUST use //! `connect_peer()` semantics (TLS connector, `Handshake`, `FullNode` peer validation, DIG //! `network_id` as the Chia **string** field). //! -//! ## Why this module exists (vs calling `dig_peer_protocol::connect_peer` directly) +//! ## Why this module exists (vs calling [`DigLink::connect`](dig_peer_protocol::DigLink::connect) directly) //! -//! Upstream [`dig_peer_protocol::connect_peer`](https://docs.rs/chia-sdk-client/latest/chia_sdk_client/fn.connect_peer.html) -//! validates the handshake but **drops** the parsed [`Handshake`] and never exposes the remote TLS -//! **SubjectPublicKeyInfo** bytes. DIG [`PeerConnection`](crate::types::peer::PeerConnection) and +//! The one-call dial validates the handshake but **drops** the parsed [`Handshake`] and never exposes +//! the remote TLS **SubjectPublicKeyInfo** bytes. DIG [`PeerConnection`](crate::types::peer::PeerConnection) and //! [`PeerId`](crate::types::peer::PeerId) (API-005) require: //! //! 1. Metadata from the responder’s [`Handshake`] (`protocol_version`, `software_version`, …). //! 2. `PeerId = SHA256(remote SPKI DER)` via [`crate::types::peer::peer_id_from_tls_spki_der`]. //! -//! We therefore mirror the small `connect.rs` flow from `chia-sdk-client` **after** capturing -//! `remote_spki_der` from the pre-`Peer::from_websocket` [`WebSocketStream`] (see upstream -//! [`chia-sdk-client/src/connect.rs`](https://github.com/Chia-Network/chia-wallet-sdk) — keep in sync -//! when bumping `chia-sdk-client`). +//! We therefore drive the dial ourselves and capture `remote_spki_der` from the +//! [`WebSocketStream`] **before** handing it to [`DigLink::from_websocket`], which consumes it. The +//! flow mirrors the upstream one-call dial step for step, so keep it in sync when bumping +//! `dig-peer-protocol`. //! //! ## `network_id` typing //! -//! [`crate::types::config::GossipConfig`] stores `network_id` as [`dig_peer_protocol::Bytes32`]. Chia’s +//! [`crate::types::config::GossipConfig`] stores `network_id` as [`chia_protocol::Bytes32`]. Chia’s //! wire [`Handshake::network_id`](chia_protocol::Handshake) is a [`String`]; the conventional -//! encoding is the **lowercase hex** of the 32 bytes (matches [`Bytes32`’s `Display`](dig_peer_protocol::Bytes32)). +//! encoding is the **lowercase hex** of the 32 bytes (matches [`Bytes32`’s `Display`](chia_protocol::Bytes32)). #![allow(clippy::result_large_err)] // Upstream [`ClientError`] is wide; we propagate it verbatim per API-004 `GossipError::ClientError`. +use crate::connection::chia_opcodes; +use crate::connection::dial_error::{non_handshake_first_frame, DialError}; +use dig_peer_protocol::LinkError; use std::net::SocketAddr; +use chia_protocol::Handshake; use dig_peer_protocol::ChiaCertificate; +use dig_peer_protocol::DigMessage; use dig_peer_protocol::Streamable; -use dig_peer_protocol::{Handshake, Message, NodeType, ProtocolMessageTypes}; use tokio::net::TcpStream; use tokio::sync::mpsc; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; -use dig_peer_protocol::{ClientError, Peer, PeerOptions}; +use dig_peer_protocol::{ClientError, DigLink, LinkOptions}; use crate::connection::handshake::{validate_remote_handshake, ADVERTISED_PROTOCOL_VERSION}; #[cfg(any(feature = "native-tls", feature = "rustls"))] use dig_peer_protocol::Connector; -/// Successful outbound dial: live [`Peer`], inbound wire channel, parsed remote handshake, SPKI DER. +/// Successful outbound dial: live [`DigLink`], inbound wire channel, parsed remote handshake, SPKI DER. /// /// SPEC §5.1 step 4 — "Wrap in PeerConnection with gossip metadata." This struct carries /// the raw materials needed to build a [`crate::types::peer::PeerConnection`]. @@ -70,8 +73,8 @@ use dig_peer_protocol::Connector; /// `remote_spki_der` is the **SubjectPublicKeyInfo** raw bytes inside the peer’s leaf certificate /// (same slice API-005 tests take from `x509-parser`). pub struct OutboundConnectResult { - pub peer: Peer, - pub inbound_rx: mpsc::Receiver, + pub peer: DigLink, + pub inbound_rx: mpsc::Receiver, pub their_handshake: Handshake, /// Raw SPKI DER bytes for [`crate::types::peer::peer_id_from_tls_spki_der`]. pub remote_spki_der: Vec, @@ -105,18 +108,18 @@ pub(crate) fn tls_connector_for_cert(cert: &ChiaCertificate) -> Result String { +pub(crate) fn network_id_handshake_string(network_id: chia_protocol::Bytes32) -> String { network_id.to_string() } -/// Extract remote **SubjectPublicKeyInfo DER** before [`Peer::from_websocket`] consumes the stream. +/// Extract remote **SubjectPublicKeyInfo DER** before [`DigLink::from_websocket`] consumes the stream. /// /// SPEC §5.3 — "Peer identity from mTLS: `PeerId = SHA256(remote_TLS_certificate_public_key)`." /// Because mTLS guarantees both sides present certificates, each side can derive the other's /// `PeerId` from the certificate exchanged during the TLS handshake. Matches Chia's /// `peer_node_id` derivation from certificate hash (`ws_connection.py:95`). /// -/// **Rationale:** `Peer::from_websocket` splits the socket and spawns the reader; certificate +/// **Rationale:** `DigLink::from_websocket` splits the socket and spawns the reader; certificate /// inspection must happen on the intact [`WebSocketStream`] returned from /// `connect_async_tls_with_config`. fn remote_spki_der_from_ws( @@ -194,9 +197,9 @@ pub(crate) async fn connect_outbound_peer( network_id: String, connector: Connector, socket_addr: SocketAddr, - options: PeerOptions, + options: LinkOptions, software_version: String, -) -> Result { +) -> Result { let uri = format!("wss://{socket_addr}/ws"); // Bound the transport buffer on the dial side too (CON-001 / §5.2) so a hostile server // cannot make tungstenite buffer up to its 64 MiB default before an app cap applies — @@ -207,10 +210,11 @@ pub(crate) async fn connect_outbound_peer( false, Some(connector), ) - .await?; + .await + .map_err(|e| DialError::Link(LinkError::from(e)))?; let remote_spki_der = remote_spki_der_from_ws(&ws)?; - let (peer, mut receiver) = Peer::from_websocket(ws, options)?; + let (peer, mut receiver) = DigLink::from_websocket(ws, options).map_err(DialError::Link)?; // SPEC §5.1 step 3 — "Sends chia-protocol::Handshake with DIG network_id." // SPEC §1.5 #1 — "connect_peer() sends chia-protocol::Handshake with capabilities list." @@ -222,37 +226,39 @@ pub(crate) async fn connect_outbound_peer( // the listener replies with, so a peer learns the same build from us either way. software_version, server_port: 0, - node_type: NodeType::Wallet, + node_type: chia_protocol::NodeType::Wallet, capabilities: vec![ (1, "1".to_string()), // SPEC §1.5 #1 — BASE protocol capability (2, "1".to_string()), // BLOCK_HEADERS capability (3, "1".to_string()), // RATE_LIMITS_V2 capability ], }) - .await?; + .await + .map_err(DialError::Link)?; + // Policy, not transport: the link opened and the peer closed it without ever completing + // the handshake, so re-dialling the same address will meet the same peer behaviour. let Some(message) = receiver.recv().await else { - return Err(ClientError::MissingHandshake); + return Err(DialError::Client(ClientError::MissingHandshake)); }; - if message.msg_type != ProtocolMessageTypes::Handshake { - return Err(ClientError::InvalidResponse( - vec![ProtocolMessageTypes::Handshake], - message.msg_type, - )); + // Policy, not transport: the peer was reached and answered with something other than a + // `Handshake`, so re-dialling the same address meets the same behaviour (`dial_error` docs). + if message.msg_type != chia_opcodes::HANDSHAKE { + return Err(non_handshake_first_frame(message.msg_type)); } - let handshake = Handshake::from_bytes(&message.data)?; + let handshake = + Handshake::from_bytes(&message.data).map_err(|e| DialError::Link(LinkError::from(e)))?; - if handshake.node_type != NodeType::FullNode { - return Err(ClientError::WrongNodeType( - NodeType::FullNode, + if handshake.node_type != chia_protocol::NodeType::FullNode { + return Err(DialError::Client(ClientError::WrongNodeType( + chia_protocol::NodeType::FullNode, handshake.node_type, - )); + ))); } - let remote_software_version_sanitized = - validate_remote_handshake(&handshake, &network_id).map_err(ClientError::from)?; + let remote_software_version_sanitized = validate_remote_handshake(&handshake, &network_id)?; Ok(OutboundConnectResult { peer, diff --git a/src/discovery/address_manager.rs b/src/discovery/address_manager.rs index 3ee540e..7d6a1a8 100644 --- a/src/discovery/address_manager.rs +++ b/src/discovery/address_manager.rs @@ -45,7 +45,7 @@ use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::sync::Mutex; -use dig_peer_protocol::TimestampedPeerInfo; +use chia_protocol::TimestampedPeerInfo; use rand::Rng; use rand::RngCore; diff --git a/src/discovery/introducer_client.rs b/src/discovery/introducer_client.rs index d44988e..57ac20d 100644 --- a/src/discovery/introducer_client.rs +++ b/src/discovery/introducer_client.rs @@ -12,29 +12,41 @@ //! //! # Design decisions //! -//! - **Mirror `dig_peer_protocol::connect_peer` handshake:** Upstream -//! [`connect_peer`](dig_peer_protocol::connect_peer) only accepts a [`std::net::SocketAddr`]. +//! - **Mirror the one-call dial's handshake:** [`DigLink::connect`](dig_peer_protocol::DigLink::connect) +//! only accepts a [`std::net::SocketAddr`]. //! Introducers advertise a full `wss://…/ws` URL ([`IntroducerConfig::endpoint`](crate::types::config::IntroducerConfig)), -//! so we call [`Peer::connect_full_uri`](dig_peer_protocol::Peer::connect_full_uri) then replay the same -//! outbound [`Handshake`] + FullNode validation as `vendor/chia-sdk-client/src/connect.rs` — +//! so we call [`DigLink::connect_full_uri`](dig_peer_protocol::DigLink::connect_full_uri) then replay the same +//! outbound [`Handshake`] + FullNode validation the one-call dial performs internally +//! (see [`src/connection/outbound.rs`](crate::connection::outbound) for how the dial sequence is replayed); //! any drift vs upstream should be fixed in lockstep when bumping `chia-sdk-client`. //! - **Whole-operation timeout:** DSC-004 requires one timeout covering connect + handshake + RPC. //! We wrap the async block in [`tokio::time::timeout`]; on expiry we return //! [`GossipError::IntroducerError`](crate::error::GossipError::IntroducerError) with a stable substring //! so tests can distinguish timeout from transport failures. -//! - **TLS feature gate:** Without `native-tls` / `rustls`, [`Peer::connect_full_uri`] does not exist; -//! [`IntroducerClient::query_peers`] returns [`GossipError::ClientError`](crate::error::GossipError::ClientError) -//! (`UnsupportedTls`) so `--no-default-features` builds remain coherent. +//! - **TLS feature gate:** [`DigLink::connect_full_uri`] exists only with `native-tls` or `rustls`, +//! so the dials below are gated on one of them being enabled. +//! +//! A build with NEITHER backend **does not compile today** — `dig_peer_protocol::{Client, +//! ClientState}` are imported unconditionally in `service::state`, `service::gossip_service` and +//! `lib.rs`, and no CI job builds without a TLS backend. This module used to carry +//! `UnsupportedTls` stubs and a claim that `--no-default-features` builds "remain coherent"; both +//! are gone, because the stubs served only that claim and the claim was false +//! (dig_ecosystem#2225 tracks making such a build work, or removing the possibility). +use crate::connection::chia_opcodes; +use crate::connection::dial_error::{non_handshake_first_frame, DialError}; +use crate::types::dig_messages::DigMessageType; +use dig_peer_protocol::LinkError; use std::time::Duration; -use dig_peer_protocol::{Bytes32, Handshake, NodeType, ProtocolMessageTypes, TimestampedPeerInfo}; +use chia_protocol::{Bytes32, Handshake, TimestampedPeerInfo}; +use dig_peer_protocol::NodeType; use crate::discovery::introducer_register_wire::{RegisterAck, RegisterPeer}; use crate::discovery::introducer_wire::{RequestPeersIntroducer, RespondPeersIntroducer}; use dig_peer_protocol::ChiaCertificate; use dig_peer_protocol::Streamable; -use dig_peer_protocol::{load_ssl_cert, ClientError, Peer, PeerOptions}; +use dig_peer_protocol::{load_ssl_cert, ClientError, DigLink, LinkOptions}; use crate::connection::handshake::ADVERTISED_PROTOCOL_VERSION; #[cfg(any(feature = "native-tls", feature = "rustls"))] @@ -70,7 +82,7 @@ impl IntroducerClient { /// * `wss_uri` — full WebSocket URI (`wss://host:port/ws`, …) from [`IntroducerConfig::endpoint`](crate::types::config::IntroducerConfig). /// * `local_certificate` — this node’s TLS identity (mutual TLS with the introducer). /// * `network_id` — DIG genesis id; encoded for the Chia handshake string via [`network_id_handshake_string`]. - /// * `peer_options` — forwarded to [`Peer::connect_full_uri`](dig_peer_protocol::Peer::connect_full_uri) (rate limits, etc.). + /// * `peer_options` — forwarded to [`DigLink::connect_full_uri`](dig_peer_protocol::DigLink::connect_full_uri) (rate limits, etc.). /// * `operation_timeout` — hard cap for **connect + handshake + introducer request** (DSC-004 acceptance). /// * `software_version` — [`GossipConfig::software_version`](crate::types::config::GossipConfig::software_version), /// advertised to the introducer exactly as it is to any other peer (dig_ecosystem#2215). An @@ -86,7 +98,7 @@ impl IntroducerClient { wss_uri: &str, local_certificate: &ChiaCertificate, network_id: Bytes32, - peer_options: PeerOptions, + peer_options: LinkOptions, operation_timeout: Duration, software_version: &str, ) -> Result, GossipError> { @@ -94,53 +106,58 @@ impl IntroducerClient { let work = async { let connector = tls_connector_for_cert(local_certificate)?; - let (peer, mut receiver) = - Peer::connect_full_uri(wss_uri, connector, peer_options).await?; + let (peer, mut receiver) = DigLink::connect_full_uri(wss_uri, connector, peer_options) + .await + .map_err(DialError::Link)?; peer.send(Handshake { network_id: network_string.clone(), protocol_version: ADVERTISED_PROTOCOL_VERSION.to_string(), software_version: software_version.to_string(), server_port: 0, - node_type: NodeType::Wallet, + node_type: chia_protocol::NodeType::Wallet, capabilities: vec![ (1, "1".to_string()), (2, "1".to_string()), (3, "1".to_string()), ], }) - .await?; + .await + .map_err(DialError::Link)?; + // Policy, not transport: the link opened and the introducer closed it without ever + // completing the handshake, so re-dialling the same endpoint meets the same behaviour. let Some(message) = receiver.recv().await else { - return Err(ClientError::MissingHandshake); + return Err(DialError::Client(ClientError::MissingHandshake)); }; - if message.msg_type != ProtocolMessageTypes::Handshake { - return Err(ClientError::InvalidResponse( - vec![ProtocolMessageTypes::Handshake], - message.msg_type, - )); + // Policy, not transport: the peer was reached and answered with something other + // than a `Handshake`, so re-dialling meets the same behaviour (`dial_error` docs). + if message.msg_type != chia_opcodes::HANDSHAKE { + return Err(non_handshake_first_frame(message.msg_type)); } - let handshake = Handshake::from_bytes(&message.data)?; + let handshake = Handshake::from_bytes(&message.data) + .map_err(|e| DialError::Link(LinkError::from(e)))?; - if handshake.node_type != NodeType::FullNode { - return Err(ClientError::WrongNodeType( - NodeType::FullNode, + if handshake.node_type != chia_protocol::NodeType::FullNode { + return Err(DialError::Client(ClientError::WrongNodeType( + chia_protocol::NodeType::FullNode, handshake.node_type, - )); + ))); } if handshake.network_id != network_string { - return Err(ClientError::WrongNetwork( + return Err(DialError::Client(ClientError::WrongNetwork( network_string, handshake.network_id, - )); + ))); } let response: RespondPeersIntroducer = peer .request_infallible(RequestPeersIntroducer::new()) - .await?; + .await + .map_err(DialError::Link)?; Ok(response.peer_list) }; @@ -156,7 +173,7 @@ impl IntroducerClient { /// Register this node’s P2P address with a DIG introducer (**DSC-005**). /// /// Mirrors [`Self::query_peers`] for TLS + [`Handshake`] validation, then performs - /// `RegisterPeer → RegisterAck` via [`Peer::request_infallible`](dig_peer_protocol::Peer::request_infallible). + /// `RegisterPeer → RegisterAck` via [`DigLink::request_infallible`](dig_peer_protocol::DigLink::request_infallible). /// /// # Returns /// @@ -168,7 +185,7 @@ impl IntroducerClient { wss_uri: &str, local_certificate: &ChiaCertificate, network_id: Bytes32, - peer_options: PeerOptions, + peer_options: LinkOptions, operation_timeout: Duration, registration: &PeerRegistration, software_version: &str, @@ -177,48 +194,52 @@ impl IntroducerClient { let work = async { let connector = tls_connector_for_cert(local_certificate)?; - let (peer, mut receiver) = - Peer::connect_full_uri(wss_uri, connector, peer_options).await?; + let (peer, mut receiver) = DigLink::connect_full_uri(wss_uri, connector, peer_options) + .await + .map_err(DialError::Link)?; peer.send(Handshake { network_id: network_string.clone(), protocol_version: ADVERTISED_PROTOCOL_VERSION.to_string(), software_version: software_version.to_string(), server_port: 0, - node_type: NodeType::Wallet, + node_type: chia_protocol::NodeType::Wallet, capabilities: vec![ (1, "1".to_string()), (2, "1".to_string()), (3, "1".to_string()), ], }) - .await?; + .await + .map_err(DialError::Link)?; + // Policy, not transport: the link opened and the introducer closed it without ever + // completing the handshake, so re-dialling the same endpoint meets the same behaviour. let Some(message) = receiver.recv().await else { - return Err(ClientError::MissingHandshake); + return Err(DialError::Client(ClientError::MissingHandshake)); }; - if message.msg_type != ProtocolMessageTypes::Handshake { - return Err(ClientError::InvalidResponse( - vec![ProtocolMessageTypes::Handshake], - message.msg_type, - )); + // Policy, not transport: the peer was reached and answered with something other + // than a `Handshake`, so re-dialling meets the same behaviour (`dial_error` docs). + if message.msg_type != chia_opcodes::HANDSHAKE { + return Err(non_handshake_first_frame(message.msg_type)); } - let handshake = Handshake::from_bytes(&message.data)?; + let handshake = Handshake::from_bytes(&message.data) + .map_err(|e| DialError::Link(LinkError::from(e)))?; - if handshake.node_type != NodeType::FullNode { - return Err(ClientError::WrongNodeType( - NodeType::FullNode, + if handshake.node_type != chia_protocol::NodeType::FullNode { + return Err(DialError::Client(ClientError::WrongNodeType( + chia_protocol::NodeType::FullNode, handshake.node_type, - )); + ))); } if handshake.network_id != network_string { - return Err(ClientError::WrongNetwork( + return Err(DialError::Client(ClientError::WrongNetwork( network_string, handshake.network_id, - )); + ))); } let body = RegisterPeer::new( @@ -226,7 +247,25 @@ impl IntroducerClient { registration.port, registration.node_type, ); - peer.request_infallible::(body).await + // Opcode 218/219 have no `ProtocolMessageTypes` variant, so they travel as raw + // DIG opcodes rather than through the Chia-typed `request_infallible` path. + let reply = peer + .request_dig( + DigMessageType::RegisterPeer as u8, + body.to_bytes() + .map_err(|e| DialError::Link(LinkError::from(e)))? + .into(), + ) + .await + .map_err(DialError::Link)?; + RegisterAck::from_dig_message(&reply) + .ok_or_else(|| { + DialError::Link(LinkError::InvalidResponse( + vec![DigMessageType::RegisterAck as u8], + reply.msg_type, + )) + })? + .map_err(|e| DialError::Link(LinkError::from(e))) }; match tokio::time::timeout(operation_timeout, work).await { @@ -236,35 +275,6 @@ impl IntroducerClient { )), } } - - /// TLS-disabled builds cannot dial introducers — fail fast with the same error shape other - /// transports use when TLS is unavailable. - #[cfg(not(any(feature = "native-tls", feature = "rustls")))] - pub async fn register_with_introducer( - _wss_uri: &str, - _local_certificate: &ChiaCertificate, - _network_id: Bytes32, - _peer_options: PeerOptions, - _operation_timeout: Duration, - _registration: &PeerRegistration, - _software_version: &str, - ) -> Result { - Err(ClientError::UnsupportedTls.into()) - } - - /// TLS-disabled builds cannot dial introducers — fail fast with the same error shape other - /// transports use when TLS is unavailable. - #[cfg(not(any(feature = "native-tls", feature = "rustls")))] - pub async fn query_peers( - _wss_uri: &str, - _local_certificate: &ChiaCertificate, - _network_id: Bytes32, - _peer_options: PeerOptions, - _operation_timeout: Duration, - _software_version: &str, - ) -> Result, GossipError> { - Err(ClientError::UnsupportedTls.into()) - } } /// Load node TLS material for introducer dials — thin wrapper so call sites share [`load_ssl_cert`] diff --git a/src/discovery/introducer_register_wire.rs b/src/discovery/introducer_register_wire.rs index 0cb867f..de50b1b 100644 --- a/src/discovery/introducer_register_wire.rs +++ b/src/discovery/introducer_register_wire.rs @@ -1,43 +1,30 @@ -//! Introducer **registration** wire bodies for DIG opcodes **218** (`RegisterPeer`) and **219** (`RegisterAck`). +//! Introducer **registration** wire bodies for DIG opcodes **218** (`RegisterPeer`) and +//! **219** (`RegisterAck`) — re-exported from `dig-peer-protocol`. //! -//! # Why this module exists (**DSC-005**) +//! # Why these live upstream now (**DSC-005**) //! -//! Introducer registration is a **DIG extension** — it is not part of stock Chia’s introducer RPC -//! ([`ProtocolMessageTypes::RequestPeersIntroducer`] / [`RespondPeersIntroducer`] only cover peer -//! list fetch). We still send traffic inside the standard [`chia_protocol::Message`] envelope so -//! [`dig_peer_protocol::Peer::request_infallible`] can correlate request/response `id`s exactly like -//! full-node RPCs. +//! Introducer registration is a **DIG extension**: stock Chia's introducer RPC covers only +//! peer-list fetch ([`ProtocolMessageTypes::RequestPeersIntroducer`] / +//! [`RespondPeersIntroducer`](chia_protocol::ProtocolMessageTypes::RespondPeersIntroducer)). //! -//! Stock **`chia-protocol` 0.26** on crates.io stops enumerating [`ProtocolMessageTypes`] at **107**, -//! which means `Message::from_bytes` would reject opcodes **218/219** during decode. `dig-gossip` -//! therefore **vendors** `chia-protocol` with two extra enum variants (see `vendor/chia-protocol/README.dig-gossip.md`). -//! The [`chia_streamable_macro::streamable`] `message` attribute maps struct names **one-to-one** -//! onto those variants — keep the Rust identifiers `RegisterPeer` / `RegisterAck`. +//! These bodies used to be declared here with `#[streamable(message)]`, which makes the +//! proc-macro emit a `ProtocolMessageTypes::RegisterPeer` path — a variant that exists only +//! in a **forked** `chia-protocol`. This forced a fork just to name the opcodes. +//! +//! `dig-peer-protocol` declares them with plain `#[streamable]` and pairs each with +//! `to_dig_message` / `from_dig_message`, carrying opcode 218/219 as a raw +//! [`DigMessage`](dig_peer_protocol::DigMessage) byte. Since DIG opcodes travel as raw `msg_type` +//! bytes—not as enum variants—the fork is no longer required. The bodies now have a single +//! definition shared with every other consumer instead of one per repo (dig_ecosystem#2228). +//! +//! The encoded bytes are unchanged; `tests/wire_golden_vectors_tests.rs` pins them. //! //! # Traceability //! //! - **DSC-005:** [`docs/requirements/domains/discovery/specs/DSC-005.md`](../../docs/requirements/domains/discovery/specs/DSC-005.md) //! - **API-009 alignment:** [`DigMessageType::RegisterPeer`](crate::types::dig_messages::DigMessageType) / -//! [`DigMessageType::RegisterAck`](crate::types::dig_messages::DigMessageType) mirror the same numeric IDs for -//! documentation, inbound rate-limit tables, and future non-`Peer` transports. +//! [`DigMessageType::RegisterAck`](crate::types::dig_messages::DigMessageType) mirror the same +//! numeric ids for documentation, inbound rate-limit tables, and future non-link transports. //! - **STR-003:** re-exported from [`crate::lib`](../../lib.rs). -use chia_streamable_macro::streamable; -use dig_peer_protocol::NodeType; - -/// Registration request: advertise this node’s P2P reachability to the introducer index. -#[streamable(message)] -pub struct RegisterPeer { - /// Externally reachable IP or hostname (operator-supplied; often **not** the bind address). - ip: String, - /// P2P listening port. - port: u16, - /// Declared service role — gossip nodes register as [`NodeType::FullNode`] per SPEC §6.5. - node_type: NodeType, -} - -/// Introducer acknowledgement — `success == false` is a **valid** wire outcome (policy rejection). -#[streamable(message)] -pub struct RegisterAck { - success: bool, -} +pub use dig_peer_protocol::{RegisterAck, RegisterPeer}; diff --git a/src/discovery/introducer_wire.rs b/src/discovery/introducer_wire.rs index ead54e8..d8cf767 100644 --- a/src/discovery/introducer_wire.rs +++ b/src/discovery/introducer_wire.rs @@ -19,8 +19,8 @@ //! - **DSC-004:** [`docs/requirements/domains/discovery/specs/DSC-004.md`](../../docs/requirements/domains/discovery/specs/DSC-004.md) //! - **STR-003:** re-exported from [`crate::lib`](../../lib.rs) alongside other protocol surface types. +use chia_protocol::TimestampedPeerInfo; use chia_streamable_macro::streamable; -use dig_peer_protocol::TimestampedPeerInfo; /// Empty introducer “get peers” request (protocol type **63**). #[streamable(message)] diff --git a/src/discovery/mod.rs b/src/discovery/mod.rs index bf652cc..22606fe 100644 --- a/src/discovery/mod.rs +++ b/src/discovery/mod.rs @@ -10,7 +10,8 @@ pub mod introducer_client; pub mod introducer_peers; /// DIG introducer **registration** wire types (**218** / **219**) — DSC-005. /// -/// Depends on the vendored [`chia_protocol::ProtocolMessageTypes`] extension (see `vendor/chia-protocol`). +/// Declares [`RegisterPeer`] and [`RegisterAck`] wire bodies (opcodes 218/219) that travel as +/// raw `msg_type` bytes in [`dig_peer_protocol::DigMessage`], re-exported from `dig-peer-protocol`. pub mod introducer_register_wire; /// Introducer wire structs for protocol IDs **63** / **64** (DSC-004). /// diff --git a/src/discovery/node_discovery.rs b/src/discovery/node_discovery.rs index 1b06feb..4260569 100644 --- a/src/discovery/node_discovery.rs +++ b/src/discovery/node_discovery.rs @@ -33,8 +33,8 @@ use std::net::SocketAddr; use std::time::Duration; +use chia_protocol::TimestampedPeerInfo; use dig_peer_protocol::Network; -use dig_peer_protocol::TimestampedPeerInfo; use crate::discovery::address_manager::AddressManager; use crate::types::config::GossipConfig; diff --git a/src/error.rs b/src/error.rs index 1a0d7a7..4a3f265 100644 --- a/src/error.rs +++ b/src/error.rs @@ -85,22 +85,41 @@ use crate::types::peer::PeerId; #[derive(Debug, Clone, Error)] pub enum GossipError { // -- Transport / wire errors ----------------------------------------------- - /// Errors originating from `chia-sdk-client` internals: `connect_peer()`, - /// TLS connector creation, WebSocket I/O, rate-limiter rejection, etc. + /// **The peer was reached and we rejected it on policy** — or a client-side TLS/certificate + /// step failed. See [`DialError`](crate::connection::dial_error::DialError) for the full + /// statement of the split and why it is load-bearing. /// /// Wrapped in [`Arc`] so that `GossipError` can derive [`Clone`] even though /// [`dig_peer_protocol::ClientError`] does not (API-004 implementation notes). /// - /// **When:** Any `chia-sdk-client` call fails (outbound connect, `Peer::send()`, - /// `Peer::request_raw()`). - /// **Caller action:** Log the inner error; depending on context, retry the - /// operation or disconnect the peer. + /// **When:** A remote handshake fails validation (wrong `network_id`, wrong `node_type`, an + /// incompatible protocol version — the typed + /// [`ClientError::WrongNetwork`](dig_peer_protocol::ClientError::WrongNetwork) and friends + /// reach the caller intact), or TLS material fails to load. + /// **Caller action:** Do **not** retry the same address — the verdict will not change. Try a + /// different peer. /// **Produced by:** [`crate::service::gossip_service::load_tls_material`], - /// [`crate::service::gossip_handle::GossipHandle::connect_to`], - /// [`crate::service::gossip_handle::GossipHandle::request`]. + /// [`crate::connection::outbound`] and + /// [`crate::connection::listener`] (both handshake legs agree), + /// [`crate::discovery::introducer_client::IntroducerClient`]. #[error("client error: {0}")] ClientError(Arc), + /// A [`DigLink`](dig_peer_protocol::DigLink) transport operation failed. + /// + /// Wrapped in [`Arc`] for the same reason as [`ClientError`](Self::ClientError): + /// [`dig_peer_protocol::LinkError`] is not [`Clone`], but `GossipError` is. + /// + /// **The peer was never reached, or the pipe broke** — connection refused, TLS failure, + /// timeout, framing error. The counterpart to [`ClientError`](Self::ClientError); see + /// [`DialError`](crate::connection::dial_error::DialError) for the split. + /// + /// **When:** Any peer-link call fails — dialling a peer, adopting an accepted + /// websocket, sending a frame, or awaiting a correlated reply. + /// **Caller action:** Retrying the same address is reasonable; the peer may simply be down. + #[error("link error: {0}")] + LinkError(Arc), + /// File-system or network I/O failure not covered by [`ClientError`](Self::ClientError). /// /// Stored as [`String`] (not `std::io::Error`) because `std::io::Error` does not @@ -183,6 +202,18 @@ pub enum GossipError { #[error("duplicate connection to peer {0}")] DuplicateConnection(PeerId), + /// The peer answered a dial with an opcode outside `ProtocolMessageTypes`, so no typed + /// [`ClientError`](dig_peer_protocol::ClientError) can name it. + /// + /// **When:** the first frame after connect is neither a `Handshake` nor any other Chia-band + /// message — a DIG-band opcode, or garbage. + /// **Caller action:** Treat as **policy**, not transport: the peer was reached and rejected on + /// content, so re-dialling the same address meets the same behaviour. + /// **Produced by:** [`GossipHandle::connect_to`](crate::service::gossip_handle::GossipHandle) + /// and the DSC-004 introducer dials. + #[error("expected a Handshake, found unknown opcode {0}")] + UnknownHandshakeOpcode(u8), + /// The target address resolved to our own listen address (self-dial guard). /// /// **When:** `connect_to` detects that the target matches @@ -218,7 +249,10 @@ pub enum GossipError { /// **Produced by:** [`crate::discovery::introducer_client::IntroducerClient`] when the /// whole-operation [`tokio::time::timeout`] fires (**DSC-004** query: `"introducer query timed out"`; /// **DSC-005** registration: `"introducer registration timed out"`). - /// Transport failures during the same call surface as [`ClientError`](Self::ClientError) instead. + /// Other failures during the same call are split by + /// [`DialError`](crate::connection::dial_error::DialError): a transport failure surfaces as + /// [`LinkError`](Self::LinkError) (retryable), a handshake-policy rejection as + /// [`ClientError`](Self::ClientError) (not). #[error("introducer error: {0}")] IntroducerError(String), @@ -376,3 +410,9 @@ impl From for GossipError { Self::ClientError(Arc::new(value)) } } + +impl From for GossipError { + fn from(value: dig_peer_protocol::LinkError) -> Self { + Self::LinkError(Arc::new(value)) + } +} diff --git a/src/gossip/backpressure.rs b/src/gossip/backpressure.rs index df1a4ab..b115a39 100644 --- a/src/gossip/backpressure.rs +++ b/src/gossip/backpressure.rs @@ -20,7 +20,7 @@ use std::collections::HashSet; -use dig_peer_protocol::Bytes32; +use chia_protocol::Bytes32; use crate::constants::{ BACKPRESSURE_BULK_DROP_THRESHOLD, BACKPRESSURE_NORMAL_DELAY_THRESHOLD, diff --git a/src/gossip/broadcaster.rs b/src/gossip/broadcaster.rs index 068645d..9dfce04 100644 --- a/src/gossip/broadcaster.rs +++ b/src/gossip/broadcaster.rs @@ -58,34 +58,47 @@ pub enum BroadcastStrategy { /// - NewTransaction → ERLAY (if feature enabled, else Plumtree) /// - RespondPeers, RespondTransaction → Unicast (not broadcast) pub fn classify_broadcast( - msg_type: ProtocolMessageTypes, + msg_type: u8, #[allow(unused_variables)] erlay_enabled: bool, ) -> BroadcastStrategy { + // The DIG band is matched first, on the raw byte. Its opcodes have no + // `ProtocolMessageTypes` variant to match against -- that gap is exactly what the + // vendored chia-protocol fork used to fill (dig_ecosystem#2228). + match msg_type { + // `StoreMelted` (221, #1316) and `HoldingsAnnounce` (222, #1428) are PUBLIC + // all-peers floods: they disseminate like the other announce broadcasts, + // terminating via the receiver's seen_set. + crate::service::store_melted::STORE_MELTED + | crate::service::holdings_announce::HOLDINGS_ANNOUNCE => { + return BroadcastStrategy::Plumtree + } + // A `DigMessage` (220) is a 1:1 directed frame (WU6), never broadcast. + crate::service::dig_message::DIG_MESSAGE => return BroadcastStrategy::Unicast, + _ => {} + } + + // Anything else is a Chia opcode; an unrecognised byte gets the safe default below. + let Ok(msg_type) = ::from_bytes(&[msg_type]) + else { + return BroadcastStrategy::Plumtree; + }; + use ProtocolMessageTypes::*; match msg_type { // ERLAY: NewTransaction uses low-fanout flooding + reconciliation #[cfg(feature = "erlay")] NewTransaction if erlay_enabled => BroadcastStrategy::Erlay, - // Plumtree: gossip messages use eager/lazy push. `StoreMelted` (opcode 221) is a - // PUBLIC all-peers flood (store-melt propagation, #1316) — it disseminates like the - // other announce broadcasts, terminating via the receiver's seen_set + the dig-node - // "only rebroadcast on a real holding→deleted transition" guard (#3). - NewPeak - | NewTransaction - | NewUnfinishedBlock - | RespondBlock - | RespondUnfinishedBlock - | StoreMelted - // `HoldingsAnnounce` (opcode 222) is a PUBLIC all-peers flood (holdings discovery, - // #1428) — it disseminates like the other announce broadcasts, deduped via the - // receiver's seen_set on the announcement bytes (a later `seq` supersedes). - | HoldingsAnnounce => BroadcastStrategy::Plumtree, + // Plumtree: gossip messages use eager/lazy push. + NewPeak | NewTransaction | NewUnfinishedBlock | RespondBlock | RespondUnfinishedBlock => { + BroadcastStrategy::Plumtree + } // Unicast: response messages + directed dig-message envelopes are never // broadcast — a `DigMessage` (opcode 220) is a 1:1 directed frame (WU6). - RespondPeers | RespondTransaction | RespondBlocks | RejectBlock | RejectBlocks - | DigMessage => BroadcastStrategy::Unicast, + RespondPeers | RespondTransaction | RespondBlocks | RejectBlock | RejectBlocks => { + BroadcastStrategy::Unicast + } // Default: Plumtree for anything else _ => BroadcastStrategy::Plumtree, diff --git a/src/gossip/compact_block.rs b/src/gossip/compact_block.rs index 2de7274..6fa3fb3 100644 --- a/src/gossip/compact_block.rs +++ b/src/gossip/compact_block.rs @@ -21,7 +21,7 @@ //! Fallback to full block via RequestBlock when >5 missing. //! SPEC §1.8#2: "90%+ block propagation bandwidth reduction." -use dig_peer_protocol::Bytes32; +use chia_protocol::Bytes32; use crate::constants::{COMPACT_BLOCK_MAX_MISSING_TXS, SHORT_TX_ID_BYTES}; diff --git a/src/gossip/erlay.rs b/src/gossip/erlay.rs index 0b2eb4f..194b5e4 100644 --- a/src/gossip/erlay.rs +++ b/src/gossip/erlay.rs @@ -27,7 +27,7 @@ use std::collections::HashSet; -use dig_peer_protocol::Bytes32; +use chia_protocol::Bytes32; use crate::constants::{ ERLAY_FLOOD_PEER_COUNT, ERLAY_FLOOD_SET_ROTATION_SECS, ERLAY_RECONCILIATION_INTERVAL_MS, diff --git a/src/gossip/message_cache.rs b/src/gossip/message_cache.rs index 74774b0..52c9588 100644 --- a/src/gossip/message_cache.rs +++ b/src/gossip/message_cache.rs @@ -12,7 +12,7 @@ //! the message bytes and insertion timestamp. `get()` checks TTL and //! returns None for expired entries. -use dig_peer_protocol::Bytes32; +use chia_protocol::Bytes32; use lru::LruCache; use std::num::NonZeroUsize; diff --git a/src/gossip/plumtree.rs b/src/gossip/plumtree.rs index da8c562..b1748cb 100644 --- a/src/gossip/plumtree.rs +++ b/src/gossip/plumtree.rs @@ -21,7 +21,7 @@ use std::collections::{HashMap, HashSet}; -use dig_peer_protocol::Bytes32; +use chia_protocol::Bytes32; use crate::constants::PLUMTREE_LAZY_TIMEOUT_MS; use crate::types::peer::{metric_unix_timestamp_secs, PeerId}; diff --git a/src/gossip/priority.rs b/src/gossip/priority.rs index 90101d2..8e8fd19 100644 --- a/src/gossip/priority.rs +++ b/src/gossip/priority.rs @@ -1,4 +1,4 @@ -//! Message priority lanes and per-connection queues (**PRI-001** through **PRI-004**). +//! DigMessage priority lanes and per-connection queues (**PRI-001** through **PRI-004**). //! //! # Requirements //! @@ -6,7 +6,7 @@ //! - **PRI-002** — PriorityOutbound: three VecDeque per connection //! - **PRI-003** — Drain order: critical → normal → one bulk //! - **PRI-004** — Starvation prevention: 1 bulk per PRIORITY_STARVATION_RATIO -//! - **Master SPEC:** §8.4 (Message Priority Lanes) +//! - **Master SPEC:** §8.4 (DigMessage Priority Lanes) //! //! # Design //! @@ -16,11 +16,11 @@ use std::collections::VecDeque; -use dig_peer_protocol::{Message, ProtocolMessageTypes}; +use dig_peer_protocol::{DigMessage, ProtocolMessageTypes}; use crate::constants::PRIORITY_STARVATION_RATIO; -/// Message priority level (**PRI-001**). +/// DigMessage priority level (**PRI-001**). /// /// SPEC §8.4: "Critical = always first. Normal = after critical. Bulk = last." #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -58,13 +58,7 @@ impl MessagePriority { | RespondPeers | RequestMempoolTransactions | RequestPeersIntroducer - | RespondPeersIntroducer - // StoreMelted (opcode 221): a small, infrequent public broadcast — never - // consensus-critical, so it rides the bulk lane (#1316). - | StoreMelted - // HoldingsAnnounce (opcode 222): a small, periodic public discovery broadcast — - // never consensus-critical, so it rides the bulk lane (#1428). - | HoldingsAnnounce => Self::Bulk, + | RespondPeersIntroducer => Self::Bulk, // Default for any unclassified type _ => Self::Normal, @@ -83,12 +77,12 @@ impl MessagePriority { // Plumtree control → Normal 214..=217 => Self::Normal, // StoreMelted (opcode 221) → Bulk: small, infrequent public broadcast (#1316). - // Kept in agreement with `from_chia_type` (221 is a `ProtocolMessageTypes` - // variant) so both classification paths route store-melted to the bulk lane. + // This is the ONLY path that classifies it: upstream `ProtocolMessageTypes` is a + // closed enum with no `StoreMelted` variant (dig_ecosystem#2228). crate::service::store_melted::STORE_MELTED => Self::Bulk, // HoldingsAnnounce (opcode 222) → Bulk: small, periodic public discovery - // broadcast (#1428). Kept in agreement with `from_chia_type` (222 is a - // `ProtocolMessageTypes` variant) so both paths route holdings to the bulk lane. + // broadcast (#1428). This is the ONLY path that classifies it: upstream + // `ProtocolMessageTypes` has no `HoldingsAnnounce` variant (dig_ecosystem#2228). crate::service::holdings_announce::HOLDINGS_ANNOUNCE => Self::Bulk, // Default _ => Self::Normal, @@ -101,9 +95,9 @@ impl MessagePriority { /// SPEC §8.4: "PriorityOutbound: three VecDeque (critical, normal, bulk)." #[derive(Debug, Default)] pub struct PriorityOutbound { - critical: VecDeque, - normal: VecDeque, - bulk: VecDeque, + critical: VecDeque, + normal: VecDeque, + bulk: VecDeque, /// Counter for starvation prevention (PRI-004). high_priority_since_last_bulk: usize, } @@ -114,7 +108,7 @@ impl PriorityOutbound { } /// Enqueue message into appropriate lane. - pub fn enqueue(&mut self, msg: Message, priority: MessagePriority) { + pub fn enqueue(&mut self, msg: DigMessage, priority: MessagePriority) { match priority { MessagePriority::Critical => self.critical.push_back(msg), MessagePriority::Normal => self.normal.push_back(msg), @@ -126,7 +120,7 @@ impl PriorityOutbound { /// /// SPEC §8.4: "exhaust critical → exhaust normal → one bulk → check critical again." /// PRI-004: "1 bulk per PRIORITY_STARVATION_RATIO critical/normal messages." - pub fn drain_next(&mut self) -> Option { + pub fn drain_next(&mut self) -> Option { // PRI-004: starvation prevention — force one bulk message periodically. if self.high_priority_since_last_bulk >= PRIORITY_STARVATION_RATIO { if let Some(msg) = self.bulk.pop_front() { diff --git a/src/gossip/seen_set.rs b/src/gossip/seen_set.rs index 1884630..e79537a 100644 --- a/src/gossip/seen_set.rs +++ b/src/gossip/seen_set.rs @@ -16,7 +16,7 @@ //! - SPEC §8.1 step 2: "if seen_set.contains(hash) → return 0 (already seen)." //! - SPEC §8.1: "hash = SHA256(msg_type || data)." -use dig_peer_protocol::Bytes32; +use chia_protocol::Bytes32; use lru::LruCache; use sha2::{Digest, Sha256}; use std::num::NonZeroUsize; diff --git a/src/lib.rs b/src/lib.rs index a8d0665..a6d8278 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! //! `dig-gossip` handles peer discovery, connection management, and message routing //! between DIG full nodes. It accepts application-level payloads (blocks, transactions, -//! attestations) as opaque `Message` bytes and delivers them to connected peers via +//! attestations) as opaque `DigMessage` bytes and delivers them to connected peers via //! a Chia-compatible gossip protocol enhanced with Plumtree, ERLAY, priority lanes, //! compact blocks, Dandelion++ privacy, and relay fallback. //! @@ -46,8 +46,8 @@ //! | Direction | Type | Description | //! |-----------|------|-------------| //! | **In** | [`GossipConfig`] | All configuration (ports, TLS, discovery, relay) | -//! | **In** | [`Message`] via [`GossipHandle::broadcast()`] | Payload to send to peers | -//! | **Out** | `(PeerId, Message)` via [`GossipHandle::inbound_receiver()`] | Received payloads | +//! | **In** | [`DigMessage`] via [`GossipHandle::broadcast()`] | Payload to send to peers | +//! | **Out** | `(PeerId, DigMessage)` via [`GossipHandle::inbound_receiver()`] | Received payloads | //! | **Out** | [`GossipStats`] via [`GossipHandle::stats()`] | Network metrics | //! //! ## Feature Flags @@ -165,18 +165,20 @@ pub use service::peer_pool::{ pub use types::config::PeerPoolConfig; // -- Chia protocol types (re-exported, not reimplemented) -- +pub use chia_protocol::{ + Bytes32, FullBlock, Handshake, NewPeak, NewTransaction, NewUnfinishedBlock, RejectBlock, + RejectBlocks, RequestBlock, RequestBlocks, RequestMempoolTransactions, RequestPeers, + RequestTransaction, RequestUnfinishedBlock, RespondBlock, RespondBlocks, RespondPeers, + RespondTransaction, RespondUnfinishedBlock, SpendBundle, TimestampedPeerInfo, +}; pub use dig_peer_protocol::ChiaCertificate; pub use dig_peer_protocol::Streamable; pub use dig_peer_protocol::{ - load_ssl_cert, Client, ClientError, ClientState, Network, Peer, PeerOptions, RateLimit, - RateLimiter, RateLimits, V2_RATE_LIMITS, + load_ssl_cert, Admission, Client, ClientError, ClientState, LinkOptions, Network, + OpcodeRateLimiter, OpcodeRateLimits, RateLimit, RateLimits, V2_RATE_LIMITS, }; pub use dig_peer_protocol::{ - Bytes, Bytes32, ChiaProtocolMessage, FullBlock, Handshake, Message, NewPeak, NewTransaction, - NewUnfinishedBlock, NodeType, ProtocolMessageTypes, RejectBlock, RejectBlocks, RequestBlock, - RequestBlocks, RequestMempoolTransactions, RequestPeers, RequestTransaction, - RequestUnfinishedBlock, RespondBlock, RespondBlocks, RespondPeers, RespondTransaction, - RespondUnfinishedBlock, SpendBundle, TimestampedPeerInfo, + Bytes, ChiaProtocolMessage, DigLink, DigMessage, LinkError, NodeType, ProtocolMessageTypes, }; // -- Feature-gated public types -- diff --git a/src/nat/discovery.rs b/src/nat/discovery.rs index 123d2d9..1068997 100644 --- a/src/nat/discovery.rs +++ b/src/nat/discovery.rs @@ -22,7 +22,7 @@ //! connected peers for their address lists ([`crate::service::gossip_handle::GossipHandle::connect_to`] //! sends `RequestPeers` on connect; [`crate::service::gossip_handle::GossipHandle::discover_from_introducer`] //! queries a dedicated introducer). Those return Chia-streamable -//! [`TimestampedPeerInfo`](dig_peer_protocol::TimestampedPeerInfo) which +//! [`TimestampedPeerInfo`](chia_protocol::TimestampedPeerInfo) which //! [`PeerRecord::from_timestamped_peer_info`] normalizes into the same record type. //! //! Both sources reduce to [`PeerRecord`], which [`merge_records_into_address_manager`] folds into the diff --git a/src/nat/mod.rs b/src/nat/mod.rs index 88ca768..3687368 100644 --- a/src/nat/mod.rs +++ b/src/nat/mod.rs @@ -11,7 +11,7 @@ //! discovery, and identity* layer for the unified protocol. It does **not** touch the gossip //! ALGORITHMS — Plumtree ([`crate::gossip::plumtree`]), ERLAY ([`crate::gossip::erlay`]), //! Dandelion++ ([`crate::privacy::dandelion`]), compact blocks ([`crate::gossip::compact_block`]), -//! priority lanes, and the seen-set/message-cache dedup all operate on opaque [`Message`](dig_peer_protocol::Message) +//! priority lanes, and the seen-set/message-cache dedup all operate on opaque [`DigMessage`](dig_peer_protocol::DigMessage) //! payloads + [`PeerId`](crate::types::peer::PeerId) keys and are transport-agnostic. They sit //! unchanged on top of whatever byte transport //! delivers a peer's messages, so routing that transport through `dig-nat`'s multiplexed streams diff --git a/src/nat/peer_record.rs b/src/nat/peer_record.rs index 64747b1..a96735a 100644 --- a/src/nat/peer_record.rs +++ b/src/nat/peer_record.rs @@ -9,7 +9,7 @@ //! expects. The JSON field names + the `kind`/`via` lowercase tokens are the wire contract and must //! match the spec byte-for-byte (see `tests/nat_transport_tests.rs`). -use dig_peer_protocol::TimestampedPeerInfo; +use chia_protocol::TimestampedPeerInfo; use serde::{Deserialize, Serialize}; #[cfg(feature = "relay")] diff --git a/src/privacy/dandelion.rs b/src/privacy/dandelion.rs index 1ce53f4..41679b1 100644 --- a/src/privacy/dandelion.rs +++ b/src/privacy/dandelion.rs @@ -16,7 +16,7 @@ //! normally. Stem timeout (30s) ensures liveness if stem path breaks. //! SPEC §1.8#10: "transaction origin privacy." -use dig_peer_protocol::Bytes32; +use chia_protocol::Bytes32; use crate::types::peer::{metric_unix_timestamp_secs, PeerId}; diff --git a/src/service/dig_message.rs b/src/service/dig_message.rs index 3a29387..d0318db 100644 --- a/src/service/dig_message.rs +++ b/src/service/dig_message.rs @@ -5,7 +5,7 @@ //! dig-gossip is the **transport** for the DIG directed-message protocol //! (`dig-message`): it carries a sealed dig-message *envelope* between peers but //! never inspects, seals, or opens it. On the wire a directed message is a stock -//! [`Message`](dig_peer_protocol::Message) with `msg_type = 220` ([`DIG_MESSAGE`]) whose +//! [`DigMessage`](dig_peer_protocol::DigMessage) with `msg_type = 220` ([`DIG_MESSAGE`]) whose //! `data` field holds the envelope as **opaque bytes** — bytes in equal bytes out. //! //! This is WU6 of epic #796 (Wave A, envelope-only): the seam that the dig-message @@ -18,7 +18,7 @@ //! (200-219 are the consensus band, [`DigMessageType`](dig_peer_protocol::DigMessageType)). //! - [`is_dig_message`] / [`dig_message_payload`] — inbound routing: recognise an //! opcode-220 frame and lift its opaque envelope. -//! - [`frame_envelope`] — build the outbound [`Message`] carrying an envelope. +//! - [`frame_envelope`] — build the outbound [`DigMessage`] carrying an envelope. //! - [`StreamFrame`] + [`StreamReassembler`] — the streaming seam: OPEN/DATA/CLOSE //! frames ride *inside* the opaque envelope payload; the reassembler restores //! in-order delivery. The streaming *state machine* itself lives in dig-message @@ -30,75 +30,63 @@ use std::collections::BTreeMap; -use dig_peer_protocol::{DigMessageType, Message, ProtocolMessageTypes, Streamable}; +use dig_peer_protocol::{DigMessage, DigMessageType}; /// Wire opcode for a directed dig-message envelope. /// -/// Canonical value **220** — the first opcode of the free 220-255 band. Mirrors -/// [`ProtocolMessageTypes::DigMessage`]; also exported as `dig_peer_protocol::DIG_MESSAGE` -/// for consumers that do not depend on dig-gossip. This value is a cross-repo -/// canonical constant — it MUST NOT drift. -pub const DIG_MESSAGE: u8 = ProtocolMessageTypes::DigMessage as u8; +/// Canonical value **220** — the first opcode of the free 220-255 band. Re-exported +/// verbatim from [`dig_peer_protocol::DIG_MESSAGE`], which is the single definition; +/// dig-gossip keeps this name so consumers importing it from here are unaffected. +/// This value is a cross-repo canonical constant — it MUST NOT drift. +pub const DIG_MESSAGE: u8 = dig_peer_protocol::DIG_MESSAGE; /// True iff `msg_type` is the directed dig-message opcode ([`DIG_MESSAGE`]). /// -/// Inbound dispatch calls this on `Message.msg_type as u8` to route opcode-220 +/// Inbound dispatch calls this on `DigMessage.msg_type` to route opcode-220 /// frames to the dig-message handler seam. #[must_use] pub fn is_dig_message(msg_type: u8) -> bool { msg_type == DIG_MESSAGE } -/// Lift the opaque dig-message envelope from an inbound [`Message`]. +/// Lift the opaque dig-message envelope from an inbound [`DigMessage`]. /// /// Returns `Some(&envelope_bytes)` iff `msg` is an opcode-220 frame, else `None`. /// The returned slice is the payload verbatim — dig-gossip does not parse it. #[must_use] -pub fn dig_message_payload(msg: &Message) -> Option<&[u8]> { - if is_dig_message(msg.msg_type as u8) { +pub fn dig_message_payload(msg: &DigMessage) -> Option<&[u8]> { + if is_dig_message(msg.msg_type) { Some(msg.data.as_ref()) } else { None } } -/// Build the outbound opcode-220 [`Message`] that carries `envelope` as opaque bytes. +/// Build the outbound opcode-220 [`DigMessage`] that carries `envelope` as opaque bytes. /// -/// `correlation_id` maps to `Message.id` — used to pair a streaming exchange (all +/// `correlation_id` maps to `DigMessage.id` — used to pair a streaming exchange (all /// frames of one stream share an id) or a request/response. `None` for a /// fire-and-forget directed message. #[must_use] -pub fn frame_envelope(envelope: &[u8], correlation_id: Option) -> Message { - Message { - msg_type: ProtocolMessageTypes::DigMessage, - id: correlation_id, - data: envelope.to_vec().into(), - } +pub fn frame_envelope(envelope: &[u8], correlation_id: Option) -> DigMessage { + DigMessage::new(DIG_MESSAGE, correlation_id, envelope.to_vec().into()) } -/// Build the on-wire [`Message`] that carries a DIG **consensus-band** opcode (200-219). +/// Build the on-wire [`DigMessage`] that carries a DIG **consensus-band** opcode (200-219). /// -/// The DIG opcodes extend Chia's namespace: the vendored [`ProtocolMessageTypes`] mirrors every -/// [`DigMessageType`] discriminant 1:1 (#1404), so a stock [`Message`] can carry a DIG opcode in -/// its `msg_type` field. This is the SINGLE opcode-encoding path for the consensus band — the +/// [`DigMessage::msg_type`] is a raw wire byte, so a DIG opcode needs no enum variant to name it — +/// which is precisely why the vendored `chia-protocol` fork is no longer required on this path +/// (dig_ecosystem#2228). This is the SINGLE opcode-encoding path for the consensus band — the /// dispatch authority ([`GossipHandle::broadcast_dig`](crate::service::gossip_handle::GossipHandle) /// / [`send_dig`](crate::service::gossip_handle::GossipHandle)) frames every DIG message through /// here so no second, drifting encoder can appear. /// -/// `body` is the already-serialized opcode payload; it is carried verbatim in `Message.data`. +/// `body` is the already-serialized opcode payload; it is carried verbatim in `DigMessage.data`. /// The frame has no correlation `id` — the consensus band is fire-and-forget on the overlay, /// unlike the directed opcode-220 envelope built by [`frame_envelope`]. #[must_use] -pub fn frame_dig_message(msg_type: DigMessageType, body: Vec) -> Message { - // Total for the whole 200-219 band: `ProtocolMessageTypes` has a variant for every - // `DigMessageType` discriminant (vendored, #1404), so this single-byte decode never fails. - let pmt = ProtocolMessageTypes::from_bytes(&[msg_type as u8]) - .expect("every DigMessageType opcode has a mirrored ProtocolMessageTypes variant (#1404)"); - Message { - msg_type: pmt, - id: None, - data: body.into(), - } +pub fn frame_dig_message(msg_type: DigMessageType, body: Vec) -> DigMessage { + DigMessage::new(msg_type as u8, None, body.into()) } // ============================================================================ diff --git a/src/service/gossip_handle.rs b/src/service/gossip_handle.rs index 67f46db..360395f 100644 --- a/src/service/gossip_handle.rs +++ b/src/service/gossip_handle.rs @@ -32,7 +32,7 @@ //! //! - **`connected_peers` / `get_connections`:** Returning owned [`crate::types::peer::PeerConnection`] //! values would duplicate [`tokio::sync::mpsc::Receiver`] halves; CON-001 keeps live -//! [`dig_peer_protocol::Peer`] handles inside [`super::state::PeerSlot::Live`] while these RPCs +//! [`dig_peer_protocol::DigLink`] handles inside [`super::state::PeerSlot::Live`] while these RPCs //! stay empty until a snapshot API lands. In the meantime, //! [`__stub_filter_count_for_tests`](GossipHandle::__stub_filter_count_for_tests) gives tests a //! way to verify filter semantics. @@ -43,12 +43,11 @@ //! (`full_node.py`, `server.py`). The key difference is that Chia's `Server` object is not //! `Clone` — callers must borrow it. Our `Arc` wrapper avoids lifetime gymnastics in async code. +use crate::connection::chia_opcodes; +use chia_protocol::{RequestPeers, RespondPeers, TimestampedPeerInfo}; use dig_nat::SafeText; -use dig_peer_protocol::Peer; -use dig_peer_protocol::{ - ChiaProtocolMessage, Message, NodeType, ProtocolMessageTypes, RequestPeers, RespondPeers, - TimestampedPeerInfo, -}; +use dig_peer_protocol::DigLink; +use dig_peer_protocol::{ChiaProtocolMessage, DigMessage, NodeType}; use crate::discovery::introducer_client::{ load_local_certificate_for_introducer, IntroducerClient, PeerRegistration, @@ -204,7 +203,9 @@ impl GossipHandle { /// - [`GossipError::ChannelClosed`] — internal mutex poisoned (should not happen in practice). /// /// See: `docs/requirements/domains/crate_api/specs/API-002.md` — `inbound_receiver` - pub fn inbound_receiver(&self) -> Result, GossipError> { + pub fn inbound_receiver( + &self, + ) -> Result, GossipError> { self.require_running()?; // Short lock: grab the broadcast Sender, then immediately subscribe (subscribe() is O(1)). let g = self @@ -220,7 +221,7 @@ impl GossipHandle { // Messaging — broadcast / send / request // ------------------------------------------------------------------ - /// Broadcast a wire [`Message`] to every connected peer (optionally excluding one). + /// Broadcast a wire [`DigMessage`] to every connected peer (optionally excluding one). /// /// Returns the number of peers that **would** receive the message. With zero connected /// peers the return value is `Ok(0)` — this is explicitly **not** an error (API-002 @@ -228,8 +229,8 @@ impl GossipHandle { /// /// # Wire behaviour (CON-001+ / CON-006) /// - /// **Live** peers receive [`Peer::send_protocol_message`](dig_peer_protocol::Peer::send_protocol_message) - /// with a cloned [`Message`]; each successful send increments that slot’s CON-006 counters by the + /// **Live** peers receive [`DigLink::send_message`](dig_peer_protocol::DigLink::send_message) + /// with a cloned [`DigMessage`]; each successful send increments that slot’s CON-006 counters by the /// shared serialized length. **Stub** peers do not have a transport — the legacy /// [`ServiceState::messages_sent`] / [`ServiceState::bytes_sent`] atomics record the same /// fan-out counts so API-008 stub tests remain stable. @@ -248,16 +249,16 @@ impl GossipHandle { /// See: `docs/requirements/domains/crate_api/specs/API-002.md` — `broadcast` pub async fn broadcast( &self, - message: Message, + message: DigMessage, exclude: Option, ) -> Result { self.require_running()?; - let wire_len = message_wire_len(&message).map_err(GossipError::from)?; + let wire_len = message_wire_len(&message); // -- INT-001: Plumtree dedup via seen set -- // SPEC §8.1 step 2: "if seen_set.contains(hash) → return 0" let msg_hash = - crate::gossip::seen_set::SeenSet::compute_hash(message.msg_type as u8, &message.data); + crate::gossip::seen_set::SeenSet::compute_hash(message.msg_type, &message.data); { let mut seen = self .inner @@ -277,7 +278,7 @@ impl GossipHandle { .message_cache .lock() .map_err(|_| GossipError::ChannelClosed)?; - cache.insert(msg_hash, message.msg_type as u8, message.data.to_vec()); + cache.insert(msg_hash, message.msg_type, message.data.to_vec()); } // -- INT-001: Route through Plumtree eager/lazy sets (SPEC §8.1) -- @@ -285,7 +286,7 @@ impl GossipHandle { // Stubs (test-only) always get counted as delivered. let (stub_deliveries, eager_jobs, lazy_pids): ( usize, - Vec<(Peer, PeerId, u64)>, + Vec<(DigLink, PeerId, u64)>, Vec, ) = { let peers = self @@ -321,7 +322,7 @@ impl GossipHandle { } // POOL-*: a `dig-nat`-dialed pool member has a multiplexed transport but its // gossip message loop over that mux lands with the dig-node integration phase, so - // `broadcast` (the WebSocket-`Peer` fan-out) does not push to it yet. It still + // `broadcast` (the WebSocket-`DigLink` fan-out) does not push to it yet. It still // COUNTS as a connected peer everywhere else (peer_count / stats / pool). PeerSlot::Nat(_) => {} } @@ -340,7 +341,7 @@ impl GossipHandle { // INT-001: Eager push — full message to eager peers (SPEC §8.1 step 5) for (peer, pid, wl) in eager_jobs.iter() { - peer.send_protocol_message(message.clone()) + peer.send_message(message.clone()) .await .map_err(GossipError::from)?; record_live_peer_outbound_bytes(&self.inner, *pid, *wl); @@ -359,7 +360,7 @@ impl GossipHandle { /// /// This is the recommended entry point for application-level broadcasts — callers work with /// concrete Chia protocol types (e.g. `NewPeak`, `NewTransaction`) rather than raw - /// [`Message`] bytes. + /// [`DigMessage`] bytes. /// /// # Errors /// @@ -379,7 +380,7 @@ impl GossipHandle { /// Send a typed message to a single peer identified by [`PeerId`]. /// /// For **live** peers (CON-001+), the message is forwarded through the underlying - /// [`dig_peer_protocol::Peer::send`] WebSocket channel. For **stub** peers (pre-CON-001 + /// [`dig_peer_protocol::DigLink::send`] WebSocket channel. For **stub** peers (pre-CON-001 /// test fixtures), the payload is serialized (to validate encoding) but not transmitted; /// the counter is still incremented so stats remain consistent. /// @@ -406,7 +407,7 @@ impl GossipHandle { // Validate serialization upfront — fail fast even for stub peers so callers // get consistent error behaviour regardless of the peer type. let msg = encode_message(&body)?; - let wire_len = message_wire_len(&msg).map_err(GossipError::from)?; + let wire_len = message_wire_len(&msg); // Ban check before touching the peer map — avoids leaking message data to a banned peer. if self @@ -417,7 +418,7 @@ impl GossipHandle { return Err(GossipError::PeerBanned(peer_id)); } - // Clone the live `Peer` handle (Arc-backed, cheap) while the lock is held, + // Clone the live `DigLink` handle (Arc-backed, cheap) while the lock is held, // then release the lock before the async send to avoid holding it across `.await`. let maybe_live = { let peers = self @@ -428,7 +429,7 @@ impl GossipHandle { match peers.get(&peer_id) { None => return Err(GossipError::PeerNotConnected(peer_id)), Some(PeerSlot::Live(l)) => Some(l.peer.clone()), - // Stub + POOL-* `dig-nat` members have no WebSocket `Peer`; the typed WS + // Stub + POOL-* `dig-nat` members have no WebSocket `DigLink`; the typed WS // send/request path treats them like a stub (the dig-node phase adds the mux RPC). Some(PeerSlot::Stub(_)) | Some(PeerSlot::Nat(_)) => None, } @@ -454,8 +455,8 @@ impl GossipHandle { /// Send a directed dig-message **envelope** to a single peer over opcode 220. /// /// dig-gossip is the transport only: `envelope` is carried as **opaque bytes** - /// in the `Message.data` field — dig-gossip never seals, opens, or parses it. - /// `correlation_id` maps to `Message.id` (pairs a streaming exchange or a + /// in the `DigMessage.data` field — dig-gossip never seals, opens, or parses it. + /// `correlation_id` maps to `DigMessage.id` (pairs a streaming exchange or a /// request/response); pass `None` for fire-and-forget. See /// [`crate::service::dig_message`] for the seam overview. /// @@ -480,7 +481,7 @@ impl GossipHandle { /// The streaming *state machine* (windowing, backpressure, timeouts) is /// dig-message's (WU4); this helper only frames + delivers the OPEN marker. /// All frames of one stream share `stream_id` (mapped to the low 16 bits of - /// `Message.id` for cheap correlation). + /// `DigMessage.id` for cheap correlation). /// /// # Errors /// @@ -552,19 +553,19 @@ impl GossipHandle { .await } - /// Deliver a pre-built directed [`Message`] to a single live peer. + /// Deliver a pre-built directed [`DigMessage`] to a single live peer. /// /// Shared by the dig-message seam helpers: runs the ban check, resolves the - /// live [`Peer`], and sends over its WebSocket. Stub / NAT-only pool members + /// live [`DigLink`], and sends over its WebSocket. Stub / NAT-only pool members /// have no WebSocket transport (the dig-node mux phase adds it), so the send /// is counted but not transmitted — mirroring [`send_to`](Self::send_to). async fn send_directed_message( &self, peer_id: PeerId, - msg: Message, + msg: DigMessage, ) -> Result<(), GossipError> { self.require_running()?; - let wire_len = message_wire_len(&msg).map_err(GossipError::from)?; + let wire_len = message_wire_len(&msg); if self .inner @@ -587,9 +588,7 @@ impl GossipHandle { } }; if let Some(p) = maybe_live { - p.send_protocol_message(msg) - .await - .map_err(GossipError::from)?; + p.send_message(msg).await.map_err(GossipError::from)?; record_live_peer_outbound_bytes(&self.inner, peer_id, wire_len); } else { self.inner @@ -708,7 +707,7 @@ impl GossipHandle { match peers.get(&peer_id) { None => return Err(GossipError::PeerNotConnected(peer_id)), Some(PeerSlot::Live(l)) => Some(l.peer.clone()), - // Stub + POOL-* `dig-nat` members have no WebSocket `Peer`; the typed WS + // Stub + POOL-* `dig-nat` members have no WebSocket `DigLink`; the typed WS // send/request path treats them like a stub (the dig-node phase adds the mux RPC). Some(PeerSlot::Stub(_)) | Some(PeerSlot::Nat(_)) => None, } @@ -862,7 +861,9 @@ impl GossipHandle { let meta = StubPeer { remote: addr, - node_type: out.their_handshake.node_type, + node_type: crate::connection::handshake::dig_node_type_of( + out.their_handshake.node_type, + ), is_outbound: true, }; let peer = out.peer; @@ -983,9 +984,9 @@ impl GossipHandle { } // Answer inbound `RequestPeers` (keepalive / discovery) with correlated `RespondPeers`. - // Upstream `Peer` routes `id: Some` messages through a local `RequestMap`; remote request - // ids are forwarded on `inbound_rx` (see `vendor/chia-sdk-client` patch) and must be - // replied to with [`Peer::send_protocol_message`]. + // `DigLink` routes `id: Some` REPLIES through its local `RequestMap`; an inbound remote + // REQUEST matches nothing there, so it is forwarded on `inbound_rx` and must be answered + // explicitly with `DigLink::send_message` carrying the same correlation id. let peer_inbound_rpc = peer_for_keepalive.clone(); if let Ok(g) = self.inner.inbound_tx.lock() { if let Some(tx) = g.as_ref() { @@ -1012,18 +1013,19 @@ impl GossipHandle { } continue; } - if let Ok(wl_in) = message_wire_len(&msg) { + { + let wl_in = message_wire_len(&msg); record_live_peer_inbound_bytes(&state_fwd, pid_task, wl_in); } - if msg.msg_type == ProtocolMessageTypes::RequestPeers { + if msg.msg_type == chia_opcodes::REQUEST_PEERS { if let Ok(body) = RespondPeers::new(vec![]).to_bytes() { - let reply = Message { - msg_type: ProtocolMessageTypes::RespondPeers, - id: msg.id, - data: body.into(), - }; - let wl_out = message_wire_len(&reply).ok(); - let _ = peer_rpc.send_protocol_message(reply).await; + let reply = DigMessage::new( + chia_opcodes::RESPOND_PEERS, + msg.id, + body.into(), + ); + let wl_out = Some(message_wire_len(&reply)); + let _ = peer_rpc.send_message(reply).await; if let Some(w) = wl_out { record_live_peer_outbound_bytes(&state_fwd, pid_task, w); } @@ -2446,7 +2448,7 @@ impl GossipHandle { /// /// **CON-006:** `messages_*` / `bytes_*` are **`sum(live per-slot [`PeerConnectionWireMetrics`]) + /// stub/synthetic atomics`** on [`ServiceState`] — live TLS paths meter exact serialized - /// [`Message`] sizes; stub [`PeerSlot::Stub`] rows and [`__inject_inbound_for_tests`] still + /// [`DigMessage`] sizes; stub [`PeerSlot::Stub`] rows and [`__inject_inbound_for_tests`] still /// use the lock-free counters (API-008 pre-CON-006 behaviour preserved for tests). pub async fn stats(&self) -> GossipStats { let (live_ms, live_mr, live_bw, live_br) = sum_live_peer_wire_metrics(&self.inner); @@ -2678,7 +2680,7 @@ impl GossipHandle { pub fn __inject_inbound_for_tests( &self, sender: PeerId, - message: Message, + message: DigMessage, ) -> Result<(), GossipError> { self.require_running()?; let g = self @@ -2687,7 +2689,7 @@ impl GossipHandle { .lock() .map_err(|_| GossipError::ChannelClosed)?; let tx = g.as_ref().ok_or(GossipError::ServiceNotStarted)?; - let wl = message_wire_len(&message).unwrap_or(0); + let wl = message_wire_len(&message); let _ = tx.send((sender, message)); self.inner .messages_received @@ -2751,15 +2753,25 @@ pub fn pool_auto_dial_traversal_methods() -> Vec { vec![Direct, Upnp, NatPmp, Pcp, HolePunch, Relayed] } -fn encode_message(body: &T) -> Result { - Ok(Message { - msg_type: T::msg_type(), - id: None, - data: body - .to_bytes() - .map_err(|e| GossipError::from(dig_peer_protocol::ClientError::Streamable(e)))? - .into(), - }) +fn encode_message( + body: &T, +) -> Result { + let to_gossip_error = |e| GossipError::from(dig_peer_protocol::ClientError::Streamable(e)); + // A Chia opcode's wire byte is the single-byte `Streamable` encoding of its + // `ProtocolMessageTypes` discriminant -- the same derivation `DigLink::send` uses, so a + // frame built here is indistinguishable from one the link builds itself. + let opcode = *T::msg_type() + .to_bytes() + .map_err(to_gossip_error)? + .first() + .ok_or_else(|| { + GossipError::IoError("protocol message type encoded to zero bytes".into()) + })?; + Ok(DigMessage::new( + opcode, + None, + body.to_bytes().map_err(to_gossip_error)?.into(), + )) } fn empty_respond_peers() -> Result { diff --git a/src/service/gossip_service.rs b/src/service/gossip_service.rs index ae07a87..65c9977 100644 --- a/src/service/gossip_service.rs +++ b/src/service/gossip_service.rs @@ -45,8 +45,8 @@ use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; +use chia_protocol::Bytes32; use dig_peer_protocol::load_ssl_cert; -use dig_peer_protocol::Bytes32; use dig_peer_protocol::{ClientError, ClientState}; use crate::error::GossipError; diff --git a/src/service/holdings_announce.rs b/src/service/holdings_announce.rs index ee9be3f..5ab95ac 100644 --- a/src/service/holdings_announce.rs +++ b/src/service/holdings_announce.rs @@ -50,7 +50,7 @@ //! therefore checks the SPKI→peer_id binding, that the SPKI is a P-256 key, the ECDSA //! signature over the exact message, and the change-count cap — fail-closed on any mismatch. -use dig_peer_protocol::{Bytes, Message, ProtocolMessageTypes}; +use dig_peer_protocol::{Bytes, DigMessage}; use dig_tls::peer_id_from_tls_spki_der; use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_ASN1}; use sha2::{Digest, Sha256}; @@ -62,9 +62,9 @@ use x509_parser::prelude::{FromDer, SubjectPublicKeyInfo}; /// Canonical value **222** — the third opcode of the 220-255 "free" band, after /// [`DIG_MESSAGE`](crate::service::dig_message::DIG_MESSAGE)`= 220` and /// [`STORE_MELTED`](crate::service::store_melted::STORE_MELTED)`= 221`. Mirrors -/// [`ProtocolMessageTypes::HoldingsAnnounce`]. This value is a cross-repo canonical -/// constant (dig-node pins it to decode the broadcast) — it MUST NOT drift. -pub const HOLDINGS_ANNOUNCE: u8 = ProtocolMessageTypes::HoldingsAnnounce as u8; +/// [`dig_peer_protocol::HOLDINGS_ANNOUNCE`], which is the single definition. This value is a +/// cross-repo canonical constant (dig-node pins it to decode the broadcast) — it MUST NOT drift. +pub const HOLDINGS_ANNOUNCE: u8 = dig_peer_protocol::HOLDINGS_ANNOUNCE; /// Domain-separation tag for the `holdings-announce` signing message. /// @@ -716,30 +716,30 @@ pub fn is_holdings_announce(msg_type: u8) -> bool { msg_type == HOLDINGS_ANNOUNCE } -/// Lift and decode a [`HoldingsAnnounce`] from an inbound [`Message`]. +/// Lift and decode a [`HoldingsAnnounce`] from an inbound [`DigMessage`]. /// /// Returns `Some(announce)` iff `msg` is an opcode-222 frame whose `data` decodes, else /// `None`. The caller MUST still [`verify_holdings_announce`] before ingesting the deltas. #[must_use] -pub fn holdings_announce_payload(msg: &Message) -> Option { - if is_holdings_announce(msg.msg_type as u8) { +pub fn holdings_announce_payload(msg: &DigMessage) -> Option { + if is_holdings_announce(msg.msg_type) { HoldingsAnnounce::decode(msg.data.as_ref()) } else { None } } -/// Build the outbound opcode-222 [`Message`] that floods `announce` to peers. +/// Build the outbound opcode-222 [`DigMessage`] that floods `announce` to peers. /// /// `id` is `None`: a holdings announcement is a fire-and-forget flood broadcast, not a /// correlated request/response. #[must_use] -pub fn frame_holdings_announce(announce: &HoldingsAnnounce) -> Message { - Message { - msg_type: ProtocolMessageTypes::HoldingsAnnounce, - id: None, - data: Bytes::new(announce.encode()), - } +pub fn frame_holdings_announce(announce: &HoldingsAnnounce) -> DigMessage { + DigMessage::new( + dig_peer_protocol::HOLDINGS_ANNOUNCE, + None, + Bytes::new(announce.encode()), + ) } #[cfg(test)] @@ -1191,7 +1191,7 @@ mod tests { fn frame_and_lift_round_trip() { let a = sample(); let msg = frame_holdings_announce(&a); - assert_eq!(msg.msg_type as u8, HOLDINGS_ANNOUNCE); + assert_eq!(msg.msg_type, HOLDINGS_ANNOUNCE); assert_eq!(msg.id, None); assert_eq!(holdings_announce_payload(&msg), Some(a)); } @@ -1209,13 +1209,11 @@ mod tests { // Public all-peers flood at bulk priority — never unicast, never consensus-critical. assert_eq!( - classify_broadcast(ProtocolMessageTypes::HoldingsAnnounce, false), + classify_broadcast(HOLDINGS_ANNOUNCE, false), BroadcastStrategy::Plumtree ); - assert_eq!( - MessagePriority::from_chia_type(ProtocolMessageTypes::HoldingsAnnounce), - MessagePriority::Bulk - ); + // Only the raw-opcode path can classify 222: upstream `ProtocolMessageTypes` is a closed + // enum with no `HoldingsAnnounce` variant, so `from_chia_type` cannot be asked about it. assert_eq!( MessagePriority::from_dig_type(HOLDINGS_ANNOUNCE), MessagePriority::Bulk diff --git a/src/service/state.rs b/src/service/state.rs index d6b09c5..8ac3991 100644 --- a/src/service/state.rs +++ b/src/service/state.rs @@ -10,8 +10,8 @@ //! ## SPEC citations //! //! - SPEC §9.1 — Crate Boundary: `dig-gossip` is a library crate wrapping -//! `chia-sdk-client` and `chia-protocol`. Input: `Message` via `broadcast()`/`send_to()`. -//! Output: `(PeerId, Message)` via inbound channel. `ServiceState` is the runtime +//! `chia-sdk-client` and `chia-protocol`. Input: `DigMessage` via `broadcast()`/`send_to()`. +//! Output: `(PeerId, DigMessage)` via inbound channel. `ServiceState` is the runtime //! interior that makes this possible. //! - SPEC §2.4 — `PeerConnection` fields: `ServiceState::peers` stores per-connection //! metadata (direction, node type, remote address, reputation, rate limiter) that @@ -42,7 +42,7 @@ //! //! # Stub peers (pre-CON-001) //! -//! Real [`crate::types::peer::PeerConnection`] values require a live [`dig_peer_protocol::Peer`]. +//! Real [`crate::types::peer::PeerConnection`] values require a live [`dig_peer_protocol::DigLink`]. //! Until CON-001 (outbound WSS connect) was implemented, we tracked synthetic peers in //! [`ServiceState::peers`] via [`PeerSlot::Stub`] so `peer_count`, `broadcast`, and //! `connect_to` semantics could be tested without TLS sockets. Stubs remain for @@ -64,15 +64,15 @@ use std::sync::atomic::{AtomicU64, AtomicU8, Ordering}; use std::sync::{Arc, Mutex}; use dig_peer_protocol::ChiaCertificate; -use dig_peer_protocol::{ClientState, Peer}; -use dig_peer_protocol::{Message, NodeType}; +use dig_peer_protocol::{ClientState, DigLink}; +use dig_peer_protocol::{DigMessage, NodeType}; use lru::LruCache; use tokio::sync::broadcast; use tokio::sync::Notify; use tokio::sync::Semaphore; use tokio::task::JoinHandle; -use dig_peer_protocol::Bytes32; +use chia_protocol::Bytes32; use crate::connection::inbound_limits::InboundRateLimiter; use crate::discovery::address_manager::AddressManager; @@ -94,7 +94,7 @@ pub(crate) const LC_STOPPED: u8 = 2; /// Minimal metadata shared by both stub rows and live TLS peers. /// /// Kept separate from [`LiveSlot`] so that unit tests can create lightweight entries -/// without a real [`Peer`] handle. Every peer -- stub or live -- has a direction, a +/// without a real [`DigLink`] handle. Every peer -- stub or live -- has a direction, a /// declared [`NodeType`] (from the Chia `Handshake`), and a remote socket address. /// /// # Fields @@ -115,7 +115,7 @@ pub(crate) struct StubPeer { pub is_outbound: bool, } -/// A *live* TLS peer with a real [`Peer`] handle (CON-001 outbound `wss://` or CON-002 inbound). +/// A *live* TLS peer with a real [`DigLink`] handle (CON-001 outbound `wss://` or CON-002 inbound). /// /// Created after a successful Chia handshake and policy validation (CON-003). The slot /// retains handshake metadata so that snapshot types like @@ -123,9 +123,9 @@ pub(crate) struct StubPeer { /// /// # Ownership /// -/// The [`Peer`] inside is an `Arc`-backed handle from `chia-sdk-client`; dropping this +/// The [`DigLink`] inside is an `Arc`-backed handle from `chia-sdk-client`; dropping this /// slot does *not* close the underlying WebSocket -- the caller must call -/// [`Peer::close()`](Peer::close) explicitly (done in +/// [`DigLink::close()`](DigLink::close) explicitly (done in /// [`GossipService::stop`](super::gossip_service::GossipService::stop)). /// /// # Requirement traceability @@ -134,15 +134,16 @@ pub(crate) struct StubPeer { /// * **CON-003** -- handshake validation decides which fields are retained. /// * **CON-004** -- [`PeerReputation`] is updated by /// [`crate::connection::keepalive::spawn_keepalive_task`] with RTT samples. -/// * **CON-005** -- [`InboundRateLimiter`] (`incoming = true`, 60 s window) enforced on the inbound -/// `mpsc` bridge before broadcast; violations call [`apply_inbound_rate_limit_violation`]. +/// * **CON-005** -- [`InboundRateLimiter`] (60 s window; `incoming = true` on the DIG half only — +/// see TODO(dig_ecosystem#2228) on [`InboundRateLimiter::new`]) enforced on the inbound `mpsc` +/// bridge before broadcast; violations call [`apply_inbound_rate_limit_violation`]. /// * **CON-006** -- [`PeerConnectionWireMetrics`] updated on each metered send/receive (wire bytes). #[derive(Debug)] pub(crate) struct LiveSlot { /// Common metadata (direction, node type, remote address) shared with [`StubPeer`]. pub meta: StubPeer, /// The `chia-sdk-client` WebSocket handle for sending/receiving wire messages. - pub peer: Peer, + pub peer: DigLink, /// Remote’s declared protocol version string from the Chia `Handshake`, retained /// after [`crate::connection::handshake::validate_remote_handshake`] succeeds (CON-003). pub remote_protocol_version: String, @@ -210,7 +211,7 @@ pub(crate) struct DigBanEntry { /// This is a peer the connected-peer pool dialed via `dig-nat`'s `connect()` (mTLS, verified /// `peer_id`, NAT-traversal ladder). It owns the multiplexed [`crate::nat::NatPeerConnection`] — the /// stream transport dig-node opens gossip channels + range streams on. Unlike a [`LiveSlot`] it has no -/// `chia-sdk-client` [`Peer`] (the WebSocket peer path); the gossip message loop over this mux +/// `chia-sdk-client` [`DigLink`] (the WebSocket peer path); the gossip message loop over this mux /// transport is wired by the dig-node integration phase. The slot exists so a `dig-nat`-dialed peer /// COUNTS as a connected pool member for `peer_count` / stats / dedup / churn from the moment it /// connects. @@ -259,7 +260,7 @@ impl fmt::Debug for NatSlot { } /// Canonical form of a `peer_id` hex for identity comparison: a stripped optional `0x` prefix, -/// lowercased. Different producers (this node's [`Bytes32`](dig_peer_protocol::Bytes32) `Display`, a +/// lowercased. Different producers (this node's [`Bytes32`](chia_protocol::Bytes32) `Display`, a /// relay's echo) may spell the same id with/without `0x` and in either case — normalizing both sides /// before comparing makes self-exclusion robust to the spelling (#924 self-filter). pub(crate) fn normalize_peer_id_hex(id: &str) -> String { @@ -294,8 +295,8 @@ pub(crate) fn peer_id_from_hex(id: &str) -> Option { /// /// # Invariant /// -/// A `Live` slot always has a valid [`Peer`] handle; a `Stub` never has one; a `Nat` slot owns a -/// verified [`crate::nat::NatPeerConnection`] but no `chia-sdk-client` `Peer`. Pattern-matching on the +/// A `Live` slot always has a valid [`DigLink`] handle; a `Stub` never has one; a `Nat` slot owns a +/// verified [`crate::nat::NatPeerConnection`] but no `chia-sdk-client` `DigLink`. Pattern-matching on the /// variant is the only way to reach the handle, preventing accidental sends to the wrong transport. #[derive(Debug)] pub(crate) enum PeerSlot { @@ -303,7 +304,7 @@ pub(crate) enum PeerSlot { /// `connect_stub_inner` test hook (#1718); never instantiated in the production library. #[cfg_attr(not(any(test, feature = "test-util")), allow(dead_code))] Stub(StubPeer), - /// Real TLS peer with a `chia-sdk-client` [`Peer`] handle. + /// Real TLS peer with a `chia-sdk-client` [`DigLink`] handle. Live(LiveSlot), /// A connected-pool member reached over the `dig-nat` transport (POOL-*). Nat(NatSlot), @@ -586,7 +587,7 @@ pub struct ServiceState { /// /// Writers: accept loop (CON-002), `test_inject_message` (API-002). /// Readers: each handle's `inbound_receiver()` subscriber. - pub inbound_tx: Mutex>>, + pub inbound_tx: Mutex>>, /// Cumulative count of messages sent (API-008). `broadcast` adds one per recipient /// that accepted the message; `send_to` adds 1. Never decremented. @@ -1279,7 +1280,7 @@ pub fn apply_inbound_rate_limit_violation( } } -/// CON-006 — increment outbound wire counters for a live peer (after a successful `Peer::send_*`). +/// CON-006 — increment outbound wire counters for a live peer (after a successful `DigLink::send_*`). pub(crate) fn record_live_peer_outbound_bytes( state: &ServiceState, peer_id: PeerId, @@ -1299,7 +1300,7 @@ pub(crate) fn record_live_peer_outbound_bytes( }; } -/// CON-006 — increment inbound wire counters for a live peer (after a decoded inbound [`Message`]). +/// CON-006 — increment inbound wire counters for a live peer (after a decoded inbound [`DigMessage`]). pub(crate) fn record_live_peer_inbound_bytes(state: &ServiceState, peer_id: PeerId, wire_len: u64) { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/src/service/store_melted.rs b/src/service/store_melted.rs index 0872aba..66062ef 100644 --- a/src/service/store_melted.rs +++ b/src/service/store_melted.rs @@ -6,7 +6,7 @@ //! melting node floods a `store-melted` announcement so every peer stops hosting the //! store's `.dig` content and reclaims disk. This module defines that wire message: //! the opcode constant, the [`StoreMeltedAnnounce`] payload with byte-exact encode/ -//! decode, the sign/verify helpers, and the [`Message`] framing. It is piece #1 of +//! decode, the sign/verify helpers, and the [`DigMessage`] framing. It is piece #1 of //! epic #1316 (store-melt propagation) — the wire dig-node consumes to build the //! receive → on-chain-verify → delete → rebroadcast handler (#3). //! @@ -31,7 +31,7 @@ //! //! # Wire layout //! -//! A `store-melted` frame is a stock [`Message`](dig_peer_protocol::Message) with +//! A `store-melted` frame is a stock [`DigMessage`](dig_peer_protocol::DigMessage) with //! `msg_type = 221` ([`STORE_MELTED`]) whose `data` is the fixed-length big-endian //! encoding of [`StoreMeltedAnnounce`] (see [`StoreMeltedAnnounce::encode`]). //! @@ -42,7 +42,8 @@ //! consensus-critical. See [`classify_broadcast`](crate::gossip::broadcaster::classify_broadcast) //! and [`MessagePriority`](crate::gossip::priority::MessagePriority). -use dig_peer_protocol::{Bytes, Bytes32, Message, ProtocolMessageTypes}; +use chia_protocol::Bytes32; +use dig_peer_protocol::{Bytes, DigMessage}; use dig_tls::bls::{sign_message, verify_signature, SecretKey}; use sha2::{Digest, Sha256}; @@ -50,9 +51,9 @@ use sha2::{Digest, Sha256}; /// /// Canonical value **221** — the second opcode of the 220-255 "free" band, after /// [`DIG_MESSAGE`](crate::service::dig_message::DIG_MESSAGE)`= 220`. Mirrors -/// [`ProtocolMessageTypes::StoreMelted`]. This value is a cross-repo canonical -/// constant (dig-node pins it to decode the broadcast) — it MUST NOT drift. -pub const STORE_MELTED: u8 = ProtocolMessageTypes::StoreMelted as u8; +/// [`dig_peer_protocol::STORE_MELTED`], which is the single definition. This value is a +/// cross-repo canonical constant (dig-node pins it to decode the broadcast) — it MUST NOT drift. +pub const STORE_MELTED: u8 = dig_peer_protocol::STORE_MELTED; /// Domain-separation tag for the `store-melted` signature preimage. /// @@ -190,38 +191,38 @@ impl StoreMeltedAnnounce { /// True iff `msg_type` is the `store-melted` opcode ([`STORE_MELTED`]). /// -/// Inbound dispatch calls this on `Message.msg_type as u8` to route opcode-221 +/// Inbound dispatch calls this on `DigMessage.msg_type as u8` to route opcode-221 /// frames to the store-melted handler seam (dig-node #3). #[must_use] pub fn is_store_melted(msg_type: u8) -> bool { msg_type == STORE_MELTED } -/// Lift and decode a [`StoreMeltedAnnounce`] from an inbound [`Message`]. +/// Lift and decode a [`StoreMeltedAnnounce`] from an inbound [`DigMessage`]. /// /// Returns `Some(announce)` iff `msg` is an opcode-221 frame whose `data` decodes /// ([`StoreMeltedAnnounce::decode`]), else `None`. #[must_use] -pub fn store_melted_payload(msg: &Message) -> Option { - if is_store_melted(msg.msg_type as u8) { +pub fn store_melted_payload(msg: &DigMessage) -> Option { + if is_store_melted(msg.msg_type) { StoreMeltedAnnounce::decode(msg.data.as_ref()) } else { None } } -/// Build the outbound opcode-221 [`Message`] that floods `announce` to peers. +/// Build the outbound opcode-221 [`DigMessage`] that floods `announce` to peers. /// /// `id` is `None`: a `store-melted` broadcast is fire-and-forget, not a correlated /// request/response. The caller broadcasts the returned message through /// [`GossipHandle::broadcast`](crate::service::gossip_handle::GossipHandle). #[must_use] -pub fn frame_store_melted(announce: &StoreMeltedAnnounce) -> Message { - Message { - msg_type: ProtocolMessageTypes::StoreMelted, - id: None, - data: Bytes::new(announce.encode()), - } +pub fn frame_store_melted(announce: &StoreMeltedAnnounce) -> DigMessage { + DigMessage::new( + dig_peer_protocol::STORE_MELTED, + None, + Bytes::new(announce.encode()), + ) } #[cfg(test)] @@ -331,7 +332,7 @@ mod tests { fn frame_and_lift_round_trip() { let announce = sample(); let msg = frame_store_melted(&announce); - assert_eq!(msg.msg_type as u8, STORE_MELTED); + assert_eq!(msg.msg_type, STORE_MELTED); assert_eq!(msg.id, None); assert_eq!(store_melted_payload(&msg), Some(announce)); } @@ -349,14 +350,11 @@ mod tests { // Public all-peers flood at bulk priority — never unicast, never consensus-critical. assert_eq!( - classify_broadcast(ProtocolMessageTypes::StoreMelted, false), + classify_broadcast(STORE_MELTED, false), BroadcastStrategy::Plumtree ); - assert_eq!( - MessagePriority::from_chia_type(ProtocolMessageTypes::StoreMelted), - MessagePriority::Bulk - ); - // The u8 path agrees with the enum path so both classifications route identically. + // Only the raw-opcode path can classify 221: upstream `ProtocolMessageTypes` is a closed + // enum with no `StoreMelted` variant, so `from_chia_type` cannot be asked about it. assert_eq!( MessagePriority::from_dig_type(STORE_MELTED), MessagePriority::Bulk diff --git a/src/types/config.rs b/src/types/config.rs index b48dcda..ec212e6 100644 --- a/src/types/config.rs +++ b/src/types/config.rs @@ -28,7 +28,7 @@ //! # Chia context //! //! Several fields in [`GossipConfig`] originate from Chia's Python `node_discovery.py` and -//! `server_api.py`: target outbound count, connect interval, and the `PeerOptions` rate limit +//! `server_api.py`: target outbound count, connect interval, and the `LinkOptions` rate limit //! factor. The [`Network`](dig_peer_protocol::Network) field delegates DNS seed lookup to //! `chia-sdk-client`'s `Network::lookup_all()`, avoiding reimplementation. //! @@ -44,8 +44,8 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::time::Duration; -use dig_peer_protocol::Bytes32; -use dig_peer_protocol::{Network, PeerOptions}; +use chia_protocol::Bytes32; +use dig_peer_protocol::{LinkOptions, Network}; use serde::{Deserialize, Serialize}; use super::peer::PeerId; @@ -228,7 +228,7 @@ pub struct GossipConfig { /// Per-connection options forwarded to `chia-sdk-client` when constructing a [`Peer`](dig_peer_protocol::Peer). /// The main knob here is `rate_limit_factor` which scales the V2 rate limits (CON-005). - pub peer_options: PeerOptions, + pub peer_options: LinkOptions, /// Dandelion++ stem/fluff configuration (SPEC §1.9.1 / PRV-001). /// Only compiled when the `dandelion` feature flag is enabled (STR-004). @@ -373,7 +373,7 @@ impl Default for GossipConfig { gossip_fanout: 8, max_seen_messages: DEFAULT_MAX_SEEN_MESSAGES, peers_file_path: PathBuf::new(), - peer_options: PeerOptions::default(), + peer_options: LinkOptions::default(), #[cfg(feature = "dandelion")] dandelion: None, peer_id_rotation: None, diff --git a/src/types/peer.rs b/src/types/peer.rs index 8ed18f0..2a05f57 100644 --- a/src/types/peer.rs +++ b/src/types/peer.rs @@ -17,7 +17,7 @@ //! //! `PeerConnection` intentionally **does not** implement [`Clone`]: it owns an //! [`tokio::sync::mpsc::Receiver`] for inbound wire messages (SPEC 2.4), which is not clonable. -//! Upstream [`dig_peer_protocol::Peer`] is cloneable (`Arc` inside), but duplicating a connection’s +//! Upstream [`dig_peer_protocol::DigLink`] is cloneable (`Arc` inside), but duplicating a connection’s //! receiver would violate single-consumer semantics. use std::fmt; @@ -27,9 +27,9 @@ use crate::constants::{ BUCKET_SIZE, HORIZON_DAYS, MAX_FAILURES, MAX_RETRIES, MIN_FAIL_DAYS, NEW_BUCKETS_PER_SOURCE_GROUP, NEW_BUCKET_COUNT, TRIED_BUCKETS_PER_GROUP, TRIED_BUCKET_COUNT, }; -use dig_peer_protocol::Peer; -use dig_peer_protocol::Streamable; -use dig_peer_protocol::{Bytes32, Message, NodeType}; +use chia_protocol::Bytes32; +use dig_peer_protocol::DigLink; +use dig_peer_protocol::{DigMessage, NodeType}; use sha2::{Digest, Sha256}; use tokio::sync::mpsc; @@ -155,7 +155,7 @@ pub struct ExtendedPeerInfo { pub peer_info: PeerInfo, /// Last time this row was updated (Unix seconds); staleness / horizon logic (DSC-001). pub timestamp: u64, - /// Peer that gossiped this address — drives **source-group** buckets in the new table. + /// DigLink that gossiped this address — drives **source-group** buckets in the new table. pub src: PeerInfo, /// Index in the random-order vector for O(1) uniform selection; [`None`] until inserted. pub random_pos: Option, @@ -275,16 +275,16 @@ pub fn metric_unix_timestamp_secs() -> u64 { .as_secs() } -/// Serialized on-wire length of a [`Message`] (header + body) — **CON-006** requires byte counters +/// Serialized on-wire length of a [`DigMessage`] (header + body) — **CON-006** requires byte counters /// to reflect wire size, not in-memory struct size. /// /// **See:** [`CON-006.md`](../../../docs/requirements/domains/connection/specs/CON-006.md) — /// “`bytes_read`/`bytes_written` should count the serialized wire size”. -#[allow(clippy::result_large_err)] // mirrors `encode_message` / Chia `Streamable` error surface -pub fn message_wire_len(msg: &Message) -> Result { - msg.to_bytes() - .map(|b| b.len() as u64) - .map_err(dig_peer_protocol::ClientError::Streamable) +#[must_use] +pub fn message_wire_len(msg: &DigMessage) -> u64 { + // `DigMessage::to_bytes` is infallible -- `msg_type` is already a raw byte and `data` is + // already serialized -- so this no longer has an error case to propagate. + msg.to_bytes().len() as u64 } /// Per-connection byte/message counters shared by [`LiveSlot`](crate::service::state::LiveSlot) @@ -351,12 +351,12 @@ pub fn aggregate_peer_connection_io(peers: &[PeerConnection]) -> (u64, u64, u64, /// Active connection with gossip bookkeeping. /// -/// Wraps [`Peer`] (TLS WebSocket I/O) with DIG-only metadata. Field layout matches +/// Wraps [`DigLink`] (TLS WebSocket I/O) with DIG-only metadata. Field layout matches /// [`SPEC.md`](../../../docs/resources/SPEC.md) §2.4; behavior (handshake, metrics, …) is filled by /// connection-domain requirements (CON-*, API-005). pub struct PeerConnection { /// Underlying Chia light-wallet-protocol peer handle. - pub peer: Peer, + pub peer: DigLink, /// Unique peer identifier (TLS cert hash / Chia rules). pub peer_id: PeerId, /// Remote socket address. @@ -367,9 +367,9 @@ pub struct PeerConnection { pub node_type: NodeType, /// Peer protocol version string. pub protocol_version: String, - /// Peer software version string (Cc/Cf stripped per CON-003 / CON-008 — Chia `ws_connection.py`). + /// DigLink software version string (Cc/Cf stripped per CON-003 / CON-008 — Chia `ws_connection.py`). pub software_version: String, - /// Peer-advertised server port from handshake. + /// DigLink-advertised server port from handshake. pub peer_server_port: u16, /// Capability tuples `(code, name)` from handshake. pub capabilities: Vec<(u16, String)>, @@ -387,8 +387,8 @@ pub struct PeerConnection { pub last_message_time: u64, /// Reputation snapshot (API-006). pub reputation: PeerReputation, - /// Inbound wire messages for this connection (`connect_peer` / `Peer::from_websocket`). - pub inbound_rx: mpsc::Receiver, + /// Inbound wire messages for this connection (`connect_peer` / `DigLink::from_websocket`). + pub inbound_rx: mpsc::Receiver, } impl fmt::Debug for PeerConnection { @@ -410,7 +410,7 @@ impl fmt::Debug for PeerConnection { .field("messages_received", &self.messages_received) .field("last_message_time", &self.last_message_time) .field("reputation", &self.reputation) - .field("inbound_rx", &">") + .field("inbound_rx", &">") .finish() } } diff --git a/tests/api_001_tests.rs b/tests/api_001_tests.rs index fccabf3..bf9d226 100644 --- a/tests/api_001_tests.rs +++ b/tests/api_001_tests.rs @@ -177,7 +177,7 @@ async fn test_handle_after_stop() { /// **Extra:** invalid `network_id` (all zero) must fail fast with [`GossipError::InvalidConfig`]. /// /// SPEC §2.10 — GossipConfig.network_id (e.g., SHA256("dig_mainnet")). -/// SPEC §1.5#7 — network_id validation: connect_peer() rejects peers with mismatched network_id. +/// SPEC §1.5#7 — network_id validation: outbound connections reject peers with mismatched network_id. /// /// A zeroed-out network_id (Bytes32::default) is invalid because it could cause the node /// to accidentally connect to peers on any network. The constructor must reject this upfront. diff --git a/tests/api_002_tests.rs b/tests/api_002_tests.rs index 6337455..eacdfae 100644 --- a/tests/api_002_tests.rs +++ b/tests/api_002_tests.rs @@ -41,9 +41,9 @@ use std::net::SocketAddr; use std::time::Duration; use dig_gossip::{ - Bytes32, ChiaProtocolMessage, GossipError, GossipHandle, GossipService, IntroducerConfig, - Message, NewPeak, NodeType, ProtocolMessageTypes, RelayConfig, RequestPeers, RespondBlock, - RespondPeers, Streamable, + Bytes32, ChiaProtocolMessage, DigMessage, GossipError, GossipHandle, GossipService, + IntroducerConfig, NewPeak, NodeType, ProtocolMessageTypes, RelayConfig, RequestPeers, + RespondBlock, RespondPeers, Streamable, }; /// Build a minimal [`NewPeak`] message suitable for broadcast tests. @@ -111,8 +111,8 @@ async fn test_broadcast_returns_peer_count() { h.__connect_stub_peer_with_direction(c, NodeType::FullNode, true) .await .unwrap(); - let dummy = Message { - msg_type: ProtocolMessageTypes::RequestPeers, + let dummy = DigMessage { + msg_type: ProtocolMessageTypes::RequestPeers as u8, id: None, data: RequestPeers::new().to_bytes().unwrap().into(), }; @@ -145,8 +145,8 @@ async fn test_broadcast_with_exclude() { h.__connect_stub_peer_with_direction(c, NodeType::FullNode, true) .await .unwrap(); - let dummy = Message { - msg_type: ProtocolMessageTypes::RequestPeers, + let dummy = DigMessage { + msg_type: ProtocolMessageTypes::RequestPeers as u8, id: None, data: RequestPeers::new().to_bytes().unwrap().into(), }; @@ -255,7 +255,7 @@ async fn test_request_timeout() { /// **Row:** `test_inbound_receiver` — subscribe on broadcast hub, inject synthetic tuple, receive it. /// /// **Precondition:** Subscribe to the handle's inbound broadcast channel via -/// `inbound_receiver()`. Then inject a synthetic `(PeerId, Message)` tuple via the +/// `inbound_receiver()`. Then inject a synthetic `(PeerId, DigMessage)` tuple via the /// test-only `__inject_inbound_for_tests` hook. /// **Assertion:** The subscription receives the exact `(sender, msg)` tuple within 2 seconds. /// **Why sufficient:** This proves the SPEC §3.3 subscription/broadcast hub works: external @@ -267,8 +267,8 @@ async fn test_inbound_receiver() { let (_s, h) = running_handle().await; let mut rx = h.inbound_receiver().expect("subscribe"); let sender = Bytes32::from([9u8; 32]); - let msg = Message { - msg_type: ProtocolMessageTypes::NewPeak, + let msg = DigMessage { + msg_type: ProtocolMessageTypes::NewPeak as u8, id: None, data: sample_new_peak().to_bytes().unwrap().into(), }; @@ -280,7 +280,7 @@ async fn test_inbound_receiver() { .expect("recv"); // The received tuple must carry the original sender id and message type. assert_eq!(got.0, sender); - assert_eq!(got.1.msg_type, ProtocolMessageTypes::NewPeak); + assert_eq!(got.1.msg_type, ProtocolMessageTypes::NewPeak as u8); } /// **Row:** `test_connected_peers` — `connected_peers()` returns an empty vec when no live diff --git a/tests/api_003_tests.rs b/tests/api_003_tests.rs index 98adcb4..a2c9ec5 100644 --- a/tests/api_003_tests.rs +++ b/tests/api_003_tests.rs @@ -27,10 +27,9 @@ use dig_gossip::ErlayConfig; #[cfg(feature = "tor")] use dig_gossip::TorConfig; use dig_gossip::{ - BackpressureConfig, Bytes32, GossipConfig, IntroducerConfig, Network, PeerId, - PeerIdRotationConfig, PeerOptions, RelayConfig, DEFAULT_DNS_SEED_BATCH_SIZE, - DEFAULT_DNS_SEED_TIMEOUT_SECS, DEFAULT_MAX_SEEN_MESSAGES, DEFAULT_P2P_PORT, - DEFAULT_TARGET_OUTBOUND_COUNT, + BackpressureConfig, Bytes32, GossipConfig, IntroducerConfig, LinkOptions, Network, PeerId, + PeerIdRotationConfig, RelayConfig, DEFAULT_DNS_SEED_BATCH_SIZE, DEFAULT_DNS_SEED_TIMEOUT_SECS, + DEFAULT_MAX_SEEN_MESSAGES, DEFAULT_P2P_PORT, DEFAULT_TARGET_OUTBOUND_COUNT, }; // ----------------------------------------------------------------------------- test plan: all fields @@ -70,7 +69,7 @@ fn test_config_all_fields_exist() { gossip_fanout: 4, max_seen_messages: 99, peers_file_path: dir.path().join("addrman.dat"), - peer_options: PeerOptions::default(), + peer_options: LinkOptions::default(), #[cfg(feature = "dandelion")] dandelion: Some(DandelionConfig::default()), peer_id_rotation: Some(PeerIdRotationConfig::default()), @@ -246,14 +245,14 @@ fn test_config_default_optional_subsystems_none() { assert!(c.peer_pool.is_none()); } -/// **Row:** `test_config_peer_options_type` — field is `chia_sdk_client::PeerOptions`. +/// **Row:** `test_config_peer_options_type` — field is `chia_sdk_client::LinkOptions`. /// /// Assignment to a locally typed binding is a compile-time + link-time proof on stable Rust (no /// `TypeId::of_val`, which is not available on MSRV here). #[test] fn test_config_peer_options_type() { let c = GossipConfig::default(); - let _: PeerOptions = c.peer_options; + let _: LinkOptions = c.peer_options; } /// **Row:** `test_config_network_id_type` — `network_id` is `chia_protocol::Bytes32`. diff --git a/tests/api_005_tests.rs b/tests/api_005_tests.rs index 4775c2e..74dad90 100644 --- a/tests/api_005_tests.rs +++ b/tests/api_005_tests.rs @@ -21,7 +21,7 @@ use std::time::Duration; use dig_gossip::ChiaCertificate; use dig_gossip::{ - peer_id_from_tls_spki_der, Message, NodeType, Peer, PeerId, PeerOptions, PeerReputation, + peer_id_from_tls_spki_der, DigLink, DigMessage, LinkOptions, NodeType, PeerId, PeerReputation, ProtocolMessageTypes, RequestPeers, }; use tokio::net::TcpListener; @@ -36,8 +36,8 @@ use x509_parser::pem::parse_x509_pem; /// /// Used by: `test_inbound_rx_receives_messages` to verify wire send/recv across paired handles. async fn loopback_ws_peers() -> ( - (Peer, tokio::sync::mpsc::Receiver), - (Peer, tokio::sync::mpsc::Receiver), + (DigLink, tokio::sync::mpsc::Receiver), + (DigLink, tokio::sync::mpsc::Receiver), ) { let listener = TcpListener::bind("127.0.0.1:0") .await @@ -49,13 +49,13 @@ async fn loopback_ws_peers() -> ( let ws = accept_async(MaybeTlsStream::Plain(tcp)) .await .expect("ws accept"); - Peer::from_websocket(ws, PeerOptions::default()).expect("server Peer::from_websocket") + DigLink::from_websocket(ws, LinkOptions::default()).expect("server DigLink::from_websocket") }; let client = async { let url = format!("ws://127.0.0.1:{}/", addr.port()); let (ws, _) = connect_async(url.as_str()).await.expect("ws connect"); - Peer::from_websocket(ws, PeerOptions::default()).expect("client Peer::from_websocket") + DigLink::from_websocket(ws, LinkOptions::default()).expect("client DigLink::from_websocket") }; let (server_res, client_res) = tokio::join!(server, client); @@ -69,7 +69,7 @@ async fn loopback_ws_peers() -> ( #[tokio::test] async fn test_peer_connection_all_fields() { let pc = common::mock_peer_connection(true).await; - let _: Peer = pc.peer; + let _: DigLink = pc.peer; let _: PeerId = pc.peer_id; let _ = pc.address; assert!(pc.is_outbound); @@ -85,7 +85,7 @@ async fn test_peer_connection_all_fields() { let _: u64 = pc.messages_received; let _: u64 = pc.last_message_time; let _: PeerReputation = pc.reputation.clone(); - let _: tokio::sync::mpsc::Receiver = pc.inbound_rx; + let _: tokio::sync::mpsc::Receiver = pc.inbound_rx; } /// **Row:** `test_peer_connection_initial_bytes` @@ -178,7 +178,7 @@ async fn test_inbound_rx_receives_messages() { .expect("recv timed out") .expect("inbound channel must stay open"); - assert_eq!(msg.msg_type, ProtocolMessageTypes::RequestPeers); + assert_eq!(msg.msg_type, ProtocolMessageTypes::RequestPeers as u8); drop(sp); drop(cp); diff --git a/tests/api_008_tests.rs b/tests/api_008_tests.rs index ccb4898..e5defad 100644 --- a/tests/api_008_tests.rs +++ b/tests/api_008_tests.rs @@ -18,8 +18,8 @@ mod common; use std::net::SocketAddr; use dig_gossip::{ - Bytes32, ChiaProtocolMessage, GossipHandle, GossipService, GossipStats, Message, NewPeak, - NodeType, RelayConfig, RelayStats, RequestPeers, Streamable, + Bytes32, DigMessage, GossipHandle, GossipService, GossipStats, NewPeak, NodeType, RelayConfig, + RelayStats, RequestPeers, Streamable, }; /// Spin up a [`GossipService`] with harness defaults and return both the service (for @@ -458,8 +458,8 @@ async fn test_stats_inject_increments_messages_received() { let (_s, h) = running_handle().await; let sender = Bytes32::from([9u8; 32]); let before = h.stats().await.messages_received; - let msg = Message { - msg_type: RequestPeers::msg_type(), + let msg = DigMessage { + msg_type: chia_protocol::ProtocolMessageTypes::RequestPeers as u8, id: None, data: RequestPeers::new().to_bytes().unwrap().into(), }; diff --git a/tests/audit_179_gossip_tests.rs b/tests/audit_179_gossip_tests.rs index 89e8578..d0e3f04 100644 --- a/tests/audit_179_gossip_tests.rs +++ b/tests/audit_179_gossip_tests.rs @@ -528,7 +528,7 @@ mod low_5_broadcast_lock_scope { use std::time::Duration; use dig_gossip::{ - Bytes32, GossipHandle, GossipService, Message, NewPeak, ProtocolMessageTypes, Streamable, + Bytes32, DigMessage, GossipHandle, GossipService, NewPeak, ProtocolMessageTypes, Streamable, }; async fn running_server() -> (tempfile::TempDir, GossipService, GossipHandle, SocketAddr) { @@ -557,7 +557,7 @@ mod low_5_broadcast_lock_scope { /// The audit flagged `broadcast()`'s eager-peer classification block for acquiring the /// `peers` + `plumtree` locks TOGETHER, then sending while (potentially) still holding them. /// `std::sync::MutexGuard` is `!Send`, so if EITHER guard were held across the - /// `peer.send_protocol_message(...).await` point, the `broadcast()` future itself would + /// `peer.send_message(...).await` point, the `broadcast()` future itself would /// become `!Send` — and `tokio::spawn` (which requires `F: Future + Send`) would fail to /// COMPILE. This test spawns `broadcast()` onto its own task via `tokio::spawn`: it is a /// compile-time proof, not a timing heuristic — if a future change re-introduces a @@ -586,8 +586,8 @@ mod low_5_broadcast_lock_scope { let msg = { let z = Bytes32::default(); let body = NewPeak::new(z, 1, 1, 0, z); - Message { - msg_type: ProtocolMessageTypes::NewPeak, + DigMessage { + msg_type: ProtocolMessageTypes::NewPeak as u8, id: None, data: body.to_bytes().unwrap().into(), } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 145c462..0cc7d15 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -16,9 +16,9 @@ //! `dig_gossip` API plus this module. Helpers intentionally live under `tests/common/` (not //! `#[cfg(test)]` in `src/`) so they stay available to **integration** tests — library unit //! tests cannot share `tests/common` without duplication (Cargo limitation). -//! - **Mock [`Peer`](chia_sdk_client::Peer):** `chia-sdk-client` exposes peers only after a -//! WebSocket exists (`Peer::connect`, `Peer::from_websocket`). For [`mock_peer_connection`] -//! we open a **plain** `ws://` loopback pair (no TLS) so [`Peer::from_websocket`] can hash a +//! - **Mock [`DigLink`](dig_peer_protocol::DigLink):** The transport type is `DigLink` from the `dig_peer_protocol` +//! crate, obtained after a WebSocket connection exists. For [`mock_peer_connection`] +//! we open a **plain** `ws://` loopback pair (no TLS) so [`DigLink::from_websocket`] can hash a //! socket address for peer id plumbing. This is **not** a production handshake path (CON-001 //! uses `wss://` + mutual TLS); it exists solely to obtain a well-formed [`PeerConnection`] //! for structure tests until CON-* lands. @@ -35,8 +35,8 @@ use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; use dig_gossip::{ - Bytes32, ChiaCertificate, GossipConfig, GossipHandle, GossipService, Network, NodeType, Peer, - PeerConnection, PeerId, PeerOptions, PeerReputation, + Bytes32, ChiaCertificate, DigLink, GossipConfig, GossipHandle, GossipService, LinkOptions, + Network, PeerConnection, PeerId, PeerReputation, }; use rand::Rng; use tokio::net::TcpListener; @@ -89,11 +89,11 @@ pub async fn mock_peer_connection(is_outbound: bool) -> PeerConnection { let server = async { let (tcp, _) = listener.accept().await.expect("accept mock websocket tcp"); - // `Peer::from_websocket` is typed for `MaybeTlsStream` (same as `connect_async`). + // `DigLink::from_websocket` is typed for `MaybeTlsStream` (same as `connect_async`). let ws = accept_async(MaybeTlsStream::Plain(tcp)) .await .expect("websocket accept"); - Peer::from_websocket(ws, PeerOptions::default()).expect("server Peer::from_websocket") + DigLink::from_websocket(ws, LinkOptions::default()).expect("server DigLink::from_websocket") }; let client = async { @@ -101,7 +101,7 @@ pub async fn mock_peer_connection(is_outbound: bool) -> PeerConnection { let (ws, _) = connect_async(url.as_str()) .await .expect("client websocket connect"); - Peer::from_websocket(ws, PeerOptions::default()).expect("client Peer::from_websocket") + DigLink::from_websocket(ws, LinkOptions::default()).expect("client DigLink::from_websocket") }; let (server_res, client_res) = tokio::join!(server, client); @@ -123,7 +123,7 @@ pub async fn mock_peer_connection(is_outbound: bool) -> PeerConnection { peer_id: random_peer_id(), address, is_outbound, - node_type: NodeType::FullNode, + node_type: dig_gossip::NodeType::FullNode, protocol_version: "0.0.35".to_string(), software_version: "dig-gossip/0.1.0".to_string(), peer_server_port: address.port(), @@ -162,7 +162,7 @@ pub fn test_network() -> Network { /// [`GossipConfig`] tuned for local integration tests (localhost, small limits, paths under `temp_dir`). /// /// **STR-005 alignment:** `listen_addr` uses port `0` for OS assignment; `target_outbound_count` -/// and `max_connections` match the STR-005 example (`2` / `10`). [`PeerOptions`] uses defaults +/// and `max_connections` match the STR-005 example (`2` / `10`). [`LinkOptions`] uses defaults /// from `chia-sdk-client`. /// The software build every test harness node advertises (#2215). /// @@ -191,7 +191,7 @@ pub fn test_gossip_config(temp_dir: &Path) -> GossipConfig { gossip_fanout: 3, max_seen_messages: 1000, peers_file_path: temp_dir.join("peers.dat"), - peer_options: PeerOptions::default(), + peer_options: LinkOptions::default(), #[cfg(feature = "dandelion")] dandelion: None, peer_id_rotation: None, diff --git a/tests/common/wss_full_node.rs b/tests/common/wss_full_node.rs index 3262750..6576586 100644 --- a/tests/common/wss_full_node.rs +++ b/tests/common/wss_full_node.rs @@ -1,9 +1,9 @@ //! One-shot **Chia-shaped WSS full node** for CON-001 integration tests. //! -//! **Why not `Peer::from_websocket` on the server?** Upstream [`chia_sdk_client::Peer`]’s inbound -//! dispatcher routes messages with `id` as **responses to this peer’s outbound requests**, not as +//! **Why not `DigLink::from_websocket` on the server?** The [`dig_peer_protocol::DigLink`] transport +//! routes messages with `id` as **responses to this peer’s outbound requests**, not as //! requests *from* the remote client. A minimal full node that answers `RequestPeers` is therefore -//! implemented with raw [`tokio_tungstenite`] binary frames + [`chia_protocol::Message`] parsing. +//! implemented with raw [`tokio_tungstenite`] binary frames + [`dig_gossip::DigMessage`] parsing. //! //! **Traceability:** [`CON-001.md`](../../docs/requirements/domains/connection/specs/CON-001.md) — //! `test_outbound_connect_handshake` / `test_request_peers_after_connect`. @@ -13,7 +13,7 @@ //! - SPEC §5.1 steps 1-7 — outbound connection lifecycle (this mock is the server half). //! - SPEC §5.2 steps 1-6 — inbound connection: receive Handshake, validate network_id, send reply. //! - SPEC §1.5#1 — Handshake with capabilities (capabilities list passed in Handshake struct). -//! - SPEC §1.5#7 — network_id validation: connect_peer() rejects peers with mismatched network_id. +//! - SPEC §1.5#7 — network_id validation: outbound connections reject peers with mismatched network_id. //! - SPEC §1.6#1 — peer exchange on outbound connect: send RequestPeers after handshake. //! - SPEC §5.3 — mandatory mutual TLS: ChiaCertificate identity for both client and server. @@ -22,7 +22,7 @@ use std::net::SocketAddr; use dig_gossip::ChiaCertificate; use dig_gossip::Streamable; use dig_gossip::{ - Handshake, Message, NodeType, ProtocolMessageTypes, RespondPeers, TimestampedPeerInfo, + DigMessage, Handshake, NodeType, ProtocolMessageTypes, RespondPeers, TimestampedPeerInfo, }; use dig_gossip::{RegisterAck, RegisterPeer, RequestPeersIntroducer, RespondPeersIntroducer}; use futures_util::{SinkExt, StreamExt}; @@ -35,13 +35,13 @@ use tokio_tungstenite::{accept_async, WebSocketStream}; /// Type alias for a TLS-wrapped WebSocket stream used by the test full-node acceptor. type Ws = WebSocketStream>; -/// Read the next Chia [`Message`] from a WebSocket stream, handling Ping/Pong transparently. +/// Read the next Chia [`DigMessage`] from a WebSocket stream, handling Ping/Pong transparently. /// -/// Binary frames are decoded as `Message::from_bytes`; Ping frames receive automatic Pong +/// Binary frames are decoded as `DigMessage::from_bytes`; Ping frames receive automatic Pong /// replies (WebSocket keepalive). Close frames and unexpected frame types are treated as errors. /// /// Used internally by [`serve_one_client`] to drive the handshake + RequestPeers sequence. -async fn next_chia_message(ws: &mut Ws) -> Result { +async fn next_chia_message(ws: &mut Ws) -> Result { loop { let raw = ws .next() @@ -50,7 +50,7 @@ async fn next_chia_message(ws: &mut Ws) -> Result { .map_err(|e| e.to_string())?; match raw { WsMsg::Binary(bin) => { - return Message::from_bytes(&bin).map_err(|e| e.to_string()); + return DigMessage::from_bytes(&bin).ok_or_else(|| "malformed frame".to_string()); } WsMsg::Ping(p) => { ws.send(WsMsg::Pong(p)).await.map_err(|e| e.to_string())?; @@ -83,7 +83,7 @@ async fn serve_one_client( // Step 1: Receive and validate the client's Handshake. // SPEC §5.2 step 5 — receive Handshake, validate network_id. let first = next_chia_message(&mut ws).await?; - if first.msg_type != ProtocolMessageTypes::Handshake { + if first.msg_type != (ProtocolMessageTypes::Handshake as u8) { return Err(format!("expected Handshake, got {:?}", first.msg_type)); } let hs = Handshake::from_bytes(&first.data).map_err(|e| e.to_string())?; @@ -102,34 +102,34 @@ async fn serve_one_client( protocol_version: "0.0.37".to_string(), software_version: "dig-gossip-test-fullnode/0".to_string(), server_port: 0, - node_type: NodeType::FullNode, + node_type: chia_protocol::NodeType::FullNode, capabilities: vec![], }; - let out = Message { - msg_type: ProtocolMessageTypes::Handshake, + let out = DigMessage { + msg_type: (ProtocolMessageTypes::Handshake as u8), id: None, data: reply_hs.to_bytes().map_err(|e| e.to_string())?.into(), }; - ws.send(WsMsg::Binary(out.to_bytes().map_err(|e| e.to_string())?)) + ws.send(WsMsg::Binary(out.to_bytes())) .await .map_err(|e| e.to_string())?; // Step 3: Receive RequestPeers from client (CON-001 sends this immediately after handshake). // SPEC §1.6#1 — peer exchange on outbound connect: send RequestPeers to discover more peers. let second = next_chia_message(&mut ws).await?; - if second.msg_type != ProtocolMessageTypes::RequestPeers { + if second.msg_type != (ProtocolMessageTypes::RequestPeers as u8) { return Err(format!("expected RequestPeers, got {:?}", second.msg_type)); } // Step 4: Reply with RespondPeers containing the test's peer_list. // SPEC §6.6 — peer exchange via chia-protocol::RequestPeers / RespondPeers. // SPEC §1.5#5 — request/response correlation: id field MUST match for SDK's RequestMap. let resp = RespondPeers::new(peer_list); - let out = Message { - msg_type: ProtocolMessageTypes::RespondPeers, + let out = DigMessage { + msg_type: (ProtocolMessageTypes::RespondPeers as u8), id: second.id, data: resp.to_bytes().map_err(|e| e.to_string())?.into(), }; - ws.send(WsMsg::Binary(out.to_bytes().map_err(|e| e.to_string())?)) + ws.send(WsMsg::Binary(out.to_bytes())) .await .map_err(|e| e.to_string())?; Ok(()) @@ -185,7 +185,7 @@ async fn serve_introducer_one_client( stall_after_request_peers_introducer: bool, ) -> Result<(), String> { let first = next_chia_message(&mut ws).await?; - if first.msg_type != ProtocolMessageTypes::Handshake { + if first.msg_type != (ProtocolMessageTypes::Handshake as u8) { return Err(format!("expected Handshake, got {:?}", first.msg_type)); } let hs = Handshake::from_bytes(&first.data).map_err(|e| e.to_string())?; @@ -211,20 +211,20 @@ async fn serve_introducer_one_client( protocol_version: "0.0.37".to_string(), software_version: "dig-gossip-test-introducer/0".to_string(), server_port: 0, - node_type: NodeType::FullNode, + node_type: chia_protocol::NodeType::FullNode, capabilities: vec![], }; - let out = Message { - msg_type: ProtocolMessageTypes::Handshake, + let out = DigMessage { + msg_type: (ProtocolMessageTypes::Handshake as u8), id: None, data: reply_hs.to_bytes().map_err(|e| e.to_string())?.into(), }; - ws.send(WsMsg::Binary(out.to_bytes().map_err(|e| e.to_string())?)) + ws.send(WsMsg::Binary(out.to_bytes())) .await .map_err(|e| e.to_string())?; let third = next_chia_message(&mut ws).await?; - if third.msg_type != ProtocolMessageTypes::RequestPeersIntroducer { + if third.msg_type != (ProtocolMessageTypes::RequestPeersIntroducer as u8) { return Err(format!( "expected RequestPeersIntroducer, got {:?}", third.msg_type @@ -238,12 +238,12 @@ async fn serve_introducer_one_client( } let resp = RespondPeersIntroducer::new(peer_list); - let out = Message { - msg_type: ProtocolMessageTypes::RespondPeersIntroducer, + let out = DigMessage { + msg_type: (ProtocolMessageTypes::RespondPeersIntroducer as u8), id: third.id, data: resp.to_bytes().map_err(|e| e.to_string())?.into(), }; - ws.send(WsMsg::Binary(out.to_bytes().map_err(|e| e.to_string())?)) + ws.send(WsMsg::Binary(out.to_bytes())) .await .map_err(|e| e.to_string())?; Ok(()) @@ -300,7 +300,7 @@ async fn serve_introducer_register_one_client( stall_after_register_peer: bool, ) -> Result<(), String> { let first = next_chia_message(&mut ws).await?; - if first.msg_type != ProtocolMessageTypes::Handshake { + if first.msg_type != (ProtocolMessageTypes::Handshake as u8) { return Err(format!("expected Handshake, got {:?}", first.msg_type)); } let hs = Handshake::from_bytes(&first.data).map_err(|e| e.to_string())?; @@ -326,20 +326,20 @@ async fn serve_introducer_register_one_client( protocol_version: "0.0.37".to_string(), software_version: "dig-gossip-test-introducer-register/0".to_string(), server_port: 0, - node_type: NodeType::FullNode, + node_type: chia_protocol::NodeType::FullNode, capabilities: vec![], }; - let out = Message { - msg_type: ProtocolMessageTypes::Handshake, + let out = DigMessage { + msg_type: (ProtocolMessageTypes::Handshake as u8), id: None, data: reply_hs.to_bytes().map_err(|e| e.to_string())?.into(), }; - ws.send(WsMsg::Binary(out.to_bytes().map_err(|e| e.to_string())?)) + ws.send(WsMsg::Binary(out.to_bytes())) .await .map_err(|e| e.to_string())?; let third = next_chia_message(&mut ws).await?; - if third.msg_type != ProtocolMessageTypes::RegisterPeer { + if third.msg_type != (dig_gossip::DigMessageType::RegisterPeer as u8) { return Err(format!("expected RegisterPeer, got {:?}", third.msg_type)); } let req = RegisterPeer::from_bytes(&third.data).map_err(|e| e.to_string())?; @@ -358,12 +358,12 @@ async fn serve_introducer_register_one_client( } let resp = RegisterAck::new(ack_success); - let out = Message { - msg_type: ProtocolMessageTypes::RegisterAck, + let out = DigMessage { + msg_type: (dig_gossip::DigMessageType::RegisterAck as u8), id: third.id, data: resp.to_bytes().map_err(|e| e.to_string())?.into(), }; - ws.send(WsMsg::Binary(out.to_bytes().map_err(|e| e.to_string())?)) + ws.send(WsMsg::Binary(out.to_bytes())) .await .map_err(|e| e.to_string())?; Ok(()) diff --git a/tests/con_001_tests.rs b/tests/con_001_tests.rs index f9712ab..53e6ee3 100644 --- a/tests/con_001_tests.rs +++ b/tests/con_001_tests.rs @@ -1,4 +1,4 @@ -//! Integration + unit tests for **CON-001: outbound connection via `connect_peer` semantics**. +//! Integration + unit tests for **CON-001: outbound connection establishment via TLS + handshake**. //! //! ## Traceability //! @@ -10,7 +10,7 @@ //! //! **TLS / connector rows** exercise [`dig_gossip::load_ssl_cert`] and [`dig_gossip::create_native_tls_connector`] //! (rustls equivalent when `--features rustls` only — STR-004). **Integration rows** spin up the local -//! [`common::wss_full_node`] acceptor so [`dig_gossip::GossipHandle::connect_to`] runs the full +//! [`common::wss_full_node`] acceptor so [`dig_gossip::GossipHandle::connect_to`] establishes the full //! `wss://` + [`Handshake`](dig_gossip::Handshake) + [`RequestPeers`](dig_gossip::RequestPeers) sequence //! against a [`NodeType::FullNode`](dig_gossip::NodeType) responder, then assert [`AddressManager`] ingestion //! via the [`GossipHandle::__con001_last_address_batch_for_tests`] hook. @@ -22,7 +22,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use dig_gossip::GossipHandle; use dig_gossip::{ create_native_tls_connector, load_ssl_cert, ChiaCertificate, GossipError, GossipService, - Handshake, NodeType, PeerId, TimestampedPeerInfo, + Handshake, PeerId, TimestampedPeerInfo, }; /// **Row:** `test_tls_cert_load` — PEM generated by STR-005 helpers loads via upstream `load_ssl_cert`. @@ -65,7 +65,7 @@ fn test_connector_creation() { /// SPEC §2.4 — PeerConnection fields: node_type, protocol_version, software_version, /// peer_server_port, capabilities (all sourced from chia-protocol::Handshake). /// -/// **Note:** Uses STR-005 [`common::mock_peer_connection`] for a real [`dig_gossip::Peer`] handle without CON-002 listener. +/// **Note:** Uses STR-005 [`common::mock_peer_connection`] for a real [`dig_gossip::DigLink`] handle without CON-002 listener. #[tokio::test] async fn test_peer_connection_wrapping() { let hs = Handshake { @@ -73,18 +73,18 @@ async fn test_peer_connection_wrapping() { protocol_version: "0.0.37".to_string(), software_version: "unit-test/1".to_string(), server_port: 8444, - node_type: NodeType::FullNode, + node_type: chia_protocol::NodeType::FullNode, capabilities: vec![(1, "x".to_string())], }; let mut base = common::mock_peer_connection(true).await; - base.node_type = hs.node_type; + base.node_type = dig_gossip::connection::handshake::dig_node_type_of(hs.node_type); base.protocol_version = hs.protocol_version.clone(); base.software_version = hs.software_version.clone(); base.peer_server_port = hs.server_port; base.capabilities = hs.capabilities.clone(); assert!(base.is_outbound); - let _: dig_gossip::Peer = base.peer; - let _: tokio::sync::mpsc::Receiver = base.inbound_rx; + let _: dig_gossip::DigLink = base.peer; + let _: tokio::sync::mpsc::Receiver = base.inbound_rx; } /// **Row:** `test_creation_time_set` — [`PeerConnection::creation_time`] is Unix seconds near “now”. @@ -125,7 +125,7 @@ async fn running_client() -> (tempfile::TempDir, GossipService, GossipHandle) { /// **Row:** `test_outbound_connect_handshake` — TLS client completes Chia handshake with matching `network_id`. /// /// Proves SPEC §5.1 steps 1-7 — full outbound connection lifecycle: load TLS cert, create -/// connector, call connect_peer(), wrap in PeerConnection, add to address manager, send +/// connector, establish via dial + handshake, wrap in PeerConnection, add to address manager, send /// RequestPeers, spawn message loop. /// SPEC §5.3 — mandatory mutual TLS with chia-ssl certificates. /// SPEC §1.6#1 — peer exchange on outbound connect: send RequestPeers after handshake. @@ -178,16 +178,19 @@ async fn test_request_peers_after_connect() { jh.await.expect("join").expect("server"); } -/// **Row:** `test_outbound_connect_failure` — unreachable WSS peer surfaces [`GossipError::ClientError`]. +/// **Row:** `test_outbound_connect_failure` — unreachable WSS peer surfaces [`GossipError::LinkError`]. /// -/// SPEC §4 — GossipError::ClientError wraps chia-sdk-client::ClientError for connection-level errors. +/// SPEC §4 — since the dial moved onto `DigLink`, a refused TCP connection is a transport failure and +/// arrives as `LinkError`. `ClientError` still carries handshake-POLICY failures (network-id mismatch, +/// incompatible protocol version), so asserting the specific variant keeps the two distinguishable — +/// `is_err()` here would pass on a handshake rejection that never reached the wire. #[tokio::test] async fn test_outbound_connect_failure() { let (_cdir, _svc, h) = running_client().await; let dead: std::net::SocketAddr = "127.0.0.1:2".parse().unwrap(); let err = h.connect_to(dead).await.unwrap_err(); assert!( - matches!(err, GossipError::ClientError(_)), - "expected ClientError, got {err:?}" + matches!(err, GossipError::LinkError(_)), + "expected LinkError, got {err:?}" ); } diff --git a/tests/con_002_tests.rs b/tests/con_002_tests.rs index 3640d92..818e174 100644 --- a/tests/con_002_tests.rs +++ b/tests/con_002_tests.rs @@ -23,8 +23,8 @@ use std::time::Duration; use dig_gossip::Streamable; use dig_gossip::{ - create_native_tls_connector, load_ssl_cert, Bytes32, GossipHandle, GossipService, Handshake, - Message, NodeType, PeerId, ProtocolMessageTypes, RespondPeers, + create_native_tls_connector, load_ssl_cert, Bytes32, DigMessage, GossipHandle, GossipService, + Handshake, PeerId, ProtocolMessageTypes, RespondPeers, }; use futures_util::{SinkExt, StreamExt}; use tokio_tungstenite::tungstenite::Message as WsMsg; @@ -209,17 +209,15 @@ async fn test_inbound_network_id_reject() { protocol_version: "0.0.37".to_string(), software_version: "test/1".to_string(), server_port: 0, - node_type: NodeType::FullNode, + node_type: chia_protocol::NodeType::FullNode, capabilities: vec![], }; - let wire = Message { - msg_type: ProtocolMessageTypes::Handshake, + let wire = DigMessage { + msg_type: ProtocolMessageTypes::Handshake as u8, id: None, data: hs.to_bytes().expect("hs").into(), }; - ws.send(WsMsg::Binary(wire.to_bytes().expect("msg"))) - .await - .ok(); + ws.send(WsMsg::Binary(wire.to_bytes())).await.ok(); // Wait for the server to process and reject (close frame or socket drop). let _ = tokio::time::timeout(Duration::from_secs(2), ws.next()) @@ -336,7 +334,7 @@ async fn test_inbound_peer_info_relay() { let Ok((_sender, msg)) = sub.recv().await else { break; }; - if msg.msg_type == ProtocolMessageTypes::RespondPeers { + if msg.msg_type == ProtocolMessageTypes::RespondPeers as u8 { let body = RespondPeers::from_bytes(&msg.data).expect("RespondPeers"); // The relay should contain exactly the second peer's address. if body.peer_list.len() == 1 && body.peer_list[0].host == "127.0.0.1" { diff --git a/tests/con_003_tests.rs b/tests/con_003_tests.rs index a8e7575..384004a 100644 --- a/tests/con_003_tests.rs +++ b/tests/con_003_tests.rs @@ -24,7 +24,7 @@ use dig_gossip::connection::handshake::{ HandshakeValidationError, MAX_SOFTWARE_VERSION_BYTES, MIN_COMPATIBLE_PROTOCOL_VERSION, }; use dig_gossip::Handshake; -use dig_gossip::{NodeType, PeerId}; +use dig_gossip::PeerId; /// Build a valid baseline [`Handshake`] for mutation in individual tests. /// @@ -37,7 +37,7 @@ fn sample_handshake_base(network_id: &str) -> Handshake { protocol_version: "0.0.37".to_string(), software_version: "dig-gossip/0.1.0".to_string(), server_port: 8444, - node_type: NodeType::FullNode, + node_type: chia_protocol::NodeType::FullNode, capabilities: vec![], } } diff --git a/tests/con_004_tests.rs b/tests/con_004_tests.rs index 774d6e2..04e3722 100644 --- a/tests/con_004_tests.rs +++ b/tests/con_004_tests.rs @@ -18,7 +18,7 @@ mod common; use std::time::Duration; -use dig_gossip::{GossipHandle, GossipService, PeerOptions, PenaltyReason}; +use dig_gossip::{GossipHandle, GossipService, LinkOptions, PenaltyReason}; use dig_gossip::{PeerId, PeerReputation}; /// Short keepalive period for tests (seconds between probes). @@ -52,9 +52,9 @@ async fn service_with_keepalive( cfg.keepalive_peer_timeout_secs = Some(timeout); // [`RequestPeers`] is capped by `V2_RATE_LIMITS` (~6/min with default `rate_limit_factor` 0.6); // sub-second probes in this file need a higher factor so keepalive is not stuck throttling. - cfg.peer_options = PeerOptions { - rate_limit_factor: 20.0, - }; + let mut link_options = LinkOptions::default(); + link_options.rate_limit_factor = 20.0; + cfg.peer_options = link_options; let svc = GossipService::new(cfg).expect("new"); let h = svc.start().await.expect("start"); (svc, h) diff --git a/tests/con_005_rate_limit_rekey_tests.rs b/tests/con_005_rate_limit_rekey_tests.rs new file mode 100644 index 0000000..a5db8ec --- /dev/null +++ b/tests/con_005_rate_limit_rekey_tests.rs @@ -0,0 +1,167 @@ +//! CON-005 — the Chia rate-limit table survives being re-keyed onto raw opcodes. +//! +//! # Why this file exists +//! +//! Inbound rate limiting used to run through `chia_sdk_client::RateLimiter`, keyed by +//! `ProtocolMessageTypes`. That enum cannot name a DIG opcode, which is one of the two +//! reasons dig-gossip vendored a forked `chia-protocol` (dig_ecosystem#2228). The fork +//! retires by moving to [`dig_peer_protocol::OpcodeRateLimiter`], which re-keys Chia's +//! own `V2_RATE_LIMITS` from the enum onto the wire byte. +//! +//! **A re-key is precisely the operation that can silently loosen every bound.** If an +//! entry fails to carry across, its opcode does not error — it quietly falls through to +//! `default_settings`, which is far more permissive than most specific rows. A rate-limit +//! table that silently becomes permissive is a DoS surface, and nothing about it is +//! visible to the compiler. +//! +//! # How these tests are built to catch that +//! +//! The bounds are pinned as **absolute literals**, deliberately *not* by comparing the +//! re-keyed table against `V2_RATE_LIMITS`. Asking the same possibly-shifted table for +//! its own expected answer only proves it agrees with itself; it cannot see a shift that +//! moved both sides together. +//! +//! The fixtures are chosen so a collapse to `default_settings` is *loudly* visible rather +//! than marginal: +//! +//! - `Handshake` is capped at **5 frames** per window against a default of 100 — so a +//! collapsed table admits the 6th frame instead of refusing it, a 20x gap. +//! - `RequestPeers` is capped at **100 bytes** per message against a default of 1 MiB — +//! so a collapsed table admits a body four orders of magnitude too large. +//! +//! Each bound is pinned from **both** sides (at-bound admitted, one-over refused): a +//! bound tested only from below can be satisfied by a limiter that refuses everything, +//! and one tested only from above by a limiter with no bound at all. + +use dig_gossip::connection::inbound_limits::InboundRateLimiter; +use dig_peer_protocol::{DigMessage, ProtocolMessageTypes, V2_RATE_LIMITS}; + +/// Wire opcode of `Handshake`, whose Chia row is far tighter than `default_settings`. +const HANDSHAKE: u8 = ProtocolMessageTypes::Handshake as u8; + +/// Wire opcode of `RequestPeers`, whose Chia row has a very small `max_size`. +const REQUEST_PEERS: u8 = ProtocolMessageTypes::RequestPeers as u8; + +/// Chia's `Handshake => 5, 10 * 1024` frequency, restated as a literal on purpose. +const HANDSHAKE_FREQUENCY: usize = 5; + +/// Chia's `RequestPeers => 10, 100` per-message size cap, restated as a literal. +const REQUEST_PEERS_MAX_SIZE: usize = 100; + +/// `default_settings` frequency — the value a collapsed table would apply instead. +const DEFAULT_FREQUENCY: usize = 100; + +/// A limiter with an unscaled budget, so the literals above are the bounds under test. +fn limiter() -> InboundRateLimiter { + InboundRateLimiter::new(1.0) +} + +/// Build an inbound frame of `opcode` carrying `body_len` bytes. +fn frame(opcode: u8, body_len: usize) -> DigMessage { + DigMessage::new(opcode, None, vec![0u8; body_len].into()) +} + +#[test] +fn handshake_keeps_its_tight_frequency_and_does_not_fall_to_the_default() { + // The fixture is only meaningful while the specific row is far below the default; if that ever + // stops holding, this test can no longer see a collapse. Both operands are consts, so clippy + // sees a constant assertion — that is the point: the guard must fail the build the moment the + // fixture goes blind. + #[allow(clippy::assertions_on_constants)] + { + assert!( + HANDSHAKE_FREQUENCY < DEFAULT_FREQUENCY, + "fixture is blind unless the Handshake row is tighter than default_settings" + ); + } + + let mut limiter = limiter(); + + for i in 1..=HANDSHAKE_FREQUENCY { + assert!( + limiter.allows(&frame(HANDSHAKE, 64)), + "frame {i} is within the Handshake budget of {HANDSHAKE_FREQUENCY}" + ); + } + + assert!( + !limiter.allows(&frame(HANDSHAKE, 64)), + "frame {} must be refused; admitting it means the Handshake row was lost and \ + default_settings ({DEFAULT_FREQUENCY}/window) is being applied instead", + HANDSHAKE_FREQUENCY + 1 + ); +} + +#[test] +fn request_peers_keeps_its_tight_size_cap_and_does_not_fall_to_the_default() { + // Two independent limiters, so the at-cap probe cannot spend budget the over-cap + // probe is then refused for -- that would make the second assertion pass for the + // wrong reason. + let mut at_cap = limiter(); + let mut over_cap = limiter(); + + assert!( + at_cap.allows(&frame(REQUEST_PEERS, REQUEST_PEERS_MAX_SIZE)), + "a body of exactly {REQUEST_PEERS_MAX_SIZE} bytes is at the cap and must pass" + ); + + assert!( + !over_cap.allows(&frame(REQUEST_PEERS, REQUEST_PEERS_MAX_SIZE + 1)), + "a {}-byte body must be refused; admitting it means the RequestPeers row was \ + lost and the 1 MiB default cap is being applied instead", + REQUEST_PEERS_MAX_SIZE + 1 + ); +} + +#[test] +fn no_chia_opcode_occupies_the_dig_band() { + // Every DIG opcode lives at 200-222. A Chia opcode there would collide after the + // re-key: two different messages would share one budget row, and the DIG bound + // dig-gossip layers on top would be applied to Chia traffic. + for (label, keys) in [ + ("tx", V2_RATE_LIMITS.tx.keys()), + ("other", V2_RATE_LIMITS.other.keys()), + ] { + for msg_type in keys { + let opcode = *msg_type as u8; + assert!( + opcode < 200, + "{label} row {msg_type:?} sits at opcode {opcode}, inside the DIG band" + ); + } + } +} + +#[test] +fn the_chia_table_is_populated() { + // A table that silently emptied would push EVERY opcode onto default_settings while + // each individual bound test above still passed for whichever rows remained. The + // floor is set well below the real count so ordinary upstream churn does not trip + // it, while an emptied or drastically truncated table does. + let entries = V2_RATE_LIMITS.tx.len() + V2_RATE_LIMITS.other.len(); + assert!( + entries >= 50, + "expected a populated Chia rate-limit table, found {entries} entries" + ); +} + +#[test] +fn an_untabled_opcode_really_is_looser_which_is_what_makes_the_tests_above_falsifiable() { + // The control. Every assertion above claims a specific row is TIGHTER than the + // fallback -- but if the fallback were itself tight (or if every opcode were capped + // at 5), those tests would pass while proving nothing. + // + // Opcode 200 is the first DIG consensus-band opcode. It has no Chia row, so it takes + // `default_settings`; and it sits below DIG_WIRE_BAND_START (220), so dig-gossip's + // own DIG bound does not apply either. It therefore measures the fallback alone. + let mut limiter = limiter(); + + for i in 1..=HANDSHAKE_FREQUENCY + 1 { + assert!( + limiter.allows(&frame(200, 64)), + "frame {i} on an untabled opcode must still be admitted: the fallback is \ + {DEFAULT_FREQUENCY}/window, so if it refuses here then the Handshake test \ + above cannot distinguish a preserved row from a collapsed one" + ); + } +} diff --git a/tests/con_005_tests.rs b/tests/con_005_tests.rs index 54f4be9..06ef981 100644 --- a/tests/con_005_tests.rs +++ b/tests/con_005_tests.rs @@ -8,9 +8,9 @@ //! //! ## Proof strategy //! -//! Outbound limiting stays inside [`chia_sdk_client::Peer`] (not duplicated here). These tests +//! Outbound limiting stays inside [`dig_peer_protocol::DigLink`] transport layer (not duplicated here). These tests //! prove the **DIG-specific** pieces: the DIG per-opcode table, independent limiters per -//! connection, [`RateLimiter::handle_message`] / [`DigRateLimiter::check`] behavior, +//! connection, [`OpcodeRateLimiter::allow`] / [`DigRateLimiter::check`] behavior, //! and the **penalty** path exercised through [`dig_gossip::apply_inbound_rate_limit_violation`] //! (integration-style with a synthetic [`ServiceState`] row). @@ -18,8 +18,8 @@ mod common; use std::sync::Arc; -use dig_gossip::{Bytes, Message, ProtocolMessageTypes}; -use dig_gossip::{RateLimit, RateLimiter, V2_RATE_LIMITS}; +use dig_gossip::{Admission, OpcodeRateLimiter, OpcodeRateLimits, RateLimit, V2_RATE_LIMITS}; +use dig_gossip::{Bytes, DigMessage, ProtocolMessageTypes}; use dig_gossip::{ apply_inbound_rate_limit_violation, dig_extension_rate_limits_map, load_ssl_cert, @@ -28,14 +28,16 @@ use dig_gossip::{ HOLDINGS_MAX_CHANGES, }; -/// A DIG limiter over the production table, shaped exactly like a live inbound connection's -/// (`incoming = true`, 60 s window) so these tests bind the real rows, not a bespoke fixture. +/// A DIG limiter over the production table, shaped exactly like the DIG half of a live inbound +/// connection's gate (`incoming = true`, 60 s window) so these tests bind the real rows, not a +/// bespoke fixture. The gate's Chia half is not inbound-shaped today — see +/// TODO(dig_ecosystem#2228) on `InboundRateLimiter::new`; these tests cover the DIG half only. fn dig_limiter(limit_factor: f64) -> DigRateLimiter { DigRateLimiter::new(true, 60, limit_factor, dig_extension_rate_limits_map()) } -/// **Row:** `test_inbound_rate_limiter_creation` — [`RateLimiter::new`] with `incoming = true`, -/// `reset_seconds = 60`, and merged limits builds successfully (CON-005 §Inbound Rate Limiting). +/// **Row:** `test_inbound_rate_limiter_creation` — the inbound limiter over the merged DIG +/// limits, with a 60 s window, builds successfully (CON-005 §Inbound Rate Limiting). #[test] fn test_inbound_rate_limiter_creation() { let lim = new_inbound_rate_limiter(1.0); @@ -51,20 +53,17 @@ fn test_separate_limiter_per_connection() { ProtocolMessageTypes::Handshake, RateLimit::new(1.0, 1_000_000.0, None), ); - let mut a = RateLimiter::new(true, 60, 1.0, limits.clone()); - let mut b = RateLimiter::new(true, 60, 1.0, limits); - let m = |t: ProtocolMessageTypes| Message { - msg_type: t, + let mut a = OpcodeRateLimiter::new(60, 1.0, OpcodeRateLimits::from(&limits)); + let mut b = OpcodeRateLimiter::new(60, 1.0, OpcodeRateLimits::from(&limits)); + let m = |t: ProtocolMessageTypes| DigMessage { + msg_type: t as u8, id: None, data: Bytes::new(vec![0u8; 10]), }; let handshake = || m(ProtocolMessageTypes::Handshake); - assert!(a.handle_message(&handshake())); - assert!(!a.handle_message(&handshake())); - assert!( - b.handle_message(&handshake()), - "B must still accept first handshake" - ); + assert!(a.allow(&handshake())); + assert!(!a.allow(&handshake())); + assert!(b.allow(&handshake()), "B must still accept first handshake"); } /// **Row:** `test_dig_message_types_added` — merged limits include CON-005 table entries `200..=208` @@ -96,14 +95,14 @@ fn test_rate_limit_allows_normal_traffic() { ProtocolMessageTypes::Handshake, RateLimit::new(10.0, 1_000_000.0, None), ); - let mut lim = RateLimiter::new(true, 60, 1.0, limits); - let msg = Message { - msg_type: ProtocolMessageTypes::Handshake, + let mut lim = OpcodeRateLimiter::new(60, 1.0, OpcodeRateLimits::from(&limits)); + let msg = DigMessage { + msg_type: ProtocolMessageTypes::Handshake as u8, id: None, data: Bytes::new(vec![0u8; 100]), }; for _ in 0..5 { - assert!(lim.handle_message(&msg), "handshake within cap should pass"); + assert!(lim.allow(&msg), "handshake within cap should pass"); } } @@ -115,35 +114,54 @@ fn test_rate_limit_blocks_excess_traffic() { ProtocolMessageTypes::Handshake, RateLimit::new(2.0, 1_000_000.0, None), ); - let mut lim = RateLimiter::new(true, 60, 1.0, limits); - let msg = Message { - msg_type: ProtocolMessageTypes::Handshake, + let mut lim = OpcodeRateLimiter::new(60, 1.0, OpcodeRateLimits::from(&limits)); + let msg = DigMessage { + msg_type: ProtocolMessageTypes::Handshake as u8, id: None, data: Bytes::new(vec![0u8; 10]), }; - assert!(lim.handle_message(&msg)); - assert!(lim.handle_message(&msg)); + assert!(lim.allow(&msg)); + assert!(lim.allow(&msg)); assert!( - !lim.handle_message(&msg), + !lim.allow(&msg), "third handshake should exceed frequency=2" ); } /// **Row:** `test_rate_limit_blocks_oversized_message` — single-frame `max_size` exceeded. +/// +/// The bound is pinned from BOTH sides: a frame exactly at `max_size` is admitted, one byte over is +/// refused. The refusal is asserted as [`Admission::Unsendable`], not merely "not admitted", because +/// a size refusal survives every window roll — a `Deferred` here would tell a retrying caller to +/// wait for a budget that can never clear. #[test] fn test_rate_limit_blocks_oversized_message() { + const MAX_SIZE: usize = 50; let mut limits = (*V2_RATE_LIMITS).clone(); limits.other.insert( ProtocolMessageTypes::Handshake, - RateLimit::new(100.0, 50.0, None), + #[allow(clippy::cast_precision_loss)] + RateLimit::new(100.0, MAX_SIZE as f64, None), ); - let mut lim = RateLimiter::new(true, 60, 1.0, limits); - let msg = Message { - msg_type: ProtocolMessageTypes::Handshake, + let handshake = |len: usize| DigMessage { + msg_type: ProtocolMessageTypes::Handshake as u8, id: None, - data: Bytes::new(vec![0u8; 100]), + data: Bytes::new(vec![0u8; len]), }; - assert!(!lim.handle_message(&msg)); + + let mut at_bound = OpcodeRateLimiter::new(60, 1.0, OpcodeRateLimits::from(&limits)); + assert_eq!( + at_bound.admit(&handshake(MAX_SIZE)), + Admission::Admitted, + "a frame exactly at max_size must pass — otherwise the over-bound case proves nothing" + ); + + let mut over_bound = OpcodeRateLimiter::new(60, 1.0, OpcodeRateLimits::from(&limits)); + assert_eq!( + over_bound.admit(&handshake(MAX_SIZE + 1)), + Admission::Unsendable, + "one byte over max_size is refused in every window, not merely deferred" + ); } /// **Row:** `test_rate_limit_penalty_applied` — [`PenaltyReason::RateLimitExceeded`] weight matches @@ -174,7 +192,7 @@ fn test_apply_inbound_rate_limit_violation_no_panic() { apply_inbound_rate_limit_violation(&state, ghost, 0); } -/// **Row:** `test_rate_limit_factor_scaling` — lower [`dig_gossip::PeerOptions::rate_limit_factor`] +/// **Row:** `test_rate_limit_factor_scaling` — lower [`dig_gossip::LinkOptions::rate_limit_factor`] /// equivalent scales effective caps (`frequency * factor`). #[test] fn test_rate_limit_factor_scaling() { @@ -183,25 +201,22 @@ fn test_rate_limit_factor_scaling() { ProtocolMessageTypes::Handshake, RateLimit::new(10.0, 1_000_000.0, None), ); - let mut strict = RateLimiter::new(true, 60, 0.5, limits.clone()); - let mut loose = RateLimiter::new(true, 60, 1.0, limits); - let msg = Message { - msg_type: ProtocolMessageTypes::Handshake, + let mut strict = OpcodeRateLimiter::new(60, 0.5, OpcodeRateLimits::from(&limits)); + let mut loose = OpcodeRateLimiter::new(60, 1.0, OpcodeRateLimits::from(&limits)); + let msg = DigMessage { + msg_type: ProtocolMessageTypes::Handshake as u8, id: None, data: Bytes::new(vec![0u8; 10]), }; // effective cap: strict 5, loose 10 first-window accepts for _ in 0..5 { - assert!(strict.handle_message(&msg)); + assert!(strict.allow(&msg)); } - assert!( - !strict.handle_message(&msg), - "6th message should exceed 10*0.5=5" - ); + assert!(!strict.allow(&msg), "6th message should exceed 10*0.5=5"); for _ in 0..10 { - assert!(loose.handle_message(&msg)); + assert!(loose.allow(&msg)); } - assert!(!loose.handle_message(&msg)); + assert!(!loose.allow(&msg)); } /// **Row:** `test_rate_limit_window_reset` — new period clears counters (`reset_seconds` shortened for speed). @@ -212,17 +227,17 @@ async fn test_rate_limit_window_reset() { ProtocolMessageTypes::Handshake, RateLimit::new(1.0, 1_000_000.0, None), ); - let mut lim = RateLimiter::new(true, 2, 1.0, limits); - let msg = Message { - msg_type: ProtocolMessageTypes::Handshake, + let mut lim = OpcodeRateLimiter::new(2, 1.0, OpcodeRateLimits::from(&limits)); + let msg = DigMessage { + msg_type: ProtocolMessageTypes::Handshake as u8, id: None, data: Bytes::new(vec![0u8; 10]), }; - assert!(lim.handle_message(&msg)); - assert!(!lim.handle_message(&msg)); + assert!(lim.allow(&msg)); + assert!(!lim.allow(&msg)); tokio::time::sleep(std::time::Duration::from_secs(3)).await; assert!( - lim.handle_message(&msg), + lim.allow(&msg), "after 2s window rolls, first handshake in new period should pass" ); } diff --git a/tests/con_006_tests.rs b/tests/con_006_tests.rs index 4e2cfa9..673366c 100644 --- a/tests/con_006_tests.rs +++ b/tests/con_006_tests.rs @@ -22,7 +22,7 @@ use std::thread; use std::time::Duration; use dig_gossip::Streamable; -use dig_gossip::{Bytes, Message, ProtocolMessageTypes, RequestPeers}; +use dig_gossip::{Bytes, DigMessage, ProtocolMessageTypes, RequestPeers}; use dig_gossip::{aggregate_peer_connection_io, message_wire_len, metric_unix_timestamp_secs}; @@ -47,18 +47,18 @@ async fn test_metrics_initialization() { } /// **Row:** `test_bytes_written_increment` — three synthetic sends with known wire sizes -/// (CON-006 §Update on Message Send). +/// (CON-006 §Update on DigMessage Send). #[tokio::test] async fn test_bytes_written_increment() { let mut pc = common::mock_peer_connection(true).await; - let m = |payload: &[u8]| Message { - msg_type: ProtocolMessageTypes::Handshake, + let m = |payload: &[u8]| DigMessage { + msg_type: ProtocolMessageTypes::Handshake as u8, id: None, data: Bytes::new(payload.to_vec()), }; - let w1 = message_wire_len(&m(&[1, 2, 3])).expect("wire len"); - let w2 = message_wire_len(&m(&[4; 50])).expect("wire len"); - let w3 = message_wire_len(&m(&[])).expect("wire len"); + let w1 = message_wire_len(&m(&[1, 2, 3])); + let w2 = message_wire_len(&m(&[4; 50])); + let w3 = message_wire_len(&m(&[])); pc.record_message_sent(w1); pc.record_message_sent(w2); pc.record_message_sent(w3); @@ -71,12 +71,12 @@ async fn test_bytes_written_increment() { async fn test_messages_sent_increment() { let mut pc = common::mock_peer_connection(true).await; for i in 0u8..5 { - let msg = Message { - msg_type: ProtocolMessageTypes::Handshake, + let msg = DigMessage { + msg_type: ProtocolMessageTypes::Handshake as u8, id: None, data: Bytes::new(vec![i]), }; - let w = message_wire_len(&msg).expect("wire len"); + let w = message_wire_len(&msg); pc.record_message_sent(w); } assert_eq!(pc.messages_sent, 5); @@ -87,12 +87,12 @@ async fn test_messages_sent_increment() { async fn test_bytes_read_increment() { let mut pc = common::mock_peer_connection(false).await; let now = metric_unix_timestamp_secs(); - let m = Message { - msg_type: ProtocolMessageTypes::RequestPeers, + let m = DigMessage { + msg_type: ProtocolMessageTypes::RequestPeers as u8, id: None, data: RequestPeers::new().to_bytes().unwrap().into(), }; - let w = message_wire_len(&m).expect("wire len"); + let w = message_wire_len(&m); pc.record_message_received(w, now); pc.record_message_received(w, now); pc.record_message_received(w, now); diff --git a/tests/con_008_tests.rs b/tests/con_008_tests.rs index daacb19..33c71a0 100644 --- a/tests/con_008_tests.rs +++ b/tests/con_008_tests.rs @@ -29,7 +29,6 @@ mod common; use dig_gossip::connection::handshake::{sanitize_software_version, validate_remote_handshake}; use dig_gossip::Handshake; -use dig_gossip::NodeType; use unicode_general_category::{get_general_category, GeneralCategory}; /// Baseline remote [`Handshake`] for CON-008 policy tests: only `software_version` is malicious. @@ -39,7 +38,7 @@ fn handshake_with_version(network_id: &str, software_version: String) -> Handsha protocol_version: "0.0.37".to_string(), software_version, server_port: 8444, - node_type: NodeType::FullNode, + node_type: chia_protocol::NodeType::FullNode, capabilities: vec![], } } diff --git a/tests/con_2767_keepalive_correlation_tests.rs b/tests/con_2767_keepalive_correlation_tests.rs new file mode 100644 index 0000000..22c7600 --- /dev/null +++ b/tests/con_2767_keepalive_correlation_tests.rs @@ -0,0 +1,169 @@ +//! **#2767** — two connected dig-gossip peers must not tear each other's link down. +//! +//! ## The defect +//! +//! `DigLink` matches an inbound frame on correlation id *before* forwarding it to the application. +//! Both peers allocate correlation ids from a counter that starts at zero, and both keepalive loops +//! start at handshake on a shared interval — so two probes can carry the **same id**. Each side's +//! waiter then receives the peer's `RequestPeers` instead of a `RespondPeers`; the peer's request +//! never reaches the forwarder, its auto-reply never fires, neither side records a success, and both +//! disconnect at the staleness check while logging a timeout that names the wrong cause. +//! +//! ## Why these tests are shaped this way +//! +//! - **The teardown is silent and slow.** It surfaces as *"no successful probe within +//! PEER_TIMEOUT_SECS"*, which is the wrong cause — so the assertion is **link survival across +//! several probe intervals**, never a log line. +//! - **The collision must be forced, not hoped for.** An outbound dial burns correlation id 0 on +//! `RequestPeers` (DSC-007) before its keepalive starts, leaving the two counters permanently +//! offset by one. [`connect_and_align`] burns the matching id on the *inbound* side so +//! both loops probe from the same id — the state that a mutual dial, or any application +//! `request()` on one side, reaches on its own in production. +//! - **Real `tokio::time`.** This is real loopback I/O; a paused clock would not exercise it. +//! +//! Traceability: CON-004 (keepalive), SPEC §2.13, §5.1 step 7. + +mod common; + +use std::time::Duration; + +use dig_gossip::{GossipHandle, GossipService, LinkOptions, PeerId}; + +/// Probe interval. Production is `PING_INTERVAL_SECS` (30); 1s keeps the test near its lower bound. +const PING_SECS: u64 = 1; +/// Staleness window. Production is `PEER_TIMEOUT_SECS` (90); 3s means the buggy build tears the +/// link down well inside [`OBSERVE_SECS`]. +const TIMEOUT_SECS: u64 = 3; +/// How long the link is observed. Six probe intervals — two full staleness windows — so a build +/// that never records a success cannot survive by luck. +const OBSERVE_SECS: u64 = 6; + +/// Build a started service whose keepalive runs on the fast test timings. +async fn service(dir: &tempfile::TempDir) -> (GossipService, GossipHandle) { + let _ = common::generate_test_certs(dir.path()); + let mut cfg = common::test_gossip_config(dir.path()); + cfg.keepalive_ping_interval_secs = Some(PING_SECS); + cfg.keepalive_peer_timeout_secs = Some(TIMEOUT_SECS); + // `RequestPeers` is capped by `V2_RATE_LIMITS` (~6/min at the default factor); sub-second + // probes need headroom or keepalive throttles instead of probing. + let mut link_options = LinkOptions::default(); + link_options.rate_limit_factor = 20.0; + cfg.peer_options = link_options; + let svc = GossipService::new(cfg).expect("new service"); + let handle = svc.start().await.expect("start service"); + (svc, handle) +} + +/// Connect `a -> b` and leave both keepalive loops probing from the SAME correlation id. +/// +/// The dialer burns id 0 on its DSC-007 `RequestPeers`; issuing one application request from the +/// accepting side burns its id 0 too. Without this the counters stay offset and the collision — the +/// whole subject of this file — never occurs. +async fn connect_and_align( + a: &GossipHandle, + b: &GossipHandle, + b_addr: std::net::SocketAddr, +) -> (PeerId, PeerId) { + let b_on_a = a.connect_to(b_addr).await.expect("A dials B"); + + // The inbound slot lands as `negotiate_inbound_over_ws` finishes; poll rather than guess, and + // stay well inside the first probe interval. + let a_on_b = tokio::time::timeout(Duration::from_millis(500), async { + loop { + if let Some(id) = b.__peer_ids_for_tests().first().copied() { + return id; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("B registers the inbound peer before the first probe"); + + b.request_peers_from(&a_on_b) + .await + .expect("the align request must be answered"); + + (b_on_a, a_on_b) +} + +/// **The regression.** Two live peers whose keepalive loops probe from identical correlation ids +/// must both still be connected after six probe intervals, with no reconnect in between. +/// +/// Before the fix both loops steal each other's probe, never record a success, and disconnect at +/// the staleness check inside three seconds. +#[tokio::test] +async fn colliding_correlation_ids_do_not_tear_the_link_down() { + let dir_b = common::test_temp_dir(); + let (_svc_b, h_b) = service(&dir_b).await; + let bound = h_b.__listen_bound_addr_for_tests().expect("B listening"); + + let dir_a = common::test_temp_dir(); + let (_svc_a, h_a) = service(&dir_a).await; + + let (b_on_a, a_on_b) = connect_and_align(&h_a, &h_b, bound).await; + + tokio::time::sleep(Duration::from_secs(OBSERVE_SECS)).await; + + assert!( + h_a.__peer_ids_for_tests().contains(&b_on_a), + "A must still hold B after {OBSERVE_SECS}s of probing" + ); + assert!( + h_b.__peer_ids_for_tests().contains(&a_on_b), + "B must still hold A after {OBSERVE_SECS}s of probing" + ); + assert_eq!( + h_a.stats().await.total_connections, + 1, + "the surviving link must be the ORIGINAL one — a reconnect would also leave a peer present" + ); + assert_eq!( + h_b.stats().await.total_connections, + 1, + "same, from B's side" + ); + + // The probes must have been observed, not merely survived: a build that stopped probing + // altogether would also pass the assertions above. + let rtt = h_a + .__con004_peer_reputation_for_tests(b_on_a) + .expect("A's live slot") + .rtt_history; + assert!( + rtt.len() >= 2, + "A must have recorded successful probes, got {rtt:?}" + ); +} + +/// **Fail open.** The reply is observed on the service-wide inbound broadcast, which does not exist +/// while the service is starting or stopping. A probe we cannot observe is not evidence the peer is +/// dead, and the only action this loop can take is to disconnect — so the round is skipped and the +/// peer is kept. +#[tokio::test] +async fn an_unobservable_probe_does_not_disconnect_the_peer() { + let dir_b = common::test_temp_dir(); + let (_svc_b, h_b) = service(&dir_b).await; + let bound = h_b.__listen_bound_addr_for_tests().expect("B listening"); + + let dir_a = common::test_temp_dir(); + let (_svc_a, h_a) = service(&dir_a).await; + + let b_on_a = h_a.connect_to(bound).await.expect("A dials B"); + + // Remove A's inbound broadcast: A can still send, but can no longer observe any reply. + { + let state = h_a.__state_arc_for_tests(); + *state.inbound_tx.lock().expect("inbound_tx mutex") = None; + } + + tokio::time::sleep(Duration::from_secs(OBSERVE_SECS)).await; + + assert!( + h_a.__peer_ids_for_tests().contains(&b_on_a), + "an unobservable probe must keep the peer, not tear it down" + ); + assert!( + h_a.__con004_penalty_points_for_tests(b_on_a).unwrap_or(0) == 0, + "no keepalive-failure penalty may be charged for a probe we could not observe" + ); +} diff --git a/tests/dmsg_001_tests.rs b/tests/dmsg_001_tests.rs index 7e46834..a51fbec 100644 --- a/tests/dmsg_001_tests.rs +++ b/tests/dmsg_001_tests.rs @@ -56,7 +56,7 @@ fn stub_addr(port: u16) -> SocketAddr { #[test] fn dig_message_opcode_is_220() { assert_eq!(DIG_MESSAGE, 220); - assert_eq!(DIG_MESSAGE, ProtocolMessageTypes::DigMessage as u8); + assert_eq!(DIG_MESSAGE, dig_peer_protocol::DIG_MESSAGE); } /// **Row:** `is_dig_message` recognises 220 and only 220. @@ -78,8 +78,8 @@ fn payload_extracted_only_from_opcode_220_frame() { assert_eq!(dig_message_payload(&msg), Some(envelope.as_slice())); let z = Bytes32::default(); - let not_dig = dig_gossip::Message { - msg_type: ProtocolMessageTypes::NewPeak, + let not_dig = dig_gossip::DigMessage { + msg_type: ProtocolMessageTypes::NewPeak as u8, id: None, data: NewPeak::new(z, 1, 1, 0, z).to_bytes().unwrap().into(), }; @@ -90,7 +90,7 @@ fn payload_extracted_only_from_opcode_220_frame() { #[test] fn dig_message_is_classified_unicast() { assert_eq!( - classify_broadcast(ProtocolMessageTypes::DigMessage, false), + classify_broadcast(dig_peer_protocol::DIG_MESSAGE, false), BroadcastStrategy::Unicast ); } diff --git a/tests/dsc_004_tests.rs b/tests/dsc_004_tests.rs index b098119..4362d7a 100644 --- a/tests/dsc_004_tests.rs +++ b/tests/dsc_004_tests.rs @@ -23,8 +23,8 @@ //! - `test_query_introducer_timeout` — [`tokio::time::timeout`] inside [`IntroducerClient::query_peers`] //! must surface [`GossipError::IntroducerError`] when the server stalls after receiving the request; //! otherwise discovery could hang forever (violates DSC-004 acceptance). -//! - `test_query_introducer_handshake_wrong_network` — handshake validation mirrors -//! [`chia_sdk_client::connect_peer`]; a spoofed `network_id` in the server [`Handshake`] must abort +//! - `test_query_introducer_handshake_wrong_network` — handshake validation mirrors standard outbound +//! connection semantics; a spoofed `network_id` in the server [`Handshake`] must abort //! before any introducer RPC is sent. mod common; @@ -33,13 +33,13 @@ use std::time::Duration; use dig_gossip::{ load_ssl_cert, ChiaCertificate, ChiaProtocolMessage, GossipError, IntroducerClient, - PeerOptions, ProtocolMessageTypes, RequestPeersIntroducer, RespondPeersIntroducer, + LinkOptions, ProtocolMessageTypes, RequestPeersIntroducer, RespondPeersIntroducer, TimestampedPeerInfo, }; /// **Row:** `test_introducer_wire_message_types` — wire structs map to protocol IDs **63** / **64**. /// -/// **Proof:** [`ChiaProtocolMessage::msg_type`] is what [`Peer::request_infallible`](dig_gossip::Peer::request_infallible) +/// **Proof:** [`ChiaProtocolMessage::msg_type`] is what [`Peer::request_infallible`](dig_gossip::DigLink::request_infallible) /// uses to build outbound frames; a typo here would send the wrong opcode while still “compiling”. #[test] fn test_introducer_wire_message_types() { @@ -82,7 +82,7 @@ async fn test_query_introducer_success() { &uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_secs(10), common::TEST_SOFTWARE_VERSION, ) @@ -116,7 +116,7 @@ async fn test_query_introducer_empty_list() { &uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_secs(10), common::TEST_SOFTWARE_VERSION, ) @@ -148,7 +148,7 @@ async fn test_query_introducer_timeout() { &uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_millis(400), common::TEST_SOFTWARE_VERSION, ) @@ -180,14 +180,17 @@ async fn test_query_introducer_connect_fail() { uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_secs(2), common::TEST_SOFTWARE_VERSION, ) .await .expect_err("nothing listens on :7"); match err { - GossipError::ClientError(_) | GossipError::IoError(_) => {} + // The dial is DigLink's now, so a refused TCP connect surfaces as LinkError. + // ClientError is retained for handshake-POLICY failures, which is a different + // failure than never reaching the wire (dig_ecosystem#2228). + GossipError::LinkError(_) | GossipError::ClientError(_) | GossipError::IoError(_) => {} GossipError::IntroducerError(msg) if msg.contains("timed out") => {} other => panic!("unexpected err: {other:?}"), } @@ -216,7 +219,7 @@ async fn test_query_introducer_handshake_wrong_network() { &uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_secs(5), common::TEST_SOFTWARE_VERSION, ) diff --git a/tests/dsc_005_tests.rs b/tests/dsc_005_tests.rs index 3992cea..b4fa15d 100644 --- a/tests/dsc_005_tests.rs +++ b/tests/dsc_005_tests.rs @@ -12,13 +12,13 @@ //! DSC-005 defines a **DIG-only** introducer extension: after the standard Chia [`Handshake`](dig_gossip::Handshake), //! the client sends [`RegisterPeer`](dig_gossip::RegisterPeer) and must receive [`RegisterAck`](dig_gossip::RegisterAck) //! on the same mutually-authenticated WSS session. Unlike DSC-004 (opcodes **63/64** already present in upstream -//! [`ProtocolMessageTypes`](dig_gossip::ProtocolMessageTypes)), **218/219** require the vendored `chia-protocol` fork -//! so [`Message::from_bytes`](dig_gossip::Message::from_bytes) can decode replies — see `vendor/chia-protocol/README.dig-gossip.md`. +//! [`ProtocolMessageTypes`](dig_gossip::ProtocolMessageTypes)), **218/219** are DIG-specific extensions +//! so [`Message::from_bytes`](dig_gossip::DigMessage::from_bytes) can decode the introducer replies. //! //! ## Causal chain (examples) //! //! - `test_register_introducer_wire_message_types` — if [`ChiaProtocolMessage::msg_type`] drifted from the enum -//! discriminants patched into `vendor/chia-protocol`, [`Peer::request_infallible`](dig_gossip::Peer::request_infallible) +//! discriminants for opcodes **218/219**, [`DigLink::request_infallible`](dig_gossip::DigLink::request_infallible) //! would serialize the wrong opcode and the mock introducer would reject the frame. //! - `test_register_introducer_success` — end-to-end TLS + handshake + RPC proves [`IntroducerClient::register_with_introducer`] //! matches the acceptance table row *test_register_success*. @@ -31,26 +31,40 @@ use std::time::Duration; use dig_gossip::Streamable; use dig_gossip::{ - load_ssl_cert, ChiaCertificate, ChiaProtocolMessage, GossipError, IntroducerClient, NodeType, - PeerOptions, PeerRegistration, ProtocolMessageTypes, RegisterAck, RegisterPeer, + load_ssl_cert, ChiaCertificate, GossipError, IntroducerClient, LinkOptions, NodeType, + PeerRegistration, RegisterAck, RegisterPeer, }; /// **Row:** `test_register_introducer_wire_message_types` — wire structs bind to **218** / **219**. /// -/// **Proof:** [`Peer::request_infallible`] compares inbound [`Message::msg_type`](dig_gossip::Message) to -/// [`RegisterAck::msg_type`]; a mismatch surfaces [`ClientError::InvalidResponse`](dig_gossip::ClientError) and would -/// fail integration tests even if payloads accidentally round-tripped. +/// **Proof:** `RegisterAck::from_dig_message` returns `None` unless the inbound opcode is 219, +/// so a mismatch surfaces as `LinkError::InvalidResponse` and would fail integration tests even +/// if payloads accidentally round-tripped. #[test] fn test_register_introducer_wire_message_types() { - assert_eq!(RegisterPeer::msg_type(), ProtocolMessageTypes::RegisterPeer); - assert_eq!(RegisterAck::msg_type(), ProtocolMessageTypes::RegisterAck); + // 218/219 are carried as raw DigMessage opcodes; there is no ProtocolMessageTypes + // variant to name them, which is why the chia-protocol fork could be deleted. + assert_eq!( + RegisterPeer::new("192.0.2.88".into(), 9555, NodeType::FullNode) + .to_dig_message(None) + .expect("encodes") + .msg_type, + dig_gossip::DigMessageType::RegisterPeer as u8 + ); + assert_eq!( + RegisterAck::new(true) + .to_dig_message(None) + .expect("encodes") + .msg_type, + dig_gossip::DigMessageType::RegisterAck as u8 + ); } /// **Row:** `test_register_message_type_in_dig_band` — opcodes stay in the documented DIG extension range (≥200). #[test] fn test_register_message_type_in_dig_band() { - assert!(u32::from(RegisterPeer::msg_type() as u8) >= 200); - assert!(u32::from(RegisterAck::msg_type() as u8) >= 200); + assert!(u32::from(dig_gossip::DigMessageType::RegisterPeer as u8) >= 200); + assert!(u32::from(dig_gossip::DigMessageType::RegisterAck as u8) >= 200); } /// **Row:** `test_register_peer_payload_roundtrip` — [`Streamable`] body encoding matches the mock server’s [`RegisterPeer::from_bytes`]. @@ -79,7 +93,7 @@ async fn test_register_introducer_success() { let reg = PeerRegistration { ip: "192.0.2.5".into(), port: 9555, - node_type: NodeType::FullNode, + node_type: dig_gossip::NodeType::FullNode, }; let (addr, jh) = common::wss_full_node::spawn_one_shot_introducer_register( server_cert, @@ -95,7 +109,7 @@ async fn test_register_introducer_success() { &uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_secs(10), ®, common::TEST_SOFTWARE_VERSION, @@ -118,7 +132,7 @@ async fn test_register_introducer_rejected() { let reg = PeerRegistration { ip: "192.0.2.6".into(), port: 9556, - node_type: NodeType::FullNode, + node_type: dig_gossip::NodeType::FullNode, }; let (addr, jh) = common::wss_full_node::spawn_one_shot_introducer_register( server_cert, @@ -134,7 +148,7 @@ async fn test_register_introducer_rejected() { &uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_secs(10), ®, common::TEST_SOFTWARE_VERSION, @@ -157,7 +171,7 @@ async fn test_register_introducer_timeout() { let reg = PeerRegistration { ip: "192.0.2.7".into(), port: 9557, - node_type: NodeType::FullNode, + node_type: dig_gossip::NodeType::FullNode, }; let (addr, jh) = common::wss_full_node::spawn_one_shot_introducer_register( server_cert, @@ -173,7 +187,7 @@ async fn test_register_introducer_timeout() { &uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_millis(500), ®, common::TEST_SOFTWARE_VERSION, @@ -199,21 +213,25 @@ async fn test_register_introducer_connect_fail() { let reg = PeerRegistration { ip: "192.0.2.1".into(), port: 9444, - node_type: NodeType::FullNode, + node_type: dig_gossip::NodeType::FullNode, }; let err = IntroducerClient::register_with_introducer( uri, &cert, common::test_network_id(), - PeerOptions::default(), + LinkOptions::default(), Duration::from_secs(2), ®, common::TEST_SOFTWARE_VERSION, ) .await - .expect_err("connect should fail"); + .expect_err("nothing listens on :7"); match err { - GossipError::ClientError(_) | GossipError::IntroducerError(_) => {} + // The dial is DigLink's now, so a refused TCP connect surfaces as LinkError. + // ClientError is retained for handshake-POLICY failures, which is a different + // failure than never reaching the wire (dig_ecosystem#2228). + GossipError::LinkError(_) | GossipError::ClientError(_) | GossipError::IoError(_) => {} + GossipError::IntroducerError(msg) if msg.contains("timed out") => {} other => panic!("unexpected error: {other:?}"), } } diff --git a/tests/int_002_tests.rs b/tests/int_002_tests.rs index dff6265..988eb8e 100644 --- a/tests/int_002_tests.rs +++ b/tests/int_002_tests.rs @@ -56,23 +56,23 @@ fn test_message_priority_classification_dig() { /// **INT-002: PriorityOutbound drain order follows PRI-003 (critical > normal > bulk).** #[test] fn test_priority_outbound_drain_order() { - use dig_gossip::{Bytes, Message, ProtocolMessageTypes}; + use dig_gossip::{Bytes, DigMessage, ProtocolMessageTypes}; let mut q = PriorityOutbound::new(); // Enqueue one of each priority in reverse order - let bulk_msg = Message { - msg_type: ProtocolMessageTypes::RequestBlocks, + let bulk_msg = DigMessage { + msg_type: ProtocolMessageTypes::RequestBlocks as u8, id: None, data: Bytes::from(vec![3u8]), }; - let normal_msg = Message { - msg_type: ProtocolMessageTypes::NewTransaction, + let normal_msg = DigMessage { + msg_type: ProtocolMessageTypes::NewTransaction as u8, id: None, data: Bytes::from(vec![2u8]), }; - let critical_msg = Message { - msg_type: ProtocolMessageTypes::NewPeak, + let critical_msg = DigMessage { + msg_type: ProtocolMessageTypes::NewPeak as u8, id: None, data: Bytes::from(vec![1u8]), }; @@ -102,11 +102,11 @@ fn test_priority_outbound_drain_order() { /// **INT-002: lane_lengths returns correct per-lane counts.** #[test] fn test_priority_outbound_lane_lengths() { - use dig_gossip::{Bytes, Message, ProtocolMessageTypes}; + use dig_gossip::{Bytes, DigMessage, ProtocolMessageTypes}; let mut q = PriorityOutbound::new(); - let msg = || Message { - msg_type: ProtocolMessageTypes::NewPeak, + let msg = || DigMessage { + msg_type: ProtocolMessageTypes::NewPeak as u8, id: None, data: Bytes::from(vec![0u8]), }; diff --git a/tests/int_013_tests.rs b/tests/int_013_tests.rs index 9756409..b10a087 100644 --- a/tests/int_013_tests.rs +++ b/tests/int_013_tests.rs @@ -51,10 +51,10 @@ fn test_discovery_types_importable() { #[test] fn test_chia_types_importable() { let _ = std::any::type_name::(); - let _ = std::any::type_name::(); + let _ = std::any::type_name::(); let _ = std::any::type_name::(); let _ = std::any::type_name::(); - let _ = std::any::type_name::(); + let _ = std::any::type_name::(); let _ = std::any::type_name::(); } diff --git a/tests/int_014_tests.rs b/tests/int_014_tests.rs index a868abc..f4d0dc6 100644 --- a/tests/int_014_tests.rs +++ b/tests/int_014_tests.rs @@ -27,7 +27,7 @@ fn test_lifecycle_types_exist() { #[test] fn test_io_contract_types() { let _ = std::any::type_name::(); - let _ = std::any::type_name::(); + let _ = std::any::type_name::(); let _ = std::any::type_name::(); let _ = std::any::type_name::(); } diff --git a/tests/int_015_tests.rs b/tests/int_015_tests.rs index 85a12b7..07f23ed 100644 --- a/tests/int_015_tests.rs +++ b/tests/int_015_tests.rs @@ -77,13 +77,13 @@ async fn test_full_lifecycle() { #[cfg(feature = "native-tls")] #[tokio::test] async fn test_broadcast_no_peers() { - use dig_gossip::{GossipService, Message, ProtocolMessageTypes}; + use dig_gossip::{DigMessage, GossipService, ProtocolMessageTypes}; let service = GossipService::new(lifecycle_config()).unwrap(); let handle = service.start().await.unwrap(); - let msg = Message { - msg_type: ProtocolMessageTypes::NewPeak, + let msg = DigMessage { + msg_type: ProtocolMessageTypes::NewPeak as u8, id: None, data: vec![1, 2, 3].into(), }; diff --git a/tests/pri_002_tests.rs b/tests/pri_002_tests.rs index e6f9eef..f7a5abc 100644 --- a/tests/pri_002_tests.rs +++ b/tests/pri_002_tests.rs @@ -6,11 +6,11 @@ //! - **Master SPEC:** `docs/resources/SPEC.md` SS8.4 use dig_gossip::gossip::priority::{MessagePriority, PriorityOutbound}; -use dig_gossip::{Message, ProtocolMessageTypes}; +use dig_gossip::{DigMessage, ProtocolMessageTypes}; -fn make_msg(msg_type: ProtocolMessageTypes) -> Message { - Message { - msg_type, +fn make_msg(msg_type: ProtocolMessageTypes) -> DigMessage { + DigMessage { + msg_type: msg_type as u8, id: None, data: vec![].into(), } diff --git a/tests/pri_003_tests.rs b/tests/pri_003_tests.rs index 83686a4..f8c0044 100644 --- a/tests/pri_003_tests.rs +++ b/tests/pri_003_tests.rs @@ -6,11 +6,11 @@ //! - **Master SPEC:** `docs/resources/SPEC.md` SS8.4 use dig_gossip::gossip::priority::{MessagePriority, PriorityOutbound}; -use dig_gossip::{Message, ProtocolMessageTypes}; +use dig_gossip::{DigMessage, ProtocolMessageTypes}; -fn make_msg(msg_type: ProtocolMessageTypes) -> Message { - Message { - msg_type, +fn make_msg(msg_type: ProtocolMessageTypes) -> DigMessage { + DigMessage { + msg_type: msg_type as u8, id: None, data: vec![].into(), } @@ -36,15 +36,15 @@ fn test_drain_order() { // Critical first let m1 = q.drain_next().unwrap(); - assert_eq!(m1.msg_type, ProtocolMessageTypes::NewPeak); + assert_eq!(m1.msg_type, ProtocolMessageTypes::NewPeak as u8); // Normal second let m2 = q.drain_next().unwrap(); - assert_eq!(m2.msg_type, ProtocolMessageTypes::NewTransaction); + assert_eq!(m2.msg_type, ProtocolMessageTypes::NewTransaction as u8); // Bulk last let m3 = q.drain_next().unwrap(); - assert_eq!(m3.msg_type, ProtocolMessageTypes::RequestBlocks); + assert_eq!(m3.msg_type, ProtocolMessageTypes::RequestBlocks as u8); assert!(q.is_empty()); } diff --git a/tests/pri_004_tests.rs b/tests/pri_004_tests.rs index 9f9920a..50416b4 100644 --- a/tests/pri_004_tests.rs +++ b/tests/pri_004_tests.rs @@ -7,11 +7,11 @@ use dig_gossip::gossip::priority::{MessagePriority, PriorityOutbound}; use dig_gossip::PRIORITY_STARVATION_RATIO; -use dig_gossip::{Message, ProtocolMessageTypes}; +use dig_gossip::{DigMessage, ProtocolMessageTypes}; -fn make_msg(msg_type: ProtocolMessageTypes) -> Message { - Message { - msg_type, +fn make_msg(msg_type: ProtocolMessageTypes) -> DigMessage { + DigMessage { + msg_type: msg_type as u8, id: None, data: vec![].into(), } @@ -37,14 +37,14 @@ fn test_starvation_prevention() { // Drain RATIO critical messages for _ in 0..PRIORITY_STARVATION_RATIO { let m = q.drain_next().unwrap(); - assert_eq!(m.msg_type, ProtocolMessageTypes::NewPeak); + assert_eq!(m.msg_type, ProtocolMessageTypes::NewPeak as u8); } // Next should be forced bulk (starvation prevention) let m = q.drain_next().unwrap(); assert_eq!( m.msg_type, - ProtocolMessageTypes::RequestBlocks, + ProtocolMessageTypes::RequestBlocks as u8, "after {} critical msgs, bulk must be forced", PRIORITY_STARVATION_RATIO ); diff --git a/tests/str_003_tests.rs b/tests/str_003_tests.rs index 8e904e1..a685330 100644 --- a/tests/str_003_tests.rs +++ b/tests/str_003_tests.rs @@ -47,12 +47,12 @@ fn test_reexport_handshake() { assert_send_sync::(); } -/// **Acceptance:** `Message` (Chia wire frame) is re-exported and `Send + Sync`. +/// **Acceptance:** `DigMessage` (Chia wire frame) is re-exported and `Send + Sync`. /// -/// `Message` wraps `msg_type + id + data` for every on-wire protocol frame. +/// `DigMessage` wraps `msg_type + id + data` for every on-wire protocol frame. #[test] fn test_reexport_message() { - assert_send_sync::(); + assert_send_sync::(); } /// **Acceptance:** `NodeType` (Chia enum: FullNode, Wallet, etc.) is re-exported. @@ -66,7 +66,7 @@ fn test_reexport_node_type() { /// **Acceptance:** `ProtocolMessageTypes` (Chia opcode enum) is re-exported. /// /// This enum carries discriminants for all Chia wire messages (Handshake, RequestPeers, etc.) -/// and is needed by any code that inspects `Message::msg_type`. +/// and is needed by any code that inspects `DigMessage::msg_type`. #[test] fn test_reexport_protocol_message_types() { assert_send_sync::(); @@ -84,8 +84,9 @@ fn test_introducer_ops_are_protocol_message_variants() { use dig_gossip::ProtocolMessageTypes as M; let _ = M::RequestPeersIntroducer; let _ = M::RespondPeersIntroducer; - let _ = M::RegisterPeer; - let _ = M::RegisterAck; + // 218/219 are DIG opcodes: they exist as DigMessageType, not as Chia enum variants. + let _ = dig_gossip::DigMessageType::RegisterPeer; + let _ = dig_gossip::DigMessageType::RegisterAck; let _: dig_gossip::RequestPeersIntroducer = dig_gossip::RequestPeersIntroducer::new(); let _: dig_gossip::RegisterPeer = dig_gossip::RegisterPeer::new("127.0.0.1".into(), 9444, dig_gossip::NodeType::FullNode); @@ -93,11 +94,11 @@ fn test_introducer_ops_are_protocol_message_variants() { /// **Acceptance:** `Peer` (chia-sdk-client WebSocket handle) is re-exported. /// -/// `Peer` is the runtime handle for sending/receiving `Message` frames over a +/// `Peer` is the runtime handle for sending/receiving `DigMessage` frames over a /// WebSocket connection. CON-001 and API-005 depend on it. #[test] fn test_reexport_peer() { - assert_send_sync::(); + assert_send_sync::(); } /// **Acceptance:** `RateLimiter` (Chia rate-limit enforcement) is re-exported. @@ -105,7 +106,7 @@ fn test_reexport_peer() { /// Used internally for per-peer message throttling per V2 rate limit tables. #[test] fn test_reexport_rate_limiter() { - assert_send_sync::(); + assert_send_sync::(); } /// **Acceptance:** `V2_RATE_LIMITS` static is re-exported. @@ -224,16 +225,16 @@ fn test_full_import_set() { dig_extension_rate_limits_map, load_ssl_cert, message_wire_len, metric_unix_timestamp_secs, new_inbound_rate_limiter, peer_id_for_addr, peer_id_from_tls_spki_der, AddressManager, BackpressureConfig, Bytes32, ChiaCertificate, ChiaProtocolMessage, Client, ClientError, - ClientState, DigMessageType, ExtendedPeerInfo, FullBlock, GossipConfig, GossipError, - GossipHandle, GossipService, GossipStats, Handshake, IntroducerClient, IntroducerConfig, - IntroducerPeers, Message, Network, NewPeak, NewTransaction, NewUnfinishedBlock, NodeType, - Peer, PeerConnection, PeerConnectionWireMetrics, PeerId, PeerIdRotationConfig, PeerInfo, - PeerOptions, PeerReputation, PenaltyReason, ProtocolMessageTypes, RateLimit, RateLimiter, - RateLimits, RelayConfig, RelayStats, RequestBlock, RequestBlocks, - RequestMempoolTransactions, RequestPeers, RequestTransaction, RequestUnfinishedBlock, - RespondBlock, RespondBlocks, RespondPeers, RespondTransaction, RespondUnfinishedBlock, - ServiceState, SpendBundle, Streamable, TimestampedPeerInfo, UnknownDigMessageType, - VettedPeer, DEFAULT_INTRODUCER_NETWORK_ID, V2_RATE_LIMITS, + ClientState, DigLink, DigMessage, DigMessageType, ExtendedPeerInfo, FullBlock, + GossipConfig, GossipError, GossipHandle, GossipService, GossipStats, Handshake, + IntroducerClient, IntroducerConfig, IntroducerPeers, LinkOptions, Network, NewPeak, + NewTransaction, NewUnfinishedBlock, NodeType, OpcodeRateLimiter, PeerConnection, + PeerConnectionWireMetrics, PeerId, PeerIdRotationConfig, PeerInfo, PeerReputation, + PenaltyReason, ProtocolMessageTypes, RateLimit, RateLimits, RelayConfig, RelayStats, + RequestBlock, RequestBlocks, RequestMempoolTransactions, RequestPeers, RequestTransaction, + RequestUnfinishedBlock, RespondBlock, RespondBlocks, RespondPeers, RespondTransaction, + RespondUnfinishedBlock, ServiceState, SpendBundle, Streamable, TimestampedPeerInfo, + UnknownDigMessageType, VettedPeer, DEFAULT_INTRODUCER_NETWORK_ID, V2_RATE_LIMITS, }; } diff --git a/tests/str_005_tests.rs b/tests/str_005_tests.rs index a90a6ae..ec6348d 100644 --- a/tests/str_005_tests.rs +++ b/tests/str_005_tests.rs @@ -145,7 +145,7 @@ fn test_generate_certs_valid() { /// **Acceptance:** [`common::connected_test_pair`] composes harness pieces and returns distinct bind addresses. /// -/// SPEC §11.2 — integration tests: connect two nodes using connect_peer(), verify handshake. +/// SPEC §11.2 — integration tests: establish outbound connection via TLS + handshake, verify connection state. /// /// **Deferred detail:** “each handle reports one connected peer” needs API-002 + CON-001; see /// `common` module docs and the ignored tests below. diff --git a/tests/wire_golden_vectors_tests.rs b/tests/wire_golden_vectors_tests.rs new file mode 100644 index 0000000..41f78b6 --- /dev/null +++ b/tests/wire_golden_vectors_tests.rs @@ -0,0 +1,339 @@ +//! Golden wire vectors — the byte-level contract dig-gossip must never break. +//! +//! # Why these exist +//! +//! dig-gossip is a **live peer network**, and its wire encoding is vendored +//! byte-identical into **dig-relay** (GPL-2.0). Any refactor of the framing code — +//! notably the migration off the vendored `chia-protocol` fork onto +//! `dig_peer_protocol`'s native types (dig_ecosystem#2228) — must leave every +//! encoded frame bit-for-bit unchanged. +//! +//! These vectors are the **instrument** for that claim, not a feature gate. They +//! pin literal hex captured from the encoder as it stands *before* any such +//! refactor, so a later change that alters the bytes fails here instead of +//! silently partitioning the network. +//! +//! # How to read a failure +//! +//! A diff here is never "update the expectation". It means the on-wire format +//! moved, which is a **coordinated network change** plus a matching dig-relay +//! update — not a refactor. Stop and escalate. +//! +//! # Both directions, one set of literals +//! +//! Every vector is a `const` hex string asserted in **both** directions: the +//! encoder must emit it, and [`DigMessage::from_bytes`] must recover the original +//! fields from it. Encode-only vectors would prove the wrong half — the vendored +//! fork existed precisely because `Message::from_bytes` *rejected* DIG opcodes, and +//! dig-relay encodes frames this crate has to decode. Sharing one literal between +//! the two directions is deliberate: two independent literals could drift into +//! agreeing with each other and with neither peer. +//! +//! # Fixture design +//! +//! Each vector is chosen to distinguish the real encoding from the nearest wrong +//! one, rather than merely to exercise the code path: +//! +//! - **Both `id` states.** `Message.id` is `Option`, encoded as a one-byte +//! presence flag plus a big-endian `u16` when present. A vector with only +//! `None` cannot see a lost or byte-swapped correlation id, so every opcode is +//! pinned in both states where it is reachable. +//! - **A payload longer than one byte, with distinguishable ends.** The `data` +//! field carries a `u32` big-endian length prefix; a one-byte or palindromic +//! payload would hide both a wrong prefix width and a reversed body. +//! - **A `node_type` that is not the default.** `NodeType::FullNode` is +//! discriminant `1`, which is indistinguishable from a bool-ish or +//! off-by-one encoding. `Introducer = 5` is pinned alongside it so a collapsed +//! discriminant mapping is visible. +//! - **Both `RegisterAck` outcomes.** `success == false` is a valid wire result +//! (policy rejection), so pinning only `true` would miss an inverted flag. +//! - **An opcode no `ProtocolMessageTypes` variant can represent.** Accepting DIG +//! opcodes is the entire reason `DigMessage` keys on a raw `u8`; without a frame +//! the forked enum genuinely rejects, nothing here separates "decodes DIG +//! opcodes" from "has not happened to reject one yet". + +use chia_traits::Streamable; +use dig_gossip::{ + frame_dig_message, frame_envelope, DigMessageType, NodeType, ProtocolMessageTypes, RegisterAck, + RegisterPeer, +}; +use dig_peer_protocol::{DigMessage, DIG_MESSAGE}; + +/// Lowercase hex of a byte slice, for readable assertion diffs. +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Bytes of a lowercase-hex literal — the inverse of [`hex`], so a decode vector reads +/// the same literal the matching encode vector asserts. +fn unhex(text: &str) -> Vec { + assert!( + text.len().is_multiple_of(2), + "hex literal has an odd length" + ); + (0..text.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex literal is well-formed")) + .collect() +} + +/// Decode a golden literal as a frame, failing loudly rather than returning `None` into +/// an assertion that could read a rejection as a mismatch. +fn decode(golden: &str) -> DigMessage { + DigMessage::from_bytes(&unhex(golden)).expect("a golden frame must decode") +} + +/// Wire opcode 218 — `RegisterPeer`. +const REGISTER_PEER: u8 = DigMessageType::RegisterPeer as u8; + +/// Wire opcode 219 — `RegisterAck`. +const REGISTER_ACK: u8 = DigMessageType::RegisterAck as u8; + +/// A payload whose two ends differ, so a reversed or truncated body is visible. +const ENVELOPE_PAYLOAD: &[u8] = &[0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x7f]; + +// ============================================================================ +// The golden literals — each asserted in both directions below. +// ============================================================================ + +const GOLDEN_220_WITH_ID: &str = "dc01123400000008deadbeef0102037f"; +const GOLDEN_220_NO_ID: &str = "dc0000000008deadbeef0102037f"; +const GOLDEN_220_EMPTY: &str = "dc0000000000"; +const GOLDEN_200: &str = "c80000000008deadbeef0102037f"; +const GOLDEN_217: &str = "d90000000008deadbeef0102037f"; +const GOLDEN_218_FULL_NODE: &str = "da010007000000110000000a3139322e302e322e3838255301"; +const GOLDEN_218_INTRODUCER: &str = "da00000000120000000b323030313a6462383a3a3124e405"; +const GOLDEN_219_SUCCESS: &str = "db0100070000000101"; +const GOLDEN_219_REJECTION: &str = "db0100070000000100"; + +/// The correlation id carried by every vector that pins the `Some` branch. +const CORRELATION_ID: u16 = 0x0007; + +// ============================================================================ +// Opcode 220 — DigMessage (the directed-envelope send path) +// ============================================================================ + +#[test] +fn golden_opcode_220_dig_message_with_correlation_id() { + let frame = frame_envelope(ENVELOPE_PAYLOAD, Some(0x1234)).to_bytes(); + assert_eq!(hex(&frame), GOLDEN_220_WITH_ID); +} + +#[test] +fn golden_opcode_220_dig_message_without_correlation_id() { + let frame = frame_envelope(ENVELOPE_PAYLOAD, None).to_bytes(); + assert_eq!(hex(&frame), GOLDEN_220_NO_ID); +} + +#[test] +fn golden_opcode_220_dig_message_empty_envelope() { + let frame = frame_envelope(&[], None).to_bytes(); + assert_eq!(hex(&frame), GOLDEN_220_EMPTY); +} + +#[test] +fn decode_opcode_220_recovers_the_correlation_id() { + let msg = decode(GOLDEN_220_WITH_ID); + assert_eq!(msg.msg_type, DIG_MESSAGE); + assert_eq!(msg.id, Some(0x1234), "a byte-swapped id would read 0x3412"); + assert_eq!(msg.data.as_ref(), ENVELOPE_PAYLOAD); +} + +#[test] +fn decode_opcode_220_recovers_the_absent_correlation_id() { + let msg = decode(GOLDEN_220_NO_ID); + assert_eq!(msg.msg_type, DIG_MESSAGE); + assert_eq!(msg.id, None, "the presence flag is 0, so there is no id"); + assert_eq!(msg.data.as_ref(), ENVELOPE_PAYLOAD); +} + +#[test] +fn decode_opcode_220_recovers_the_empty_envelope() { + let msg = decode(GOLDEN_220_EMPTY); + assert_eq!(msg.msg_type, DIG_MESSAGE); + assert_eq!(msg.id, None); + assert!( + msg.data.is_empty(), + "a zero-length payload is a valid frame, not a truncated one" + ); +} + +// ============================================================================ +// Consensus band 200-217 — frame_dig_message +// ============================================================================ + +#[test] +fn golden_consensus_band_first_opcode_200() { + let frame = + frame_dig_message(DigMessageType::NewAttestation, ENVELOPE_PAYLOAD.to_vec()).to_bytes(); + assert_eq!(hex(&frame), GOLDEN_200); +} + +#[test] +fn golden_consensus_band_last_opcode_217() { + let frame = frame_dig_message( + DigMessageType::PlumtreeRequestByHash, + ENVELOPE_PAYLOAD.to_vec(), + ) + .to_bytes(); + assert_eq!(hex(&frame), GOLDEN_217); +} + +#[test] +fn decode_consensus_band_first_opcode_200() { + let msg = decode(GOLDEN_200); + assert_eq!(msg.msg_type, DigMessageType::NewAttestation as u8); + assert_eq!(msg.id, None); + assert_eq!(msg.data.as_ref(), ENVELOPE_PAYLOAD); +} + +#[test] +fn decode_consensus_band_last_opcode_217() { + let msg = decode(GOLDEN_217); + assert_eq!(msg.msg_type, DigMessageType::PlumtreeRequestByHash as u8); + assert_eq!(msg.id, None); + assert_eq!(msg.data.as_ref(), ENVELOPE_PAYLOAD); +} + +// ============================================================================ +// Opcodes 218/219 — introducer registration +// ============================================================================ + +/// Wrap an already-serialized opcode body in the standard frame envelope and return +/// its full on-wire bytes. +/// +/// This is the same envelope `DigLink` puts on the socket for the introducer opcodes, +/// reproduced here so the vector pins the *frame* rather than just the body. +/// +/// The opcode is passed in rather than derived from the body type: 218/219 have no +/// `ProtocolMessageTypes` variant to derive one from, which is the whole reason this +/// path moved off the forked enum. +fn envelope_bytes(body: &T, opcode: u8, id: Option) -> Vec { + DigMessage::new(opcode, id, body.to_bytes().expect("body serializes").into()).to_bytes() +} + +/// Recover an introducer body from a golden literal, asserting the frame around it first. +/// +/// The opcode and id are checked here rather than in each caller so a body-level +/// assertion can never pass on a frame addressed to the wrong opcode. +fn decode_body(golden: &str, opcode: u8, id: Option) -> T { + let msg = decode(golden); + assert_eq!(msg.msg_type, opcode, "frame carries the wrong opcode"); + assert_eq!(msg.id, id, "frame carries the wrong correlation id"); + T::from_bytes(&msg.data).expect("a golden body must decode") +} + +#[test] +fn golden_opcode_218_register_peer_full_node() { + let body = RegisterPeer::new("192.0.2.88".into(), 9555, NodeType::FullNode); + assert_eq!( + hex(&envelope_bytes(&body, REGISTER_PEER, Some(CORRELATION_ID))), + GOLDEN_218_FULL_NODE + ); +} + +#[test] +fn golden_opcode_218_register_peer_introducer_no_id() { + let body = RegisterPeer::new("2001:db8::1".into(), 9444, NodeType::Introducer); + assert_eq!( + hex(&envelope_bytes(&body, REGISTER_PEER, None)), + GOLDEN_218_INTRODUCER + ); +} + +#[test] +fn golden_opcode_219_register_ack_success() { + assert_eq!( + hex(&envelope_bytes( + &RegisterAck::new(true), + REGISTER_ACK, + Some(CORRELATION_ID) + )), + GOLDEN_219_SUCCESS + ); +} + +#[test] +fn golden_opcode_219_register_ack_rejection() { + assert_eq!( + hex(&envelope_bytes( + &RegisterAck::new(false), + REGISTER_ACK, + Some(CORRELATION_ID) + )), + GOLDEN_219_REJECTION + ); +} + +#[test] +fn decode_opcode_218_register_peer_full_node() { + let body: RegisterPeer = decode_body(GOLDEN_218_FULL_NODE, REGISTER_PEER, Some(CORRELATION_ID)); + assert_eq!( + body, + RegisterPeer::new("192.0.2.88".into(), 9555, NodeType::FullNode) + ); +} + +#[test] +fn decode_opcode_218_register_peer_introducer_no_id() { + let body: RegisterPeer = decode_body(GOLDEN_218_INTRODUCER, REGISTER_PEER, None); + assert_eq!( + body, + RegisterPeer::new("2001:db8::1".into(), 9444, NodeType::Introducer), + "Introducer is discriminant 5 — a collapsed mapping would decode as FullNode" + ); +} + +#[test] +fn decode_opcode_219_register_ack_success() { + let body: RegisterAck = decode_body(GOLDEN_219_SUCCESS, REGISTER_ACK, Some(CORRELATION_ID)); + assert_eq!(body, RegisterAck::new(true)); +} + +#[test] +fn decode_opcode_219_register_ack_rejection() { + let body: RegisterAck = decode_body(GOLDEN_219_REJECTION, REGISTER_ACK, Some(CORRELATION_ID)); + assert_eq!( + body, + RegisterAck::new(false), + "an inverted flag would decode a policy rejection as an acceptance" + ); +} + +// ============================================================================ +// The negative vector — an opcode the forked enum cannot represent +// ============================================================================ + +/// An opcode assigned by neither Chia nor DIG. +/// +/// Deliberately outside the DIG bands too: a DIG opcode would prove only that *these* +/// extensions decode, where the contract is that **any** byte does. +const UNASSIGNED_OPCODE: u8 = 0xfe; + +/// The same shape as [`GOLDEN_220_NO_ID`], differing only in the opcode byte. +const GOLDEN_UNASSIGNED_OPCODE: &str = "fe0000000008deadbeef0102037f"; + +#[test] +fn the_unassigned_opcode_really_is_unrepresentable_as_a_protocol_message_type() { + // Without this the negative vector below is unfalsifiable: it would pass just as + // happily on an opcode the forked enum accepts. + assert!( + ProtocolMessageTypes::from_bytes(&[UNASSIGNED_OPCODE]).is_err(), + "0xfe must have no ProtocolMessageTypes variant, or the vector proves nothing" + ); +} + +#[test] +fn golden_unassigned_opcode_encodes() { + let frame = + DigMessage::new(UNASSIGNED_OPCODE, None, ENVELOPE_PAYLOAD.to_vec().into()).to_bytes(); + assert_eq!(hex(&frame), GOLDEN_UNASSIGNED_OPCODE); +} + +#[test] +fn decode_unassigned_opcode_succeeds_where_the_forked_enum_rejected() { + let msg = decode(GOLDEN_UNASSIGNED_OPCODE); + assert_eq!(msg.msg_type, UNASSIGNED_OPCODE); + assert_eq!(msg.id, None); + assert_eq!(msg.data.as_ref(), ENVELOPE_PAYLOAD); +} diff --git a/vendor/chia-protocol/.cargo-ok b/vendor/chia-protocol/.cargo-ok deleted file mode 100644 index 5f8b795..0000000 --- a/vendor/chia-protocol/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/vendor/chia-protocol/.cargo_vcs_info.json b/vendor/chia-protocol/.cargo_vcs_info.json deleted file mode 100644 index f2cd0b1..0000000 --- a/vendor/chia-protocol/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "e70391495ad263bbd03cb1bb1a60245e7482ddd5" - }, - "path_in_vcs": "crates/chia-protocol" -} \ No newline at end of file diff --git a/vendor/chia-protocol/Cargo.lock b/vendor/chia-protocol/Cargo.lock deleted file mode 100644 index 30ee434..0000000 --- a/vendor/chia-protocol/Cargo.lock +++ /dev/null @@ -1,1346 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" - -[[package]] -name = "arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blst" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" -dependencies = [ - "cc", - "glob", - "threadpool", - "zeroize", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cc" -version = "1.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chia-bls" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86eaaa00a83a7511e8eb74ccb1ebe4358d927f43e06d45da5350e19ef2df1ca" -dependencies = [ - "blst", - "chia-sha2 0.22.0", - "chia-traits 0.22.0", - "hex", - "hkdf", - "linked-hash-map", - "sha2", - "thiserror", -] - -[[package]] -name = "chia-bls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc9ac7e90ae816e814dc26fb332615a5449d1d26e965eb7ad068ae530f9c3c7b" -dependencies = [ - "arbitrary", - "blst", - "chia-serde", - "chia-sha2 0.26.0", - "chia-traits 0.26.0", - "chia_py_streamable_macro", - "hex", - "hkdf", - "linked-hash-map", - "pyo3", - "serde", - "sha2", - "thiserror", -] - -[[package]] -name = "chia-protocol" -version = "0.26.0" -dependencies = [ - "anyhow", - "arbitrary", - "chia-bls 0.26.0", - "chia-serde", - "chia-sha2 0.26.0", - "chia-traits 0.26.0", - "chia_py_streamable_macro", - "chia_streamable_macro 0.26.0", - "clvm-traits", - "clvm-utils", - "clvmr", - "hex", - "indoc", - "pyo3", - "rstest", - "serde", - "serde_json", -] - -[[package]] -name = "chia-serde" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b013e263888739bfecf7456552eb4a9f2260f920db8b46b0723fb553ab1e030" -dependencies = [ - "hex", - "serde", -] - -[[package]] -name = "chia-sha2" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b37c5362dfb1c9449902139e94a91bbd8d3773c13939972fd88e4f14c4f9d" -dependencies = [ - "sha2", -] - -[[package]] -name = "chia-sha2" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3ab374b1f248d87516c34e4c128a91d2086e76a46bb44e386c96a71ea39129" -dependencies = [ - "sha2", -] - -[[package]] -name = "chia-traits" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340ad823ef953d2ab9f937deae99b04bab73c0bf1a6aea1353331a5f875192e0" -dependencies = [ - "chia-sha2 0.22.0", - "chia_streamable_macro 0.22.0", - "thiserror", -] - -[[package]] -name = "chia-traits" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440a88dfa8f685a87c4bae5f0b12c7bec1466ca1ff44dfc6b09acc605f7848fe" -dependencies = [ - "chia-sha2 0.26.0", - "chia_streamable_macro 0.26.0", - "pyo3", - "thiserror", -] - -[[package]] -name = "chia_py_streamable_macro" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fae158850ec0c7206011a2fac3f7fb7aff5d2c5ec55939994902aad3d846632" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "chia_streamable_macro" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed16ee21f14ed878cfc23b0354c8c6703190a5565d03625ea44324a3f21717f1" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "chia_streamable_macro" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068278c78a0c53786012f6330a088f3f6ffe83bd39cb8f21d308eeec2f43a3dc" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clvm-derive" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ded863cb5482335498872e91989487283f012adb7ca4ddf54ad7c6995058db" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clvm-traits" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469c44226c83de37509415482c89c00763f58de198844c34d2d62161b087d0ce" -dependencies = [ - "clvm-derive", - "clvmr", - "num-bigint", - "thiserror", -] - -[[package]] -name = "clvm-utils" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5646cd8d02f9c55c76ef6ff7598df54e6c8361f27fe8c9122e3ae9ef55935fcd" -dependencies = [ - "chia-sha2 0.26.0", - "clvm-traits", - "clvmr", - "hex", -] - -[[package]] -name = "clvmr" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6b7142674607f3a213addaf261bd70b07537297e460a9cc6a389068a62934f" -dependencies = [ - "bitvec", - "bumpalo", - "chia-bls 0.22.0", - "chia-sha2 0.22.0", - "hex", - "hex-literal", - "k256", - "lazy_static", - "num-bigint", - "num-integer", - "num-traits", - "p256", - "rand", - "sha1", - "sha3", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "pem-rfc7468", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "ff" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hex-literal" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "indexmap" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - -[[package]] -name = "inventory" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2", - "signature", -] - -[[package]] -name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "portable-atomic" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" - -[[package]] -name = "ppv-lite86" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2288c0e17cc8d342c712bb43a257a80ebffce59cdb33d5000d8348f3ec02528b" -dependencies = [ - "zerocopy", - "zerocopy-derive", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" -dependencies = [ - "toml_edit 0.22.20", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pyo3" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229" -dependencies = [ - "cfg-if", - "indoc", - "inventory", - "libc", - "memoffset", - "num-bigint", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "regex" -version = "1.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "rstest" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" -dependencies = [ - "futures", - "futures-timer", - "rstest_macros", - "rustc_version", -] - -[[package]] -name = "rstest_macros" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" -dependencies = [ - "cfg-if", - "glob", - "proc-macro-crate 3.2.0", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn", - "unicode-ident", -] - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest", - "keccak", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "target-lexicon" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow 0.6.18", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unindent" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" -dependencies = [ - "memchr", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/vendor/chia-protocol/Cargo.toml b/vendor/chia-protocol/Cargo.toml deleted file mode 100644 index 6f67725..0000000 --- a/vendor/chia-protocol/Cargo.toml +++ /dev/null @@ -1,165 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2021" -name = "chia-protocol" -version = "0.26.0" -authors = ["Arvid Norberg "] -build = false -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Chia network protocol message types" -homepage = "https://github.com/Chia-Network/chia_rs" -readme = false -license = "Apache-2.0" -repository = "https://github.com/Chia-Network/chia_rs" - -[features] -arbitrary = [ - "dep:arbitrary", - "chia-bls/arbitrary", -] -py-bindings = [ - "dep:pyo3", - "dep:chia_py_streamable_macro", - "chia-traits/py-bindings", - "chia-bls/py-bindings", -] -serde = [ - "dep:serde", - "dep:chia-serde", - "chia-bls/serde", -] - -[lib] -name = "chia_protocol" -crate-type = ["rlib"] -path = "src/lib.rs" - -[dependencies.arbitrary] -version = "1.4.1" -features = ["derive"] -optional = true - -[dependencies.chia-bls] -version = "0.26.0" - -[dependencies.chia-serde] -version = "0.26.0" -optional = true - -[dependencies.chia-sha2] -version = "0.26.0" - -[dependencies.chia-traits] -version = "0.26.0" - -[dependencies.chia_py_streamable_macro] -version = "0.26.0" -optional = true - -[dependencies.chia_streamable_macro] -version = "0.26.0" - -[dependencies.clvm-traits] -version = "0.26.0" -features = ["derive"] - -[dependencies.clvm-utils] -version = "0.26.0" - -[dependencies.clvmr] -version = "0.14.0" - -[dependencies.hex] -version = "0.4.3" - -[dependencies.pyo3] -version = "0.24.1" -features = [ - "multiple-pymethods", - "num-bigint", -] -optional = true - -[dependencies.serde] -version = "1.0.219" -features = ["derive"] -optional = true - -[dev-dependencies.anyhow] -version = "1.0.97" - -[dev-dependencies.indoc] -version = "2.0.6" - -[dev-dependencies.rstest] -version = "0.22.0" - -[dev-dependencies.serde_json] -version = "1.0.140" - -[lints.clippy] -cast_lossless = "allow" -cast_possible_truncation = "allow" -cast_possible_wrap = "allow" -cast_precision_loss = "allow" -cast_sign_loss = "allow" -doc_markdown = "allow" -implicit_hasher = "allow" -missing_errors_doc = "allow" -missing_panics_doc = "allow" -module_name_repetitions = "allow" -must_use_candidate = "allow" -similar_names = "allow" -too_many_lines = "allow" -wildcard_imports = "allow" - -[lints.clippy.all] -level = "deny" -priority = -1 - -[lints.clippy.pedantic] -level = "warn" -priority = -1 - -[lints.rust] -dead_code = "deny" -deprecated = "deny" -deprecated_in_future = "deny" -non_ascii_idents = "deny" -trivial_casts = "deny" -trivial_numeric_casts = "deny" -unreachable_code = "deny" -unreachable_patterns = "deny" -unsafe_code = "deny" -unused_import_braces = "deny" -unused_imports = "warn" - -[lints.rust.future_incompatible] -level = "deny" -priority = -1 - -[lints.rust.nonstandard_style] -level = "deny" -priority = -1 - -[lints.rust.rust_2018_idioms] -level = "deny" -priority = -1 - -[lints.rust.rust_2021_compatibility] -level = "deny" -priority = -1 diff --git a/vendor/chia-protocol/Cargo.toml.orig b/vendor/chia-protocol/Cargo.toml.orig deleted file mode 100644 index f1e3bd3..0000000 --- a/vendor/chia-protocol/Cargo.toml.orig +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "chia-protocol" -version = "0.26.0" -edition = "2021" -license = "Apache-2.0" -description = "Chia network protocol message types" -authors = ["Arvid Norberg "] -homepage = "https://github.com/Chia-Network/chia_rs" -repository = "https://github.com/Chia-Network/chia_rs" - -[lints] -workspace = true - -[features] -py-bindings = ["dep:pyo3", "dep:chia_py_streamable_macro", "chia-traits/py-bindings", "chia-bls/py-bindings"] -arbitrary = ["dep:arbitrary", "chia-bls/arbitrary"] -serde = ["dep:serde", "dep:chia-serde", "chia-bls/serde"] - -[dependencies] -pyo3 = { workspace = true, features = ["multiple-pymethods", "num-bigint"], optional = true } -hex = { workspace = true } -chia_streamable_macro = { workspace = true } -chia_py_streamable_macro = { workspace = true, optional = true } -clvmr = { workspace = true } -chia-traits = { workspace = true } -chia-sha2 = { workspace = true } -clvm-traits = { workspace = true, features = ["derive"] } -clvm-utils = { workspace = true } -chia-bls = { workspace = true } -arbitrary = { workspace = true, features = ["derive"], optional = true } -serde = { workspace = true, optional = true, features = ["derive"] } -chia-serde = { workspace = true, optional = true } - -[dev-dependencies] -rstest = { workspace = true } -serde_json = { workspace = true } -anyhow = { workspace = true } -indoc = { workspace = true } - -[lib] -crate-type = ["rlib"] diff --git a/vendor/chia-protocol/README.dig-gossip.md b/vendor/chia-protocol/README.dig-gossip.md deleted file mode 100644 index 1aad761..0000000 --- a/vendor/chia-protocol/README.dig-gossip.md +++ /dev/null @@ -1,47 +0,0 @@ -# dig-gossip vendor fork: `chia-protocol` - -Vendored via `[patch.crates-io]` in the workspace `Cargo.toml`. The tree is an unpacked -**crates.io `chia-protocol` 0.26.0** tarball, so the pristine crate of the same version is the -exact baseline and everything the diff reports is DIG's. - -## Regenerate this delta — do not hand-maintain it - -```sh -vendor/fork-delta.sh chia-protocol --summary # the file list -vendor/fork-delta.sh chia-protocol # the full unified diff -``` - -A hand-written delta has been wrong twice before (dig_ecosystem#2228): claiming `RegisterPeer` and -`RegisterAck` only (opcodes 218–219), when the fork actually adds 23 opcodes (200–222). **The compiler -and the diff are the record; this file is a summary of them and must be regenerated when either -changes.** - -## What the fork changes — one file, 23 opcodes - -`--summary` reports exactly one differing file, `src/chia_protocol.rs`: - -**`ProtocolMessageTypes` enum adds 23 opcodes** (200–222), in three groups: - -1. **DIG L2 consensus band (200–217, 18 opcodes):** `NewAttestation`, `NewCheckpointProposal`, - `NewCheckpointSignature`, `RequestCheckpointSignatures`, `RespondCheckpointSignatures`, - `RequestStatus`, `RespondStatus`, `NewCheckpointSubmission`, `ValidatorAnnounce`, - `RequestBlockTransactions`, `RespondBlockTransactions`, `ReconciliationSketch`, - `ReconciliationResponse`, `StemTransaction`, `PlumtreeLazyAnnounce`, `PlumtreePrune`, - `PlumtreeGraft`, `PlumtreeRequestByHash` (#1404). These extend Chia's namespace so a stock - `Message` can carry a DIG consensus opcode on the wire. Each MUST equal the matching - `dig_peer_protocol::DigMessageType` discriminant; `frame_dig_message` in dig-gossip converts - one to the other losslessly. **Additive** (§5.1): no existing opcode moves. - -2. **DIG introducer registration (218–219, 2 opcodes):** `RegisterPeer`, `RegisterAck` (DSC-005). - Required so `Message::from_bytes` accepts replies on the introducer WebSocket; the stock enum - stops at 107. Additive only. - -3. **DIG directed-envelope and broadcast (220–222, 3 opcodes):** `DigMessage` (WU6 / epic #796), - `StoreMelted` (epic #1316), `HoldingsAnnounce` (#1428). Carry opaque DIG payloads or announce - store/holdings state to all peers. Additive only. - -## Upstream status - -All 23 are purely additive — no renumbering or semantic changes to existing opcodes. The fork -exists only because `ProtocolMessageTypes` is upstream-owned; if upstream accepts the opcodes, -this fork retires. diff --git a/vendor/chia-protocol/src/block_record.rs b/vendor/chia-protocol/src/block_record.rs deleted file mode 100644 index 880ef08..0000000 --- a/vendor/chia-protocol/src/block_record.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::{calculate_ip_iters, calculate_sp_iters}; -use crate::{Bytes32, ClassgroupElement, Coin, SubEpochSummary}; -use chia_streamable_macro::streamable; -use chia_traits::chia_error::Result; -#[cfg(feature = "py-bindings")] -use pyo3::exceptions::PyValueError; - -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -// This class is not included or hashed into the blockchain, but it is kept in memory as a more -// efficient way to maintain data about the blockchain. This allows us to validate future blocks, -// difficulty adjustments, etc, without saving the whole header block in memory. -#[streamable] -pub struct BlockRecord { - header_hash: Bytes32, - // Header hash of the previous block - prev_hash: Bytes32, - height: u32, - // Total cumulative difficulty of all ancestor blocks since genesis - weight: u128, - // Total number of VDF iterations since genesis, including this block - total_iters: u128, - signage_point_index: u8, - // This is the intermediary VDF output at ip_iters in challenge chain - challenge_vdf_output: ClassgroupElement, - // This is the intermediary VDF output at ip_iters in infused cc, iff deficit <= 3 - infused_challenge_vdf_output: Option, - // The reward chain infusion output, input to next VDF - reward_infusion_new_challenge: Bytes32, - // Hash of challenge chain data, used to validate end of slots in the future - challenge_block_info_hash: Bytes32, - // Current network sub_slot_iters parameter - sub_slot_iters: u64, - // Need to keep track of these because Coins are created in a future block - pool_puzzle_hash: Bytes32, - farmer_puzzle_hash: Bytes32, - // The number of iters required for this proof of space - required_iters: u64, - // A deficit of 16 is an overflow block after an infusion. Deficit of 15 is a challenge block - deficit: u8, - overflow: bool, - prev_transaction_block_height: u32, - - // Transaction block (present iff is_transaction_block) - timestamp: Option, - // Header hash of the previous transaction block - prev_transaction_block_hash: Option, - fees: Option, - reward_claims_incorporated: Option>, - - // Slot (present iff this is the first SB in sub slot) - finished_challenge_slot_hashes: Option>, - finished_infused_challenge_slot_hashes: Option>, - finished_reward_slot_hashes: Option>, - - // Sub-epoch (present iff this is the first SB after sub-epoch) - sub_epoch_summary_included: Option, -} - -impl BlockRecord { - pub fn is_transaction_block(&self) -> bool { - self.timestamp.is_some() - } - - pub fn first_in_sub_slot(&self) -> bool { - self.finished_challenge_slot_hashes.is_some() - } - - pub fn is_challenge_block(&self, min_blocks_per_challenge_block: u8) -> bool { - self.deficit == min_blocks_per_challenge_block - 1 - } - - pub fn sp_iters_impl(&self, num_sps_sub_slot: u8) -> Result { - calculate_sp_iters( - num_sps_sub_slot, - self.sub_slot_iters, - self.signage_point_index, - ) - } - - pub fn ip_iters_impl(&self, num_sps_sub_slot: u8, num_sp_intervals_extra: u8) -> Result { - calculate_ip_iters( - num_sps_sub_slot, - num_sp_intervals_extra, - self.sub_slot_iters, - self.signage_point_index, - self.required_iters, - ) - } -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl BlockRecord { - #[getter] - #[pyo3(name = "is_transaction_block")] - fn py_is_transaction_block(&self) -> bool { - self.is_transaction_block() - } - - #[getter] - #[pyo3(name = "first_in_sub_slot")] - fn py_first_in_sub_slot(&self) -> bool { - self.first_in_sub_slot() - } - - #[pyo3(name = "is_challenge_block")] - fn py_is_challenge_block(&self, constants: &Bound<'_, PyAny>) -> PyResult { - Ok(self.is_challenge_block( - constants - .getattr("MIN_BLOCKS_PER_CHALLENGE_BLOCK")? - .extract::()?, - )) - } - - #[pyo3(name = "ip_sub_slot_total_iters")] - fn ip_sub_slot_total_iters_impl(&self, constants: &Bound<'_, PyAny>) -> PyResult { - self.total_iters - .checked_sub(self.py_ip_iters_impl(constants)? as u128) - .ok_or(PyValueError::new_err("uint128 overflow")) - } - - #[pyo3(name = "sp_iters")] - fn py_sp_iters_impl(&self, constants: &Bound<'_, PyAny>) -> PyResult { - let num_sps_sub_slot = constants.getattr("NUM_SPS_SUB_SLOT")?.extract::()?; - self.sp_iters_impl(num_sps_sub_slot).map_err(Into::into) - } - - #[pyo3(name = "ip_iters")] - fn py_ip_iters_impl(&self, constants: &Bound<'_, PyAny>) -> PyResult { - let num_sps_sub_slot = constants.getattr("NUM_SPS_SUB_SLOT")?.extract::()?; - let num_sp_intervals_extra = constants - .getattr("NUM_SP_INTERVALS_EXTRA")? - .extract::()?; - self.ip_iters_impl(num_sps_sub_slot, num_sp_intervals_extra) - .map_err(Into::into) - } - - #[pyo3(name = "sp_sub_slot_total_iters")] - fn sp_sub_slot_total_iters_impl(&self, constants: &Bound<'_, PyAny>) -> PyResult { - let ret = self - .total_iters - .checked_sub(self.py_ip_iters_impl(constants)? as u128) - .ok_or(PyValueError::new_err("uint128 overflow"))?; - if self.overflow { - ret.checked_sub(self.sub_slot_iters as u128) - .ok_or(PyValueError::new_err("uint128 overflow")) - } else { - Ok(ret) - } - } - - #[pyo3(name = "sp_total_iters")] - fn sp_total_iters_impl(&self, constants: &Bound<'_, PyAny>) -> PyResult { - self.sp_sub_slot_total_iters_impl(constants)? - .checked_add(self.py_sp_iters_impl(constants)? as u128) - .ok_or(PyValueError::new_err("uint128 overflow")) - } -} diff --git a/vendor/chia-protocol/src/bytes.rs b/vendor/chia-protocol/src/bytes.rs deleted file mode 100644 index 506f091..0000000 --- a/vendor/chia-protocol/src/bytes.rs +++ /dev/null @@ -1,704 +0,0 @@ -use chia_sha2::Sha256; -use chia_traits::{chia_error, read_bytes, Streamable}; -use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, ToClvm, ToClvmError}; -use clvm_utils::TreeHash; -use clvmr::Atom; -use std::array::TryFromSliceError; -use std::fmt; -use std::io::Cursor; -use std::ops::Deref; - -#[cfg(feature = "py-bindings")] -use chia_traits::{ChiaToPython, FromJsonDict, ToJsonDict}; -#[cfg(feature = "py-bindings")] -use hex::FromHex; -#[cfg(feature = "py-bindings")] -use pyo3::exceptions::PyValueError; -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; -#[cfg(feature = "py-bindings")] -use pyo3::types::PyBytes; - -#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub struct Bytes(Vec); - -impl Bytes { - pub fn new(bytes: Vec) -> Self { - Self(bytes) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn as_slice(&self) -> &[u8] { - self.0.as_slice() - } - - pub fn to_vec(&self) -> Vec { - self.0.clone() - } - - pub fn into_inner(self) -> Vec { - self.0 - } -} - -impl fmt::Debug for Bytes { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&hex::encode(self)) - } -} - -impl fmt::Display for Bytes { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&hex::encode(self)) - } -} - -#[cfg(feature = "serde")] -impl serde::Serialize for Bytes { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - chia_serde::ser_bytes(self, serializer, false) - } -} - -#[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for Bytes { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - chia_serde::de_bytes(deserializer) - } -} - -impl Streamable for Bytes { - fn update_digest(&self, digest: &mut Sha256) { - (self.0.len() as u32).update_digest(digest); - digest.update(&self.0); - } - - fn stream(&self, out: &mut Vec) -> chia_error::Result<()> { - if self.0.len() > u32::MAX as usize { - Err(chia_error::Error::SequenceTooLarge) - } else { - (self.0.len() as u32).stream(out)?; - out.extend_from_slice(&self.0); - Ok(()) - } - } - - fn parse(input: &mut Cursor<&[u8]>) -> chia_error::Result { - let len = u32::parse::(input)?; - Ok(Bytes(read_bytes(input, len as usize)?.to_vec())) - } -} - -#[cfg(feature = "py-bindings")] -impl ToJsonDict for Bytes { - fn to_json_dict(&self, py: Python<'_>) -> PyResult { - Ok(format!("0x{self}").into_pyobject(py)?.into_any().unbind()) - } -} - -#[cfg(feature = "py-bindings")] -impl FromJsonDict for Bytes { - fn from_json_dict(o: &Bound<'_, PyAny>) -> PyResult { - let s: String = o.extract()?; - if !s.starts_with("0x") { - return Err(PyValueError::new_err( - "bytes object is expected to start with 0x", - )); - } - let s = &s[2..]; - let buf = match Vec::from_hex(s) { - Err(_) => { - return Err(PyValueError::new_err("invalid hex")); - } - Ok(v) => v, - }; - Ok(buf.into()) - } -} - -impl> ToClvm for Bytes { - fn to_clvm(&self, encoder: &mut E) -> Result { - encoder.encode_atom(Atom::Borrowed(self.0.as_slice())) - } -} - -impl> FromClvm for Bytes { - fn from_clvm(decoder: &D, node: N) -> Result { - let bytes = decoder.decode_atom(&node)?; - Ok(Self(bytes.as_ref().to_vec())) - } -} - -impl From<&[u8]> for Bytes { - fn from(value: &[u8]) -> Self { - Self(value.to_vec()) - } -} - -impl From> for Bytes { - fn from(value: BytesImpl) -> Self { - Self(value.0.to_vec()) - } -} - -impl From> for Bytes { - fn from(value: Vec) -> Self { - Self(value) - } -} - -impl From for Vec { - fn from(value: Bytes) -> Self { - value.0 - } -} - -impl AsRef<[u8]> for Bytes { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl Deref for Bytes { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - &self.0 - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub struct BytesImpl([u8; N]); - -impl BytesImpl { - pub const fn new(bytes: [u8; N]) -> Self { - Self(bytes) - } - - pub const fn len(&self) -> usize { - N - } - - pub const fn is_empty(&self) -> bool { - N == 0 - } - - pub fn as_slice(&self) -> &[u8] { - &self.0 - } - - pub fn to_bytes(self) -> [u8; N] { - self.0 - } - - pub fn to_vec(&self) -> Vec { - self.0.to_vec() - } -} - -impl Default for BytesImpl { - fn default() -> Self { - Self([0; N]) - } -} - -impl fmt::Debug for BytesImpl { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - formatter.write_str(&hex::encode(self)) - } -} - -impl fmt::Display for BytesImpl { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&hex::encode(self)) - } -} - -#[cfg(feature = "serde")] -impl serde::Serialize for BytesImpl { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - chia_serde::ser_bytes(self, serializer, true) - } -} - -#[cfg(feature = "serde")] -impl<'de, const N: usize> serde::Deserialize<'de> for BytesImpl { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - chia_serde::de_bytes(deserializer) - } -} - -impl Streamable for BytesImpl { - fn update_digest(&self, digest: &mut Sha256) { - digest.update(self.0); - } - fn stream(&self, out: &mut Vec) -> chia_error::Result<()> { - out.extend_from_slice(&self.0); - Ok(()) - } - - fn parse(input: &mut Cursor<&[u8]>) -> chia_error::Result { - Ok(BytesImpl(read_bytes(input, N)?.try_into().unwrap())) - } -} - -#[cfg(feature = "py-bindings")] -impl ToJsonDict for BytesImpl { - fn to_json_dict(&self, py: Python<'_>) -> PyResult { - Ok(format!("0x{self}").into_pyobject(py)?.into_any().unbind()) - } -} - -#[cfg(feature = "py-bindings")] -impl FromJsonDict for BytesImpl { - fn from_json_dict(o: &Bound<'_, PyAny>) -> PyResult { - let s: String = o.extract()?; - if !s.starts_with("0x") { - return Err(PyValueError::new_err( - "bytes object is expected to start with 0x", - )); - } - let s = &s[2..]; - let buf = match Vec::from_hex(s) { - Err(_) => { - return Err(PyValueError::new_err("invalid hex")); - } - Ok(v) => v, - }; - if buf.len() != N { - return Err(PyValueError::new_err(format!( - "invalid length {} expected {}", - buf.len(), - N - ))); - } - Ok(buf.try_into().unwrap()) - } -} - -impl, const LEN: usize> ToClvm for BytesImpl { - fn to_clvm(&self, encoder: &mut E) -> Result { - encoder.encode_atom(Atom::Borrowed(self.0.as_slice())) - } -} - -impl, const LEN: usize> FromClvm for BytesImpl { - fn from_clvm(decoder: &D, node: N) -> Result { - let bytes = decoder.decode_atom(&node)?; - if bytes.as_ref().len() != LEN { - return Err(FromClvmError::WrongAtomLength { - expected: LEN, - found: bytes.as_ref().len(), - }); - } - Ok(Self::try_from(bytes.as_ref()).unwrap()) - } -} - -impl TryFrom<&[u8]> for BytesImpl { - type Error = TryFromSliceError; - - fn try_from(value: &[u8]) -> Result { - Ok(Self(value.try_into()?)) - } -} - -impl TryFrom> for BytesImpl { - type Error = TryFromSliceError; - - fn try_from(value: Vec) -> Result { - value.as_slice().try_into() - } -} - -impl TryFrom<&Vec> for BytesImpl { - type Error = TryFromSliceError; - - fn try_from(value: &Vec) -> Result { - value.as_slice().try_into() - } -} - -impl TryFrom for BytesImpl { - type Error = TryFromSliceError; - - fn try_from(value: Bytes) -> Result { - value.0.as_slice().try_into() - } -} - -impl TryFrom<&Bytes> for BytesImpl { - type Error = TryFromSliceError; - - fn try_from(value: &Bytes) -> Result { - value.0.as_slice().try_into() - } -} - -impl From> for Vec { - fn from(value: BytesImpl) -> Self { - value.to_vec() - } -} - -impl From<[u8; N]> for BytesImpl { - fn from(value: [u8; N]) -> Self { - Self(value) - } -} - -impl From<&[u8; N]> for BytesImpl { - fn from(value: &[u8; N]) -> Self { - Self(*value) - } -} - -impl From> for [u8; N] { - fn from(value: BytesImpl) -> Self { - value.0 - } -} - -impl<'a, const N: usize> From<&'a BytesImpl> for &'a [u8; N] { - fn from(value: &'a BytesImpl) -> &'a [u8; N] { - &value.0 - } -} - -impl From<&BytesImpl> for [u8; N] { - fn from(value: &BytesImpl) -> [u8; N] { - value.0 - } -} - -impl<'a, const N: usize> From<&'a BytesImpl> for &'a [u8] { - fn from(value: &'a BytesImpl) -> &'a [u8] { - &value.0 - } -} - -impl AsRef<[u8]> for BytesImpl { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl Deref for BytesImpl { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - &self.0 - } -} - -pub type Bytes32 = BytesImpl<32>; -pub type Bytes48 = BytesImpl<48>; -pub type Bytes96 = BytesImpl<96>; -pub type Bytes100 = BytesImpl<100>; - -impl From for TreeHash { - fn from(value: Bytes32) -> Self { - Self::new(value.0) - } -} - -impl From for Bytes32 { - fn from(value: TreeHash) -> Self { - Self(value.to_bytes()) - } -} - -#[cfg(feature = "py-bindings")] -impl<'py, const N: usize> IntoPyObject<'py> for BytesImpl { - type Target = PyAny; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - ChiaToPython::to_python(&self, py) - } -} - -#[cfg(feature = "py-bindings")] -impl ChiaToPython for BytesImpl { - fn to_python<'a>(&self, py: Python<'a>) -> PyResult> { - if N == 32 { - let bytes_module = PyModule::import(py, "chia_rs.sized_bytes")?; - let ty = bytes_module.getattr("bytes32")?; - ty.call1((self.0.into_pyobject(py)?,)) - } else if N == 48 { - let bytes_module = PyModule::import(py, "chia_rs.sized_bytes")?; - let ty = bytes_module.getattr("bytes48")?; - ty.call1((self.0.into_pyobject(py)?,)) - } else { - Ok(PyBytes::new(py, &self.0).into_any()) - } - } -} - -#[cfg(feature = "py-bindings")] -impl<'py, const N: usize> FromPyObject<'py> for BytesImpl { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - let b = obj.downcast::()?; - let slice: &[u8] = b.as_bytes(); - let buf: [u8; N] = slice.try_into()?; - Ok(BytesImpl::(buf)) - } -} - -#[cfg(feature = "py-bindings")] -impl<'py> IntoPyObject<'py> for Bytes { - type Target = PyAny; - type Output = Bound<'py, Self::Target>; - type Error = std::convert::Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(PyBytes::new(py, &self.0).into_any()) - } -} - -#[cfg(feature = "py-bindings")] -impl ChiaToPython for Bytes { - fn to_python<'a>(&self, py: Python<'a>) -> PyResult> { - Ok(PyBytes::new(py, &self.0).into_any()) - } -} - -#[cfg(feature = "py-bindings")] -impl<'py> FromPyObject<'py> for Bytes { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - let b = obj.downcast::()?; - Ok(Bytes(b.as_bytes().to_vec())) - } -} - -#[cfg(test)] -#[allow(clippy::needless_pass_by_value)] -mod tests { - use super::*; - - use clvmr::{ - serde::{node_from_bytes, node_to_bytes}, - Allocator, - }; - use rstest::rstest; - - #[rstest] - // Bytess32 - #[case( - "0000000000000000000000000000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - true - )] - #[case( - "0000000000000000000000000000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000100", - false - )] - #[case( - "fff0000000000000000000000000000000000000000000000000000000000100", - "fff0000000000000000000000000000000000000000000000000000000000100", - true - )] - // Bytes - #[case("000000", "000000", true)] - #[case("123456", "125456", false)] - #[case("000001", "00000001", false)] - #[case("00000001", "000001", false)] - #[case("ffff01", "ffff01", true)] - #[case("", "", true)] - fn test_bytes_comparisons(#[case] lhs: &str, #[case] rhs: &str, #[case] expect_equal: bool) { - let lhs_vec: Vec = hex::decode(lhs).expect("hex::decode"); - let rhs_vec: Vec = hex::decode(rhs).expect("hex::decode"); - - if lhs_vec.len() == 32 && rhs_vec.len() == 32 { - let lhs = Bytes32::try_from(&lhs_vec).unwrap(); - let rhs = Bytes32::try_from(&rhs_vec).unwrap(); - - assert_eq!(lhs.len(), 32); - assert_eq!(rhs.len(), 32); - - assert_eq!(lhs.is_empty(), lhs_vec.is_empty()); - assert_eq!(rhs.is_empty(), rhs_vec.is_empty()); - - if expect_equal { - assert_eq!(lhs, rhs); - assert_eq!(rhs, lhs); - } else { - assert!(lhs != rhs); - assert!(rhs != lhs); - } - } else { - let lhs = Bytes::from(lhs_vec.clone()); - let rhs = Bytes::from(rhs_vec.clone()); - - assert_eq!(lhs.len(), lhs_vec.len()); - assert_eq!(rhs.len(), rhs_vec.len()); - - assert_eq!(lhs.is_empty(), lhs_vec.is_empty()); - assert_eq!(rhs.is_empty(), rhs_vec.is_empty()); - - if expect_equal { - assert_eq!(lhs, rhs); - assert_eq!(rhs, lhs); - } else { - assert!(lhs != rhs); - assert!(rhs != lhs); - } - } - } - - fn from_bytes(buf: &[u8], expected: T) { - let mut input = Cursor::<&[u8]>::new(buf); - assert_eq!(T::parse::(&mut input).unwrap(), expected); - } - - fn from_bytes_fail( - buf: &[u8], - expected: chia_error::Error, - ) { - let mut input = Cursor::<&[u8]>::new(buf); - assert_eq!(T::parse::(&mut input).unwrap_err(), expected); - } - - fn stream(v: &T) -> Vec { - let mut buf = Vec::::new(); - v.stream(&mut buf).unwrap(); - let mut ctx1 = Sha256::new(); - let mut ctx2 = Sha256::new(); - v.update_digest(&mut ctx1); - ctx2.update(&buf); - assert_eq!(&ctx1.finalize(), &ctx2.finalize()); - buf - } - - #[test] - fn test_stream_bytes32() { - let buf = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, - ]; - let out = stream(&Bytes32::from(buf)); - assert_eq!(buf.as_slice(), &out); - } - - #[test] - fn test_stream_bytes() { - let val: Bytes = vec![ - 1_u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, - 24, 25, 26, 27, 28, 29, 30, 31, 32, - ] - .into(); - println!("{val:?}"); - let buf = stream(&val); - println!("buf: {buf:?}"); - from_bytes(&buf, val); - } - - #[test] - fn test_parse_bytes_empty() { - let buf: &[u8] = &[0, 0, 0, 0]; - from_bytes::(buf, [].to_vec().into()); - } - - #[test] - fn test_parse_bytes() { - let buf: &[u8] = &[0, 0, 0, 3, 1, 2, 3]; - from_bytes::(buf, [1_u8, 2, 3].to_vec().into()); - } - - #[test] - fn test_parse_truncated_len() { - let buf: &[u8] = &[0, 0, 1]; - from_bytes_fail::(buf, chia_error::Error::EndOfBuffer); - } - - #[test] - fn test_parse_truncated() { - let buf: &[u8] = &[0, 0, 0, 4, 1, 2, 3]; - from_bytes_fail::(buf, chia_error::Error::EndOfBuffer); - } - - #[test] - fn test_parse_bytes32() { - let buf = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, - ]; - from_bytes::(&buf, Bytes32::from(buf)); - from_bytes_fail::(&buf[0..30], chia_error::Error::EndOfBuffer); - } - - #[test] - fn test_parse_bytes48() { - let buf = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, - 47, 48, - ]; - from_bytes::(&buf, Bytes48::from(buf)); - from_bytes_fail::(&buf[0..47], chia_error::Error::EndOfBuffer); - } - - #[test] - fn bytes_roundtrip() { - let a = &mut Allocator::new(); - let expected = "84facef00d"; - let expected_bytes = hex::decode(expected).unwrap(); - - let ptr = node_from_bytes(a, &expected_bytes).unwrap(); - let bytes = Bytes::from_clvm(a, ptr).unwrap(); - - let round_trip = bytes.to_clvm(a).unwrap(); - assert_eq!(expected, hex::encode(node_to_bytes(a, round_trip).unwrap())); - } - - #[test] - fn bytes32_roundtrip() { - let a = &mut Allocator::new(); - let expected = "a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9"; - let expected_bytes = hex::decode(expected).unwrap(); - - let ptr = node_from_bytes(a, &expected_bytes).unwrap(); - let bytes32 = Bytes32::from_clvm(a, ptr).unwrap(); - - let round_trip = bytes32.to_clvm(a).unwrap(); - assert_eq!(expected, hex::encode(node_to_bytes(a, round_trip).unwrap())); - } - - #[test] - fn bytes32_failure() { - let a = &mut Allocator::new(); - let bytes = - hex::decode("f07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9").unwrap(); - let ptr = a.new_atom(&bytes).unwrap(); - assert!(Bytes32::from_clvm(a, ptr).is_err()); - - let ptr = a.new_pair(a.one(), a.one()).unwrap(); - assert_eq!( - Bytes32::from_clvm(a, ptr).unwrap_err(), - FromClvmError::ExpectedAtom - ); - } -} diff --git a/vendor/chia-protocol/src/chia_protocol.rs b/vendor/chia-protocol/src/chia_protocol.rs deleted file mode 100644 index 8e8f5b6..0000000 --- a/vendor/chia-protocol/src/chia_protocol.rs +++ /dev/null @@ -1,273 +0,0 @@ -use chia_streamable_macro::{streamable, Streamable}; - -use crate::Bytes; - -#[cfg(feature = "py-bindings")] -use chia_py_streamable_macro::{PyJsonDict, PyStreamable}; - -#[repr(u8)] -#[cfg_attr(feature = "py-bindings", derive(PyJsonDict, PyStreamable))] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive(Streamable, Hash, Debug, Copy, Clone, Eq, PartialEq)] -pub enum ProtocolMessageTypes { - // Shared protocol (all services) - Handshake = 1, - - // Harvester protocol (harvester <-> farmer) - HarvesterHandshake = 3, - // NewSignagePointHarvester = 4 Changed to 66 in new protocol - NewProofOfSpace = 5, - RequestSignatures = 6, - RespondSignatures = 7, - - // Farmer protocol (farmer <-> fullNode) - NewSignagePoint = 8, - DeclareProofOfSpace = 9, - RequestSignedValues = 10, - SignedValues = 11, - FarmingInfo = 12, - - // Timelord protocol (timelord <-> fullNode) - NewPeakTimelord = 13, - NewUnfinishedBlockTimelord = 14, - NewInfusionPointVdf = 15, - NewSignagePointVdf = 16, - NewEndOfSubSlotVdf = 17, - RequestCompactProofOfTime = 18, - RespondCompactProofOfTime = 19, - - // Full node protocol (fullNode <-> fullNode) - NewPeak = 20, - NewTransaction = 21, - RequestTransaction = 22, - RespondTransaction = 23, - RequestProofOfWeight = 24, - RespondProofOfWeight = 25, - RequestBlock = 26, - RespondBlock = 27, - RejectBlock = 28, - RequestBlocks = 29, - RespondBlocks = 30, - RejectBlocks = 31, - NewUnfinishedBlock = 32, - RequestUnfinishedBlock = 33, - RespondUnfinishedBlock = 34, - NewSignagePointOrEndOfSubSlot = 35, - RequestSignagePointOrEndOfSubSlot = 36, - RespondSignagePoint = 37, - RespondEndOfSubSlot = 38, - RequestMempoolTransactions = 39, - RequestCompactVDF = 40, - RespondCompactVDF = 41, - NewCompactVDF = 42, - RequestPeers = 43, - RespondPeers = 44, - NoneResponse = 91, - - // Wallet protocol (wallet <-> fullNode) - RequestPuzzleSolution = 45, - RespondPuzzleSolution = 46, - RejectPuzzleSolution = 47, - SendTransaction = 48, - TransactionAck = 49, - NewPeakWallet = 50, - RequestBlockHeader = 51, - RespondBlockHeader = 52, - RejectHeaderRequest = 53, - RequestRemovals = 54, - RespondRemovals = 55, - RejectRemovalsRequest = 56, - RequestAdditions = 57, - RespondAdditions = 58, - RejectAdditionsRequest = 59, - RequestHeaderBlocks = 60, - RejectHeaderBlocks = 61, - RespondHeaderBlocks = 62, - - // Introducer protocol (introducer <-> fullNode) - RequestPeersIntroducer = 63, - RespondPeersIntroducer = 64, - - // Simulator protocol - FarmNewBlock = 65, - - // New harvester protocol - NewSignagePointHarvester = 66, - RequestPlots = 67, - RespondPlots = 68, - PlotSyncStart = 78, - PlotSyncLoaded = 79, - PlotSyncRemoved = 80, - PlotSyncInvalid = 81, - PlotSyncKeysMissing = 82, - PlotSyncDuplicates = 83, - PlotSyncDone = 84, - PlotSyncResponse = 85, - - // More wallet protocol - CoinStateUpdate = 69, - RegisterForPhUpdates = 70, - RespondToPhUpdates = 71, - RegisterForCoinUpdates = 72, - RespondToCoinUpdates = 73, - RequestChildren = 74, - RespondChildren = 75, - RequestSesInfo = 76, - RespondSesInfo = 77, - RequestBlockHeaders = 86, - RejectBlockHeaders = 87, - RespondBlockHeaders = 88, - RequestFeeEstimates = 89, - RespondFeeEstimates = 90, - - // Unfinished block protocol - NewUnfinishedBlock2 = 92, - RequestUnfinishedBlock2 = 93, - - // New wallet sync protocol - RequestRemovePuzzleSubscriptions = 94, - RespondRemovePuzzleSubscriptions = 95, - RequestRemoveCoinSubscriptions = 96, - RespondRemoveCoinSubscriptions = 97, - RequestPuzzleState = 98, - RespondPuzzleState = 99, - RejectPuzzleState = 100, - RequestCoinState = 101, - RespondCoinState = 102, - RejectCoinState = 103, - - // Wallet protocol mempool updates - MempoolItemsAdded = 104, - MempoolItemsRemoved = 105, - RequestCostInfo = 106, - RespondCostInfo = 107, - - // ------------------------------------------------------------------------- - // DIG L2 consensus band (200-217) — the `DigMessageType` opcodes (#1404). - // These extend Chia's namespace so a stock `Message` can carry a DIG consensus - // opcode on the wire (its `msg_type` field is a `ProtocolMessageTypes`). Each - // value MUST equal the matching `dig_peer_protocol::DigMessageType` discriminant; - // `frame_dig_message` in dig-gossip converts one to the other losslessly. They are - // ADDITIVE (§5.1): no existing opcode moves. dig-gossip's `broadcast_dig`/`send_dig` - // (via `route_dig_message`) are the ONLY sanctioned way to put them on the wire. - // ------------------------------------------------------------------------- - NewAttestation = 200, - NewCheckpointProposal = 201, - NewCheckpointSignature = 202, - RequestCheckpointSignatures = 203, - RespondCheckpointSignatures = 204, - RequestStatus = 205, - RespondStatus = 206, - NewCheckpointSubmission = 207, - ValidatorAnnounce = 208, - RequestBlockTransactions = 209, - RespondBlockTransactions = 210, - ReconciliationSketch = 211, - ReconciliationResponse = 212, - StemTransaction = 213, - PlumtreeLazyAnnounce = 214, - PlumtreePrune = 215, - PlumtreeGraft = 216, - PlumtreeRequestByHash = 217, - - // ------------------------------------------------------------------------- - // DIG dig-gossip extension — introducer registration (DSC-005 / SPEC §6.5). - // Upstream Chia does not assign these; `dig-gossip` vendors `chia-protocol` to reserve - // stable opcodes for `RegisterPeer` / `RegisterAck` bodies (`introducer_register_wire.rs`). - // ------------------------------------------------------------------------- - RegisterPeer = 218, - RegisterAck = 219, - - // ------------------------------------------------------------------------- - // DIG dig-message directed-envelope transport (WU6 / epic #796, Wave A). - // Opcode 220 carries a dig-message envelope as OPAQUE bytes in `Message.data`. - // dig-gossip is the transport only — it never seals/opens the envelope. This - // is the FIRST opcode of the 220-255 "free" band (200-219 are the consensus - // band, `DigMessageType`); the canonical constant is `dig_protocol::DIG_MESSAGE` - // and `crate::service::dig_message::DIG_MESSAGE`. - // ------------------------------------------------------------------------- - DigMessage = 220, - - // ------------------------------------------------------------------------- - // DIG store-melted broadcast (epic #1316, piece #1). - // Opcode 221 announces that a dig-store's on-chain coin has been melted so peers - // stop hosting its `.dig` content. A PUBLIC all-peers flood broadcast (public-by- - // nature: store deletion is addressed to everyone), signed + mTLS-authenticated, - // NOT recipient-sealed (§5.4-EXEMPT, same carve-out as L2 consensus gossip). The - // payload is `StoreMeltedAnnounce` (`crate::service::store_melted`); the canonical - // constant is `crate::service::store_melted::STORE_MELTED`. Second opcode of the - // 220-255 "free" band, after `DigMessage = 220`. - // ------------------------------------------------------------------------- - StoreMelted = 221, - - // ------------------------------------------------------------------------- - // DIG holdings-announce broadcast (#1428, decider-locked spec #1394). - // Opcode 222 announces a batch of signed holdings add/remove deltas so peers learn - // which content a provider holds (feeds dig-dht's holder set). A PUBLIC all-peers - // flood broadcast (public discovery, addressed to everyone), signed + mTLS- - // authenticated, NOT recipient-sealed (§5.4-EXEMPT, same carve-out as L2 consensus - // gossip). The payload is `HoldingsAnnounce` (`crate::service::holdings_announce`); - // the canonical constant is `crate::service::holdings_announce::HOLDINGS_ANNOUNCE`. - // Third opcode of the 220-255 "free" band, after `StoreMelted = 221`. - // ------------------------------------------------------------------------- - HoldingsAnnounce = 222, -} - -#[cfg(feature = "py-bindings")] -impl chia_traits::ChiaToPython for ProtocolMessageTypes { - fn to_python<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - Ok(pyo3::IntoPyObject::into_pyobject(*self as u8, py)? - .clone() - .into_any()) - } -} - -pub trait ChiaProtocolMessage { - fn msg_type() -> ProtocolMessageTypes; -} - -#[repr(u8)] -#[cfg_attr(feature = "py-bindings", derive(PyJsonDict, PyStreamable))] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive(Streamable, Hash, Debug, Copy, Clone, Eq, PartialEq)] -pub enum NodeType { - FullNode = 1, - Harvester = 2, - Farmer = 3, - Timelord = 4, - Introducer = 5, - Wallet = 6, - DataLayer = 7, -} - -#[cfg(feature = "py-bindings")] -impl chia_traits::ChiaToPython for NodeType { - fn to_python<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - Ok(pyo3::IntoPyObject::into_pyobject(*self as u8, py)? - .clone() - .into_any()) - } -} - -#[streamable(no_serde)] -pub struct Message { - msg_type: ProtocolMessageTypes, - id: Option, - data: Bytes, -} - -#[streamable(message)] -pub struct Handshake { - // Network id, usually the genesis challenge of the blockchain - network_id: String, - // Protocol version to determine which messages the peer supports - protocol_version: String, - // Version of the software, to debug and determine feature support - software_version: String, - // Which port the server is listening on - server_port: u16, - // NodeType (full node, wallet, farmer, etc.) - node_type: NodeType, - // Key value dict to signal support for additional capabilities/features - capabilities: Vec<(u16, String)>, -} diff --git a/vendor/chia-protocol/src/classgroup.rs b/vendor/chia-protocol/src/classgroup.rs deleted file mode 100644 index 52f1790..0000000 --- a/vendor/chia-protocol/src/classgroup.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::Bytes100; -use chia_streamable_macro::streamable; - -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -#[streamable] -#[derive(Copy)] -pub struct ClassgroupElement { - data: Bytes100, -} - -impl Default for ClassgroupElement { - fn default() -> Self { - let mut data = [0_u8; 100]; - data[0] = 0x08; - Self { data: data.into() } - } -} - -impl ClassgroupElement { - pub const SIZE: usize = 100; -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl ClassgroupElement { - #[staticmethod] - pub fn create(bytes: &[u8]) -> ClassgroupElement { - if bytes.len() == 100 { - ClassgroupElement { - data: bytes.try_into().unwrap(), - } - } else { - assert!(bytes.len() < 100); - let mut data = [0_u8; 100]; - data[..bytes.len()].copy_from_slice(bytes); - ClassgroupElement { data: data.into() } - } - } - - #[staticmethod] - #[pyo3(name = "get_default_element")] - pub fn py_get_default_element() -> ClassgroupElement { - Self::default() - } - - #[staticmethod] - #[pyo3(name = "get_size")] - pub fn py_get_size() -> i32 { - Self::SIZE as i32 - } -} diff --git a/vendor/chia-protocol/src/coin.rs b/vendor/chia-protocol/src/coin.rs deleted file mode 100644 index 0a63428..0000000 --- a/vendor/chia-protocol/src/coin.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::{Bytes32, BytesImpl}; -use chia_sha2::Sha256; -use chia_streamable_macro::streamable; -use clvm_traits::{ - clvm_list, destructure_list, match_list, ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, - ToClvm, ToClvmError, -}; - -#[cfg(feature = "py-bindings")] -use pyo3::exceptions::PyNotImplementedError; -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; -#[cfg(feature = "py-bindings")] -use pyo3::types::PyType; - -#[streamable] -#[derive(Copy)] -pub struct Coin { - parent_coin_info: Bytes32, - puzzle_hash: Bytes32, - amount: u64, -} - -impl Coin { - pub fn coin_id(&self) -> Bytes32 { - let mut hasher = Sha256::new(); - hasher.update(self.parent_coin_info); - hasher.update(self.puzzle_hash); - - let amount_bytes = self.amount.to_be_bytes(); - if self.amount >= 0x8000_0000_0000_0000_u64 { - hasher.update([0_u8]); - hasher.update(amount_bytes); - } else { - let start = match self.amount { - n if n >= 0x0080_0000_0000_0000_u64 => 0, - n if n >= 0x8000_0000_0000_u64 => 1, - n if n >= 0x0080_0000_0000_u64 => 2, - n if n >= 0x8000_0000_u64 => 3, - n if n >= 0x0080_0000_u64 => 4, - n if n >= 0x8000_u64 => 5, - n if n >= 0x80_u64 => 6, - n if n > 0 => 7, - _ => 8, - }; - hasher.update(&amount_bytes[start..]); - } - - let coin_id: [u8; 32] = hasher.finalize().as_slice().try_into().unwrap(); - Bytes32::new(coin_id) - } -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl Coin { - fn name(&self) -> Bytes32 { - self.coin_id() - } -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl Coin { - #[classmethod] - #[pyo3(name = "from_parent")] - pub fn from_parent(_cls: &Bound<'_, PyType>, _coin: Self) -> PyResult { - Err(PyNotImplementedError::new_err( - "Coin does not support from_parent().", - )) - } -} - -impl> ToClvm for Coin { - fn to_clvm(&self, encoder: &mut E) -> Result { - clvm_list!(self.parent_coin_info, self.puzzle_hash, self.amount).to_clvm(encoder) - } -} - -impl> FromClvm for Coin { - fn from_clvm(decoder: &D, node: N) -> Result { - let destructure_list!(parent_coin_info, puzzle_hash, amount) = - , BytesImpl<32>, u64)>::from_clvm(decoder, node)?; - Ok(Coin { - parent_coin_info, - puzzle_hash, - amount, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use clvmr::{ - serde::{node_from_bytes, node_to_bytes}, - Allocator, - }; - use rstest::rstest; - - #[rstest] - #[case(0, &[])] - #[case(1, &[1])] - #[case(0xff, &[0, 0xff])] - #[case(0xffff, &[0, 0xff, 0xff])] - #[case(0x00ff_ffff, &[0, 0xff, 0xff, 0xff])] - #[case(0xffff_ffff, &[0, 0xff, 0xff, 0xff, 0xff])] - #[case(0x00ff_ffff_ffff, &[0, 0xff, 0xff, 0xff, 0xff, 0xff])] - #[case(0xffff_ffff_ffff_ffff, &[0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])] - #[case(0x7f, &[0x7f])] - #[case(0x7fff, &[0x7f, 0xff])] - #[case(0x007f_ffff, &[0x7f, 0xff, 0xff])] - #[case(0x7fff_ffff, &[0x7f, 0xff, 0xff, 0xff])] - #[case(0x007f_ffff_ffff, &[0x7f, 0xff, 0xff, 0xff, 0xff])] - #[case(0x7fff_ffff_ffff_ffff, &[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])] - #[case(0x80, &[0, 0x80])] - #[case(0x8000, &[0, 0x80, 0x00])] - #[case(0x0080_0000, &[0, 0x80, 0x00, 0x00])] - #[case(0x8000_0000, &[0, 0x80, 0x00, 0x00, 0x00])] - #[case(0x0080_0000_0000, &[0, 0x80, 0x00, 0x00, 0x00, 0x00])] - #[case(0x8000_0000_0000_0000, &[0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])] - fn coin_id(#[case] amount: u64, #[case] bytes: &[u8]) { - let parent_coin = b"---foo--- "; - let puzzle_hash = b"---bar--- "; - - let c = Coin::new(parent_coin.into(), puzzle_hash.into(), amount); - let mut sha256 = Sha256::new(); - sha256.update(parent_coin); - sha256.update(puzzle_hash); - sha256.update(bytes); - assert_eq!(c.coin_id().to_bytes(), sha256.finalize().as_ref()); - } - - #[test] - fn coin_roundtrip() { - let a = &mut Allocator::new(); - let expected = "ffa09e144397decd2b831551f9710c17ae776d9c5a3ae5283c5f9747263fd1255381ffa0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ff0180"; - let expected_bytes = hex::decode(expected).unwrap(); - - let ptr = node_from_bytes(a, &expected_bytes).unwrap(); - let coin = Coin::from_clvm(a, ptr).unwrap(); - - let round_trip = coin.to_clvm(a).unwrap(); - assert_eq!(expected, hex::encode(node_to_bytes(a, round_trip).unwrap())); - } -} diff --git a/vendor/chia-protocol/src/coin_spend.rs b/vendor/chia-protocol/src/coin_spend.rs deleted file mode 100644 index 0cfdd39..0000000 --- a/vendor/chia-protocol/src/coin_spend.rs +++ /dev/null @@ -1,28 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::coin::Coin; -use crate::program::Program; -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; -#[cfg(feature = "py-bindings")] -use pyo3::types::PyType; - -#[streamable] -pub struct CoinSpend { - coin: Coin, - puzzle_reveal: Program, - solution: Program, -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl CoinSpend { - #[classmethod] - #[pyo3(name = "from_parent")] - pub fn from_parent(cls: &Bound<'_, PyType>, py: Python<'_>, cs: Self) -> PyResult { - // Convert result into potential child class - let instance = cls.call1((cs.coin, cs.puzzle_reveal, cs.solution))?; - - Ok(instance.into_pyobject(py)?.unbind()) - } -} diff --git a/vendor/chia-protocol/src/coin_state.rs b/vendor/chia-protocol/src/coin_state.rs deleted file mode 100644 index 07c16de..0000000 --- a/vendor/chia-protocol/src/coin_state.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::coin::Coin; -use chia_streamable_macro::streamable; - -#[streamable] -#[derive(Copy)] -pub struct CoinState { - coin: Coin, - spent_height: Option, - created_height: Option, -} diff --git a/vendor/chia-protocol/src/end_of_sub_slot_bundle.rs b/vendor/chia-protocol/src/end_of_sub_slot_bundle.rs deleted file mode 100644 index 4db3bc5..0000000 --- a/vendor/chia-protocol/src/end_of_sub_slot_bundle.rs +++ /dev/null @@ -1,14 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::ChallengeChainSubSlot; -use crate::InfusedChallengeChainSubSlot; -use crate::RewardChainSubSlot; -use crate::SubSlotProofs; - -#[streamable] -pub struct EndOfSubSlotBundle { - challenge_chain: ChallengeChainSubSlot, - infused_challenge_chain: Option, - reward_chain: RewardChainSubSlot, - proofs: SubSlotProofs, -} diff --git a/vendor/chia-protocol/src/fee_estimate.rs b/vendor/chia-protocol/src/fee_estimate.rs deleted file mode 100644 index 1d97e2d..0000000 --- a/vendor/chia-protocol/src/fee_estimate.rs +++ /dev/null @@ -1,22 +0,0 @@ -use chia_streamable_macro::streamable; - -#[streamable] -pub struct FeeRate { - // Represents Fee Rate in mojos divided by CLVM Cost. - // Performs XCH/mojo conversion. - // Similar to 'Fee per cost'. - mojos_per_clvm_cost: u64, -} - -#[streamable] -pub struct FeeEstimate { - error: Option, - time_target: u64, // unix time stamp in seconds - estimated_fee_rate: FeeRate, // Mojos per clvm cost -} - -#[streamable] -pub struct FeeEstimateGroup { - error: Option, - estimates: Vec, -} diff --git a/vendor/chia-protocol/src/foliage.rs b/vendor/chia-protocol/src/foliage.rs deleted file mode 100644 index 78698d6..0000000 --- a/vendor/chia-protocol/src/foliage.rs +++ /dev/null @@ -1,51 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::Bytes32; -use crate::Coin; -use crate::PoolTarget; -use chia_bls::G2Element; - -#[streamable] -pub struct TransactionsInfo { - // Information that goes along with each transaction block - generator_root: Bytes32, // sha256 of the block generator in this block - generator_refs_root: Bytes32, // sha256 of the concatenation of the generator ref list entries - aggregated_signature: G2Element, - fees: u64, // This only includes user fees, not block rewards - cost: u64, // This is the total cost of this block, including CLVM cost, cost of program size and conditions - reward_claims_incorporated: Vec, // These can be in any order -} - -#[streamable] -pub struct FoliageTransactionBlock { - // Information that goes along with each transaction block that is relevant for light clients - prev_transaction_block_hash: Bytes32, - timestamp: u64, - filter_hash: Bytes32, - additions_root: Bytes32, - removals_root: Bytes32, - transactions_info_hash: Bytes32, -} - -#[streamable] -pub struct FoliageBlockData { - // Part of the block that is signed by the plot key - unfinished_reward_block_hash: Bytes32, - pool_target: PoolTarget, - pool_signature: Option, // Iff ProofOfSpace has a pool pk - farmer_reward_puzzle_hash: Bytes32, - extension_data: Bytes32, // Used for future updates. Can be any 32 byte value initially -} - -#[streamable] -pub struct Foliage { - // The entire foliage block, containing signature and the unsigned back pointer - // The hash of this is the "header hash". Note that for unfinished blocks, the prev_block_hash - // Is the prev from the signage point, and can be replaced with a more recent block - prev_block_hash: Bytes32, - reward_block_hash: Bytes32, - foliage_block_data: FoliageBlockData, - foliage_block_data_signature: G2Element, - foliage_transaction_block_hash: Option, - foliage_transaction_block_signature: Option, -} diff --git a/vendor/chia-protocol/src/full_node_protocol.rs b/vendor/chia-protocol/src/full_node_protocol.rs deleted file mode 100644 index 246c9b9..0000000 --- a/vendor/chia-protocol/src/full_node_protocol.rs +++ /dev/null @@ -1,179 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::EndOfSubSlotBundle; -use crate::FullBlock; -use crate::SpendBundle; -use crate::TimestampedPeerInfo; -use crate::UnfinishedBlock; -use crate::VDFInfo; -use crate::VDFProof; -use crate::WeightProof; -use crate::{Bytes, Bytes32}; - -#[streamable(message)] -pub struct NewPeak { - header_hash: Bytes32, - height: u32, - weight: u128, - fork_point_with_previous_peak: u32, - unfinished_reward_block_hash: Bytes32, -} - -#[streamable(message)] -pub struct NewTransaction { - transaction_id: Bytes32, - cost: u64, - fees: u64, -} - -#[streamable(message)] -pub struct RequestTransaction { - transaction_id: Bytes32, -} - -#[streamable(message)] -pub struct RespondTransaction { - transaction: SpendBundle, -} - -#[streamable(message)] -pub struct RequestProofOfWeight { - total_number_of_blocks: u32, - tip: Bytes32, -} - -#[streamable(message)] -pub struct RespondProofOfWeight { - wp: WeightProof, - tip: Bytes32, -} - -#[streamable(message)] -pub struct RequestBlock { - height: u32, - include_transaction_block: bool, -} - -#[streamable(message)] -pub struct RejectBlock { - height: u32, -} - -#[streamable(message)] -pub struct RequestBlocks { - start_height: u32, - end_height: u32, - include_transaction_block: bool, -} - -#[streamable(message)] -pub struct RespondBlocks { - start_height: u32, - end_height: u32, - blocks: Vec, -} - -#[streamable(message)] -pub struct RejectBlocks { - start_height: u32, - end_height: u32, -} - -#[streamable(message)] -pub struct RespondBlock { - block: FullBlock, -} - -#[streamable(message)] -pub struct NewUnfinishedBlock { - unfinished_reward_hash: Bytes32, -} - -#[streamable(message)] -pub struct RequestUnfinishedBlock { - unfinished_reward_hash: Bytes32, -} - -#[streamable(message)] -pub struct RespondUnfinishedBlock { - unfinished_block: UnfinishedBlock, -} - -#[streamable(message)] -pub struct NewSignagePointOrEndOfSubSlot { - prev_challenge_hash: Option, - challenge_hash: Bytes32, - index_from_challenge: u8, - last_rc_infusion: Bytes32, -} - -#[streamable(message)] -pub struct RequestSignagePointOrEndOfSubSlot { - challenge_hash: Bytes32, - index_from_challenge: u8, - last_rc_infusion: Bytes32, -} - -#[streamable(message)] -pub struct RespondSignagePoint { - index_from_challenge: u8, - challenge_chain_vdf: VDFInfo, - challenge_chain_proof: VDFProof, - reward_chain_vdf: VDFInfo, - reward_chain_proof: VDFProof, -} - -#[streamable(message)] -pub struct RespondEndOfSubSlot { - end_of_slot_bundle: EndOfSubSlotBundle, -} - -#[streamable(message)] -pub struct RequestMempoolTransactions { - filter: Bytes, -} - -#[streamable(message)] -pub struct NewCompactVDF { - height: u32, - header_hash: Bytes32, - field_vdf: u8, - vdf_info: VDFInfo, -} - -#[streamable(message)] -pub struct RequestCompactVDF { - height: u32, - header_hash: Bytes32, - field_vdf: u8, - vdf_info: VDFInfo, -} - -#[streamable(message)] -pub struct RespondCompactVDF { - height: u32, - header_hash: Bytes32, - field_vdf: u8, - vdf_info: VDFInfo, - vdf_proof: VDFProof, -} - -#[streamable(message)] -pub struct RequestPeers {} - -#[streamable(message)] -pub struct RespondPeers { - peer_list: Vec, -} - -#[streamable(message)] -pub struct NewUnfinishedBlock2 { - unfinished_reward_hash: Bytes32, - foliage_hash: Option, -} - -#[streamable(message)] -pub struct RequestUnfinishedBlock2 { - unfinished_reward_hash: Bytes32, - foliage_hash: Option, -} diff --git a/vendor/chia-protocol/src/fullblock.rs b/vendor/chia-protocol/src/fullblock.rs deleted file mode 100644 index 53f3657..0000000 --- a/vendor/chia-protocol/src/fullblock.rs +++ /dev/null @@ -1,140 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::Bytes32; -use crate::Coin; -use crate::EndOfSubSlotBundle; -use crate::Program; -use crate::RewardChainBlock; -use crate::VDFProof; -use crate::{Foliage, FoliageTransactionBlock, TransactionsInfo}; -use chia_traits::Streamable; - -#[streamable] -pub struct FullBlock { - finished_sub_slots: Vec, - reward_chain_block: RewardChainBlock, - challenge_chain_sp_proof: Option, // # If not first sp in sub-slot - challenge_chain_ip_proof: VDFProof, - reward_chain_sp_proof: Option, // # If not first sp in sub-slot - reward_chain_ip_proof: VDFProof, - infused_challenge_chain_ip_proof: Option, // # Iff deficit < 4 - foliage: Foliage, // # Reward chain foliage data - foliage_transaction_block: Option, // # Reward chain foliage data (tx block) - transactions_info: Option, // Reward chain foliage data (tx block additional) - transactions_generator: Option, // Program that generates transactions - transactions_generator_ref_list: Vec, // List of block heights of previous generators referenced in this block -} - -impl FullBlock { - pub fn prev_header_hash(&self) -> Bytes32 { - self.foliage.prev_block_hash - } - - pub fn header_hash(&self) -> Bytes32 { - self.foliage.hash().into() - } - - pub fn is_transaction_block(&self) -> bool { - self.foliage.foliage_transaction_block_hash.is_some() - } - - pub fn total_iters(&self) -> u128 { - self.reward_chain_block.total_iters - } - - pub fn height(&self) -> u32 { - self.reward_chain_block.height - } - - pub fn weight(&self) -> u128 { - self.reward_chain_block.weight - } - - pub fn get_included_reward_coins(&self) -> Vec { - if let Some(ti) = &self.transactions_info { - ti.reward_claims_incorporated.clone() - } else { - vec![] - } - } - - pub fn is_fully_compactified(&self) -> bool { - for sub_slot in &self.finished_sub_slots { - if sub_slot.proofs.challenge_chain_slot_proof.witness_type != 0 - || !sub_slot - .proofs - .challenge_chain_slot_proof - .normalized_to_identity - { - return false; - } - if let Some(proof) = &sub_slot.proofs.infused_challenge_chain_slot_proof { - if proof.witness_type != 0 || !proof.normalized_to_identity { - return false; - } - } - } - - if let Some(proof) = &self.challenge_chain_sp_proof { - if proof.witness_type != 0 || !proof.normalized_to_identity { - return false; - } - } - self.challenge_chain_ip_proof.witness_type == 0 - && self.challenge_chain_ip_proof.normalized_to_identity - } -} - -#[cfg(feature = "py-bindings")] -use chia_traits::ChiaToPython; -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl FullBlock { - #[getter] - #[pyo3(name = "prev_header_hash")] - fn py_prev_header_hash(&self) -> Bytes32 { - self.prev_header_hash() - } - - #[getter] - #[pyo3(name = "header_hash")] - fn py_header_hash(&self) -> Bytes32 { - self.header_hash() - } - - #[pyo3(name = "is_transaction_block")] - fn py_is_transaction_block(&self) -> bool { - self.is_transaction_block() - } - - #[getter] - #[pyo3(name = "total_iters")] - fn py_total_iters<'a>(&self, py: Python<'a>) -> PyResult> { - ChiaToPython::to_python(&self.total_iters(), py) - } - - #[getter] - #[pyo3(name = "height")] - fn py_height<'a>(&self, py: Python<'a>) -> PyResult> { - ChiaToPython::to_python(&self.height(), py) - } - - #[getter] - #[pyo3(name = "weight")] - fn py_weight<'a>(&self, py: Python<'a>) -> PyResult> { - ChiaToPython::to_python(&self.weight(), py) - } - - #[pyo3(name = "get_included_reward_coins")] - fn py_get_included_reward_coins(&self) -> Vec { - self.get_included_reward_coins() - } - - #[pyo3(name = "is_fully_compactified")] - fn py_is_fully_compactified(&self) -> bool { - self.is_fully_compactified() - } -} diff --git a/vendor/chia-protocol/src/header_block.rs b/vendor/chia-protocol/src/header_block.rs deleted file mode 100644 index 1e61076..0000000 --- a/vendor/chia-protocol/src/header_block.rs +++ /dev/null @@ -1,151 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::unfinished_header_block::UnfinishedHeaderBlock; -use crate::Bytes; -use crate::Bytes32; -use crate::EndOfSubSlotBundle; -use crate::RewardChainBlock; -use crate::VDFProof; -use crate::{Foliage, FoliageTransactionBlock, TransactionsInfo}; -use chia_traits::Streamable; - -#[streamable] -pub struct HeaderBlock { - // If first sb - finished_sub_slots: Vec, - // Reward chain trunk data - reward_chain_block: RewardChainBlock, - // If not first sp in sub-slot - challenge_chain_sp_proof: Option, - challenge_chain_ip_proof: VDFProof, - // If not first sp in sub-slot - reward_chain_sp_proof: Option, - reward_chain_ip_proof: VDFProof, - // Iff deficit < 4 - infused_challenge_chain_ip_proof: Option, - // Reward chain foliage data - foliage: Foliage, - // Reward chain foliage data (tx block) - foliage_transaction_block: Option, - // Filter for block transactions - transactions_filter: Bytes, - // Reward chain foliage data (tx block additional) - transactions_info: Option, -} - -impl HeaderBlock { - pub fn prev_header_hash(&self) -> Bytes32 { - self.foliage.prev_block_hash - } - - pub fn prev_hash(&self) -> Bytes32 { - self.foliage.prev_block_hash - } - - pub fn height(&self) -> u32 { - self.reward_chain_block.height - } - - pub fn weight(&self) -> u128 { - self.reward_chain_block.weight - } - - pub fn header_hash(&self) -> Bytes32 { - self.foliage.hash().into() - } - - pub fn total_iters(&self) -> u128 { - self.reward_chain_block.total_iters - } - - pub fn log_string(&self) -> String { - format!( - "block {:?} sb_height {} ", - self.header_hash(), - self.height() - ) - } - - pub fn is_transaction_block(&self) -> bool { - self.reward_chain_block.is_transaction_block - } - - pub fn first_in_sub_slot(&self) -> bool { - !self.finished_sub_slots.is_empty() - } - - pub fn into_unfinished_header_block(self) -> UnfinishedHeaderBlock { - UnfinishedHeaderBlock { - finished_sub_slots: self.finished_sub_slots, - reward_chain_block: self.reward_chain_block.get_unfinished(), - challenge_chain_sp_proof: self.challenge_chain_sp_proof, - reward_chain_sp_proof: self.reward_chain_sp_proof, - foliage: self.foliage, - foliage_transaction_block: self.foliage_transaction_block, - transactions_filter: self.transactions_filter, - } - } -} - -#[cfg(feature = "py-bindings")] -use chia_traits::ChiaToPython; -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl HeaderBlock { - #[getter] - #[pyo3(name = "prev_header_hash")] - fn py_prev_header_hash(&self) -> Bytes32 { - self.prev_header_hash() - } - - #[getter] - #[pyo3(name = "prev_hash")] - fn py_prev_hash(&self) -> Bytes32 { - self.prev_hash() - } - - #[getter] - #[pyo3(name = "height")] - fn py_height<'a>(&self, py: Python<'a>) -> PyResult> { - ChiaToPython::to_python(&self.height(), py) - } - - #[getter] - #[pyo3(name = "weight")] - fn py_weight<'a>(&self, py: Python<'a>) -> PyResult> { - ChiaToPython::to_python(&self.weight(), py) - } - - #[getter] - #[pyo3(name = "header_hash")] - fn py_header_hash(&self) -> Bytes32 { - self.header_hash() - } - - #[getter] - #[pyo3(name = "total_iters")] - fn py_total_iters<'a>(&self, py: Python<'a>) -> PyResult> { - ChiaToPython::to_python(&self.total_iters(), py) - } - - #[getter] - #[pyo3(name = "log_string")] - fn py_log_string(&self) -> String { - self.log_string() - } - - #[getter] - #[pyo3(name = "is_transaction_block")] - fn py_is_transaction_block(&self) -> bool { - self.is_transaction_block() - } - - #[getter] - #[pyo3(name = "first_in_sub_slot")] - fn py_first_in_sub_slot(&self) -> bool { - self.first_in_sub_slot() - } -} diff --git a/vendor/chia-protocol/src/lazy_node.rs b/vendor/chia-protocol/src/lazy_node.rs deleted file mode 100644 index 8bde2f0..0000000 --- a/vendor/chia-protocol/src/lazy_node.rs +++ /dev/null @@ -1,44 +0,0 @@ -use clvmr::{allocator::NodePtr, allocator::SExp, Allocator}; -use pyo3::prelude::*; -use pyo3::types::PyBytes; -use std::rc::Rc; - -#[pyclass(subclass, unsendable, frozen)] -#[derive(Clone)] -pub struct LazyNode { - allocator: Rc, - node: NodePtr, -} - -#[pymethods] -impl LazyNode { - #[getter(pair)] - pub fn pair(&self, py: Python<'_>) -> PyResult> { - match &self.allocator.sexp(self.node) { - SExp::Pair(p1, p2) => { - let r1 = Self::new(self.allocator.clone(), *p1); - let r2 = Self::new(self.allocator.clone(), *p2); - let v = (r1, r2).into_pyobject(py)?; - Ok(Some(v.into())) - } - SExp::Atom => Ok(None), - } - } - - #[getter(atom)] - pub fn atom(&self, py: Python<'_>) -> Option { - match &self.allocator.sexp(self.node) { - SExp::Atom => Some(PyBytes::new(py, self.allocator.atom(self.node).as_ref()).into()), - SExp::Pair(..) => None, - } - } -} - -impl LazyNode { - pub const fn new(a: Rc, n: NodePtr) -> Self { - Self { - allocator: a, - node: n, - } - } -} diff --git a/vendor/chia-protocol/src/lib.rs b/vendor/chia-protocol/src/lib.rs deleted file mode 100644 index 873239a..0000000 --- a/vendor/chia-protocol/src/lib.rs +++ /dev/null @@ -1,70 +0,0 @@ -// The Python bindings have unsafe methods so if you derive Deserialize, -// Rust assumes that you may not be upholding invariants, and therefore -// Deserialize (which is a safe trait) may not be safe to implement for the type. -// We know that the Python bindings are safe with arbitrary values, so we can suppress this warning. -#![allow(clippy::unsafe_derive_deserialize)] - -mod block_record; -mod bytes; -mod chia_protocol; -mod classgroup; -mod coin; -mod coin_spend; -mod coin_state; -mod end_of_sub_slot_bundle; -mod fee_estimate; -mod foliage; -mod full_node_protocol; -mod fullblock; -mod header_block; -mod peer_info; -mod pool_target; -mod pos_quality; -mod pot_iterations; -mod program; -mod proof_of_space; -mod reward_chain_block; -mod slots; -mod spend_bundle; -mod sub_epoch_summary; -mod unfinished_block; -mod unfinished_header_block; -mod vdf; -mod wallet_protocol; -mod weight_proof; - -#[cfg(feature = "py-bindings")] -mod lazy_node; - -// export shorter names -pub use crate::block_record::*; -pub use crate::bytes::*; -pub use crate::chia_protocol::*; -pub use crate::classgroup::*; -pub use crate::coin::*; -pub use crate::coin_spend::*; -pub use crate::coin_state::*; -pub use crate::end_of_sub_slot_bundle::*; -pub use crate::fee_estimate::*; -pub use crate::foliage::*; -pub use crate::full_node_protocol::*; -pub use crate::fullblock::*; -pub use crate::header_block::*; -pub use crate::peer_info::*; -pub use crate::pool_target::*; -pub use crate::pos_quality::*; -pub use crate::pot_iterations::*; -pub use crate::program::*; -pub use crate::proof_of_space::*; -pub use crate::reward_chain_block::*; -pub use crate::slots::*; -pub use crate::spend_bundle::*; -pub use crate::sub_epoch_summary::*; -pub use crate::unfinished_block::*; -pub use crate::unfinished_header_block::*; -pub use crate::vdf::*; -pub use crate::wallet_protocol::*; -pub use crate::weight_proof::*; - -#[cfg(feature = "py-bindings")] -pub use crate::lazy_node::*; diff --git a/vendor/chia-protocol/src/peer_info.rs b/vendor/chia-protocol/src/peer_info.rs deleted file mode 100644 index 19d1933..0000000 --- a/vendor/chia-protocol/src/peer_info.rs +++ /dev/null @@ -1,8 +0,0 @@ -use chia_streamable_macro::streamable; - -#[streamable] -pub struct TimestampedPeerInfo { - host: String, - port: u16, - timestamp: u64, -} diff --git a/vendor/chia-protocol/src/pool_target.rs b/vendor/chia-protocol/src/pool_target.rs deleted file mode 100644 index fa7317e..0000000 --- a/vendor/chia-protocol/src/pool_target.rs +++ /dev/null @@ -1,9 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::Bytes32; - -#[streamable] -pub struct PoolTarget { - puzzle_hash: Bytes32, - max_height: u32, // A max height of 0 means it is valid forever -} diff --git a/vendor/chia-protocol/src/pos_quality.rs b/vendor/chia-protocol/src/pos_quality.rs deleted file mode 100644 index d824057..0000000 --- a/vendor/chia-protocol/src/pos_quality.rs +++ /dev/null @@ -1,34 +0,0 @@ -use chia_traits::chia_error::Result; - -// The actual space in bytes of a plot, is _expected_plot_size(k) * UI_ACTUAL_SPACE_CONSTANT_FACTO -// This is not used in consensus, only for display purposes - -pub const UI_ACTUAL_SPACE_CONSTANT_FACTOR: f32 = 0.78; - -pub fn expected_plot_size(k: u32) -> Result { - // """ - // Given the plot size parameter k (which is between 32 and 59), computes the - // expected size of the plot in bytes (times a constant factor). This is based on efficient encoding - // of the plot, and aims to be scale agnostic, so larger plots don't - // necessarily get more rewards per byte. The +1 is added to give half a bit more space per entry, which - // is necessary to store the entries in the plot. - // """ - - Ok((2 * k as u64 + 1) * (1_u64 << (k - 1))) -} - -// TODO: Update this when new plot format releases -#[cfg(feature = "py-bindings")] -#[pyo3::pyfunction] -#[pyo3(name = "expected_plot_size")] -pub fn py_expected_plot_size(k: u32) -> pyo3::PyResult { - // """ - // Given the plot size parameter k (which is between 32 and 59), computes the - // expected size of the plot in bytes (times a constant factor). This is based on efficient encoding - // of the plot, and aims to be scale agnostic, so larger plots don't - // necessarily get more rewards per byte. The +1 is added to give half a bit more space per entry, which - // is necessary to store the entries in the plot. - // """ - - Ok(expected_plot_size(k)?) -} diff --git a/vendor/chia-protocol/src/pot_iterations.rs b/vendor/chia-protocol/src/pot_iterations.rs deleted file mode 100644 index 3650daa..0000000 --- a/vendor/chia-protocol/src/pot_iterations.rs +++ /dev/null @@ -1,230 +0,0 @@ -use chia_traits::chia_error::{Error, Result}; - -fn add_catch_overflow(a: u64, b: u64) -> Result { - a.checked_add(b).ok_or(Error::InvalidPotIteration) -} - -fn mult_catch_overflow(a: u64, b: u64) -> Result { - a.checked_mul(b).ok_or(Error::InvalidPotIteration) -} - -fn mod_catch_error(a: u64, b: u64) -> Result { - a.checked_rem(b).ok_or(Error::InvalidPotIteration) -} - -fn div_catch_error(a: u64, b: u64) -> Result { - a.checked_div(b).ok_or(Error::InvalidPotIteration) -} - -pub fn is_overflow_block( - num_sps_sub_slot: u8, - num_sp_intervals_extra: u8, - signage_point_index: u8, -) -> Result { - if signage_point_index >= num_sps_sub_slot { - return Err(Error::InvalidPotIteration); - } - Ok(signage_point_index - >= num_sps_sub_slot - .checked_sub(num_sp_intervals_extra) - .ok_or(Error::InvalidPotIteration)?) -} - -pub fn calculate_sp_interval_iters(num_sps_sub_slot: u8, sub_slot_iters: u64) -> Result { - if mod_catch_error(sub_slot_iters, num_sps_sub_slot as u64)? != 0 { - return Err(Error::InvalidPotIteration); - } - div_catch_error(sub_slot_iters, num_sps_sub_slot as u64) -} - -pub fn calculate_sp_iters( - num_sps_sub_slot: u8, - sub_slot_iters: u64, - signage_point_index: u8, -) -> Result { - if signage_point_index >= num_sps_sub_slot { - return Err(Error::InvalidPotIteration); - } - mult_catch_overflow( - calculate_sp_interval_iters(num_sps_sub_slot, sub_slot_iters)?, - signage_point_index as u64, - ) -} - -pub fn calculate_ip_iters( - num_sps_sub_slot: u8, - num_sp_intervals_extra: u8, - sub_slot_iters: u64, - signage_point_index: u8, - required_iters: u64, -) -> Result { - let sp_interval_iters = calculate_sp_interval_iters(num_sps_sub_slot, sub_slot_iters)?; - let sp_iters = calculate_sp_iters(num_sps_sub_slot, sub_slot_iters, signage_point_index)?; - if mod_catch_error(sp_iters, sp_interval_iters)? != 0 - || sp_iters > sub_slot_iters - || required_iters >= sp_interval_iters - || required_iters == 0 - { - return Err(Error::InvalidPotIteration); - } - mod_catch_error( - add_catch_overflow( - add_catch_overflow( - sp_iters, - mult_catch_overflow(num_sp_intervals_extra as u64, sp_interval_iters)?, - )?, - required_iters, - )?, - sub_slot_iters, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - static NUM_SPS_SUB_SLOT: u8 = 32; - static NUM_SP_INTERVALS_EXTRA: u8 = 3; - - #[test] - fn test_is_overflow_block() { - assert!( - !is_overflow_block(NUM_SPS_SUB_SLOT, NUM_SP_INTERVALS_EXTRA, 27) - .expect("valid SP index") - ); - assert!( - !is_overflow_block(NUM_SPS_SUB_SLOT, NUM_SP_INTERVALS_EXTRA, 28) - .expect("valid SP index") - ); - assert!( - is_overflow_block(NUM_SPS_SUB_SLOT, NUM_SP_INTERVALS_EXTRA, 29) - .expect("valid SP index") - ); - assert!( - is_overflow_block(NUM_SPS_SUB_SLOT, NUM_SP_INTERVALS_EXTRA, 30) - .expect("valid SP index") - ); - assert!( - is_overflow_block(NUM_SPS_SUB_SLOT, NUM_SP_INTERVALS_EXTRA, 31) - .expect("valid SP index") - ); - assert!(is_overflow_block(NUM_SPS_SUB_SLOT, NUM_SP_INTERVALS_EXTRA, 32).is_err()); - } - - #[test] - fn test_calculate_sp_iters() { - let ssi: u64 = 100_001 * 64 * 4; - assert!(calculate_sp_iters(NUM_SPS_SUB_SLOT, ssi, 32).is_err()); - calculate_sp_iters(NUM_SPS_SUB_SLOT, ssi, 31).expect("valid_result"); - } - - #[test] - fn test_calculate_ip_iters() { - // # num_sps_sub_slot: u8, - // # num_sp_intervals_extra: u8, - // # sub_slot_iters: u64, - // # signage_point_index: u8, - // # required_iters: u64, - let ssi: u64 = 100_001 * 64 * 4; - let sp_interval_iters = ssi / NUM_SPS_SUB_SLOT as u64; - - // Invalid signage point index - assert_eq!( - calculate_ip_iters(NUM_SPS_SUB_SLOT, NUM_SP_INTERVALS_EXTRA, ssi, 123, 100_000) - .unwrap_err(), - Error::InvalidPotIteration - ); - - let sp_iters = sp_interval_iters * 13; - - // required_iters too high - assert_eq!( - calculate_ip_iters( - NUM_SPS_SUB_SLOT, - NUM_SP_INTERVALS_EXTRA, - ssi, - 13, - sp_interval_iters - ) - .unwrap_err(), - Error::InvalidPotIteration - ); - - // required_iters too high - assert_eq!( - calculate_ip_iters( - NUM_SPS_SUB_SLOT, - NUM_SP_INTERVALS_EXTRA, - ssi, - 13, - sp_interval_iters * 12 - ) - .unwrap_err(), - Error::InvalidPotIteration - ); - - // required_iters too low (0) - assert_eq!( - calculate_ip_iters(NUM_SPS_SUB_SLOT, NUM_SP_INTERVALS_EXTRA, ssi, 255, 0).unwrap_err(), - Error::InvalidPotIteration - ); - - let required_iters = sp_interval_iters - 1; - let ip_iters = calculate_ip_iters( - NUM_SPS_SUB_SLOT, - NUM_SP_INTERVALS_EXTRA, - ssi, - 13, - required_iters, - ) - .expect("should be valid"); - assert_eq!( - ip_iters, - sp_iters + (NUM_SP_INTERVALS_EXTRA as u64 * sp_interval_iters) + required_iters - ); - - let required_iters = 1_u64; - let ip_iters = calculate_ip_iters( - NUM_SPS_SUB_SLOT, - NUM_SP_INTERVALS_EXTRA, - ssi, - 13, - required_iters, - ) - .expect("valid"); - assert_eq!( - ip_iters, - sp_iters + (NUM_SP_INTERVALS_EXTRA as u64 * sp_interval_iters) + required_iters - ); - - let required_iters: u64 = ssi * 4 / 300; - let ip_iters = calculate_ip_iters( - NUM_SPS_SUB_SLOT, - NUM_SP_INTERVALS_EXTRA, - ssi, - 13, - required_iters, - ) - .expect("valid"); - assert_eq!( - ip_iters, - sp_iters + (NUM_SP_INTERVALS_EXTRA as u64 * sp_interval_iters) + required_iters - ); - assert!(sp_iters < ip_iters); - - // Overflow - let sp_iters = sp_interval_iters * (NUM_SPS_SUB_SLOT - 1) as u64; - let ip_iters = calculate_ip_iters( - NUM_SPS_SUB_SLOT, - NUM_SP_INTERVALS_EXTRA, - ssi, - NUM_SPS_SUB_SLOT - 1, - required_iters, - ) - .expect("valid"); - assert_eq!( - ip_iters, - (sp_iters + (NUM_SP_INTERVALS_EXTRA as u64 * sp_interval_iters) + required_iters) % ssi - ); - assert!(sp_iters > ip_iters); - } -} diff --git a/vendor/chia-protocol/src/program.rs b/vendor/chia-protocol/src/program.rs deleted file mode 100644 index 1224bdb..0000000 --- a/vendor/chia-protocol/src/program.rs +++ /dev/null @@ -1,539 +0,0 @@ -use crate::bytes::Bytes; -#[cfg(feature = "py-bindings")] -use crate::LazyNode; -use chia_sha2::Sha256; -use chia_traits::chia_error::{Error, Result}; -use chia_traits::Streamable; -use clvm_traits::{FromClvm, FromClvmError, ToClvm, ToClvmError}; -use clvmr::allocator::NodePtr; -use clvmr::cost::Cost; -use clvmr::reduction::EvalErr; -use clvmr::run_program; -use clvmr::serde::{ - node_from_bytes, node_from_bytes_backrefs, node_to_bytes, serialized_length_from_bytes, - serialized_length_from_bytes_trusted, -}; -#[cfg(feature = "py-bindings")] -use clvmr::SExp; -use clvmr::{Allocator, ChiaDialect}; -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; -#[cfg(feature = "py-bindings")] -use pyo3::types::PyType; -use std::io::Cursor; -use std::ops::Deref; -#[cfg(feature = "py-bindings")] -use std::rc::Rc; - -#[cfg(feature = "py-bindings")] -use clvm_utils::CurriedProgram; - -#[cfg_attr(feature = "py-bindings", pyclass(subclass), derive(PyStreamable))] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Program(Bytes); - -impl Default for Program { - fn default() -> Self { - Self(vec![0x80].into()) - } -} - -impl Program { - pub fn new(bytes: Bytes) -> Self { - Self(bytes) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn as_slice(&self) -> &[u8] { - self.0.as_slice() - } - - pub fn to_vec(&self) -> Vec { - self.0.to_vec() - } - - pub fn into_inner(self) -> Bytes { - self.0 - } - - pub fn into_bytes(self) -> Vec { - self.0.into_inner() - } - - pub fn run>( - &self, - a: &mut Allocator, - flags: u32, - max_cost: Cost, - arg: &A, - ) -> std::result::Result<(Cost, NodePtr), EvalErr> { - let arg = arg.to_clvm(a).map_err(|_| { - EvalErr( - a.nil(), - "failed to convert argument to CLVM objects".to_string(), - ) - })?; - let program = - node_from_bytes_backrefs(a, self.0.as_ref()).expect("invalid SerializedProgram"); - let dialect = ChiaDialect::new(flags); - let reduction = run_program(a, &dialect, program, arg, max_cost)?; - Ok((reduction.0, reduction.1)) - } -} - -impl From for Program { - fn from(value: Bytes) -> Self { - Self(value) - } -} - -impl From for Bytes { - fn from(value: Program) -> Self { - value.0 - } -} - -impl From> for Program { - fn from(value: Vec) -> Self { - Self(Bytes::new(value)) - } -} - -impl From<&[u8]> for Program { - fn from(value: &[u8]) -> Self { - Self(value.into()) - } -} - -impl From for Vec { - fn from(value: Program) -> Self { - value.0.into() - } -} - -impl AsRef<[u8]> for Program { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl Deref for Program { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - &self.0 - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for Program { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - // generate an arbitrary CLVM structure. Not likely a valid program. - let mut items_left = 1; - let mut total_items = 0; - let mut buf = Vec::::with_capacity(200); - - while items_left > 0 { - if total_items < 100 && u.ratio(1, 4).unwrap() { - // make a pair - buf.push(0xff); - items_left += 2; - } else { - // make an atom. just single bytes for now - buf.push(u.int_in_range(0..=0x80).unwrap()); - } - total_items += 1; - items_left -= 1; - } - Ok(Self(buf.into())) - } -} - -#[cfg(feature = "py-bindings")] -use chia_traits::{FromJsonDict, ToJsonDict}; - -#[cfg(feature = "py-bindings")] -use chia_py_streamable_macro::PyStreamable; - -#[cfg(feature = "py-bindings")] -use pyo3::types::{PyList, PyTuple}; - -#[cfg(feature = "py-bindings")] -use pyo3::exceptions::*; - -// TODO: this conversion function should probably be converted to a type holding -// the PyAny object implementing the ToClvm trait. That way, the Program::to() -// function could turn a python structure directly into bytes, without taking -// the detour via Allocator. propagating python errors through ToClvmError is a -// bit tricky though -#[cfg(feature = "py-bindings")] -fn clvm_convert(a: &mut Allocator, o: &Bound<'_, PyAny>) -> PyResult { - // None - if o.is_none() { - Ok(a.nil()) - // bytes - } else if let Ok(buffer) = o.extract::<&[u8]>() { - a.new_atom(buffer) - .map_err(|e| PyMemoryError::new_err(e.to_string())) - // str - } else if let Ok(text) = o.extract::() { - a.new_atom(text.as_bytes()) - .map_err(|e| PyMemoryError::new_err(e.to_string())) - // int - } else if let Ok(val) = o.extract::() { - a.new_number(val) - .map_err(|e| PyMemoryError::new_err(e.to_string())) - // Tuple (SExp-like) - } else if let Ok(pair) = o.downcast::() { - if pair.len() == 2 { - let left = clvm_convert(a, &pair.get_item(0)?)?; - let right = clvm_convert(a, &pair.get_item(1)?)?; - a.new_pair(left, right) - .map_err(|e| PyMemoryError::new_err(e.to_string())) - } else { - Err(PyValueError::new_err(format!( - "can't cast tuple of size {}", - pair.len() - ))) - } - // List - } else if let Ok(list) = o.downcast::() { - let mut rev = Vec::new(); - for py_item in list.iter() { - rev.push(py_item); - } - let mut ret = a.nil(); - for py_item in rev.into_iter().rev() { - let item = clvm_convert(a, &py_item)?; - ret = a - .new_pair(item, ret) - .map_err(|e| PyMemoryError::new_err(e.to_string()))?; - } - Ok(ret) - // SExp (such as clvm.SExp) - } else if let (Ok(atom), Ok(pair)) = (o.getattr("atom"), o.getattr("pair")) { - if atom.is_none() { - if pair.is_none() { - Err(PyTypeError::new_err(format!("invalid SExp item {o}"))) - } else { - let pair = pair.downcast::()?; - let left = clvm_convert(a, &pair.get_item(0)?)?; - let right = clvm_convert(a, &pair.get_item(1)?)?; - a.new_pair(left, right) - .map_err(|e| PyMemoryError::new_err(e.to_string())) - } - } else { - a.new_atom(atom.extract::<&[u8]>()?) - .map_err(|e| PyMemoryError::new_err(e.to_string())) - } - // Program itself. This is interpreted as a program in serialized form, and - // just a buffer of that serialization. This is an optimization to finding - // __bytes__() and calling it - } else if let Ok(prg) = o.extract::() { - a.new_atom(prg.0.as_slice()) - .map_err(|e| PyMemoryError::new_err(e.to_string())) - // anything convertible to bytes - } else if let Ok(fun) = o.getattr("__bytes__") { - let bytes = fun.call0()?; - let buffer = bytes.extract::<&[u8]>()?; - a.new_atom(buffer) - .map_err(|e| PyMemoryError::new_err(e.to_string())) - } else { - Err(PyTypeError::new_err(format!( - "unknown parameter to run_with_cost() {o}" - ))) - } -} - -#[cfg(feature = "py-bindings")] -fn clvm_serialize(a: &mut Allocator, o: &Bound<'_, PyAny>) -> PyResult { - /* - When passing arguments to run(), there's some special treatment, before falling - back on the regular python -> CLVM conversion (implemented by clvm_convert - above). This function mimics the _serialize() function in python: - - def _serialize(node: object) -> bytes: - if isinstance(node, list): - serialized_list = bytearray() - for a in node: - serialized_list += b"\xff" - serialized_list += _serialize(a) - serialized_list += b"\x80" - return bytes(serialized_list) - if type(node) is SerializedProgram: - return bytes(node) - if type(node) is Program: - return bytes(node) - else: - ret: bytes = SExp.to(node).as_bin() - return ret - */ - - // List - if let Ok(list) = o.downcast::() { - let mut rev = Vec::new(); - for py_item in list.iter() { - rev.push(py_item); - } - let mut ret = a.nil(); - for py_item in rev.into_iter().rev() { - let item = clvm_serialize(a, &py_item)?; - ret = a - .new_pair(item, ret) - .map_err(|e| PyMemoryError::new_err(e.to_string()))?; - } - Ok(ret) - // Program itself - } else if let Ok(prg) = o.extract::() { - Ok(node_from_bytes_backrefs(a, prg.0.as_slice())?) - } else { - clvm_convert(a, o) - } -} - -#[cfg(feature = "py-bindings")] -#[allow(clippy::needless_pass_by_value)] -#[pymethods] -impl Program { - #[pyo3(name = "default")] - #[staticmethod] - fn py_default() -> Self { - Self::default() - } - - #[staticmethod] - #[pyo3(name = "to")] - fn py_to(args: &Bound<'_, PyAny>) -> PyResult { - let mut a = Allocator::new_limited(500_000_000); - let clvm = clvm_convert(&mut a, args)?; - Program::from_clvm(&a, clvm) - .map_err(|error| PyErr::new::(error.to_string())) - } - - fn get_tree_hash(&self) -> crate::Bytes32 { - clvm_utils::tree_hash_from_bytes(self.0.as_ref()) - .unwrap() - .into() - } - - #[staticmethod] - fn fromhex(h: String) -> Result { - let s = if let Some(st) = h.strip_prefix("0x") { - st - } else { - &h[..] - }; - Self::from_bytes(hex::decode(s).map_err(|_| Error::InvalidString)?.as_slice()) - } - - fn run_rust( - &self, - py: Python<'_>, - max_cost: u64, - flags: u32, - args: &Bound<'_, PyAny>, - ) -> PyResult<(u64, LazyNode)> { - use clvmr::reduction::Response; - - let mut a = Allocator::new_limited(500_000_000); - // The python behavior here is a bit messy, and is best not emulated - // on the rust side. We must be able to pass a Program as an argument, - // and it being treated as the CLVM structure it represents. In python's - // SerializedProgram, we have a hack where we interpret the first - // "layer" of SerializedProgram, or lists of SerializedProgram this way. - // But if we encounter an Optional or tuple, we defer to the clvm - // wheel's conversion function to SExp. This level does not have any - // special treatment for SerializedProgram (as that would cause a - // circular dependency). - let clvm_args = clvm_serialize(&mut a, args)?; - - let r: Response = (|| -> PyResult { - let program = node_from_bytes_backrefs(&mut a, self.0.as_ref())?; - let dialect = ChiaDialect::new(flags); - - Ok(py.allow_threads(|| run_program(&mut a, &dialect, program, clvm_args, max_cost))) - })()?; - match r { - Ok(reduction) => { - let val = LazyNode::new(Rc::new(a), reduction.1); - Ok((reduction.0, val)) - } - Err(eval_err) => { - let blob = node_to_bytes(&a, eval_err.0).ok().map(hex::encode); - Err(PyValueError::new_err((eval_err.1, blob))) - } - } - } - - fn uncurry_rust(&self) -> PyResult<(LazyNode, LazyNode)> { - let mut a = Allocator::new_limited(500_000_000); - let prg = node_from_bytes_backrefs(&mut a, self.0.as_ref())?; - let Ok(uncurried) = CurriedProgram::::from_clvm(&a, prg) else { - let a = Rc::new(a); - let prg = LazyNode::new(a.clone(), prg); - let ret = a.nil(); - let ret = LazyNode::new(a, ret); - return Ok((prg, ret)); - }; - - let mut curried_args = Vec::::new(); - let mut args = uncurried.args; - loop { - if let SExp::Atom = a.sexp(args) { - break; - } - // the args of curried puzzles are in the form of: - // (c . ((q . ) . ( . ()))) - let (_, ((_, arg), (rest, ()))) = - <( - clvm_traits::MatchByte<4>, - (clvm_traits::match_quote!(NodePtr), (NodePtr, ())), - ) as FromClvm>::from_clvm(&a, args) - .map_err(|error| PyErr::new::(error.to_string()))?; - curried_args.push(arg); - args = rest; - } - let mut ret = a.nil(); - for item in curried_args.into_iter().rev() { - ret = a.new_pair(item, ret).map_err(|_e| Error::EndOfBuffer)?; - } - let a = Rc::new(a); - let prg = LazyNode::new(a.clone(), uncurried.program); - let ret = LazyNode::new(a, ret); - Ok((prg, ret)) - } -} - -impl Streamable for Program { - fn update_digest(&self, digest: &mut Sha256) { - digest.update(&self.0); - } - - fn stream(&self, out: &mut Vec) -> Result<()> { - out.extend_from_slice(self.0.as_ref()); - Ok(()) - } - - fn parse(input: &mut Cursor<&[u8]>) -> Result { - let pos = input.position(); - let buf: &[u8] = &input.get_ref()[pos as usize..]; - let len = if TRUSTED { - serialized_length_from_bytes_trusted(buf).map_err(|_e| Error::EndOfBuffer)? - } else { - serialized_length_from_bytes(buf).map_err(|_e| Error::EndOfBuffer)? - }; - if buf.len() < len as usize { - return Err(Error::EndOfBuffer); - } - let program = buf[..len as usize].to_vec(); - input.set_position(pos + len); - Ok(Program(program.into())) - } -} - -#[cfg(feature = "py-bindings")] -impl ToJsonDict for Program { - fn to_json_dict(&self, py: Python<'_>) -> PyResult { - self.0.to_json_dict(py) - } -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl Program { - #[classmethod] - #[pyo3(name = "from_parent")] - pub fn from_parent(_cls: &Bound<'_, PyType>, _instance: &Self) -> PyResult { - Err(PyNotImplementedError::new_err( - "This class does not support from_parent().", - )) - } -} - -#[cfg(feature = "py-bindings")] -impl FromJsonDict for Program { - fn from_json_dict(o: &Bound<'_, PyAny>) -> PyResult { - let bytes = Bytes::from_json_dict(o)?; - let len = - serialized_length_from_bytes(bytes.as_slice()).map_err(|_e| Error::EndOfBuffer)?; - if len as usize != bytes.len() { - // If the bytes in the JSON string is not a valid CLVM - // serialization, or if it has garbage at the end of the string, - // reject it - return Err(Error::InvalidClvm)?; - } - Ok(Self(bytes)) - } -} - -impl FromClvm for Program { - fn from_clvm(a: &Allocator, node: NodePtr) -> std::result::Result { - Ok(Self( - node_to_bytes(a, node) - .map_err(|error| FromClvmError::Custom(error.to_string()))? - .into(), - )) - } -} - -impl ToClvm for Program { - fn to_clvm(&self, a: &mut Allocator) -> std::result::Result { - node_from_bytes(a, self.0.as_ref()).map_err(|error| ToClvmError::Custom(error.to_string())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn program_roundtrip() { - let a = &mut Allocator::new(); - let expected = "ff01ff02ff62ff0480"; - let expected_bytes = hex::decode(expected).unwrap(); - - let ptr = node_from_bytes(a, &expected_bytes).unwrap(); - let program = Program::from_clvm(a, ptr).unwrap(); - - let round_trip = program.to_clvm(a).unwrap(); - assert_eq!(expected, hex::encode(node_to_bytes(a, round_trip).unwrap())); - } - - #[test] - fn program_run() { - let a = &mut Allocator::new(); - - // (+ 2 5) - let prg = Program::from_bytes(&hex::decode("ff10ff02ff0580").expect("hex::decode")) - .expect("from_bytes"); - let (cost, result) = prg.run(a, 0, 1000, &[1300, 37]).expect("run"); - assert_eq!(cost, 869); - assert_eq!(a.number(result), 1337.into()); - } -} - -#[cfg(all(test, feature = "serde"))] -mod serde_tests { - use super::*; - - #[test] - fn test_program_is_bytes() -> anyhow::Result<()> { - let bytes = Bytes::new(vec![1, 2, 3]); - let program = Program::new(bytes.clone()); - - let bytes_json = serde_json::to_string(&bytes)?; - let program_json = serde_json::to_string(&program)?; - - assert_eq!(program_json, bytes_json); - - Ok(()) - } -} diff --git a/vendor/chia-protocol/src/proof_of_space.rs b/vendor/chia-protocol/src/proof_of_space.rs deleted file mode 100644 index 6f1f0bf..0000000 --- a/vendor/chia-protocol/src/proof_of_space.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::bytes::{Bytes, Bytes32}; -use chia_bls::G1Element; -use chia_streamable_macro::streamable; - -#[streamable(no_json)] -pub struct ProofOfSpace { - challenge: Bytes32, - pool_public_key: Option, - pool_contract_puzzle_hash: Option, - plot_public_key: G1Element, - // this field was renamed when adding support for v2 plots since the top - // bit now means whether it's v1 or v2. To stay backwards compabible with - // JSON serialization, we still serialize this as its original name - #[cfg_attr(feature = "serde", serde(rename = "size", alias = "version_and_size"))] - version_and_size: u8, - proof: Bytes, -} - -#[derive(Debug, PartialEq)] -pub enum PlotSize { - V1(u8), - V2(u8), -} - -impl ProofOfSpace { - pub fn size(&self) -> PlotSize { - if (self.version_and_size & 0x80) == 0 { - PlotSize::V1(self.version_and_size) - } else { - PlotSize::V2(self.version_and_size & 0x7f) - } - } -} - -#[cfg(feature = "py-bindings")] -use chia_traits::{FromJsonDict, ToJsonDict}; -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -#[cfg(feature = "py-bindings")] -#[pyclass(name = "PlotSize")] -pub struct PyPlotSize { - #[pyo3(get)] - pub size_v1: Option, - #[pyo3(get)] - pub size_v2: Option, -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl PyPlotSize { - #[staticmethod] - fn make_v1(s: u8) -> Self { - Self { - size_v1: Some(s), - size_v2: None, - } - } - - #[staticmethod] - fn make_v2(s: u8) -> Self { - Self { - size_v1: None, - size_v2: Some(s), - } - } -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl ProofOfSpace { - #[pyo3(name = "size")] - fn py_size(&self) -> PyPlotSize { - match self.size() { - PlotSize::V1(s) => PyPlotSize { - size_v1: Some(s), - size_v2: None, - }, - PlotSize::V2(s) => PyPlotSize { - size_v1: None, - size_v2: Some(s), - }, - } - } -} - -#[cfg(feature = "py-bindings")] -impl ToJsonDict for ProofOfSpace { - fn to_json_dict(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { - use pyo3::prelude::PyDictMethods; - let ret = pyo3::types::PyDict::new(py); - - ret.set_item("challenge", self.challenge.to_json_dict(py)?)?; - ret.set_item("pool_public_key", self.pool_public_key.to_json_dict(py)?)?; - ret.set_item( - "pool_contract_puzzle_hash", - self.pool_contract_puzzle_hash.to_json_dict(py)?, - )?; - ret.set_item("plot_public_key", self.plot_public_key.to_json_dict(py)?)?; - - // "size" was the original name of this field. We keep it to remain backwards compatible - ret.set_item("size", self.version_and_size.to_json_dict(py)?)?; - ret.set_item("proof", self.proof.to_json_dict(py)?)?; - - Ok(ret.into()) - } -} - -#[cfg(feature = "py-bindings")] -impl FromJsonDict for ProofOfSpace { - fn from_json_dict(o: &pyo3::Bound<'_, pyo3::PyAny>) -> pyo3::PyResult { - use pyo3::prelude::PyAnyMethods; - Ok(Self { - challenge: ::from_json_dict(&o.get_item("challenge")?)?, - pool_public_key: as FromJsonDict>::from_json_dict( - &o.get_item("pool_public_key")?, - )?, - pool_contract_puzzle_hash: as FromJsonDict>::from_json_dict( - &o.get_item("pool_contract_puzzle_hash")?, - )?, - plot_public_key: ::from_json_dict( - &o.get_item("plot_public_key")?, - )?, - version_and_size: ::from_json_dict(&o.get_item("size")?)?, - proof: ::from_json_dict(&o.get_item("proof")?)?, - }) - } -} - -#[cfg(test)] -#[allow(clippy::needless_pass_by_value)] -mod tests { - use super::*; - use rstest::rstest; - - #[rstest] - #[case(0x00, PlotSize::V1(0))] - #[case(0x01, PlotSize::V1(1))] - #[case(0x08, PlotSize::V1(8))] - #[case(0x7f, PlotSize::V1(0x7f))] - #[case(0x80, PlotSize::V2(0))] - #[case(0x81, PlotSize::V2(1))] - #[case(0x80 + 28, PlotSize::V2(28))] - #[case(0x80 + 30, PlotSize::V2(30))] - #[case(0x80 + 32, PlotSize::V2(32))] - #[case(0xff, PlotSize::V2(0x7f))] - fn proof_of_space_size(#[case] size_field: u8, #[case] expect: PlotSize) { - let pos = ProofOfSpace::new( - Bytes32::from(b"abababababababababababababababab"), - None, - None, - G1Element::default(), - size_field, - Bytes::from(vec![]), - ); - - assert_eq!(pos.size(), expect); - } -} diff --git a/vendor/chia-protocol/src/reward_chain_block.rs b/vendor/chia-protocol/src/reward_chain_block.rs deleted file mode 100644 index 914db6e..0000000 --- a/vendor/chia-protocol/src/reward_chain_block.rs +++ /dev/null @@ -1,55 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::Bytes32; -use crate::ProofOfSpace; -use crate::VDFInfo; -use chia_bls::G2Element; - -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -#[streamable] -pub struct RewardChainBlockUnfinished { - total_iters: u128, - signage_point_index: u8, - pos_ss_cc_challenge_hash: Bytes32, - proof_of_space: ProofOfSpace, - challenge_chain_sp_vdf: Option, // Not present for first sp in slot - challenge_chain_sp_signature: G2Element, - reward_chain_sp_vdf: Option, // Not present for first sp in slot - reward_chain_sp_signature: G2Element, -} - -#[streamable] -pub struct RewardChainBlock { - weight: u128, - height: u32, - total_iters: u128, - signage_point_index: u8, - pos_ss_cc_challenge_hash: Bytes32, - proof_of_space: ProofOfSpace, - challenge_chain_sp_vdf: Option, // Not present for first sp in slot - challenge_chain_sp_signature: G2Element, - challenge_chain_ip_vdf: VDFInfo, - reward_chain_sp_vdf: Option, // Not present for first sp in slot - reward_chain_sp_signature: G2Element, - reward_chain_ip_vdf: VDFInfo, - infused_challenge_chain_ip_vdf: Option, // Iff deficit < 16 - is_transaction_block: bool, -} - -#[cfg_attr(feature = "py-bindings", pymethods)] -impl RewardChainBlock { - pub fn get_unfinished(&self) -> RewardChainBlockUnfinished { - RewardChainBlockUnfinished { - total_iters: self.total_iters, - signage_point_index: self.signage_point_index, - pos_ss_cc_challenge_hash: self.pos_ss_cc_challenge_hash, - proof_of_space: self.proof_of_space.clone(), - challenge_chain_sp_vdf: self.challenge_chain_sp_vdf.clone(), - challenge_chain_sp_signature: self.challenge_chain_sp_signature.clone(), - reward_chain_sp_vdf: self.reward_chain_sp_vdf.clone(), - reward_chain_sp_signature: self.reward_chain_sp_signature.clone(), - } - } -} diff --git a/vendor/chia-protocol/src/slots.rs b/vendor/chia-protocol/src/slots.rs deleted file mode 100644 index c0ba902..0000000 --- a/vendor/chia-protocol/src/slots.rs +++ /dev/null @@ -1,45 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::Bytes32; -use crate::ProofOfSpace; -use crate::VDFInfo; -use crate::VDFProof; -use chia_bls::G2Element; - -// The hash of this is used as the challenge_hash for the ICC VDF -#[streamable] -pub struct ChallengeBlockInfo { - proof_of_space: ProofOfSpace, - challenge_chain_sp_vdf: Option, // Only present if not the first sp - challenge_chain_sp_signature: G2Element, - challenge_chain_ip_vdf: VDFInfo, -} - -#[streamable] -pub struct ChallengeChainSubSlot { - challenge_chain_end_of_slot_vdf: VDFInfo, - infused_challenge_chain_sub_slot_hash: Option, // Only at the end of a slot - subepoch_summary_hash: Option, // Only once per sub-epoch, and one sub-epoch delayed - new_sub_slot_iters: Option, // Only at the end of epoch, sub-epoch, and slot - new_difficulty: Option, // Only at the end of epoch, sub-epoch, and slot -} - -#[streamable] -pub struct InfusedChallengeChainSubSlot { - infused_challenge_chain_end_of_slot_vdf: VDFInfo, -} - -#[streamable] -pub struct RewardChainSubSlot { - end_of_slot_vdf: VDFInfo, - challenge_chain_sub_slot_hash: Bytes32, - infused_challenge_chain_sub_slot_hash: Option, - deficit: u8, // 16 or less. usually zero -} - -#[streamable] -pub struct SubSlotProofs { - challenge_chain_slot_proof: VDFProof, - infused_challenge_chain_slot_proof: Option, - reward_chain_slot_proof: VDFProof, -} diff --git a/vendor/chia-protocol/src/spend_bundle.rs b/vendor/chia-protocol/src/spend_bundle.rs deleted file mode 100644 index fe0e5f9..0000000 --- a/vendor/chia-protocol/src/spend_bundle.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::coin_spend::CoinSpend; -use crate::Bytes32; -use crate::Coin; -use chia_bls::G2Element; -use chia_streamable_macro::streamable; -use chia_traits::Streamable; -use clvm_traits::FromClvm; -use clvmr::allocator::{NodePtr, SExp}; -use clvmr::cost::Cost; -use clvmr::op_utils::{first, rest}; -use clvmr::reduction::EvalErr; -use clvmr::Allocator; - -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; -#[cfg(feature = "py-bindings")] -use pyo3::types::PyType; - -#[streamable(subclass)] -pub struct SpendBundle { - coin_spends: Vec, - aggregated_signature: G2Element, -} - -impl SpendBundle { - pub fn aggregate(spend_bundles: &[SpendBundle]) -> SpendBundle { - let mut coin_spends = Vec::::new(); - let mut aggregated_signature = G2Element::default(); - for sb in spend_bundles { - coin_spends.extend_from_slice(&sb.coin_spends[..]); - aggregated_signature.aggregate(&sb.aggregated_signature); - } - SpendBundle { - coin_spends, - aggregated_signature, - } - } - - pub fn name(&self) -> Bytes32 { - self.hash().into() - } - - pub fn additions(&self) -> Result, EvalErr> { - const CREATE_COIN_COST: Cost = 1_800_000; - const CREATE_COIN: u8 = 51; - - let mut ret = Vec::::new(); - let mut cost_left = 11_000_000_000; - let mut a = Allocator::new(); - let checkpoint = a.checkpoint(); - - for cs in &self.coin_spends { - a.restore_checkpoint(&checkpoint); - let (cost, mut conds) = cs.puzzle_reveal.run(&mut a, 0, cost_left, &cs.solution)?; - if cost > cost_left { - return Err(EvalErr(a.nil(), "cost exceeded".to_string())); - } - cost_left -= cost; - let parent_coin_info: Bytes32 = cs.coin.coin_id(); - - while let Some((c, tail)) = a.next(conds) { - conds = tail; - let op = first(&a, c)?; - let c = rest(&a, c)?; - let buf = match a.sexp(op) { - SExp::Atom => a.atom(op), - SExp::Pair(..) => return Err(EvalErr(op, "invalid condition".to_string())), - }; - let buf = buf.as_ref(); - if buf.len() != 1 { - continue; - } - if buf[0] == CREATE_COIN { - let (puzzle_hash, (amount, _)) = <(Bytes32, (u64, NodePtr))>::from_clvm(&a, c) - .map_err(|_| EvalErr(c, "failed to parse spend".to_string()))?; - ret.push(Coin { - parent_coin_info, - puzzle_hash, - amount, - }); - if CREATE_COIN_COST > cost_left { - return Err(EvalErr(a.nil(), "cost exceeded".to_string())); - } - cost_left -= CREATE_COIN_COST; - } - } - } - Ok(ret) - } -} - -#[cfg(feature = "py-bindings")] -#[pymethods] -#[allow(clippy::needless_pass_by_value)] -impl SpendBundle { - #[classmethod] - #[pyo3(name = "aggregate")] - fn py_aggregate( - cls: &Bound<'_, PyType>, - py: Python<'_>, - spend_bundles: Vec, - ) -> PyResult { - let aggregated = Bound::new(py, Self::aggregate(&spend_bundles))?; - if aggregated.is_exact_instance(cls) { - Ok(aggregated.into_pyobject(py)?.unbind().into_any()) - } else { - let instance = cls.call_method1("from_parent", (aggregated.into_pyobject(py)?,))?; - Ok(instance.into_pyobject(py)?.unbind().into_any()) - } - } - - #[classmethod] - #[pyo3(name = "from_parent")] - pub fn from_parent( - cls: &Bound<'_, PyType>, - py: Python<'_>, - spend_bundle: Self, - ) -> PyResult { - // Convert result into potential child class - let instance = cls.call( - (spend_bundle.coin_spends, spend_bundle.aggregated_signature), - None, - )?; - - Ok(instance.into_pyobject(py)?.unbind()) - } - - #[pyo3(name = "name")] - fn py_name(&self) -> Bytes32 { - self.name() - } - - fn removals(&self) -> Vec { - let mut ret = Vec::::with_capacity(self.coin_spends.len()); - for cs in &self.coin_spends { - ret.push(cs.coin); - } - ret - } - - #[pyo3(name = "additions")] - fn py_additions(&self) -> PyResult> { - self.additions() - .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.1)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::Program; - use rstest::rstest; - use std::fs; - - #[rstest] - #[case( - "e3c0", - "fd65e4b0f21322f78d1025e8a8ff7a1df77cd40b86885b851f4572e5ce06e4ff", - "e3c000a395f8f69d5e263a9548f13bffb1c4b701ab8f3faa03f7647c8750d077" - )] - #[case( - "bb13", - "6b2aaee962cb1de3fdeb1f0506c02df4b9e162e2af3dd1db22048454b5122a87", - "bb13d1e13438736c7ba0217c7b82ee4db56a7f4fb9d22c703c2152362b2314ee" - )] - fn test_additions_ff( - #[case] spend_file: &str, - #[case] expect_parent: &str, - #[case] expect_ph: &str, - ) { - let spend_bytes = - fs::read(format!("../../ff-tests/{spend_file}.spend")).expect("read file"); - let spend = CoinSpend::from_bytes(&spend_bytes).expect("parse CoinSpend"); - let bundle = SpendBundle::new(vec![spend], G2Element::default()); - - let additions = bundle.additions().expect("additions"); - - assert_eq!(additions.len(), 1); - assert_eq!( - additions[0].parent_coin_info.as_ref(), - &hex::decode(expect_parent).expect("hex::decode") - ); - assert_eq!( - additions[0].puzzle_hash.as_ref(), - &hex::decode(expect_ph).expect("hex::decode") - ); - assert_eq!(additions[0].amount, 1); - } - - fn test_impl(solution: &str, body: F) { - let solution = hex::decode(solution).expect("hex::decode"); - let test_coin = Coin::new( - hex::decode("4444444444444444444444444444444444444444444444444444444444444444") - .unwrap() - .try_into() - .unwrap(), - hex::decode("3333333333333333333333333333333333333333333333333333333333333333") - .unwrap() - .try_into() - .unwrap(), - 1, - ); - let spend = CoinSpend::new( - test_coin, - Program::new(vec![1_u8].into()), - Program::new(solution.into()), - ); - let bundle = SpendBundle::new(vec![spend], G2Element::default()); - body(test_coin, bundle); - } - - // TODO: Once we have condition types that implement ToClvm and an Encoder - // that serialize directly to bytes, these test solutions can be expressed - // in a much more readable way - #[test] - fn test_single_create_coin() { - // This is a solution to the identity puzzle: - // ((CREATE_COIN . (222222..22 . (1 . NIL))) . - // )) - let solution = "ff\ -ff33\ -ffa02222222222222222222222222222222222222222222222222222222222222222\ -ff01\ -80\ -80"; - test_impl(solution, |test_coin: Coin, bundle: SpendBundle| { - let additions = bundle.additions().expect("additions"); - - let new_coin = Coin::new( - test_coin.coin_id(), - hex::decode("2222222222222222222222222222222222222222222222222222222222222222") - .unwrap() - .try_into() - .unwrap(), - 1, - ); - assert_eq!(additions, [new_coin]); - }); - } - - #[test] - fn test_invalid_condition() { - // This is a solution to the identity puzzle: - // (((1 . CREATE_COIN) . (222222..22 . (1 . NIL))) . - // )) - let solution = "ff\ -ffff0133\ -ffa02222222222222222222222222222222222222222222222222222222222222222\ -ff01\ -80\ -80"; - - test_impl(solution, |_test_coin, bundle: SpendBundle| { - assert_eq!(bundle.additions().unwrap_err().1, "invalid condition"); - }); - } - - #[test] - fn test_invalid_spend() { - // This is a solution to the identity puzzle: - // ((CREATE_COIN . (222222..22 . ((1 . 1) . NIL))) . - // )) - let solution = "ff\ -ff33\ -ffa02222222222222222222222222222222222222222222222222222222222222222\ -ffff0101\ -80\ -80"; - - test_impl(solution, |_test_coin, bundle: SpendBundle| { - assert_eq!(bundle.additions().unwrap_err().1, "failed to parse spend"); - }); - } -} - -#[cfg(all(test, feature = "serde"))] -mod serde_tests { - use chia_bls::Signature; - use indoc::indoc; - - use crate::Program; - - use super::*; - - #[test] - fn test_json_spend_bundle() -> anyhow::Result<()> { - let json = serde_json::to_string_pretty(&SpendBundle::new( - vec![CoinSpend::new( - Coin::new([0; 32].into(), [1; 32].into(), 42), - Program::from(b"abc".to_vec()), - Program::from(b"xyz".to_vec()), - )], - Signature::default(), - ))?; - - let output = indoc! {r#"{ - "coin_spends": [ - { - "coin": { - "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", - "puzzle_hash": "0x0101010101010101010101010101010101010101010101010101010101010101", - "amount": 42 - }, - "puzzle_reveal": "616263", - "solution": "78797a" - } - ], - "aggregated_signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }"#}; - - assert_eq!(json, output); - - Ok(()) - } -} diff --git a/vendor/chia-protocol/src/sub_epoch_summary.rs b/vendor/chia-protocol/src/sub_epoch_summary.rs deleted file mode 100644 index 44bccf7..0000000 --- a/vendor/chia-protocol/src/sub_epoch_summary.rs +++ /dev/null @@ -1,12 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::Bytes32; - -#[streamable] -pub struct SubEpochSummary { - prev_subepoch_summary_hash: Bytes32, - reward_chain_hash: Bytes32, // hash of reward chain at end of last segment - num_blocks_overflow: u8, // How many more blocks than 384*(N-1) - new_difficulty: Option, // Only once per epoch (diff adjustment) - new_sub_slot_iters: Option, // Only once per epoch (diff adjustment) -} diff --git a/vendor/chia-protocol/src/unfinished_block.rs b/vendor/chia-protocol/src/unfinished_block.rs deleted file mode 100644 index 3e7dbdb..0000000 --- a/vendor/chia-protocol/src/unfinished_block.rs +++ /dev/null @@ -1,73 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::Bytes32; -use crate::EndOfSubSlotBundle; -use crate::Program; -use crate::RewardChainBlockUnfinished; -use crate::VDFProof; -use crate::{Foliage, FoliageTransactionBlock, TransactionsInfo}; -use chia_traits::Streamable; - -#[streamable] -pub struct UnfinishedBlock { - // Full block, without the final VDFs - finished_sub_slots: Vec, // If first sb - reward_chain_block: RewardChainBlockUnfinished, // Reward chain trunk data - challenge_chain_sp_proof: Option, // If not first sp in sub-slot - reward_chain_sp_proof: Option, // If not first sp in sub-slot - foliage: Foliage, // Reward chain foliage data - foliage_transaction_block: Option, // Reward chain foliage data (tx block) - transactions_info: Option, // Reward chain foliage data (tx block additional) - transactions_generator: Option, // Program that generates transactions - transactions_generator_ref_list: Vec, // List of block heights of previous generators referenced in this block -} - -impl UnfinishedBlock { - pub fn prev_header_hash(&self) -> Bytes32 { - self.foliage.prev_block_hash - } - - pub fn partial_hash(&self) -> Bytes32 { - self.reward_chain_block.hash().into() - } - - pub fn is_transaction_block(&self) -> bool { - self.foliage.foliage_transaction_block_hash.is_some() - } - - pub fn total_iters(&self) -> u128 { - self.reward_chain_block.total_iters - } -} - -#[cfg(feature = "py-bindings")] -use chia_traits::ChiaToPython; -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl UnfinishedBlock { - #[getter] - #[pyo3(name = "prev_header_hash")] - fn py_prev_header_hash(&self) -> Bytes32 { - self.prev_header_hash() - } - - #[getter] - #[pyo3(name = "partial_hash")] - fn py_partial_hash(&self) -> Bytes32 { - self.partial_hash() - } - - #[pyo3(name = "is_transaction_block")] - fn py_is_transaction_block(&self) -> bool { - self.is_transaction_block() - } - - #[getter] - #[pyo3(name = "total_iters")] - fn py_total_iters<'a>(&self, py: Python<'a>) -> PyResult> { - ChiaToPython::to_python(&self.total_iters(), py) - } -} diff --git a/vendor/chia-protocol/src/unfinished_header_block.rs b/vendor/chia-protocol/src/unfinished_header_block.rs deleted file mode 100644 index 1ea8fcc..0000000 --- a/vendor/chia-protocol/src/unfinished_header_block.rs +++ /dev/null @@ -1,74 +0,0 @@ -use chia_streamable_macro::streamable; -use chia_traits::Streamable; - -use crate::{ - Bytes, Bytes32, EndOfSubSlotBundle, Foliage, FoliageTransactionBlock, - RewardChainBlockUnfinished, VDFProof, -}; - -#[streamable] -pub struct UnfinishedHeaderBlock { - /// Same as a FullBlock but without TransactionInfo and Generator, used by light clients. - /// If first sb. - finished_sub_slots: Vec, - - /// Reward chain trunk data. - reward_chain_block: RewardChainBlockUnfinished, - - /// If not first sp in sub-slot. - challenge_chain_sp_proof: Option, - - /// If not first sp in sub-slot. - reward_chain_sp_proof: Option, - - /// Reward chain foliage data. - foliage: Foliage, - - /// Reward chain foliage data (tx block). - foliage_transaction_block: Option, - - /// Filter for block transactions. - transactions_filter: Bytes, -} - -impl UnfinishedHeaderBlock { - pub fn prev_header_hash(&self) -> Bytes32 { - self.foliage.prev_block_hash - } - - pub fn header_hash(&self) -> Bytes32 { - self.foliage.hash().into() - } - - pub fn total_iters(&self) -> u128 { - self.reward_chain_block.total_iters - } -} - -#[cfg(feature = "py-bindings")] -use chia_traits::ChiaToPython; - -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -#[cfg(feature = "py-bindings")] -#[pymethods] -impl UnfinishedHeaderBlock { - #[getter] - #[pyo3(name = "prev_header_hash")] - fn py_prev_header_hash(&self) -> Bytes32 { - self.prev_header_hash() - } - - #[getter] - #[pyo3(name = "header_hash")] - fn py_header_hash(&self) -> Bytes32 { - self.header_hash() - } - - #[getter] - #[pyo3(name = "total_iters")] - fn py_total_iters<'a>(&self, py: Python<'a>) -> PyResult> { - ChiaToPython::to_python(&self.total_iters(), py) - } -} diff --git a/vendor/chia-protocol/src/vdf.rs b/vendor/chia-protocol/src/vdf.rs deleted file mode 100644 index 839dcb3..0000000 --- a/vendor/chia-protocol/src/vdf.rs +++ /dev/null @@ -1,18 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::ClassgroupElement; -use crate::{Bytes, Bytes32}; - -#[streamable] -pub struct VDFInfo { - challenge: Bytes32, - number_of_iterations: u64, - output: ClassgroupElement, -} - -#[streamable] -pub struct VDFProof { - witness_type: u8, - witness: Bytes, - normalized_to_identity: bool, -} diff --git a/vendor/chia-protocol/src/wallet_protocol.rs b/vendor/chia-protocol/src/wallet_protocol.rs deleted file mode 100644 index 43b5307..0000000 --- a/vendor/chia-protocol/src/wallet_protocol.rs +++ /dev/null @@ -1,356 +0,0 @@ -use chia_streamable_macro::{streamable, Streamable}; - -use crate::Coin; -use crate::CoinState; -use crate::FeeEstimateGroup; -use crate::HeaderBlock; -use crate::Program; -use crate::SpendBundle; -use crate::{Bytes, Bytes32}; - -#[streamable(message)] -pub struct RequestPuzzleSolution { - coin_name: Bytes32, - height: u32, -} - -#[streamable] -pub struct PuzzleSolutionResponse { - coin_name: Bytes32, - height: u32, - puzzle: Program, - solution: Program, -} - -#[streamable(message)] -pub struct RespondPuzzleSolution { - response: PuzzleSolutionResponse, -} - -#[streamable(message)] -pub struct RejectPuzzleSolution { - coin_name: Bytes32, - height: u32, -} - -#[streamable(message)] -pub struct SendTransaction { - transaction: SpendBundle, -} - -#[streamable(message)] -pub struct TransactionAck { - txid: Bytes32, - status: u8, // MempoolInclusionStatus - error: Option, -} - -#[streamable(message)] -pub struct NewPeakWallet { - header_hash: Bytes32, - height: u32, - weight: u128, - fork_point_with_previous_peak: u32, -} - -#[streamable(message)] -pub struct RequestBlockHeader { - height: u32, -} - -#[streamable(message)] -pub struct RespondBlockHeader { - header_block: HeaderBlock, -} - -#[streamable(message)] -pub struct RejectHeaderRequest { - height: u32, -} - -#[streamable(message)] -pub struct RequestRemovals { - height: u32, - header_hash: Bytes32, - coin_names: Option>, -} - -#[streamable(message)] -pub struct RespondRemovals { - height: u32, - header_hash: Bytes32, - coins: Vec<(Bytes32, Option)>, - proofs: Option>, -} - -#[streamable(message)] -pub struct RejectRemovalsRequest { - height: u32, - header_hash: Bytes32, -} - -#[streamable(message)] -pub struct RequestAdditions { - height: u32, - header_hash: Option, - puzzle_hashes: Option>, -} - -#[streamable(message)] -pub struct RespondAdditions { - height: u32, - header_hash: Bytes32, - coins: Vec<(Bytes32, Vec)>, - proofs: Option)>>, -} - -#[streamable(message)] -pub struct RejectAdditionsRequest { - height: u32, - header_hash: Bytes32, -} - -#[streamable(message)] -pub struct RespondBlockHeaders { - start_height: u32, - end_height: u32, - header_blocks: Vec, -} - -#[streamable(message)] -pub struct RejectBlockHeaders { - start_height: u32, - end_height: u32, -} - -#[streamable(message)] -pub struct RequestBlockHeaders { - start_height: u32, - end_height: u32, - return_filter: bool, -} - -#[streamable(message)] -pub struct RequestHeaderBlocks { - start_height: u32, - end_height: u32, -} - -#[streamable(message)] -pub struct RejectHeaderBlocks { - start_height: u32, - end_height: u32, -} - -#[streamable(message)] -pub struct RespondHeaderBlocks { - start_height: u32, - end_height: u32, - header_blocks: Vec, -} - -#[streamable(message)] -pub struct RegisterForPhUpdates { - puzzle_hashes: Vec, - min_height: u32, -} - -#[streamable(message)] -pub struct RespondToPhUpdates { - puzzle_hashes: Vec, - min_height: u32, - coin_states: Vec, -} - -#[streamable(message)] -pub struct RegisterForCoinUpdates { - coin_ids: Vec, - min_height: u32, -} - -#[streamable(message)] -pub struct RespondToCoinUpdates { - coin_ids: Vec, - min_height: u32, - coin_states: Vec, -} - -#[streamable(message)] -pub struct CoinStateUpdate { - height: u32, - fork_height: u32, - peak_hash: Bytes32, - items: Vec, -} - -#[streamable(message)] -pub struct RequestChildren { - coin_name: Bytes32, -} - -#[streamable(message)] -pub struct RespondChildren { - coin_states: Vec, -} - -#[streamable(message)] -pub struct RequestSesInfo { - start_height: u32, - end_height: u32, -} - -#[streamable(message)] -pub struct RespondSesInfo { - reward_chain_hash: Vec, - heights: Vec>, -} - -#[streamable(message)] -pub struct RequestFeeEstimates { - time_targets: Vec, -} - -#[streamable(message)] -pub struct RespondFeeEstimates { - estimates: FeeEstimateGroup, -} - -#[streamable(message)] -pub struct RequestRemovePuzzleSubscriptions { - puzzle_hashes: Option>, -} - -#[streamable(message)] -pub struct RespondRemovePuzzleSubscriptions { - puzzle_hashes: Vec, -} - -#[streamable(message)] -pub struct RequestRemoveCoinSubscriptions { - coin_ids: Option>, -} - -#[streamable(message)] -pub struct RespondRemoveCoinSubscriptions { - coin_ids: Vec, -} - -#[streamable] -pub struct CoinStateFilters { - include_spent: bool, - include_unspent: bool, - include_hinted: bool, - min_amount: u64, -} - -#[streamable(message)] -pub struct RequestPuzzleState { - puzzle_hashes: Vec, - previous_height: Option, - header_hash: Bytes32, - filters: CoinStateFilters, - subscribe_when_finished: bool, -} - -#[streamable(message)] -pub struct RespondPuzzleState { - puzzle_hashes: Vec, - height: u32, - header_hash: Bytes32, - is_finished: bool, - coin_states: Vec, -} - -#[streamable(message)] -pub struct RejectPuzzleState { - reason: RejectStateReason, -} - -#[streamable(message)] -pub struct RequestCoinState { - coin_ids: Vec, - previous_height: Option, - header_hash: Bytes32, - subscribe: bool, -} - -#[streamable(message)] -pub struct RespondCoinState { - coin_ids: Vec, - coin_states: Vec, -} - -#[streamable(message)] -pub struct RejectCoinState { - reason: RejectStateReason, -} - -#[cfg(feature = "py-bindings")] -use chia_py_streamable_macro::{PyJsonDict, PyStreamable}; - -#[repr(u8)] -#[cfg_attr(feature = "py-bindings", derive(PyJsonDict, PyStreamable))] -#[derive(Streamable, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub enum RejectStateReason { - Reorg = 0, - ExceededSubscriptionLimit = 1, -} - -#[cfg(feature = "py-bindings")] -impl chia_traits::ChiaToPython for RejectStateReason { - fn to_python<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - Ok(pyo3::IntoPyObject::into_pyobject(*self as u8, py)? - .clone() - .into_any()) - } -} - -#[repr(u8)] -#[cfg_attr(feature = "py-bindings", derive(PyJsonDict, PyStreamable))] -#[derive(Streamable, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub enum MempoolRemoveReason { - Conflict = 1, - BlockInclusion = 2, - PoolFull = 3, - Expired = 4, -} - -#[cfg(feature = "py-bindings")] -impl chia_traits::ChiaToPython for MempoolRemoveReason { - fn to_python<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - Ok(pyo3::IntoPyObject::into_pyobject(*self as u8, py)? - .clone() - .into_any()) - } -} - -#[streamable(no_serde)] -pub struct RemovedMempoolItem { - transaction_id: Bytes32, - reason: MempoolRemoveReason, -} - -#[streamable(message)] -pub struct MempoolItemsAdded { - transaction_ids: Vec, -} - -#[streamable(message)] -pub struct MempoolItemsRemoved { - removed_items: Vec, -} - -#[streamable(message)] -pub struct RequestCostInfo {} - -#[streamable(message)] -pub struct RespondCostInfo { - max_transaction_cost: u64, - max_block_cost: u64, - max_mempool_cost: u64, - mempool_cost: u64, - mempool_fee: u64, - bump_fee_per_cost: u8, -} diff --git a/vendor/chia-protocol/src/weight_proof.rs b/vendor/chia-protocol/src/weight_proof.rs deleted file mode 100644 index c38962a..0000000 --- a/vendor/chia-protocol/src/weight_proof.rs +++ /dev/null @@ -1,88 +0,0 @@ -use chia_streamable_macro::streamable; - -use crate::Bytes32; -use crate::EndOfSubSlotBundle; -use crate::HeaderBlock; -use crate::ProofOfSpace; -use crate::RewardChainBlock; -use crate::{VDFInfo, VDFProof}; - -#[streamable] -pub struct SubEpochData { - reward_chain_hash: Bytes32, - num_blocks_overflow: u8, - new_sub_slot_iters: Option, - new_difficulty: Option, -} - -// number of challenge blocks -// Average iters for challenge blocks -// |--A-R----R-------R--------R------R----R----------R-----R--R---| Honest difficulty 1000 -// 0.16 - -// compute total reward chain blocks -// |----------------------------A---------------------------------| Attackers chain 1000 -// 0.48 -// total number of challenge blocks == total number of reward chain blocks - -#[streamable] -pub struct SubSlotData { - proof_of_space: Option, - cc_signage_point: Option, - cc_infusion_point: Option, - icc_infusion_point: Option, - cc_sp_vdf_info: Option, - signage_point_index: Option, - cc_slot_end: Option, - icc_slot_end: Option, - cc_slot_end_info: Option, - icc_slot_end_info: Option, - cc_ip_vdf_info: Option, - icc_ip_vdf_info: Option, - total_iters: Option, -} - -#[cfg(feature = "py-bindings")] -use pyo3::prelude::*; - -#[cfg_attr(feature = "py-bindings", pymethods)] -impl SubSlotData { - pub fn is_end_of_slot(&self) -> bool { - self.cc_slot_end_info.is_some() - } - - pub fn is_challenge(&self) -> bool { - self.proof_of_space.is_some() - } -} - -#[streamable] -pub struct SubEpochChallengeSegment { - sub_epoch_n: u32, - sub_slots: Vec, - rc_slot_end_info: Option, -} - -#[streamable] -pub struct SubEpochSegments { - challenge_segments: Vec, -} - -// this is used only for serialization to database -#[streamable] -pub struct RecentChainData { - recent_chain_data: Vec, -} - -#[streamable] -pub struct ProofBlockHeader { - finished_sub_slots: Vec, - reward_chain_block: RewardChainBlock, -} - -#[streamable] -pub struct WeightProof { - sub_epochs: Vec, - sub_epoch_segments: Vec, // sampled sub epoch - recent_chain_data: Vec, -} diff --git a/vendor/chia-sdk-client/Cargo.toml b/vendor/chia-sdk-client/Cargo.toml deleted file mode 100644 index d111d72..0000000 --- a/vendor/chia-sdk-client/Cargo.toml +++ /dev/null @@ -1,171 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2021" -name = "chia-sdk-client" -version = "0.28.0" -authors = ["Brandon Haggstrom "] -build = false -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Utilities for connecting to Chia full node peers via the light wallet protocol." -homepage = "https://github.com/Rigidity/chia-wallet-sdk" -readme = "README.md" -keywords = [ - "chia", - "wallet", - "blockchain", - "crypto", -] -categories = [ - "cryptography::cryptocurrencies", - "development-tools", -] -license = "Apache-2.0" -repository = "https://github.com/Rigidity/chia-wallet-sdk" - -[package.metadata.cargo-machete] -ignored = ["aws-lc-rs"] - -[features] -native-tls = [ - "dep:native-tls", - "tokio-tungstenite/native-tls", -] -rustls = [ - "dep:rustls", - "dep:rustls-pemfile", - "dep:aws-lc-rs", - "tokio-tungstenite/rustls-tls-webpki-roots", -] - -[lib] -name = "chia_sdk_client" -path = "src/lib.rs" - -[dependencies.aws-lc-rs] -version = "1" -features = ["bindgen"] -optional = true - -[dependencies.chia-protocol] -version = "0.26.0" - -[dependencies.chia-sdk-types] -version = "0.28.0" - -[dependencies.chia-ssl] -version = "0.26.0" - -[dependencies.chia-traits] -version = "0.26.0" - -[dependencies.futures-util] -version = "0.3.30" - -[dependencies.native-tls] -version = "0.2.14" -optional = true - -[dependencies.once_cell] -version = "1.21.3" - -[dependencies.rustls] -version = "0.23.29" -features = ["aws_lc_rs"] -optional = true - -[dependencies.rustls-pemfile] -version = "2.2.0" -optional = true - -[dependencies.thiserror] -version = "2.0.12" - -[dependencies.tokio] -version = "1.47.0" -features = [ - "sync", - "time", - "rt", -] - -[dependencies.tokio-tungstenite] -version = "0.24.0" - -[dependencies.tracing] -version = "0.1.41" - -[dependencies.tungstenite] -version = "0.24.0" - -[lints.clippy] -cargo_common_metadata = "allow" -missing_errors_doc = "allow" -missing_panics_doc = "allow" -module_name_repetitions = "allow" -multiple_crate_versions = "allow" -must_use_candidate = "allow" -too_many_lines = "allow" - -[lints.clippy.all] -level = "deny" -priority = -1 - -[lints.clippy.cargo] -level = "warn" -priority = -1 - -[lints.clippy.pedantic] -level = "warn" -priority = -1 - -[lints.rust] -dead_code = "deny" -deprecated = "deny" -deprecated_in_future = "deny" -missing_copy_implementations = "warn" -missing_debug_implementations = "warn" -non_ascii_idents = "deny" -trivial_casts = "deny" -trivial_numeric_casts = "deny" -unreachable_code = "warn" -unreachable_patterns = "deny" -unreachable_pub = "warn" -unsafe_code = "deny" -unused_extern_crates = "deny" - -[lints.rust.future_incompatible] -level = "deny" -priority = -1 - -[lints.rust.nonstandard_style] -level = "deny" -priority = -1 - -[lints.rust.rust_2018_idioms] -level = "deny" -priority = -1 - -[lints.rust.rust_2021_compatibility] -level = "deny" -priority = -1 - -[lints.rustdoc] -missing_crate_level_docs = "allow" - -[lints.rustdoc.all] -level = "deny" -priority = -1 diff --git a/vendor/chia-sdk-client/Cargo.toml.orig b/vendor/chia-sdk-client/Cargo.toml.orig deleted file mode 100644 index 71bc135..0000000 --- a/vendor/chia-sdk-client/Cargo.toml.orig +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "chia-sdk-client" -version = "0.28.0" -edition = "2021" -license = "Apache-2.0" -description = "Utilities for connecting to Chia full node peers via the light wallet protocol." -authors = ["Brandon Haggstrom "] -homepage = "https://github.com/Rigidity/chia-wallet-sdk" -repository = "https://github.com/Rigidity/chia-wallet-sdk" -readme = { workspace = true } -keywords = { workspace = true } -categories = { workspace = true } - -[lints] -workspace = true - -[features] -native-tls = ["dep:native-tls", "tokio-tungstenite/native-tls"] -rustls = ["dep:rustls", "dep:rustls-pemfile", "dep:aws-lc-rs", "tokio-tungstenite/rustls-tls-webpki-roots"] - -[dependencies] -chia-sdk-types = { workspace = true } -chia-protocol = { workspace = true } -chia-traits = { workspace = true } -chia-ssl = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync", "time", "rt"] } -tungstenite = { workspace = true } -native-tls = { workspace = true, optional = true } -rustls = { workspace = true, optional = true, features = ["aws_lc_rs"] } -rustls-pemfile = { workspace = true, optional = true } -tracing = { workspace = true } -futures-util = { workspace = true } -tokio-tungstenite = { workspace = true } -once_cell = { workspace = true } - -# This is to ensure that the bindgen feature is enabled for the aws-lc-rs crate. -# https://aws.github.io/aws-lc-rs/platform_support.html#tested-platforms -aws-lc-rs = { version = "1", features = ["bindgen"], optional = true } - -[package.metadata.cargo-machete] -ignored = ["aws-lc-rs"] diff --git a/vendor/chia-sdk-client/README.dig-gossip.md b/vendor/chia-sdk-client/README.dig-gossip.md deleted file mode 100644 index 6f2f1a2..0000000 --- a/vendor/chia-sdk-client/README.dig-gossip.md +++ /dev/null @@ -1,52 +0,0 @@ -# `chia-sdk-client` — the dig-gossip fork delta - -Vendored via `[patch.crates-io]` in the workspace `Cargo.toml`. The tree is an unpacked -**crates.io `chia-sdk-client` 0.28.0** tarball, so the pristine crate of the same version is the -exact baseline and everything the diff reports is DIG's. - -## Regenerate this delta — do not hand-maintain it - -```sh -vendor/fork-delta.sh chia-sdk-client --summary # the file list -vendor/fork-delta.sh chia-sdk-client # the full unified diff -``` - -A hand-written delta has been wrong twice (dig_ecosystem#2228): this README did not describe the -patch at all, and the rate-limit surface was attributed to the wrong crate. **The compiler and the -diff are the record; this file is a summary of them and must be regenerated when either changes.** - -## What the fork changes — one file, three items - -`--summary` reports exactly one differing file, `src/peer.rs`: - -1. **`Peer::from_server_websocket`** (#1371) — construct a `Peer` from an already-established - **server-side** WebSocket. `from_websocket` recovers the peer address by inspecting a client - `MaybeTlsStream`, which a `tokio_rustls::server::TlsStream` cannot inhabit (the enum is - `#[non_exhaustive]`). Supporting it required type-erasing the split halves behind `BoxedSink` / - `BoxedStream` so `Peer` itself stays non-generic — which in turn forces a manual `Debug` for - `PeerInner` and a shared `from_parts` constructor. Needed by dig-gossip's rustls inbound acceptor. -2. **`Peer::send_protocol_message`** — send a fully-formed wire `Message` preserving its `id`, so an - inbound request can be answered on the same correlation id. -3. **Inbound `RequestPeers` is routed to the application channel** rather than matched against the - outbound `RequestMap`. A remote's `RequestPeers` id comes from the *sender's* map and may collide - with one of our in-flight request ids, which would deliver it to an unrelated waiter and surface - as `ClientError::InvalidResponse`. - -Items 2 and 3 are one change: 3 makes the inbound request reachable, 2 answers it. - -## Upstream status - -All three are additive, carry **no DIG semantics**, and are unimplementable outside the crate only -because `Peer`'s fields are private — so all three are genuine upstream candidates for -[xch-dev/chia-wallet-sdk](https://github.com/xch-dev/chia-wallet-sdk). If upstream takes them, this -fork retires. Note that item 3 is a behavioural fix rather than pure API addition, so it needs to be -argued as such in that PR (dig_ecosystem#2228 S3). - -## What used to be here and no longer is - -`RateLimits::dig_wire` and `RateLimiter::check_dig_extension` were removed in dig_ecosystem#2228. -The DIG per-opcode bound is keyed by the raw wire byte, never by `ProtocolMessageTypes`, and its -accounting was already fully parallel to Chia's — so it never needed the fork. It now lives in -dig-gossip as `connection::dig_rate_limiter::DigRateLimiter`, composed with Chia's `RateLimiter` by -`connection::inbound_limits::InboundRateLimiter`. `src/rate_limits.rs` and `src/rate_limiter.rs` are -byte-identical to upstream again. diff --git a/vendor/chia-sdk-client/README.md b/vendor/chia-sdk-client/README.md deleted file mode 100644 index 6c3f73e..0000000 --- a/vendor/chia-sdk-client/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Chia Wallet SDK - -[![crate](https://img.shields.io/crates/v/chia-wallet-sdk.svg)](https://crates.io/crates/chia-wallet-sdk) -[![documentation](https://docs.rs/chia-wallet-sdk/badge.svg)](https://docs.rs/chia-wallet-sdk) -[![minimum rustc 1.81.0](https://img.shields.io/badge/rustc-1.81.0+-red.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) -[![codecov](https://codecov.io/github/Rigidity/chia-wallet-sdk/graph/badge.svg?token=M2MPMFGCCA)](https://codecov.io/github/Rigidity/chia-wallet-sdk) - -This is a library for building applications that need to interact with coins on the [Chia blockchain](https://chia.net), including wallets and dApps (decentralized apps). However, it is _not_ a prebuilt wallet. If you are looking for that, you can use [Sage Wallet](https://github.com/xch-dev/sage), which is a light wallet built using the Wallet SDK that provides an RPC interface. - -## Getting Started - -To learn more about developing applications on Chia, I'd recommend reading [xch.dev](https://xch.dev). - -There are also [Rust docs](https://docs.rs/chia-wallet-sdk/latest/chia_wallet_sdk) will get you going with the crate, and assume you are already familiar with Rust and Chia's coin set model. - -## Credits - -Special thanks to: - -- [SumSet Tech, LLC](https://sumset.tech) for sponsoring the initial development of the Wallet SDK -- [Solomons Lot](https://solslot.com) for sponsoring the WASM bindings -- [FireAcademy.io](https://www.fireacademy.io/) -- All of the open source contributors who have helped it get this far diff --git a/vendor/chia-sdk-client/src/client.rs b/vendor/chia-sdk-client/src/client.rs deleted file mode 100644 index 5e8a349..0000000 --- a/vendor/chia-sdk-client/src/client.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - fmt, - net::{IpAddr, SocketAddr}, - ops::Deref, - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; - -use chia_protocol::Message; -use tokio::sync::{mpsc, Mutex}; -use tokio_tungstenite::Connector; - -use crate::{connect_peer, ClientError, Network, Peer, PeerOptions}; - -#[derive(Clone)] -pub struct Client { - network_id: String, - network: Network, - connector: Connector, - state: Arc>, -} - -#[allow(clippy::missing_fields_in_debug)] -impl fmt::Debug for Client { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Client") - .field("network_id", &self.network_id) - .field("network", &self.network) - .finish() - } -} - -impl Deref for Client { - type Target = Mutex; - - fn deref(&self) -> &Self::Target { - &self.state - } -} - -#[derive(Debug, Default, Clone)] -pub struct ClientState { - peers: HashMap, - banned_peers: HashMap, - trusted_peers: HashSet, -} - -impl Client { - pub fn new(network_id: String, network: Network, connector: Connector) -> Self { - Self { - network_id, - network, - connector, - state: Arc::new(Mutex::new(ClientState::default())), - } - } - - pub fn network_id(&self) -> &str { - &self.network_id - } - - pub fn network(&self) -> &Network { - &self.network - } - - pub async fn connect( - &self, - socket_addr: SocketAddr, - options: PeerOptions, - ) -> Result, ClientError> { - let (peer, receiver) = connect_peer( - self.network_id.clone(), - self.connector.clone(), - socket_addr, - options, - ) - .await?; - - let mut state = self.state.lock().await; - let ip_addr = peer.socket_addr().ip(); - - if state.is_banned(&ip_addr) { - return Err(ClientError::BannedPeer); - } - - state.peers.insert(peer.socket_addr().ip(), peer); - - Ok(receiver) - } -} - -impl ClientState { - pub fn peers(&self) -> impl Iterator { - self.peers.values() - } - - pub fn disconnect(&mut self, ip_addr: &IpAddr) -> bool { - self.peers.remove(ip_addr).is_some() - } - - pub fn is_banned(&self, ip_addr: &IpAddr) -> bool { - self.banned_peers.contains_key(ip_addr) - } - - pub fn is_trusted(&self, ip_addr: &IpAddr) -> bool { - self.trusted_peers.contains(ip_addr) - } - - pub fn ban(&mut self, ip_addr: IpAddr) -> bool { - if self.is_trusted(&ip_addr) { - return false; - } - - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - - self.disconnect(&ip_addr); - self.banned_peers.insert(ip_addr, timestamp).is_none() - } - - pub fn unban(&mut self, ip_addr: IpAddr) -> bool { - self.banned_peers.remove(&ip_addr).is_some() - } - - pub fn trust(&mut self, ip_addr: IpAddr) -> bool { - let result = self.trusted_peers.insert(ip_addr); - self.banned_peers.remove(&ip_addr); - result - } - - pub fn untrust(&mut self, ip_addr: IpAddr) -> bool { - self.trusted_peers.remove(&ip_addr) - } -} diff --git a/vendor/chia-sdk-client/src/connect.rs b/vendor/chia-sdk-client/src/connect.rs deleted file mode 100644 index 100e764..0000000 --- a/vendor/chia-sdk-client/src/connect.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::net::SocketAddr; - -use chia_protocol::{Handshake, Message, NodeType, ProtocolMessageTypes}; -use chia_traits::Streamable; -use tokio::sync::mpsc; -use tokio_tungstenite::Connector; -use tracing::instrument; - -use crate::{ClientError, Peer, PeerOptions}; - -#[instrument(skip(connector))] -pub async fn connect_peer( - network_id: String, - connector: Connector, - socket_addr: SocketAddr, - options: PeerOptions, -) -> Result<(Peer, mpsc::Receiver), ClientError> { - let (peer, mut receiver) = Peer::connect(socket_addr, connector, options).await?; - - peer.send(Handshake { - network_id: network_id.clone(), - protocol_version: "0.0.37".to_string(), - software_version: "0.0.0".to_string(), - server_port: 0, - node_type: NodeType::Wallet, - capabilities: vec![ - (1, "1".to_string()), - (2, "1".to_string()), - (3, "1".to_string()), - ], - }) - .await?; - - let Some(message) = receiver.recv().await else { - return Err(ClientError::MissingHandshake); - }; - - if message.msg_type != ProtocolMessageTypes::Handshake { - return Err(ClientError::InvalidResponse( - vec![ProtocolMessageTypes::Handshake], - message.msg_type, - )); - } - - let handshake = Handshake::from_bytes(&message.data)?; - - if handshake.node_type != NodeType::FullNode { - return Err(ClientError::WrongNodeType( - NodeType::FullNode, - handshake.node_type, - )); - } - - if handshake.network_id != network_id { - return Err(ClientError::WrongNetwork( - network_id.to_string(), - handshake.network_id, - )); - } - - Ok((peer, receiver)) -} diff --git a/vendor/chia-sdk-client/src/error.rs b/vendor/chia-sdk-client/src/error.rs deleted file mode 100644 index a778c4d..0000000 --- a/vendor/chia-sdk-client/src/error.rs +++ /dev/null @@ -1,58 +0,0 @@ -use chia_protocol::{NodeType, ProtocolMessageTypes}; -use thiserror::Error; -use tokio::sync::oneshot::error::RecvError; - -#[derive(Debug, Error)] -pub enum ClientError { - #[error("SSL error: {0}")] - Ssl(#[from] chia_ssl::Error), - - #[error("TLS method is not supported")] - UnsupportedTls, - - #[error("Streamable error: {0}")] - Streamable(#[from] chia_traits::Error), - - #[error("WebSocket error: {0}")] - WebSocket(#[from] tungstenite::Error), - - #[cfg(feature = "native-tls")] - #[error("Native TLS error: {0}")] - NativeTls(#[from] native_tls::Error), - - #[cfg(feature = "rustls")] - #[error("Rustls error: {0}")] - Rustls(#[from] rustls::Error), - - #[cfg(feature = "rustls")] - #[error("Missing pkcs8 private key")] - MissingPkcs8Key, - - #[cfg(feature = "rustls")] - #[error("Missing CA cert")] - MissingCa, - - #[error("Unexpected message received with type {0:?}")] - UnexpectedMessage(ProtocolMessageTypes), - - #[error("Expected response with type {0:?}, found {1:?}")] - InvalidResponse(Vec, ProtocolMessageTypes), - - #[error("Failed to receive message")] - Recv(#[from] RecvError), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("Missing response during handshake")] - MissingHandshake, - - #[error("Expected node type {0:?}, but found {1:?}")] - WrongNodeType(NodeType, NodeType), - - #[error("Expected network {0}, but found {1}")] - WrongNetwork(String, String), - - #[error("The peer is banned")] - BannedPeer, -} diff --git a/vendor/chia-sdk-client/src/lib.rs b/vendor/chia-sdk-client/src/lib.rs deleted file mode 100644 index fca64f9..0000000 --- a/vendor/chia-sdk-client/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -mod error; -mod network; -mod peer; -mod rate_limiter; -mod rate_limits; -mod request_map; -mod tls; - -pub use error::*; -pub use network::*; -pub use peer::*; -pub use rate_limiter::*; -pub use rate_limits::*; -pub use tls::*; - -#[cfg(any(feature = "native-tls", feature = "rustls"))] -mod client; -#[cfg(any(feature = "native-tls", feature = "rustls"))] -mod connect; - -#[cfg(any(feature = "native-tls", feature = "rustls"))] -pub use client::*; -#[cfg(any(feature = "native-tls", feature = "rustls"))] -pub use connect::*; - -#[cfg(any(feature = "native-tls", feature = "rustls"))] -pub use tokio_tungstenite::Connector; diff --git a/vendor/chia-sdk-client/src/network.rs b/vendor/chia-sdk-client/src/network.rs deleted file mode 100644 index 5730203..0000000 --- a/vendor/chia-sdk-client/src/network.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::{net::SocketAddr, time::Duration}; - -use chia_protocol::Bytes32; -use chia_sdk_types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS}; -use futures_util::{stream::FuturesUnordered, StreamExt}; -use tracing::{info, instrument, warn}; - -use crate::ClientError; - -#[derive(Debug, Clone)] -pub struct Network { - pub default_port: u16, - pub genesis_challenge: Bytes32, - pub dns_introducers: Vec, -} - -impl Network { - pub fn default_mainnet() -> Self { - Self { - default_port: 8444, - genesis_challenge: MAINNET_CONSTANTS.genesis_challenge, - dns_introducers: vec![ - "dns-introducer.chia.net".to_string(), - "chia.ctrlaltdel.ch".to_string(), - "seeder.dexie.space".to_string(), - "chia.hoffmang.com".to_string(), - ], - } - } - - pub fn default_testnet11() -> Self { - Self { - default_port: 58444, - genesis_challenge: TESTNET11_CONSTANTS.genesis_challenge, - dns_introducers: vec!["dns-introducer-testnet11.chia.net".to_string()], - } - } - - #[instrument] - pub async fn lookup_all(&self, timeout: Duration, batch_size: usize) -> Vec { - let mut result = Vec::new(); - - for batch in self.dns_introducers.chunks(batch_size) { - let mut futures = FuturesUnordered::new(); - - for dns_introducer in batch { - futures.push(async move { - match tokio::time::timeout(timeout, self.lookup_host(dns_introducer)).await { - Ok(Ok(addrs)) => addrs, - Ok(Err(error)) => { - warn!("Failed to lookup DNS introducer {dns_introducer}: {error}"); - Vec::new() - } - Err(_timeout) => { - warn!("Timeout looking up DNS introducer {dns_introducer}"); - Vec::new() - } - } - }); - } - - while let Some(addrs) = futures.next().await { - result.extend(addrs); - } - } - - result - } - - #[instrument] - pub async fn lookup_host(&self, dns_introducer: &str) -> Result, ClientError> { - info!("Looking up DNS introducer {dns_introducer}"); - let mut result = Vec::new(); - for addr in tokio::net::lookup_host(format!("{dns_introducer}:80")).await? { - result.push(SocketAddr::new(addr.ip(), self.default_port)); - } - Ok(result) - } -} diff --git a/vendor/chia-sdk-client/src/peer.rs b/vendor/chia-sdk-client/src/peer.rs deleted file mode 100644 index b7e157f..0000000 --- a/vendor/chia-sdk-client/src/peer.rs +++ /dev/null @@ -1,476 +0,0 @@ -use std::{net::SocketAddr, sync::Arc, time::Duration}; - -use chia_protocol::{ - Bytes32, ChiaProtocolMessage, CoinStateFilters, Message, ProtocolMessageTypes, - PuzzleSolutionResponse, - RegisterForCoinUpdates, RegisterForPhUpdates, RejectCoinState, RejectPuzzleSolution, - RejectPuzzleState, RequestChildren, RequestCoinState, RequestPeers, RequestPuzzleSolution, - RequestPuzzleState, RequestRemoveCoinSubscriptions, RequestRemovePuzzleSubscriptions, - RequestTransaction, RespondChildren, RespondCoinState, RespondPeers, RespondPuzzleSolution, - RespondPuzzleState, RespondRemoveCoinSubscriptions, RespondRemovePuzzleSubscriptions, - RespondToCoinUpdates, RespondToPhUpdates, RespondTransaction, SendTransaction, SpendBundle, - TransactionAck, -}; -use chia_traits::Streamable; -use futures_util::{SinkExt, StreamExt}; -use tokio::{ - net::TcpStream, - sync::{mpsc, oneshot, Mutex}, - task::JoinHandle, -}; -use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; -use tracing::{debug, warn}; - -use crate::{request_map::RequestMap, ClientError, RateLimiter, V2_RATE_LIMITS}; - -#[cfg(any(feature = "native-tls", feature = "rustls"))] -use tokio_tungstenite::Connector; - -type WebSocket = WebSocketStream>; -type Response = std::result::Result; - -/// Type-erased WebSocket write half. -/// -/// Boxing decouples [`PeerInner`] from the concrete transport so a **server-side** TLS stream -/// (e.g. `tokio_rustls::server::TlsStream`, which cannot inhabit the `#[non_exhaustive]` -/// client-oriented [`MaybeTlsStream`] enum) can back a [`Peer`] via [`Peer::from_server_websocket`] -/// while [`Peer`] itself stays non-generic (dig-gossip #1371). -type BoxedSink = - Box + Send + Unpin>; - -/// Type-erased WebSocket read half — the counterpart to [`BoxedSink`] (dig-gossip #1371). -type BoxedStream = Box< - dyn futures_util::Stream> + Send + Unpin, ->; - -#[derive(Debug, Clone, Copy)] -pub struct PeerOptions { - pub rate_limit_factor: f64, -} - -impl Default for PeerOptions { - fn default() -> Self { - Self { - rate_limit_factor: 0.6, - } - } -} - -#[derive(Debug, Clone)] -pub struct Peer(Arc); - -struct PeerInner { - sink: Mutex, - inbound_handle: JoinHandle<()>, - requests: Arc, - socket_addr: SocketAddr, - outbound_rate_limiter: Mutex, -} - -// Manual `Debug`: `BoxedSink` is a trait object without a `Debug` bound, so `#[derive(Debug)]` -// (required by `Peer`'s derive) cannot see through it. Only the stable, printable fields are shown. -impl std::fmt::Debug for PeerInner { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PeerInner") - .field("socket_addr", &self.socket_addr) - .finish_non_exhaustive() - } -} - -impl Peer { - /// Connects to a peer using its IP address and port. - #[cfg(any(feature = "native-tls", feature = "rustls"))] - pub async fn connect( - socket_addr: SocketAddr, - connector: Connector, - options: PeerOptions, - ) -> Result<(Self, mpsc::Receiver), ClientError> { - Self::connect_full_uri(&format!("wss://{socket_addr}/ws"), connector, options).await - } - - /// Connects to a peer using its full websocket URI. - /// For example, `wss://127.0.0.1:8444/ws`. - #[cfg(any(feature = "native-tls", feature = "rustls"))] - pub async fn connect_full_uri( - uri: &str, - connector: Connector, - options: PeerOptions, - ) -> Result<(Self, mpsc::Receiver), ClientError> { - let (ws, _) = - tokio_tungstenite::connect_async_tls_with_config(uri, None, false, Some(connector)) - .await?; - Self::from_websocket(ws, options) - } - - /// Creates a peer from an existing websocket connection. - /// The connection must be secured with TLS, so that the certificate can be hashed in a peer id. - pub fn from_websocket( - ws: WebSocket, - options: PeerOptions, - ) -> Result<(Self, mpsc::Receiver), ClientError> { - let socket_addr = match ws.get_ref() { - #[cfg(feature = "native-tls")] - MaybeTlsStream::NativeTls(tls) => { - let tls_stream = tls.get_ref(); - let tcp_stream = tls_stream.get_ref().get_ref(); - tcp_stream.peer_addr()? - } - #[cfg(feature = "rustls")] - MaybeTlsStream::Rustls(tls) => { - let (tcp_stream, _) = tls.get_ref(); - tcp_stream.peer_addr()? - } - MaybeTlsStream::Plain(plain) => plain.peer_addr()?, - _ => return Err(ClientError::UnsupportedTls), - }; - - let (sink, stream) = ws.split(); - Ok(Self::from_parts( - Box::new(sink), - Box::new(stream), - socket_addr, - options, - )) - } - - /// Creates a peer from an already-established **server-side** WebSocket connection. - /// - /// Unlike [`Peer::from_websocket`] — which inspects a client [`MaybeTlsStream`] to recover the - /// peer socket address — an inbound acceptor already knows `socket_addr` and holds a server-side - /// TLS stream (e.g. `tokio_rustls::server::TlsStream`) that cannot inhabit the - /// `#[non_exhaustive]` client-oriented [`MaybeTlsStream`] enum. This constructor is therefore - /// generic over the underlying transport, keeping [`Peer`] non-generic by boxing the split - /// halves. Used by dig-gossip's rustls inbound acceptor (#1371). - /// - /// The caller is responsible for deriving `PeerId` from the captured client certificate before - /// calling this (the peer certificate is not reachable once the stream is split). - pub fn from_server_websocket( - ws: WebSocketStream, - socket_addr: SocketAddr, - options: PeerOptions, - ) -> Result<(Self, mpsc::Receiver), ClientError> - where - S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static, - { - let (sink, stream) = ws.split(); - Ok(Self::from_parts( - Box::new(sink), - Box::new(stream), - socket_addr, - options, - )) - } - - /// Wire the split WebSocket halves into a live [`Peer`] + inbound message channel. - /// - /// Shared by [`Peer::from_websocket`] (client/`MaybeTlsStream`) and - /// [`Peer::from_server_websocket`] (server transport) so both construction paths stay identical - /// below the type-erasure boundary. - fn from_parts( - sink: BoxedSink, - stream: BoxedStream, - socket_addr: SocketAddr, - options: PeerOptions, - ) -> (Self, mpsc::Receiver) { - let (sender, receiver) = mpsc::channel(32); - - let requests = Arc::new(RequestMap::new()); - let requests_clone = requests.clone(); - - let inbound_handle = tokio::spawn(async move { - if let Err(error) = handle_inbound_messages(stream, sender, requests_clone).await { - debug!("Error handling message: {error}"); - } - }); - - let peer = Self(Arc::new(PeerInner { - sink: Mutex::new(sink), - inbound_handle, - requests, - socket_addr, - outbound_rate_limiter: Mutex::new(RateLimiter::new( - false, - 60, - options.rate_limit_factor, - V2_RATE_LIMITS.clone(), - )), - })); - - (peer, receiver) - } - - /// The IP address and port of the peer connection. - pub fn socket_addr(&self) -> SocketAddr { - self.0.socket_addr - } - - pub async fn send_transaction( - &self, - spend_bundle: SpendBundle, - ) -> Result { - self.request_infallible(SendTransaction::new(spend_bundle)) - .await - } - - pub async fn request_puzzle_state( - &self, - puzzle_hashes: Vec, - previous_height: Option, - header_hash: Bytes32, - filters: CoinStateFilters, - subscribe_when_finished: bool, - ) -> Result, ClientError> { - self.request_fallible(RequestPuzzleState::new( - puzzle_hashes, - previous_height, - header_hash, - filters, - subscribe_when_finished, - )) - .await - } - - pub async fn request_coin_state( - &self, - coin_ids: Vec, - previous_height: Option, - header_hash: Bytes32, - subscribe: bool, - ) -> Result, ClientError> { - self.request_fallible(RequestCoinState::new( - coin_ids, - previous_height, - header_hash, - subscribe, - )) - .await - } - - pub async fn register_for_ph_updates( - &self, - puzzle_hashes: Vec, - min_height: u32, - ) -> Result { - self.request_infallible(RegisterForPhUpdates::new(puzzle_hashes, min_height)) - .await - } - - pub async fn register_for_coin_updates( - &self, - coin_ids: Vec, - min_height: u32, - ) -> Result { - self.request_infallible(RegisterForCoinUpdates::new(coin_ids, min_height)) - .await - } - - pub async fn remove_puzzle_subscriptions( - &self, - puzzle_hashes: Option>, - ) -> Result { - self.request_infallible(RequestRemovePuzzleSubscriptions::new(puzzle_hashes)) - .await - } - - pub async fn remove_coin_subscriptions( - &self, - coin_ids: Option>, - ) -> Result { - self.request_infallible(RequestRemoveCoinSubscriptions::new(coin_ids)) - .await - } - - pub async fn request_transaction( - &self, - transaction_id: Bytes32, - ) -> Result { - self.request_infallible(RequestTransaction::new(transaction_id)) - .await - } - - pub async fn request_puzzle_and_solution( - &self, - coin_id: Bytes32, - height: u32, - ) -> Result, ClientError> { - match self - .request_fallible::(RequestPuzzleSolution::new( - coin_id, height, - )) - .await? - { - Ok(response) => Ok(Ok(response.response)), - Err(rejection) => Ok(Err(rejection)), - } - } - - pub async fn request_children(&self, coin_id: Bytes32) -> Result { - self.request_infallible(RequestChildren::new(coin_id)).await - } - - pub async fn request_peers(&self) -> Result { - self.request_infallible(RequestPeers::new()).await - } - - /// Sends a message to the peer, but does not expect any response. - pub async fn send(&self, body: T) -> Result<(), ClientError> - where - T: Streamable + ChiaProtocolMessage, - { - self.send_raw(Message { - msg_type: T::msg_type(), - id: None, - data: body.to_bytes()?.into(), - }) - .await?; - - Ok(()) - } - - /// Sends a message to the peer and expects a message that's either a response or a rejection. - pub async fn request_fallible(&self, body: B) -> Result, ClientError> - where - T: Streamable + ChiaProtocolMessage, - E: Streamable + ChiaProtocolMessage, - B: Streamable + ChiaProtocolMessage, - { - let message = self.request_raw(body).await?; - if message.msg_type != T::msg_type() && message.msg_type != E::msg_type() { - return Err(ClientError::InvalidResponse( - vec![T::msg_type(), E::msg_type()], - message.msg_type, - )); - } - if message.msg_type == T::msg_type() { - Ok(Ok(T::from_bytes(&message.data)?)) - } else { - Ok(Err(E::from_bytes(&message.data)?)) - } - } - - /// Sends a message to the peer and expects a specific response message. - pub async fn request_infallible(&self, body: B) -> Result - where - T: Streamable + ChiaProtocolMessage, - B: Streamable + ChiaProtocolMessage, - { - let message = self.request_raw(body).await?; - if message.msg_type != T::msg_type() { - return Err(ClientError::InvalidResponse( - vec![T::msg_type()], - message.msg_type, - )); - } - Ok(T::from_bytes(&message.data)?) - } - - /// Sends a message to the peer and expects any arbitrary protocol message without parsing it. - pub async fn request_raw(&self, body: T) -> Result - where - T: Streamable + ChiaProtocolMessage, - { - let (sender, receiver) = oneshot::channel(); - - self.send_raw(Message { - msg_type: T::msg_type(), - id: Some(self.0.requests.insert(sender).await), - data: body.to_bytes()?.into(), - }) - .await?; - - Ok(receiver.await?) - } - - /// Sends a fully-formed wire `Message`, preserving `id` for correlated RPC replies. - /// - /// Used when answering inbound `RequestPeers` (forwarded on the `mpsc` receiver from - /// `from_websocket`) with `RespondPeers` carrying the same `id`. - pub async fn send_protocol_message(&self, message: Message) -> Result<(), ClientError> { - self.send_raw(message).await - } - - async fn send_raw(&self, message: Message) -> Result<(), ClientError> { - loop { - if !self - .0 - .outbound_rate_limiter - .lock() - .await - .handle_message(&message) - { - tokio::time::sleep(Duration::from_secs(1)).await; - continue; - } - - self.0 - .sink - .lock() - .await - .send(message.to_bytes()?.into()) - .await?; - - return Ok(()); - } - } - - pub async fn close(&self) -> Result<(), ClientError> { - self.0.sink.lock().await.close().await?; - Ok(()) - } -} - -impl Drop for PeerInner { - fn drop(&mut self) { - self.inbound_handle.abort(); - } -} - -async fn handle_inbound_messages( - mut stream: BoxedStream, - sender: mpsc::Sender, - requests: Arc, -) -> Result<(), ClientError> { - use tungstenite::Message::{Binary, Close, Frame, Ping, Pong, Text}; - - while let Some(message) = stream.next().await { - let message = message?; - - match message { - Frame(..) => unreachable!(), - Close(..) => break, - Ping(..) | Pong(..) => {} - Text(text) => { - warn!("Received unexpected text message: {text}"); - } - Binary(binary) => { - let message = Message::from_bytes(&binary)?; - - let Some(id) = message.id else { - sender.send(message).await.ok(); - continue; - }; - - // Remote `RequestPeers` uses ids from the sender's `RequestMap`. Our outbound - // `request_raw` may use the *same* numeric id for an unrelated waiter; matching - // `remove(id)` first would deliver `RequestPeers` to a `RespondPeers` waiter and - // yield `ClientError::InvalidResponse`. Always treat inbound `RequestPeers` as - // application-level RPC (see `send_protocol_message` for replies). - if message.msg_type == ProtocolMessageTypes::RequestPeers { - sender.send(message).await.ok(); - continue; - } - - let Some(request) = requests.remove(id).await else { - warn!( - "Received {:?} message with untracked id {id}", - message.msg_type - ); - return Err(ClientError::UnexpectedMessage(message.msg_type)); - }; - - request.send(message); - } - } - } - Ok(()) -} diff --git a/vendor/chia-sdk-client/src/rate_limiter.rs b/vendor/chia-sdk-client/src/rate_limiter.rs deleted file mode 100644 index c55fea7..0000000 --- a/vendor/chia-sdk-client/src/rate_limiter.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::{ - collections::HashMap, - time::{SystemTime, UNIX_EPOCH}, -}; - -use chia_protocol::{Message, ProtocolMessageTypes}; - -use crate::RateLimits; - -#[derive(Debug, Clone)] -pub struct RateLimiter { - incoming: bool, - reset_seconds: u64, - period: u64, - message_counts: HashMap, - message_cumulative_sizes: HashMap, - limit_factor: f64, - non_tx_count: f64, - non_tx_size: f64, - rate_limits: RateLimits, -} - -impl RateLimiter { - pub fn new( - incoming: bool, - reset_seconds: u64, - limit_factor: f64, - rate_limits: RateLimits, - ) -> Self { - Self { - incoming, - reset_seconds, - period: time() / reset_seconds, - message_counts: HashMap::new(), - message_cumulative_sizes: HashMap::new(), - limit_factor, - non_tx_count: 0.0, - non_tx_size: 0.0, - rate_limits, - } - } - - pub fn handle_message(&mut self, message: &Message) -> bool { - let size: u32 = message.data.len().try_into().expect("Message too large"); - let size = f64::from(size); - let period = time() / self.reset_seconds; - - if self.period != period { - self.period = period; - self.message_counts.clear(); - self.message_cumulative_sizes.clear(); - self.non_tx_count = 0.0; - self.non_tx_size = 0.0; - } - - let new_message_count = self.message_counts.get(&message.msg_type).unwrap_or(&0.0) + 1.0; - let new_cumulative_size = self - .message_cumulative_sizes - .get(&message.msg_type) - .unwrap_or(&0.0) - + size; - let mut new_non_tx_count = self.non_tx_count; - let mut new_non_tx_size = self.non_tx_size; - - let passed = 'checker: { - let mut limits = self.rate_limits.default_settings; - - if let Some(tx_limits) = self.rate_limits.tx.get(&message.msg_type) { - limits = *tx_limits; - } else if let Some(other_limits) = self.rate_limits.other.get(&message.msg_type) { - limits = *other_limits; - - new_non_tx_count += 1.0; - new_non_tx_size += size; - - if new_non_tx_count > self.rate_limits.non_tx_frequency * self.limit_factor { - break 'checker false; - } - - if new_non_tx_size > self.rate_limits.non_tx_max_total_size * self.limit_factor { - break 'checker false; - } - } - - let max_total_size = limits - .max_total_size - .unwrap_or(limits.frequency * limits.max_size); - - if new_message_count > limits.frequency * self.limit_factor { - break 'checker false; - } - - if size > limits.max_size { - break 'checker false; - } - - if new_cumulative_size > max_total_size * self.limit_factor { - break 'checker false; - } - - true - }; - - if self.incoming || passed { - *self.message_counts.entry(message.msg_type).or_default() = new_message_count; - *self - .message_cumulative_sizes - .entry(message.msg_type) - .or_default() = new_cumulative_size; - self.non_tx_count = new_non_tx_count; - self.non_tx_size = new_non_tx_size; - } - - passed - } -} - -fn time() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() -} diff --git a/vendor/chia-sdk-client/src/rate_limits.rs b/vendor/chia-sdk-client/src/rate_limits.rs deleted file mode 100644 index 2370ece..0000000 --- a/vendor/chia-sdk-client/src/rate_limits.rs +++ /dev/null @@ -1,214 +0,0 @@ -use std::collections::HashMap; - -use chia_protocol::ProtocolMessageTypes; -use once_cell::sync::Lazy; - -#[derive(Debug, Clone)] -pub struct RateLimits { - pub default_settings: RateLimit, - pub non_tx_frequency: f64, - pub non_tx_max_total_size: f64, - pub tx: HashMap, - pub other: HashMap, -} - -impl RateLimits { - pub fn extend(&mut self, other: &Self) { - self.default_settings = other.default_settings; - self.non_tx_frequency = other.non_tx_frequency; - self.non_tx_max_total_size = other.non_tx_max_total_size; - self.tx.extend(other.tx.clone()); - self.other.extend(other.other.clone()); - } -} - -#[derive(Debug, Clone, Copy)] -pub struct RateLimit { - pub frequency: f64, - pub max_size: f64, - pub max_total_size: Option, -} - -impl RateLimit { - pub fn new(frequency: f64, max_size: f64, max_total_size: Option) -> Self { - Self { - frequency, - max_size, - max_total_size, - } - } -} - -macro_rules! settings { - ($($message:ident => $frequency:expr, $max_size:expr $(, $max_total_size:expr)? ;)*) => { - { - let mut settings = HashMap::new(); - $( - #[allow(unused_mut, unused_assignments)] - let mut max_total_size = None; - $( max_total_size = Some($max_total_size); )? - settings.insert( - ProtocolMessageTypes::$message, - RateLimit::new( - $frequency.into(), - $max_size.into(), - max_total_size.map(|num: u32| num.into()), - ) - ); - )* - settings - } - }; -} - -// TODO: Fix commented out rate limits. -pub static V1_RATE_LIMITS: Lazy = Lazy::new(|| RateLimits { - default_settings: RateLimit::new(100.0, 1024.0 * 1024.0, Some(100.0 * 1024.0 * 1024.0)), - non_tx_frequency: 1000.0, - non_tx_max_total_size: 100.0 * 1024.0 * 1024.0, - tx: settings! { - NewTransaction => 5000, 100, 5000 * 100; - RequestTransaction => 5000, 100, 5000 * 100; - RespondTransaction => 5000, 1024 * 1024, 20 * 1024 * 1024; - SendTransaction => 5000, 1024 * 1024; - TransactionAck => 5000, 2048; - }, - other: settings! { - Handshake => 5, 10 * 1024, 5 * 10 * 1024; - HarvesterHandshake => 5, 1024 * 1024; - NewSignagePointHarvester => 100, 4886; - NewProofOfSpace => 100, 2048; - RequestSignatures => 100, 2048; - RespondSignatures => 100, 2048; - NewSignagePoint => 200, 2048; - DeclareProofOfSpace => 100, 10 * 1024; - RequestSignedValues => 100, 10 * 1024; - FarmingInfo => 100, 1024; - SignedValues => 100, 1024; - NewPeakTimelord => 100, 20 * 1024; - NewUnfinishedBlockTimelord => 100, 10 * 1024; - NewSignagePointVdf => 100, 100 * 1024; - NewInfusionPointVdf => 100, 100 * 1024; - NewEndOfSubSlotVdf => 100, 100 * 1024; - RequestCompactProofOfTime => 100, 10 * 1024; - RespondCompactProofOfTime => 100, 100 * 1024; - NewPeak => 200, 512; - RequestProofOfWeight => 5, 100; - RespondProofOfWeight => 5, 50 * 1024 * 1024, 100 * 1024 * 1024; - RequestBlock => 200, 100; - RejectBlock => 200, 100; - RequestBlocks => 500, 100; - RespondBlocks => 100, 50 * 1024 * 1024, 5 * 50 * 1024 * 1024; - RejectBlocks => 100, 100; - RespondBlock => 200, 2 * 1024 * 1024, 10 * 2 * 1024 * 1024; - NewUnfinishedBlock => 200, 100; - RequestUnfinishedBlock => 200, 100; - NewUnfinishedBlock2 => 200, 100; - RequestUnfinishedBlock2 => 200, 100; - RespondUnfinishedBlock => 200, 2 * 1024 * 1024, 10 * 2 * 1024 * 1024; - NewSignagePointOrEndOfSubSlot => 200, 200; - RequestSignagePointOrEndOfSubSlot => 200, 200; - RespondSignagePoint => 200, 50 * 1024; - RespondEndOfSubSlot => 100, 50 * 1024; - RequestMempoolTransactions => 5, 1024 * 1024; - RequestCompactVDF => 200, 1024; - RespondCompactVDF => 200, 100 * 1024; - NewCompactVDF => 100, 1024; - RequestPeers => 10, 100; - RespondPeers => 10, 1024 * 1024; - RequestPuzzleSolution => 1000, 100; - RespondPuzzleSolution => 1000, 1024 * 1024; - RejectPuzzleSolution => 1000, 100; - NewPeakWallet => 200, 300; - RequestBlockHeader => 500, 100; - RespondBlockHeader => 500, 500 * 1024; - RejectHeaderRequest => 500, 100; - RequestRemovals => 500, 50 * 1024, 10 * 1024 * 1024; - RespondRemovals => 500, 1024 * 1024, 10 * 1024 * 1024; - RejectRemovalsRequest => 500, 100; - RequestAdditions => 500, 1024 * 1024, 10 * 1024 * 1024; - RespondAdditions => 500, 1024 * 1024, 10 * 1024 * 1024; - RejectAdditionsRequest => 500, 100; - RequestHeaderBlocks => 500, 100; - RejectHeaderBlocks => 100, 100; - RespondHeaderBlocks => 500, 2 * 1024 * 1024, 100 * 1024 * 1024; - RequestPeersIntroducer => 100, 100; - RespondPeersIntroducer => 100, 1024 * 1024; - FarmNewBlock => 200, 200; - RequestPlots => 10, 10 * 1024 * 1024; - RespondPlots => 10, 100 * 1024 * 1024; - PlotSyncStart => 1000, 100 * 1024 * 1024; - PlotSyncLoaded => 1000, 100 * 1024 * 1024; - PlotSyncRemoved => 1000, 100 * 1024 * 1024; - PlotSyncInvalid => 1000, 100 * 1024 * 1024; - PlotSyncKeysMissing => 1000, 100 * 1024 * 1024; - PlotSyncDuplicates => 1000, 100 * 1024 * 1024; - PlotSyncDone => 1000, 100 * 1024 * 1024; - PlotSyncResponse => 3000, 100 * 1024 * 1024; - CoinStateUpdate => 1000, 100 * 1024 * 1024; - RegisterForPhUpdates => 1000, 100 * 1024 * 1024; - RespondToPhUpdates => 1000, 100 * 1024 * 1024; - RegisterForCoinUpdates => 1000, 100 * 1024 * 1024; - RespondToCoinUpdates => 1000, 100 * 1024 * 1024; - RequestRemovePuzzleSubscriptions => 1000, 100 * 1024 * 1024; - RespondRemovePuzzleSubscriptions => 1000, 100 * 1024 * 1024; - RequestRemoveCoinSubscriptions => 1000, 100 * 1024 * 1024; - RespondRemoveCoinSubscriptions => 1000, 100 * 1024 * 1024; - RequestPuzzleState => 1000, 100 * 1024 * 1024; - RespondPuzzleState => 1000, 100 * 1024 * 1024; - RejectPuzzleState => 200, 100; - RequestCoinState => 1000, 100 * 1024 * 1024; - RespondCoinState => 1000, 100 * 1024 * 1024; - RejectCoinState => 200, 100; - // MempoolItemsAdded => 1000, 100 * 1024 * 1024; - // MempoolItemsRemoved => 1000, 100 * 1024 * 1024; - // RequestCostInfo => 1000, 100; - // RespondCostInfo => 1000, 1024; - // RequestSesHashes => 2000, 1 * 1024 * 1024; - // RespondSesHashes => 2000, 1 * 1024 * 1024; - RequestChildren => 2000, 1024 * 1024; - RespondChildren => 2000, 1024 * 1024; - }, -}); - -// TODO: Fix commented out rate limits. -// Also, why are these in tx? -static V2_RATE_LIMIT_CHANGES: Lazy = Lazy::new(|| RateLimits { - default_settings: RateLimit::new(100.0, 1024.0 * 1024.0, Some(100.0 * 1024.0 * 1024.0)), - non_tx_frequency: 1000.0, - non_tx_max_total_size: 100.0 * 1024.0 * 1024.0, - tx: settings! { - RequestBlockHeader => 500, 100; - RespondBlockHeader => 500, 500 * 1024; - RejectHeaderRequest => 500, 100; - RequestRemovals => 5000, 50 * 1024, 10 * 1024 * 1024; - RespondRemovals => 5000, 1024 * 1024, 10 * 1024 * 1024; - RejectRemovalsRequest => 500, 100; - RequestAdditions => 50000, 100 * 1024 * 1024; - RespondAdditions => 50000, 100 * 1024 * 1024; - RejectAdditionsRequest => 500, 100; - RejectHeaderBlocks => 1000, 100; - RespondHeaderBlocks => 5000, 2 * 1024 * 1024; - RequestBlockHeaders => 5000, 100; - RejectBlockHeaders => 1000, 100; - RespondBlockHeaders => 5000, 2 * 1024 * 1024; - // RequestSesHashes => 2000, 1 * 1024 * 1024; - // RespondSesHashes => 2000, 1 * 1024 * 1024; - RequestChildren => 2000, 1024 * 1024; - RespondChildren => 2000, 1024 * 1024; - RequestPuzzleSolution => 5000, 100; - RespondPuzzleSolution => 5000, 1024 * 1024; - RejectPuzzleSolution => 5000, 100; - NoneResponse => 500, 100; - // Error => 50000, 100; - }, - other: settings! { - RequestHeaderBlocks => 5000, 100; - }, -}); - -pub static V2_RATE_LIMITS: Lazy = Lazy::new(|| { - let mut rate_limits = V1_RATE_LIMITS.clone(); - rate_limits.extend(&V2_RATE_LIMIT_CHANGES); - rate_limits -}); diff --git a/vendor/chia-sdk-client/src/request_map.rs b/vendor/chia-sdk-client/src/request_map.rs deleted file mode 100644 index 697b9b5..0000000 --- a/vendor/chia-sdk-client/src/request_map.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use chia_protocol::Message; -use tokio::sync::{oneshot, Mutex, OwnedSemaphorePermit, Semaphore}; - -#[derive(Debug)] -pub(crate) struct Request { - sender: oneshot::Sender, - _permit: OwnedSemaphorePermit, -} - -impl Request { - pub(crate) fn send(self, message: Message) { - self.sender.send(message).ok(); - } -} - -#[derive(Debug)] -pub(crate) struct RequestMap { - items: Mutex>, - semaphore: Arc, -} - -impl RequestMap { - pub(crate) fn new() -> Self { - Self { - items: Mutex::new(HashMap::new()), - semaphore: Arc::new(Semaphore::new(u16::MAX as usize)), - } - } - - pub(crate) async fn insert(&self, sender: oneshot::Sender) -> u16 { - let permit = self - .semaphore - .clone() - .acquire_owned() - .await - .expect("semaphore closed"); - - let mut items = self.items.lock().await; - - items.retain(|_, v| !v.sender.is_closed()); - - let index = (0..=u16::MAX) - .find(|i| !items.contains_key(i)) - .expect("exceeded expected number of requests"); - - items.insert( - index, - Request { - sender, - _permit: permit, - }, - ); - index - } - - pub(crate) async fn remove(&self, id: u16) -> Option { - self.items.lock().await.remove(&id) - } -} diff --git a/vendor/chia-sdk-client/src/tls.rs b/vendor/chia-sdk-client/src/tls.rs deleted file mode 100644 index a600b1f..0000000 --- a/vendor/chia-sdk-client/src/tls.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::fs; - -use chia_ssl::ChiaCertificate; - -#[cfg(any(feature = "native-tls", feature = "rustls"))] -use tokio_tungstenite::Connector; - -use crate::ClientError; - -/// Loads an SSL certificate, or creates it if it doesn't exist already. -pub fn load_ssl_cert(cert_path: &str, key_path: &str) -> Result { - fs::read_to_string(cert_path) - .and_then(|cert| { - fs::read_to_string(key_path).map(|key| ChiaCertificate { - cert_pem: cert, - key_pem: key, - }) - }) - .or_else(|_| { - let cert = ChiaCertificate::generate()?; - fs::write(cert_path, &cert.cert_pem)?; - fs::write(key_path, &cert.key_pem)?; - Ok(cert) - }) -} - -/// Creates a native-tls connector from a certificate. -#[cfg(feature = "native-tls")] -pub fn create_native_tls_connector(cert: &ChiaCertificate) -> Result { - use native_tls::{Identity, TlsConnector}; - - let identity = Identity::from_pkcs8(cert.cert_pem.as_bytes(), cert.key_pem.as_bytes())?; - let tls_connector = TlsConnector::builder() - .identity(identity) - .danger_accept_invalid_certs(true) - .build()?; - - Ok(Connector::NativeTls(tls_connector)) -} - -/// Creates a rustls connector from a certificate. -#[cfg(feature = "rustls")] -pub fn create_rustls_connector(cert: &ChiaCertificate) -> Result { - use std::sync::Arc; - - use chia_ssl::CHIA_CA_CRT; - use rustls::{ - client::danger::HandshakeSignatureValid, - crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}, - pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}, - ClientConfig, DigitallySignedStruct, RootCertStore, - }; - - #[derive(Debug)] - struct NoCertificateVerification(CryptoProvider); - - impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification { - fn verify_server_cert( - &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp: &[u8], - _now: UnixTime, - ) -> Result { - Ok(rustls::client::danger::ServerCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - verify_tls12_signature( - message, - cert, - dss, - &self.0.signature_verification_algorithms, - ) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - verify_tls13_signature( - message, - cert, - dss, - &self.0.signature_verification_algorithms, - ) - } - - fn supported_verify_schemes(&self) -> Vec { - self.0.signature_verification_algorithms.supported_schemes() - } - } - - let mut root_cert_store = RootCertStore::empty(); - - let ca: Vec> = - rustls_pemfile::certs(&mut CHIA_CA_CRT.as_bytes()).collect::>()?; - - root_cert_store.add(ca.into_iter().next().ok_or(ClientError::MissingCa)?)?; - - let cert_chain: Vec> = - rustls_pemfile::certs(&mut cert.cert_pem.as_bytes()).collect::>()?; - - let key = rustls_pemfile::pkcs8_private_keys(&mut cert.key_pem.as_bytes()) - .next() - .ok_or(ClientError::MissingPkcs8Key)??; - - let mut config = ClientConfig::builder() - .with_root_certificates(root_cert_store) - .with_client_auth_cert(cert_chain, PrivateKeyDer::Pkcs8(key))?; - - config - .dangerous() - .set_certificate_verifier(Arc::new(NoCertificateVerification( - rustls::crypto::aws_lc_rs::default_provider(), - ))); - - Ok(Connector::Rustls(Arc::new(config))) -} diff --git a/vendor/native-tls/README.dig-gossip.md b/vendor/native-tls/README.dig-gossip.md index 8de3bc7..ce5de17 100644 --- a/vendor/native-tls/README.dig-gossip.md +++ b/vendor/native-tls/README.dig-gossip.md @@ -25,6 +25,29 @@ The patch block is marked **dig-gossip vendor patch**. See the comment at that s `chia_ca.crt` is copied from the matching `chia-ssl` release (the Chia Network's vendored CA bundle). +## Why upstream cannot replace this + +`native-tls` is dig-gossip's **default** feature, and `connection::listener::native_tls_acceptor` is +compiled under `all(feature = "native-tls", not(feature = "rustls"))` — so a stock `cargo build` runs +through this patched acceptor. Upstream's `TlsAcceptorBuilder` exposes only `min_protocol_version`, +`max_protocol_version`, `accept_alpn` and `build`; it offers no way to request or require a client +certificate. + +Dropping the patch would therefore **not fail to compile**. It would silently accept inbound peers +presenting **no client certificate at all**, defeating CON-009 mTLS. That is why this is the one fork +that survived dig_ecosystem#2228 — the `chia-protocol` and `chia-sdk-client` forks existed for a +runtime decode that `dig_peer_protocol::DigLink` now handles, and both were deleted, but no equivalent +exists for requiring a client certificate. + +The same property is why the crates.io publish stays blocked: `cargo publish` strips +`[patch.crates-io]`, so a published dig-gossip would build against upstream `native-tls` and lose the +requirement with nothing red anywhere (dig_ecosystem#2647). + +Only a consumer that opts out reaches a different path: dig-node takes dig-gossip with +`default-features = false, features = ["rustls", "relay"]` and uses the rustls inbound acceptor, which +requests and captures the client certificate directly. That is the one configuration in which this +patch is not load-bearing. + ## Platform scope OpenSSL backend is used on Linux/Android; macOS (SecureTransport) and Windows (SChannel) paths are