From c1093be4b1cc7ad059e50b8d0645bce37e355ee0 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sun, 30 Aug 2026 04:18:26 -0700 Subject: [PATCH 1/6] chore(mirror): open step 7 lane (observation + DHT pointer) --- .step7-wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 .step7-wip diff --git a/.step7-wip b/.step7-wip new file mode 100644 index 00000000..a5ff0e78 --- /dev/null +++ b/.step7-wip @@ -0,0 +1 @@ +step 7 lane: mirror bond observation + DHT pointer. WIP. From 2614d07c450e2cdec4f36212b1f326037a64b4cc Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sun, 30 Aug 2026 04:29:09 -0700 Subject: [PATCH 2/6] build(deps): take the peer stack's ^0.21 tier now dig-gossip v0.32.0 clears dig-nat --- Cargo.lock | 114 +-- Cargo.toml | 52 +- crates/dig-node-core/Cargo.toml | 1175 ++++++++++++++++--------------- 3 files changed, 647 insertions(+), 694 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1daa60e9..5e867b9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1342,21 +1342,6 @@ dependencies = [ "thiserror 2.0.20", ] -[[package]] -name = "chia-sdk-utils" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7831343041ceb5f170ef4ff8d8c2b91c055ab75f8a8e075e8bef2c423a0c47c" -dependencies = [ - "bech32", - "chia-protocol 0.36.1", - "hex", - "indexmap", - "rand 0.9.5", - "rand_chacha 0.9.0", - "thiserror 2.0.20", -] - [[package]] name = "chia-sdk-utils" version = "0.36.0" @@ -1963,7 +1948,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -2601,7 +2586,7 @@ dependencies = [ "chia-sha2 0.36.1", "chia-traits 0.36.1", "chia_streamable_macro 0.36.1", - "dig-message 0.7.0", + "dig-message", "sha2 0.10.9", ] @@ -2662,9 +2647,9 @@ dependencies = [ [[package]] name = "dig-dht" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c688e8e15124843953e52d75e8f9b114e18cb1c7351fb87e84fd9c2e4acea7a" +checksum = "74e4c62744a1051edd768adcae93c20e3723acde53f7ffe20a4cefcfbec466b3" dependencies = [ "async-trait", "dig-ip", @@ -2697,12 +2682,12 @@ dependencies = [ [[package]] name = "dig-download" -version = "0.20.4" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be658f713ebd801a0fa7fe3e9756e34ffae8dd1239cd7adee029b59a86076876" +checksum = "7c187e30254a3a94448c76f6b58b384239e28bd002f3970cdb459cedde79c026" dependencies = [ "async-trait", - "dig-constants 0.10.1", + "dig-constants 0.11.2", "dig-dht", "dig-nat", "dig-peer", @@ -2729,8 +2714,8 @@ dependencies = [ [[package]] name = "dig-gossip" -version = "0.30.0" -source = "git+https://github.com/DIG-Network/dig-gossip?rev=48a14ed2ed395a41c913d5c8f944e6ea0b747075#48a14ed2ed395a41c913d5c8f944e6ea0b747075" +version = "0.32.0" +source = "git+https://github.com/DIG-Network/dig-gossip?rev=1a3391662ecce1a3cbe8b74122a52bbb1b28d3ee#1a3391662ecce1a3cbe8b74122a52bbb1b28d3ee" dependencies = [ "bincode 1.3.3", "chia-bls 0.36.1", @@ -2763,21 +2748,6 @@ dependencies = [ "x509-parser", ] -[[package]] -name = "dig-identity" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa7453530fe41c51d40100b0a68f77bab0b523ddbeb0e6514e7efbb2360b5472" -dependencies = [ - "blst", - "chia-bls 0.36.1", - "chia-protocol 0.36.1", - "chia-sdk-utils 0.34.0", - "sha2 0.10.9", - "sparse-merkle-tree", - "thiserror 2.0.20", -] - [[package]] name = "dig-identity" version = "0.7.1" @@ -2888,26 +2858,6 @@ dependencies = [ "thiserror 2.0.20", ] -[[package]] -name = "dig-message" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b6907ad0a2fab0f3c3af42825a72732a10dd5e541e127b4d6c440e53dd9a072" -dependencies = [ - "chacha20poly1305", - "chia-bls 0.36.1", - "chia-protocol 0.36.1", - "chia-sha2 0.36.1", - "chia-traits 0.36.1", - "chia_streamable_macro 0.36.1", - "dig-identity 0.6.0", - "getrandom 0.2.17", - "hkdf", - "sha2 0.10.9", - "thiserror 1.0.69", - "zstd", -] - [[package]] name = "dig-message" version = "0.7.0" @@ -2920,7 +2870,7 @@ dependencies = [ "chia-sha2 0.36.1", "chia-traits 0.36.1", "chia_streamable_macro 0.36.1", - "dig-identity 0.7.1", + "dig-identity", "getrandom 0.2.17", "hkdf", "sha2 0.10.9", @@ -2963,15 +2913,15 @@ dependencies = [ [[package]] name = "dig-nat" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4788ac71fdb3cfb4deee26886ad21f0a2592016d2a383e9f68cf47ba4d94a6e9" +checksum = "f32d4a8ddbb282a8326dfa4a23d1af37f3d6fdd9d66bd67cba6b7e46658cb915" dependencies = [ "arc-swap", "async-trait", "base64", - "dig-constants 0.10.1", - "dig-identity 0.6.0", + "dig-constants 0.11.2", + "dig-identity", "dig-ip", "dig-tls", "futures", @@ -3033,12 +2983,12 @@ dependencies = [ "dig-dht", "dig-download", "dig-gossip", - "dig-identity 0.7.1", + "dig-identity", "dig-ip", "dig-ipc-protocol", "dig-keystore", "dig-keystore-hardware", - "dig-message 0.7.0", + "dig-message", "dig-nat", "dig-peer", "dig-peer-selector", @@ -3165,13 +3115,13 @@ dependencies = [ [[package]] name = "dig-peer" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788bbfa5f64c30f793799daaef4695f7f1ade876968e64e0e67add19981da1b6" +checksum = "80e4e58ca84bd44119c829273f6badc92f42ce1e8a3191a756cfdbef4523d9e4" dependencies = [ "chia-protocol 0.36.1", "chia-traits 0.36.1", - "dig-message 0.6.1", + "dig-message", "dig-nat", "dig-rpc-protocol", "dig-tls", @@ -3205,9 +3155,9 @@ dependencies = [ [[package]] name = "dig-peer-selector" -version = "0.9.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1207adf6950888ed3e3fb1ac6e2c75bb56f9f29442363ca296a0740c6d5662" +checksum = "c21c975473af910434801a34f8437037f44cd5204ebaae1389e519524414eb3b" dependencies = [ "dig-dht", "dig-nat", @@ -3760,7 +3710,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3908,7 +3858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4535,7 +4485,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.5", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -4766,7 +4716,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5100,7 +5050,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5711,7 +5661,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.3", "rustls", - "socket2 0.6.5", + "socket2 0.5.10", "thiserror 2.0.20", "tokio", "tracing", @@ -5749,9 +5699,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2 0.5.10", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6501,7 +6451,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7213,7 +7163,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8347,7 +8297,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f3e19147..0bdc5d42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,35 +56,31 @@ overflow-checks = true # dig-nat from crates.io, so cargo resolves ONE dig-nat instance without any git redirect. # dig-constants is likewise a plain crates.io dep everywhere now. # -# The RESOLVED line today is dig-nat **0.18.0** (see `crates/dig-node-core/Cargo.toml`, which owns the -# declaration and the reasoning). Do not read a version out of this comment: an earlier revision of it -# asserted "dig-nat 0.7 ... resolves ONE dig-nat 0.7 instance", which was true of the #1280 cascade and -# was eleven minors stale by the time anyone read it. `tests/dependency_tree.rs` asserts the -# single-instance invariant against the resolved LOCK, which is the only claim that cannot go stale. +# Do NOT read a version out of this comment. `tests/dependency_tree.rs` asserts the single-instance +# invariant against the resolved LOCK, which is the only claim about a version that cannot go stale. +# An earlier revision of this block asserted "dig-nat 0.7 ... resolves ONE dig-nat 0.7 instance", +# which was eleven minors stale by the time anyone read it. # -# THE PEER STACK IS NOW ON THE 0.36.1 LINE (dig_ecosystem#3161, 2026-08-26). +# THE PEER STACK IS ON THE ^0.21 TIER (dig-node#412 step 7, 2026-08-30). # -# The wall this comment used to describe was chia-bls: every published stack member required -# `dig-nat ^0.20` -> `dig-tls ^0.4`, which is the chia-bls 0.36.1 uplift, while dig-gossip was pinned -# at a rev reaching chia-protocol 0.26 through `dig-peer-protocol 0.6.0`. Adopting any of them split -# chia-bls across two lines inside one crate. +# Two walls have now been cleared here in sequence, and both cleared UPSTREAM rather than by an edit +# in this file. The first was chia-bls: the stack required `dig-nat ^0.20` -> `dig-tls ^0.4` (the +# chia-bls 0.36.1 uplift) while dig-gossip was pinned at a rev reaching chia-protocol 0.26 through +# `dig-peer-protocol 0.6.0`. dig-gossip v0.30.0 cleared it. The second was dig-nat itself: that same +# dig-gossip release declared `dig-nat ^0.20`, so the ^0.21 tier resolved TWO dig-nat lines while +# cargo printed success. **dig-gossip v0.32.0 (`main`, rev 1a339166) declares `dig-nat = "0.21"`, +# which cleared it.** # -# The predicted unblock is exactly what happened, and it happened UPSTREAM: dig-gossip `main` -# (v0.30.0) moved to `dig-peer-protocol 0.7` and chia 0.36.1. Repointing the git rev in -# `crates/dig-node-core/Cargo.toml` is what let the stack move; nothing was shimmed, and nothing in -# this file needed to change to allow it. +# The resolved stack is dig-nat 0.21, dig-dht 0.13, dig-download 0.21, dig-peer 0.13, +# dig-peer-selector 0.10 -- and dig-nat, dig-dht, dig-tls, chia-bls and chia-protocol each resolve to +# exactly the line count they had before the move. # -# The resolved stack is dig-nat 0.20, dig-dht 0.12.1, dig-download 0.20.4, dig-peer 0.12.0, -# dig-peer-selector 0.9.2 -- the ^0.20 TIER, deliberately not the newest. -# -# WHY THE OLDER TIER IS THE CORRECT CHOICE, and the distinction a future lane will get wrong: the -# step from dig-nat 0.20 to 0.21 carries ZERO chia delta (both take `dig-tls ^0.4`). It is still not -# takeable, for a reason one level up -- dig-gossip v0.30.0 declares `dig-nat ^0.20`, while -# dig-dht 0.13 / dig-peer 0.13 / dig-download 0.21 / dig-peer-selector 0.10 all require `^0.21`. -# Taking the newer tier resolves TWO dig-nat lines while cargo prints success, MEASURED here, and -# dig-nat values cross from this crate into dig-download/dig-gossip/dig-peer-selector. One line per -# family beats the highest version numbers (CLAUDE.md §2.4b). -# -# So "0.21 is chia-blocked" is FALSE and "dig-node cannot reach 0.21" is still TRUE. The remaining -# unblock is a dig-gossip release against `dig-nat ^0.21`, upstream, never an edit in this file. Do -# not bridge two lines with a shim -- that is the §4.1 byte-drift class. +# THE ONE THING A FUTURE LANE WILL GET WRONG: **dig-dht 0.15 is NOT takeable, and the blocker has +# moved down a level.** dig-download 0.21.0 and dig-peer-selector 0.10.0 -- the LATEST published of +# each -- both require `dig-dht ^0.13`, measured from the crates.io index and not from a caret. +# Declaring `dig-dht = "0.15"` resolves TWO dig-dht lines while cargo prints success, and dig-dht +# values cross from this crate into both of them. dig-dht 0.15 carries +# `ProviderRecord::unverified_mirror_coin_id` (dig-dht#23), so that field is unreachable here until a +# dig-download AND a dig-peer-selector release against `dig-dht ^0.15` exist -- upstream, never an +# edit in this file. One line per family beats the highest version numbers (CLAUDE.md §2.4b), and +# bridging two lines with a shim is the §4.1 byte-drift class. diff --git a/crates/dig-node-core/Cargo.toml b/crates/dig-node-core/Cargo.toml index 21a5ecf4..00169914 100644 --- a/crates/dig-node-core/Cargo.toml +++ b/crates/dig-node-core/Cargo.toml @@ -1,584 +1,591 @@ -[package] -name = "dig-node-core" -# The engine library's OWN version, independent of the released binary's (`[workspace.package].version` -# in the root manifest, which only dig-node-service tracks). It moves when this crate's PUBLIC surface -# moves: 0.24.0 acknowledges that the surface now exposes dig-nat 0.14 / dig-dht 0.8 types, a -# compatibility-relevant change even though the crate is consumed in-workspace and is not published to -# crates.io. Holding it at 0.23.0 would have overstated stability to anyone reading the manifest for -# what the exported types are (dig_ecosystem#1785c). 0.26.0 does the same for the 0.15 line: the -# exported dig-nat / dig-dht types rotate identity again, and `DhtError` — reachable through this -# crate's dig-dht re-exports — now carries `SafeText` instead of `String` (dig_ecosystem#1674/#1675). -# 0.27.0 adds the pure `relevance` module (CacheTier / relevance() / evict_key / should_displace) — -# a new, backwards-compatible public export (dig_ecosystem#1986), hence a MINOR bump. -# 0.28.0 adds the pure `tier0_selector` module (Candidate / select_within_budget / tier0_budget_bytes / -# should_displace_tier0) built on 0.27's relevance module — a new, backwards-compatible public export -# (dig_ecosystem#1988), hence another MINOR bump. -# 0.31.0 adds cache observability to `cache.stats` (§7.10e) — `refetch_count` + per-tier `tiers` -# occupancy fields, and makes `InboundDemand::entry_count` a real (no-longer-`#[cfg(test)]`) public -# API — a new, backwards-compatible surface (dig_ecosystem#1991), hence a MINOR bump. -# 0.47.0 adopts dig-gossip 0.25.0 and splits the announce seams by ORIGIN: `ProfileTransport` gains a -# required `announce_root_local` and `MeltBroadcast` a required `broadcast_local`, so a locally -# originated announce takes dig-gossip's dedup-exempt path and a re-announce of an unchanged root is -# no longer suppressed forever (dig_ecosystem#3061). Adding a required trait method is BREAKING for an -# out-of-workspace implementor; this crate is consumed in-workspace only and is pre-1.0, so it is a -# MINOR bump under SemVer's 0.x rule -- recorded here rather than letting the number imply the trait -# surface held still. -# 0.53.0 adds the module-granularity capsule relay leg (`seams/dig_peer/module_relay.rs`, gated OFF -# by default) and makes the provider/candidate locator readers absence-safe: `NodeContent::find_providers` -# and `NatModuleTransport::discovered_candidates` now return `BestEffort` rather than a bare `Vec`, -# so a source that could not be consulted can no longer be read as a proven absence (dig_ecosystem#3128, -# dig-node#276/#296). Changing a public return type is BREAKING for an out-of-workspace implementor; -# this crate is consumed in-workspace only and is pre-1.0, so it is a MINOR bump under SemVer's 0.x -# rule -- recorded here rather than letting the number imply the locator surface held still. -version = "0.62.0" -edition = "2021" -license = "GPL-2.0-only" -description = "The canonical DIG node ENGINE library (crate `dig_node_core`): the JSON-RPC dispatch (`handle_rpc`, the same contract as rpc.dig.net), local-first content serve/fetch/redirect from LOCAL .dig store modules (via digstore_host::serve_blind), chain-anchored-root resolution, chain-watch + subscriptions + generation gap-fill, the LRU cache, and the full P2P stack. Shared UNCHANGED by both host shells: the `dig-node` OS-service binary (dig-node-service) and the DIG Browser's in-process cdylib (dig-runtime). Native Rust so the compiled-module serve path works." - -[lib] -name = "dig_node_core" -path = "src/lib.rs" - -[dependencies] -# Bounds the whole-module descriptor memo (`seams/dig_peer/module_serve.rs`, #1615/G2): a per-process -# cache of `dig.getModuleInfo` answers keyed by (store, root), capped so a long-running node cannot -# accumulate one entry per module it has EVER described (each entry is tens of KB — up to 512 -# chunk_hashes + chunk_lens). Already a transitive dependency (pulled in via the workspace's other -# crates); promoted to a direct one now that this crate uses it itself. -lru = "0.12" -# The `.dig` STORE-FORMAT LIBRARY crates. These live in the digstore repo (the store -# CLI + format/read libraries) and are consumed here as GIT dependencies: the node -# depends on the store libs, NEVER the reverse (digstore is only ever an RPC client of -# a node). Pinned to a single digstore rev so the whole store-format surface the node -# reads/verifies/decrypts against is one coherent version. Bump all seven together. -# -# digstore-host — instantiate a compiled module + `serve_blind` (ciphertext + proof). -# digstore-core — wire types (ContentResponse), codec, Bytes32/48, the PublicManifest -# type + `datasection::read_public_manifest` (#176 Phase C). -# digstore-remote — the §21 remote client + persistent identity signer (§21.9). -# digstore-chain — CHIP-0035 singleton lineage walk (chain-anchored root). -# digstore-stage — the shared stage→compile engine for the `dig.stage` RPC (turn a -# folder into a capsule in-process, #95 Pass C). Its build script -# embeds the guest wasm (BINDING contract D6) via the DIGSTORE_GUEST_WASM -# override this repo sets in .cargo/config.toml (see vendor/). -# digstore-crypto — read-path crypto primitives. -# digstore-compiler — `extract_data_section_blob` (#176 Phase C): pulls the embedded DIGS -# data-section blob out of a compiled `.dig` module's wasm binary so -# `dig.getManifest` can read section 13 (the PublicManifest) WITHOUT a -# wasmtime instantiation (the manifest is PUBLIC, unencrypted data — no -# `serve_blind` decrypt needed). Already a transitive dependency via -# digstore-stage; pinned directly at the SAME rev so its public API is -# usable here too. -# -# Bumped to a rev past digstore's "Add normalized public manifest to the .dig format" -# commit (08f5d39) so `PublicManifest`/`read_public_manifest`/`extract_data_section_blob` -# exist. -# -# Bumped again to 5872240f (digstore v0.19.2, fix #1473) to adopt the HARDENED, -# unforgeable `digstore_chain::singleton::verify_pinned_root`. The rev before that (d5e52fb) -# anchored store identity on the attacker-controllable curried `SingletonStruct.launcher_id`, -# so a forged singleton currying `launcher_id == store_id` (hinted to store_id) could pass as -# chain-verified. 5872240f anchors identity on the UNFORGEABLE launcher coin (`coin_id == -# store_id`) via a bounded backward `parent_coin_info` walk, making the read-root anti-rollback -# check (#747/#841/#852-node) SOUND. -# -# Bumped again to the CURRENT rev, 161c2a3108cb4bb6c8791e96a26588ec99afb029 (digs#59), which is a -# descendant of 5872240f — so the launcher-coin anchoring above still holds — and additionally -# carries digstore-chain onto `chia-wallet-sdk` 0.36. See the SDK note below for why that bump is -# a security fix rather than housekeeping. -# -# Both bumps leave the public signature unchanged (a pure rev bump). Bump all seven together. -# NC-7 exception: dig_ecosystem#901 (the crates.io repoint) leaves the seven `digstore-*` deps below -# git-pinned ON PURPOSE. Three independent facts each block the repoint on their own, and all three are -# upstream of this workspace: -# -# 1. THE SET IS ATOMIC, so `digstore-core` cannot move alone even though it IS published (0.28.0). -# Inside the digstore workspace every sibling declares `digstore-core = { path = "../digstore-core" }`, -# so a git-sourced `digstore-host` always carries the git `digstore-core`. Repointing only the -# published members would put TWO `digstore-core` in the graph — a crates.io one for this crate's -# direct imports and a git one inside its siblings — and `ContentResponse`/`Bytes32` would stop -# unifying across the serve seam. They move together or not at all. -# -# 2. MOST OF THE SET IS UNPUBLISHED, and the closure is wider than it looks. Absent from crates.io: -# `digstore-host`, `digstore-remote`, `digstore-stage`, `digstore-crypto`, `digstore-compiler` — -# plus `digstore-prover`, `digstore-store`, `digstore-chunker` and `digstore-cli`, which the five -# pull in transitively and which no checklist had named. Each declares its siblings as a BARE -# `path = "../x"` with no `version` key, which `cargo publish` refuses outright, so publishing them -# is real work in DIG-Network/digs rather than a release button. -# -# 3. (CLEARED 2026-08-26, dig_ecosystem#3161.) This used to read "the published members are on the -# OTHER chia line", because digstore declared the primitives at 0.36.1 while this workspace was -# held at 0.26 by dig-gossip -> dig-peer-protocol 0.6. Both halves moved: the dig-gossip rev now -# carries 0.36.1, and the digstore rev below is digstore's own ceiling fix. The primitives are -# one line across the boundary, so nothing is split. -# -# The SDK family has now collapsed too (2026-08-26, dig-node#374). This used to read that -# digstore's rev took `chia-wallet-sdk 0.34` against dig-wallet's 0.36.0, and that closing the -# gap needed a digstore release — upstream, not here. That release happened: DIG-Network/digs#59 -# moved `digstore-chain` onto `chia-wallet-sdk 0.36` (and `datalayer-driver` 5, which is what -# required the SDK bump), and the rev below is that commit. -# -# Repointing was a SECURITY fix, not tidiness. `chia-sdk-driver` 0.30 and 0.34 decode the oracle -# fee out of an attacker-authored CREATE_COIN memo as -# `BigInt::from_signed_bytes_be(&memo).to_u64_digits().1[0]`, and an empty or explicit-zero memo -# makes that index an empty vec — a panic reachable from any spend dig-node walks through -# `Datastore::from_spend` (the lineage walk, `verify_pinned_root`, `sync_datastore`). 0.36 guards -# the empty case and uses `try_into`, so both become `DriverError::InvalidMemo`; it also stops -# silently discarding a negative memo's sign and truncating an over-`u64` fee to its low digit. -# -# So this row is still downstream of publishing the digstore workspace. Do NOT repoint these -# piecemeal, and never let the pin regress to a floating git BRANCH: the single `rev` is what makes -# the whole store-format surface reproducible. -digstore-host = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-core = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-remote = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-chain = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-stage = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-crypto = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-compiler = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -chia-protocol = "0.36.1" -# The `Streamable` to_bytes/from_bytes trait, used to serialize the chat `ChatMessage` payload before -# the dig-message seal. Same 0.36.1 as chia-protocol so the byte layout agrees across the workspace. -chia-traits = "0.36.1" -# The ecosystem's canonical address-family / IPv6-first primitive (CLAUDE.md §5.2). The advertise -# path keys + orders candidate addresses via `dig_ip::Family` and aggregates them source-tagged + -# de-duplicated via `dig_ip::PeerCandidates` — never a hand-rolled family sort (epic #1020, #1032). -# A leaf crate (no transitive DIG deps), so the crates.io release is used directly. -dig-ip = "0.1.2" -# The canonical dig-app ⇄ dig-node IPC session/signing contract (engine role-half + wire types + -# domain builders + frame bounds). SSOT shared with dig-app so the two halves can never drift (#1080). -# Pinned EXACTLY: 0.3.0 is the first release on this line and the caret range would silently accept -# a future 0.3.x. The crate reaches this one through the IPC surface, so an unreviewed minor is a -# wire-shape change arriving without a PR (dig-node#369 gate). -dig-ipc-protocol = "=0.3.0" -# On-chain DID resolution (WU3, #778): the honest chain-authenticated read path the production -# DidSigningKeyResolver delegates to. -# -# NOW A CRATES.IO DEP (dig_ecosystem#901). It was git-pinned at `tag = "v0.4.0"` because the crate was -# unpublished; it is published now, so the git source is retired and NC-7 is satisfied for this dep. -# -# 0.4.2 IS THE CORRECT TARGET, AND IT IS NOT THE LATEST (0.7.1). Two separate constraints pick it, and -# both are "one line per family" (CLAUDE.md §2.4b), not staleness: -# -# 1. CHIA LINE. This crate re-exports `Bytes32` and `Coin` FROM dig_identity (`seams/dig_peer/ -# session.rs`), so dig-identity's chia-protocol MUST be this workspace's line. Published lines: -# 0.4.1 / 0.4.2 / 0.5.0 -> chia-protocol 0.26, chia-sdk-utils 0.30 <- this workspace -# 0.6.0 / 0.7.0 / 0.7.1 -> chia-protocol 0.36.1, chia-sdk-utils 0.34+ -# Taking 0.7.1 links a SECOND chia-protocol and stops every `Bytes32` crossing the resolver seam -# from unifying — the same split the dig-gossip and dig-constants notes below describe. The 0.6+ -# uplift is downstream of the chia-0.36 cascade (dig_ecosystem#2761), not an edit here. -# -# 2. DEDUPLICATION. A 0.x minor is a semver-INCOMPATIBLE boundary, so this pin must match whatever -# the rest of the graph already resolves or the tree carries TWO dig-identity copies — which for -# an identity/key crate is the whole point of pinning it at all. -# -# 0.7.1 is that value on the chia-0.36.1 ceiling (dig_ecosystem#3161): `dig-nat 0.20` and -# `dig-message 0.7` both declare `dig-identity ^0.7`, and 0.7.1 is the only line whose `chia-bls` is -# 0.36.1. The previous pin here was 0.4.2, chosen by the same rule against the then-current -# `dig-message 0.5` / `dig-nat 0.18`; it is the RULE that is stable, not the number. Re-derive it -# from `Cargo.lock` rather than trusting this comment. -dig-identity = "0.7.1" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "net", "io-util", "time", "signal"] } -# Object-safe async trait for the injectable anchored-root resolver (the -# mandatory read-path chain pin, #127): production walks coinset, tests inject a -# deterministic resolver. -async-trait = "0.1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -# The canonical node<->node JSON-RPC contract: the Method catalogue (wire names + -# per-method tier) and the mTLS peer-reachability allowlist. dig-node-core reads its -# method names + the peer allowlist from HERE (never hand-rolled) so the contract -# cannot drift from the other node implementation or the discovery document (#1075). -dig-rpc-protocol = "0.10.2" -# The directed-message base protocol (epic #793/#796): the e2e seal/open pipeline + the typed envelope -# the chat subsystem seals into. dig-node is the TRANSPORT — it seals an app-supplied opaque DIGCHAT1 -# envelope to the recipient's 0x0010 BLS identity key and dig-gossip directed-sends the sealed bytes. -dig-message = "0.7" -# The chat message-TYPE layer: the five chat payloads on dig-message's dig-chat band + the registry -# wiring. A crypto-free, content-blind type layer — the DIGCHAT1 seal is carried as opaque bytes. -dig-chat-protocol = { path = "../dig-chat-protocol", version = "0.2" } -base64 = "0.22" -hex = "0.4" -# The whole-module pull's one content-addressing primitive (#1576): the module descriptor's per-chunk + -# whole-blob hashes on the SERVE side, and the pre-announce re-hash that proves the artifact about to be -# cached + announced is byte-identical to the one the anchor gate admitted. Production, not test-only — -# this hash is what stands between a promoted artifact and a promoted-but-unverified one. -sha2 = "0.10" -filetime = "0.2" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -fs4 = { version = "1.1.0", features = ["sync"] } -directories = "6.0.0" -# OS CSPRNG for the per-stage content-authoring BLS key (#95 Pass C dig.stage). -getrandom = "0.2" -# -- dig_ecosystem#2168: the node MACHINE key at rest -------------------------------------- -# The canonical DIG keystore. Consumed with `custody` OFF (it is off by default) because this -# crate seals only the node's OWN machine identity seed and must never be able to name the -# user-custody API -- the #908 boundary expressed in the dependency graph rather than in prose -# (dig-keystore SPEC.md 18.2, which names this engine as the intended opaque-only consumer). -# 0.10 rather than 0.9: 0.10 is a BREAKING release that moves the crate's re-exported chia-bls -# onto the ecosystem 0.36.1 line. That line is ALREADY in this lock (six chia-bls lines resolve -# here today), and the `opaque` surface this crate uses is bytes + Password with no BLS type on -# it at all, so nothing crosses between lines -- and adopting 0.10 now avoids routing the node's -# identity onto a type identity that was about to change under it. -# -# 0.13 is the FLOOR for taking hardware binding live, and the reason is not the providers. -# dig-keystore#18 made `create_with_rng` an atomic check-then-write in 0.13.0; 0.12 will happily -# overwrite an existing record. On a SOFTWARE tier an overwritten machine key is recoverable from -# a backup of two plaintext-sealed halves. Hardware wrapping binds the wrapping key to a -# non-exportable component, so the same overwrite becomes PERMANENT -- there is nothing to restore -# from. Sealing therefore upgrades that bug from recoverable to unrecoverable, and 0.12 is not a -# version this seam may be enabled on. -dig-keystore = "0.13" -# The platform trusted-component providers (Windows TPM 2.0 via CNG, Apple Secure Enclave, Linux -# TPM 2.0). A SEPARATE crate because `dig-keystore` forbids `unsafe` and every one of these is FFI; -# it is the workspace member dig_ecosystem#1693 asked for, and it landed in dig-keystore v0.12.0. -# Without it the node's hardware seam -- already fully composed at `machine_key.rs` -- passes a -# hardcoded `None` and can never report anything but `Software(NotRequested)`. -# 0.2 pairs with dig-keystore 0.13: it adds the macOS Secure Enclave and Linux TPM 2.0 providers, -# and reclassifies a TPM AUTHORIZATION refusal (owner password set, dictionary-attack lockout) as -# `Absent` rather than `Indeterminate`, so an ordinary locked-down Windows box degrades instead of -# refusing. Only a genuinely uninspectable host is indeterminate now. -dig-keystore-hardware = "0.2" -# The machine key lives in the SAME user-global identity dir digstore_remote used for the -# plaintext seed (`/dig`), so the #2168 migration finds it in place and the node -# keeps its peer_id. digstore keeps its own `identity_dir` private, so the path is reproduced. -dirs = "5" -# Keeps the unsealed seed off the heap after use, matching what dig-keystore hands back. -zeroize = "1" - -# -- L7 DIG Node peer network (PHASE-2b, #162) ------------------------------------------------------- -# The NAT-traversal + connected-peer-pool layer. The bespoke in-node relay client (formerly relay.rs) -# is RETIRED: the relay connection now lives inside dig-nat (the `connect()` ladder's last-resort tier -# + the persistent reservation) and dig-gossip (the introducer-backed connected peer pool). dig-node -# consumes both to auto-discover peers via relay.dig.net, maintain a pool, and serve/issue the L7 peer -# RPC over mTLS (peer_id = SHA256(TLS SPKI DER)). -# -# Sourcing (#1280/#1393/#1494 crates.io cascade): dig-nat 0.11 is on crates.io — the whole peer stack -# (dig-gossip 0.15.0, dig-dht 0.5.1, dig-download 0.7.1, dig-peer-selector 0.5.2) now depends on dig-nat -# ">=0.10, <0.12" from crates.io, so consuming it here as a plain crates.io version unifies the ENTIRE -# graph on ONE dig-nat 0.11 instance (the old bare-git unification patch is retired). 0.11 adds the -# relayed RESPONDER path — `RelayStatus::enable_accept` surfaces an introduced circuit as a -# `RelayTunnel`, and `RelayAcceptor` runs the mTLS SERVER half over it (#1536, Leg B) — on top of -# 0.10's SPKI-pinned dialer (#1422) and the STUN happy-eyeballs reflexive discovery, and auto-composes -# the FULL NAT ladder via `connect_with_runtime` + `NatRuntime`, taking a CA-signed NodeCert. -# 0.11.2 is REQUIRED (#1586): before it, `RangeFrame::bytes` was read with `serde_bytes`, which over -# the JSON frame wire takes the served base64 string as its literal characters — so every -# `dig.fetchRange` window arrived as its own base64 text, the reassembler rejected the frame, and the -# read leg aborted before reading a byte. 0.11.2 reads/writes `bytes` as base64 (the canonical -# `dig_rpc_protocol::types::RangeFrame` wire the node's own serve path emits). -# dig-nat 0.13 is the release that introduced the FALLIBLE, payload-capped `RangeFrame::encode` plus -# the paged-prologue sender API (`with_identity` carrying `chunk_count`, `with_chunk_lens_page` / -# `chunk_lens_offset`, `with_chunk_index`, `RangeRequest::skip_layout`). Everything 0.14 adds is the -# RECEIVER (`ChunkLensAssembler`) plus the `split_chunk_lens_pages` helper, so 0.13 carries the whole -# serve-side fix for #1640/#1668. -# -# The dig-nat MAJOR must match the whole peer stack's, and it moves as ONE step (#1771). This crate -# builds its own `dig_nat` `NodeCert` / `NatConfig` / `NatRuntime` / `RelayStatus` / `TraversalKind` -# values and passes them INTO dig-download, dig-gossip and dig-peer-selector, so a second dig-nat -# instance does not merely bloat the tree -- those calls stop typechecking (verified: E0308 `expected -# dig_nat::relay::RelayStatus, found dig_nat::RelayStatus`). So dig-nat cannot be bumped alone, and -# dig-gossip/dig-dht/dig-download/dig-peer/dig-peer-selector cannot be bumped past it: the dig-nat -# 0.18 line (dig-dht 0.11, dig-download 0.19, dig-peer 0.11, dig-peer-selector 0.9, dig-gossip on the -# pinned rev below) is a single atomic cascade, every member of which is published. -# `tests/dependency_tree.rs` asserts the single-instance invariant against the resolved LOCK so a -# partial cascade cannot quietly reintroduce the duplicate. -# -# This line carries the SafeText peer-text cascade (#1674/#1675): error text of remote origin is -# held as `dig_nat::SafeText` rather than `String`, so a hostile peer's control characters cannot forge -# a line in an operator's log. On a partial bump the failure is now LOUDER than the RelayStatus mismatch -# above -- `SafeText` crosses dig-dht's AND dig-peer's public error surfaces, so two dig-nat versions is -# an outright compile error on those seams rather than merely two mTLS stacks. See -# `seams/dig_peer/dht.rs::stage_failed` for this crate's side of the adoption. -dig-nat = "0.20" -# dig-gossip is the ONE peer-stack exception: not yet published to crates.io (its publish is guarded -# pending dig-peer-protocol, #681), so it stays a git dependency PINNED to a release commit — here -# v0.17.12 (rev 3e352d91). -# -# v0.17.12 is REQUIRED, not merely current (#1771): it carries all THREE duplicate-connection fixes, -# and until it landed here the node's pool refused connections that would have worked. Every pool-add -# path now supersedes a stale slot for an already-authenticated `peer_id` instead of refusing the newer -# session — inbound (#1691), `connect_to` (#1703), and `adopt_nat_connection` (#1762), the last being -# the path EVERY dig-nat connection is adopted through, where a dead relay circuit's leftover slot -# refused the direct dial while the peer reported zero connections. `DuplicateConnection` still exists -# as an error variant but is no longer raised on those paths. Consequence for this crate: `PeerAdded` is -# REPUBLISHED on a supersede, so every consumer of it must be keyed by `peer_id` and idempotent (the DHT -# routing feed, the selector registry, and the download-side connected pool all are — see -# `download.rs::on_pool_event`), and `GossipStats::total_connections` is a LIFETIME counter that a -# supersede increments, so it must never be read as a count of unique peers (this crate reads -# `connected_peers` / `pool_stats().connected`, which are the live keyed-map sizes). -# -# Also on 0.17: the outbound /16+AS diversity caps (#1710/#1716) apply to the adoption path, so a -# test fixture adopting two peers must place them in DIFFERENT /16 groups. -# Pulled rustls-only (no native-tls) -- note `--all-features` is NOT a runnable configuration for -# dig-gossip: with both `ring` and `aws-lc-rs` reachable rustls cannot pick a provider. Its -# dig-nat/dig-tls/dig-constants come from crates.io (same versions as here), so the graph still -# unifies to ONE of each. -# -# There are no vendored chia-protocol/chia-sdk-client forks to patch any more: dig-gossip 0.23.0 -# deleted both (dig_ecosystem#2228) and the workspace `[patch.crates-io]` section is RETIRED (see the -# root manifest). The #1529 three-rev lockstep is retired with it. An earlier version of this comment -# still described those patches as live; it was wrong, and a false manifest comment is how the next -# lane reasons wrongly about the version line. -# -# THIS DEP WAS THE CHIA-0.36 MIGRATION BLOCKER, AND IS NO LONGER (dig_ecosystem#3161, 2026-08-26). -# -# The old block, recorded because a future lane will otherwise re-derive it: the pinned rev was -# v0.25.0, which took `dig-peer-protocol = "0.6"`, whose only published 0.6.x declares -# `chia-protocol 0.26`. That put 0.26 into the graph through a path no dig-node manifest could edit, -# so no assignment of versions HERE could remove it. -# -# What cleared it was upstream moving, exactly as that comment predicted: dig-gossip `main` (v0.30.0, -# the rev pinned below) is on `dig-peer-protocol 0.7` and declares chia-protocol/traits/sha2/bls at -# 0.36.1. Repointing the rev is what let this workspace take the ceiling; it was never a local edit. -# -# The consequence that mattered is now discharged rather than merely avoided: `Bytes32` reaches the -# store-melt authorization path in `seams/dig_peer/store_melted.rs` from dig_gossip and from -# `digstore-chain` as the SAME type, so that path is no longer split across two chia-protocol -# versions. No conversion shim was added, and none may be — bridging two lines is the byte-drift -# class this migration exists to remove. -# -# STILL PINNED HERE, and deliberately: this rev takes `dig-nat ^0.20`, while the published -# dig-dht/dig-peer/dig-download/dig-peer-selector tips have moved to `^0.21`. So the peer stack below -# is held at its ^0.20 tier to keep ONE dig-nat in the graph. Taking the newer tier resolves TWO -# dig-nat lines while cargo prints success. The fix is a dig-gossip release against `dig-nat ^0.21`, -# upstream, never a version edit here. -# NC-7 exception: dig-gossip is git-pinned PERMANENTLY, not pending a publish — dig_ecosystem#2647. -# It cannot be published while its `native-tls` [patch.crates-io] fork stands, because `cargo publish` -# STRIPS [patch.crates-io] from the published metadata. A published dig-gossip would therefore build -# against UPSTREAM native-tls, compile cleanly, and silently accept inbound peers presenting no client -# certificate at all — a mutual-TLS bypass that no consumer could see from its own manifest. Its -# publish.yml guard is unconditional and deliberate: do not weaken it, do not `--allow-dirty` past it. -# dig_ecosystem#900 ("publish dig-gossip") was closed as superseded by #2647. -dig-gossip = { git = "https://github.com/DIG-Network/dig-gossip", rev = "48a14ed2ed395a41c913d5c8f944e6ea0b747075", default-features = false, features = ["rustls", "relay"] } -# The canonical DIG network id (genesis challenge) for the gossip handshake / introducer scope. -# -# 0.9 is the TIP OF DIG-NODE'S CHIA LINE, and that — not 0.10 — is the correct target here (#2072). -# dig-constants 0.10.0 moved to chia-protocol 0.36.1 / chia-wallet-sdk 0.34, while this workspace -# builds against 0.26 / 0.30, which dig-gossip pins there via dig-peer-protocol 0.6 (see the -# dig-gossip entry above). Depending on 0.10 links a SECOND chia_protocol and every `Bytes32` this -# crate reads off `DIG_MAINNET` stops unifying with the rest of the workspace. Being current on -# dig-constants is therefore downstream of the chia 0.36 migration, not a version-string edit. -# -# Values are identical from 0.4.0 through 0.10.0 — the L2 genesis challenge and its six AGG_SIG -# domains were finalized in 0.4.0 and have not moved since — so this bump is value-neutral and buys -# the additive surface (DIG_ASSET_ID, the treasury hash/address, `dig.local`, `rpc.dig.net`). -# -# The graph still carries 0.4.0/0.5.1/0.8.0 because upstream crates hold them down through their -# PUBLISHED ranges (dig-gossip `>=0.2, <0.5`, dig-nat `>=0.4, <0.6`, dig-download `^0.8`), which a -# consumer cannot edit. Collapsing to a single copy needs each of those to release against 0.9 — -# the release-first cascade on #2072. Do NOT pin this BACK to widen a window: the fix is upstream -# moving forward, never dig-node drifting backward. -dig-constants = "0.13.0" -# -- Portable profile body format (DPB) — epic #3008 W6 ----------------------------------------------- -# -# `default-features = false` deliberately: the default `chia` feature pulls dig-did/dig-store/ -# chia-protocol 0.36.1, which is a DIFFERENT chia line from the 0.26 this workspace builds against, so -# the default surface would put a SECOND chia-protocol into the always-running service. Without the -# feature the crate is a pure BYTE CORE (`dig-smt + sha2 + thiserror`) exposing exactly what the node -# needs: the DPB container, `AnchoredRoot` and `VerifiedBody`. -# -# Consequence to keep in mind: `dig_social_profile::resolve` is NOT available here. The node resolves -# a store's on-chain root through its OWN `AnchoredRootResolver` (the coinset lineage walk), which is -# the same authority, reached by the node's existing seam. -dig-social-profile = { version = "0.7", default-features = false } -# -- L7 content-location DHT (PHASE-B, #163) --------------------------------------------------------- -# The Kademlia DHT that maps DIG content (store / capsule / root / resource) to the peer_ids holding -# it. The node LOCATES content it lacks via `find_providers`, and keeps its OWN held-inventory provider -# records CURRENT in the DHT (`announce_provider` / `withdraw_provider` / `republish`). It rides the -# SAME dig-nat mTLS transport as the rest of the peer network via a `DhtTransport` adapter (`dht.rs`). -# -# Sourcing: the crates.io release (NC-7: prefer the published version over bare-git now that dig-dht -# is on crates.io). dig-download + dig-peer-selector below ALSO resolve dig-dht from crates.io "^0.5", -# so cargo unifies to ONE dig-dht instance — dig-dht's `PeerId` / `Contact` / `DhtTransport` share the -# exact types `dig_nat::connect` returns. dig-dht 0.5 deps dig-nat "^0.10" (crates.io), the SAME -# dig-nat "0.10" this crate pins, so the whole graph shares ONE dig-nat 0.10 instance. -dig-dht = "0.12.1" -# -- L7 Peer Exchange (PEX, #166) -------------------------------------------------------------------- -# The node<->node peer-sharing protocol: over each established mTLS peer connection, a dedicated PEX -# logical stream exchanges delta-based first-hand known-peer sets (handshake -> snapshot -> deltas), -# feeding discovered peers into the pool as dial candidates (hints, verified by dialing) and formalizing -# what the ad-hoc `dig.getPeers` polling did. dig-pex ships a transport-agnostic sans-IO `PexEngine` -# this crate embeds; `pex.rs` is the dig-nat mux I/O adapter (SPEC §10.1, Appendix A). -# -# Sourcing: bare git form (no `rev`, like dig-nat/dig-dht) so cargo unifies any shared transitive deps -# to ONE copy across the graph; resolved to the tracked rev via the lockfile. dig-pex keeps a minimal -# dependency surface (serde/serde_json/tokio/rand — it mirrors the address shape rather than importing -# the transport crates), so there is no cross-crate type-unification concern here. -dig-pex = "0.1.1" -# -- Multi-source content fetch (#164) + redirect-on-miss (#165) -------------------------------------- -# The node-side download orchestrator: locate holders via dig-dht `find_providers`, confirm with -# `dig.getAvailability`, fan byte ranges across MULTIPLE providers simultaneously (`dig.fetchRange` -# over dig-nat), verify each range + the whole resource against the chain-anchored root, and -# reassemble — with pause/resume + `.download.tmp` GC. dig-node wires the real seams per the crate's -# implementers' note (see `download.rs`): DhtProviderLocator over our DhtService, NatRangeTransport -# over our mTLS identity, MerkleVerifier bound to the digstore proof format, FileSink/FileStateStore -# under the node's cache dir. -# -# Sourcing: the crates.io release (NC-7), matching dig-dht above. dig-download 0.6 deps dig-dht "^0.5" + -# dig-nat "^0.10" + dig-tls "^0.3" from crates.io, so cargo unifies its dig-dht + dig-nat + dig-tls with -# ours — ONE crate instance each across the graph (its re-exported `ContentId`/`ProviderRecord` ARE our -# dig-dht types). 0.6 keeps the injectable `SourceSelector` seam (dig-node implements it over -# dig-peer-selector, see `seams/dig_peer/selector_adapter.rs`), records range outcomes internally, and the -# fetch leg composes the FULL NAT ladder from an injected shared `NatRuntime` (`NatRangeTransport::new_with_runtime`). -# -# 0.8.1 adds the `ModuleDownloader` — the whole-`.dig`-module peer pull that turns a node which READ a -# resource into a complete resharer of the capsule (#1576, the reshare leg of the MVP flywheel). Its two -# seams are supplied here: `seams/dig_peer/module_transport.rs` (`NatModuleTransport`) and -# `seams/dig_peer/module_anchor.rs` (`ChainAnchoredModuleVerifier`). NOTE the `testkit` feature is -# deliberately NOT enabled: it is what makes the fail-OPEN `AcceptAnyModuleAnchor` nameable, and this -# crate's anchor gate is the reshare path's ONLY root of trust. -# -# HELD on the 0.19 line, resolving 0.19.1 (dig_ecosystem#3128). 0.19.1 is a deliberate BACKPORT of the -# capsule-checkpoint filename fix (dig-download#38): `FileStateStore` hex-encoded the download key into -# the filename, so a module key `module:<64hex>:<64hex>` (136 bytes) became a 277-character name and -# EVERY capsule checkpoint write on Linux failed with `File name too long (os error 36)`. The same fix -# also shipped as 0.20.1, but 0.20 requires dig-dht ^0.12 / dig-nat ^0.20 / dig-peer ^0.12 / -# dig-tls ^0.4 — the whole dig_ecosystem#3152 cascade — which this crate cannot take yet, so the -# backport keeps the 0.19-line pins intact. Regression-tested here through the node's OWN production -# checkpoint store (`download.rs::capturing_state_store_checkpoints_a_real_module_download_key`), -# because dig-download's own suite missed it: every `module.rs` test used `InMemoryStateStore` (no -# filename at all) and the one `FileStateStore` test used a 3-character key. -dig-download = "0.20.4" -# -- The shared peer client (#1283/#1576) ------------------------------------------------------------- -# `DigPeer` — the ONE DIG Network peer client: peer_id-pinned mTLS over the full NAT ladder plus typed -# RPC. Depended on DIRECTLY (not only transitively through dig-download) because dig-node supplies the -# `ModuleTransport` half of dig-download's module pull, which speaks `dig.getModuleInfo` / -# `dig.fetchModuleRange` — the two client methods dig-peer 0.5 added for exactly this leg. -# -# 0.5 is REQUIRED, not merely current: dig-peer 0.4 pulled dig-rpc-protocol 0.3, so a tree containing -# both it and dig-download's dig-rpc-protocol 0.5 carried TWO `ModuleInfo` types either side of the -# module pull's trust boundary — on the fields that drive the whole pull plan. dig-download 0.8.1 is on -# dig-peer 0.5 too, so exactly ONE dig-rpc-protocol + ONE dig-peer resolve here (asserted by -# `crates/dig-node-core/tests/dependency_tree.rs`). -dig-peer = "0.12.0" -# -- Self-optimizing peer selection (#178) ------------------------------------------------------------ -# The decision + learning layer between dig-dht discovery and dig-download execution: it ranks the -# providers `find_providers` returns (learning throughput/rtt/reliability + a per-class saturation -# point + an adaptive relayed penalty from the REAL measured outcome of every range) and hands -# dig-download the ranked subset + per-peer concurrency, instead of dig-download picking sources -# blindly. Every completed/failed range streams back via `record_outcome` in real time, and a dropped -# source drives a `rebalance`. dig-node bridges dig-download's `SourceSelector` seam to this crate via -# `seams/dig_peer/selector_adapter.rs` (the @30↔@30 composition-root bridge — dig-download does NOT -# depend on dig-peer-selector; dig-node, one level up, wires them together). -# -# Sourcing: the crates.io release (NC-7), matching dig-dht/dig-download above. It deps dig-dht "^0.5" + -# dig-nat "^0.10" from crates.io, so cargo unifies the selector's `dig-nat` + `dig-dht` with ours — ONE -# crate instance each (its re-used `PeerId`/`ContentId`/`ProviderRecord`/`CandidateAddr` ARE our -# dig-nat/dig-dht types). The selector mirrors `dig_gossip::PoolEvent` LOCALLY (it does not depend on -# dig-gossip, which lags as a git dep), so the node maps `dig_gossip::PoolEvent` → the selector's local -# `PoolEvent` 1:1 (see `download.rs::pool_event_to_selector`). -# 0.9.2 IS the chia-0.36 cascade landing (dig_ecosystem#3161). The old note here said "NOT bumped to -# 0.9.1: that release moves onto dig-tls 0.4, and this crate still resolves 0.3 through -# dig-nat/dig-gossip, so the graph would carry TWO `dig_tls::PeerId` types that do not unify". Both -# halves of that split are gone — dig-nat 0.20 and the dig-gossip rev both take `dig-tls ^0.4`, and -# the lock now resolves dig-tls ONCE at 0.4.0. -# -# NOT bumped to 0.10: that release requires `dig-nat ^0.21`, which dig-gossip v0.30.0 does not take -# (it declares `^0.20`). Taking it splits dig-nat in two while cargo prints success. See the peer-stack -# note in the ROOT manifest for why the ^0.20 tier is the correct choice rather than the newest. -dig-peer-selector = "0.9.2" -# The canonical DIG mTLS certificate crate (L00, crates.io). The node's PERSISTENT machine identity -# is a CA-signed `dig_tls::NodeCert` minted from the node's own BLS identity key and persisted 0600 in -# the data dir (#908 identity boundary: this is the MACHINE key, never a user key). Replaces the -# legacy self-signed `rcgen` cert as the node dials/serves over the dig-nat CA-signed mTLS model -# (#1280). `peer_id = SHA256(SPKI DER)` is stable across restarts. dig-tls re-exports chia-bls's -# `SecretKey`, unifying with this crate's chia-bls 0.36.1. 0.4 is the chia-bls 0.36.1 uplift and is -# now TAKEN (dig_ecosystem#3161): the old hold — "taking it here would split `dig_tls::PeerId` -# against the 0.3 that dig-nat/dig-gossip still resolve" — expired when dig-nat 0.20 and the -# dig-gossip rev both moved onto `dig-tls ^0.4`. The lock resolves dig-tls ONCE, at 0.4.0. -dig-tls = "0.4" -# -- The store-exchange DECISION layer (dig_ecosystem#3130) ------------------------------------------- -# `dig-sex` owns every cache/tier/relevance/selection DECISION this node used to make in-tree: what to -# keep, what to sacrifice first, and whether a fresh candidate is worth displacing an incumbent. It is -# PURE — no clock, no socket, no filesystem — so this crate keeps owning all of the I/O and hands it -# only the facts. The in-tree `relevance` module and `tier0_prefetch::effective_tier` moved there -# verbatim and are now consumed as `dig_sex::*`. -# -# It also re-exports the ONE `CapsuleIdentity` the ecosystem speaks, so this crate never declares its -# own and never pulls a second major of `dig-store` into the graph. -dig-sex = "0.5" -# The cache/eviction SEAM `dig_sex::TieredPolicy` implements. Declared DIRECTLY rather than reached -# through `dig-sex`, which re-exports `CapsuleIdentity` alone — naming `EvictionEntry`, -# `EvictionContext` and the `EvictionPolicy` trait (whose `select_evictions` must be in scope to call) -# requires the crate itself. The version is pinned to the one `dig-sex` resolves, so the `CapsuleIdentity` -# this crate builds and the one `TieredPolicy` matches on are the SAME type; a split would silently -# fail to unify. `dig_sex::TieredPolicy` is the first non-LRU implementation of the seam, and the -# module sweep here is its first consumer. -dig-store-cache = "0.1.1" -# The mTLS peer-RPC LISTENER (server side): present the node cert + REQUIRE a client cert (peer_id -# enforced). rustls/tokio-rustls with ring only (no OpenSSL/aws-lc for OUR usage), matching dig-nat. -rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } -# IPv6-first, IPv4-fallback (ecosystem HARD RULE): bind the peer-RPC listener on the IPv6 unspecified -# address `[::]` as a DUAL-STACK socket (IPV6_V6ONLY cleared) so ONE socket serves both native IPv6 -# and IPv4-mapped peers. Mirrors dig-relay's `net.rs` / dig-gossip's own dual-stack bind. -socket2 = "0.5" -# -- Engine-side identity session (NODE-1, #910) ----------------------------------------------------- -# The engine is IDENTITY-AGNOSTIC: it holds NO user signing key. `control.session.attach` proves the -# attaching dig-app holds the profile's slot-0x0010 identity key by VERIFYING an Ed25519 signature over -# the byte-identical `DIGNET-SESSION-v1 ‖ nonce ‖ profile_did` challenge (session.rs). Ed25519 -# verification rides `ring` — ALREADY in the locked graph (the rustls/tokio-rustls transport uses it), -# so it adds no new crate. VERIFY-ONLY: the engine never holds a signing key. (dig-app signs with -# ed25519-dalek; ring verifies RFC-8032 signatures + is strict against malleable/small-order inputs.) -ring = "0.17" -tracing = "0.1" -# Best-effort CONCURRENT fan-out of the multi-source provider union (`UnionLocator`, #1443): query -# dig-dht + PEX + relay-introducer sources at once via `future::join_all` (order-preserving, so the -# dedup keeps DHT-first precedence). rustls/tokio already pull futures transitively; declaring it here -# makes it a first-class prod dep of the union seam. Minimal feature set (no executor). -futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } - -[dev-dependencies] -tempfile = "3" -# `dig_keystore::hardware::double::FakeDevice` -- the only way to exercise a SPECIFIC hardware -# outcome from a test. The real providers ship now (dig-keystore-hardware above), but which one a -# host answers with is a property of that host: no CI runner can be made to produce an -# indeterminate TPM probe on demand, and that is the outcome the refusal path turns on. -# Test-only: `testing` is never enabled for the library build. -dig-keystore = { version = "0.13", features = ["testing"] } -# Same git rev as the normal dependency above, with dig-gossip's `test-util` feature added. That -# feature gates `__connect_stub_peer_with_direction`, which registers a connected peer without real -# TLS — the only way to assert what a broadcast ACTUALLY put on the wire from a test. Declared here -# rather than on the normal dependency so the shipped binary never carries the test hook: cargo -# unifies the feature into test builds only, since dev-dependencies are not built for `cargo build`. -# NC-7 exception: same permanent git pin as the runtime dep above (dig_ecosystem#2647). -dig-gossip = { git = "https://github.com/DIG-Network/dig-gossip", rev = "48a14ed2ed395a41c913d5c8f944e6ea0b747075", default-features = false, features = ["rustls", "relay", "test-util"] } -# Generates the P-256 leaf key pairs the opcode-222 holdings tests sign with: `public_key_der()` IS the -# leaf `SubjectPublicKeyInfo` DER the wire carries as `provider_spki`, and whose SHA-256 is the -# announcing peer_id — so a test signer is a real §5.2 identity rather than a stand-in that could hide -# an attribution bug (#1429). -rcgen = "0.13" -# dig-download's in-memory harness (`MockRangeTransport`, `MockContent`, …), which the download-path -# tests drive the real orchestrator over. -# -# It is a DEV dependency and MUST STAY ONE (#1576). The `testkit` feature is also what makes -# `AcceptAnyModuleAnchor` — a fail-OPEN module anchor verifier — nameable at all; the crate compiles it -# out of a default consumer build precisely so a production wiring cannot reach it. The reshare path's -# anchor gate is its ONLY root of trust, so enabling this feature on the PRODUCTION `dig-download` entry -# above would put a bypass of that root of trust one `use` away. Dev-dependency features do not -# propagate to a consumer's build, so the node binaries never see it. -# -# Pinned by the `the_fail_open_anchor_verifier_is_not_reachable_from_a_production_build` test, which -# fails if `testkit` ever appears on the production entry. -dig-download = { version = "0.20.4", features = ["testkit"] } -# Captures the peer-facing serve's real emitted tracing records into an in-memory buffer, so the -# serve-observability tests (#1595) assert what an operator would actually see in the node log — -# and that no payload byte or proof ever reaches it. -tracing-subscriber = { version = "0.3", features = ["fmt"] } -# Builds canonical `did:chia:` strings for the on-chain DID-resolver tests (matches dig-identity's codec). -chia-sdk-utils = "0.36.0" -# The session attach tests sign challenges with the BLS12-381 G2 AugScheme (the app's slot-0x0010 -# identity key), matching dig-ipc-protocol's signing contract and dig-identity's published G1 key. -chia-bls = "0.36.1" -# axum is a TEST-ONLY dependency of the engine library: the node no longer serves its -# own HTTP (the `dig-node-service` shell owns the axum transport, `dig-runtime` owns the -# in-process FFI path — dispatch is `handle_rpc`, which is transport-agnostic). The -# integration/unit tests still spawn small in-process axum mock servers (the §21 remote -# + a module server) to exercise the fetch/cache path hermetically. -axum = "0.7" -# In-process loopback mTLS peer drives the peer-network tests with NO real network (the CA-signed -# `dig_tls::NodeCert` minted by `load_or_generate_node_cert` is the test client's mTLS identity). -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "net", "io-util", "time", "test-util"] } -futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } -# The peer_network.rs integration test implements PeerRpcResponder (an async trait). -async-trait = "0.1" +[package] +name = "dig-node-core" +# The engine library's OWN version, independent of the released binary's (`[workspace.package].version` +# in the root manifest, which only dig-node-service tracks). It moves when this crate's PUBLIC surface +# moves: 0.24.0 acknowledges that the surface now exposes dig-nat 0.14 / dig-dht 0.8 types, a +# compatibility-relevant change even though the crate is consumed in-workspace and is not published to +# crates.io. Holding it at 0.23.0 would have overstated stability to anyone reading the manifest for +# what the exported types are (dig_ecosystem#1785c). 0.26.0 does the same for the 0.15 line: the +# exported dig-nat / dig-dht types rotate identity again, and `DhtError` — reachable through this +# crate's dig-dht re-exports — now carries `SafeText` instead of `String` (dig_ecosystem#1674/#1675). +# 0.27.0 adds the pure `relevance` module (CacheTier / relevance() / evict_key / should_displace) — +# a new, backwards-compatible public export (dig_ecosystem#1986), hence a MINOR bump. +# 0.28.0 adds the pure `tier0_selector` module (Candidate / select_within_budget / tier0_budget_bytes / +# should_displace_tier0) built on 0.27's relevance module — a new, backwards-compatible public export +# (dig_ecosystem#1988), hence another MINOR bump. +# 0.31.0 adds cache observability to `cache.stats` (§7.10e) — `refetch_count` + per-tier `tiers` +# occupancy fields, and makes `InboundDemand::entry_count` a real (no-longer-`#[cfg(test)]`) public +# API — a new, backwards-compatible surface (dig_ecosystem#1991), hence a MINOR bump. +# 0.47.0 adopts dig-gossip 0.25.0 and splits the announce seams by ORIGIN: `ProfileTransport` gains a +# required `announce_root_local` and `MeltBroadcast` a required `broadcast_local`, so a locally +# originated announce takes dig-gossip's dedup-exempt path and a re-announce of an unchanged root is +# no longer suppressed forever (dig_ecosystem#3061). Adding a required trait method is BREAKING for an +# out-of-workspace implementor; this crate is consumed in-workspace only and is pre-1.0, so it is a +# MINOR bump under SemVer's 0.x rule -- recorded here rather than letting the number imply the trait +# surface held still. +# 0.53.0 adds the module-granularity capsule relay leg (`seams/dig_peer/module_relay.rs`, gated OFF +# by default) and makes the provider/candidate locator readers absence-safe: `NodeContent::find_providers` +# and `NatModuleTransport::discovered_candidates` now return `BestEffort` rather than a bare `Vec`, +# so a source that could not be consulted can no longer be read as a proven absence (dig_ecosystem#3128, +# dig-node#276/#296). Changing a public return type is BREAKING for an out-of-workspace implementor; +# this crate is consumed in-workspace only and is pre-1.0, so it is a MINOR bump under SemVer's 0.x +# rule -- recorded here rather than letting the number imply the locator surface held still. +version = "0.62.0" +edition = "2021" +license = "GPL-2.0-only" +description = "The canonical DIG node ENGINE library (crate `dig_node_core`): the JSON-RPC dispatch (`handle_rpc`, the same contract as rpc.dig.net), local-first content serve/fetch/redirect from LOCAL .dig store modules (via digstore_host::serve_blind), chain-anchored-root resolution, chain-watch + subscriptions + generation gap-fill, the LRU cache, and the full P2P stack. Shared UNCHANGED by both host shells: the `dig-node` OS-service binary (dig-node-service) and the DIG Browser's in-process cdylib (dig-runtime). Native Rust so the compiled-module serve path works." + +[lib] +name = "dig_node_core" +path = "src/lib.rs" + +[dependencies] +# Bounds the whole-module descriptor memo (`seams/dig_peer/module_serve.rs`, #1615/G2): a per-process +# cache of `dig.getModuleInfo` answers keyed by (store, root), capped so a long-running node cannot +# accumulate one entry per module it has EVER described (each entry is tens of KB — up to 512 +# chunk_hashes + chunk_lens). Already a transitive dependency (pulled in via the workspace's other +# crates); promoted to a direct one now that this crate uses it itself. +lru = "0.12" +# The `.dig` STORE-FORMAT LIBRARY crates. These live in the digstore repo (the store +# CLI + format/read libraries) and are consumed here as GIT dependencies: the node +# depends on the store libs, NEVER the reverse (digstore is only ever an RPC client of +# a node). Pinned to a single digstore rev so the whole store-format surface the node +# reads/verifies/decrypts against is one coherent version. Bump all seven together. +# +# digstore-host — instantiate a compiled module + `serve_blind` (ciphertext + proof). +# digstore-core — wire types (ContentResponse), codec, Bytes32/48, the PublicManifest +# type + `datasection::read_public_manifest` (#176 Phase C). +# digstore-remote — the §21 remote client + persistent identity signer (§21.9). +# digstore-chain — CHIP-0035 singleton lineage walk (chain-anchored root). +# digstore-stage — the shared stage→compile engine for the `dig.stage` RPC (turn a +# folder into a capsule in-process, #95 Pass C). Its build script +# embeds the guest wasm (BINDING contract D6) via the DIGSTORE_GUEST_WASM +# override this repo sets in .cargo/config.toml (see vendor/). +# digstore-crypto — read-path crypto primitives. +# digstore-compiler — `extract_data_section_blob` (#176 Phase C): pulls the embedded DIGS +# data-section blob out of a compiled `.dig` module's wasm binary so +# `dig.getManifest` can read section 13 (the PublicManifest) WITHOUT a +# wasmtime instantiation (the manifest is PUBLIC, unencrypted data — no +# `serve_blind` decrypt needed). Already a transitive dependency via +# digstore-stage; pinned directly at the SAME rev so its public API is +# usable here too. +# +# Bumped to a rev past digstore's "Add normalized public manifest to the .dig format" +# commit (08f5d39) so `PublicManifest`/`read_public_manifest`/`extract_data_section_blob` +# exist. +# +# Bumped again to 5872240f (digstore v0.19.2, fix #1473) to adopt the HARDENED, +# unforgeable `digstore_chain::singleton::verify_pinned_root`. The rev before that (d5e52fb) +# anchored store identity on the attacker-controllable curried `SingletonStruct.launcher_id`, +# so a forged singleton currying `launcher_id == store_id` (hinted to store_id) could pass as +# chain-verified. 5872240f anchors identity on the UNFORGEABLE launcher coin (`coin_id == +# store_id`) via a bounded backward `parent_coin_info` walk, making the read-root anti-rollback +# check (#747/#841/#852-node) SOUND. +# +# Bumped again to the CURRENT rev, 161c2a3108cb4bb6c8791e96a26588ec99afb029 (digs#59), which is a +# descendant of 5872240f — so the launcher-coin anchoring above still holds — and additionally +# carries digstore-chain onto `chia-wallet-sdk` 0.36. See the SDK note below for why that bump is +# a security fix rather than housekeeping. +# +# Both bumps leave the public signature unchanged (a pure rev bump). Bump all seven together. +# NC-7 exception: dig_ecosystem#901 (the crates.io repoint) leaves the seven `digstore-*` deps below +# git-pinned ON PURPOSE. Three independent facts each block the repoint on their own, and all three are +# upstream of this workspace: +# +# 1. THE SET IS ATOMIC, so `digstore-core` cannot move alone even though it IS published (0.28.0). +# Inside the digstore workspace every sibling declares `digstore-core = { path = "../digstore-core" }`, +# so a git-sourced `digstore-host` always carries the git `digstore-core`. Repointing only the +# published members would put TWO `digstore-core` in the graph — a crates.io one for this crate's +# direct imports and a git one inside its siblings — and `ContentResponse`/`Bytes32` would stop +# unifying across the serve seam. They move together or not at all. +# +# 2. MOST OF THE SET IS UNPUBLISHED, and the closure is wider than it looks. Absent from crates.io: +# `digstore-host`, `digstore-remote`, `digstore-stage`, `digstore-crypto`, `digstore-compiler` — +# plus `digstore-prover`, `digstore-store`, `digstore-chunker` and `digstore-cli`, which the five +# pull in transitively and which no checklist had named. Each declares its siblings as a BARE +# `path = "../x"` with no `version` key, which `cargo publish` refuses outright, so publishing them +# is real work in DIG-Network/digs rather than a release button. +# +# 3. (CLEARED 2026-08-26, dig_ecosystem#3161.) This used to read "the published members are on the +# OTHER chia line", because digstore declared the primitives at 0.36.1 while this workspace was +# held at 0.26 by dig-gossip -> dig-peer-protocol 0.6. Both halves moved: the dig-gossip rev now +# carries 0.36.1, and the digstore rev below is digstore's own ceiling fix. The primitives are +# one line across the boundary, so nothing is split. +# +# The SDK family has now collapsed too (2026-08-26, dig-node#374). This used to read that +# digstore's rev took `chia-wallet-sdk 0.34` against dig-wallet's 0.36.0, and that closing the +# gap needed a digstore release — upstream, not here. That release happened: DIG-Network/digs#59 +# moved `digstore-chain` onto `chia-wallet-sdk 0.36` (and `datalayer-driver` 5, which is what +# required the SDK bump), and the rev below is that commit. +# +# Repointing was a SECURITY fix, not tidiness. `chia-sdk-driver` 0.30 and 0.34 decode the oracle +# fee out of an attacker-authored CREATE_COIN memo as +# `BigInt::from_signed_bytes_be(&memo).to_u64_digits().1[0]`, and an empty or explicit-zero memo +# makes that index an empty vec — a panic reachable from any spend dig-node walks through +# `Datastore::from_spend` (the lineage walk, `verify_pinned_root`, `sync_datastore`). 0.36 guards +# the empty case and uses `try_into`, so both become `DriverError::InvalidMemo`; it also stops +# silently discarding a negative memo's sign and truncating an over-`u64` fee to its low digit. +# +# So this row is still downstream of publishing the digstore workspace. Do NOT repoint these +# piecemeal, and never let the pin regress to a floating git BRANCH: the single `rev` is what makes +# the whole store-format surface reproducible. +digstore-host = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-core = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-remote = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-chain = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-stage = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-crypto = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-compiler = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +chia-protocol = "0.36.1" +# The `Streamable` to_bytes/from_bytes trait, used to serialize the chat `ChatMessage` payload before +# the dig-message seal. Same 0.36.1 as chia-protocol so the byte layout agrees across the workspace. +chia-traits = "0.36.1" +# The ecosystem's canonical address-family / IPv6-first primitive (CLAUDE.md §5.2). The advertise +# path keys + orders candidate addresses via `dig_ip::Family` and aggregates them source-tagged + +# de-duplicated via `dig_ip::PeerCandidates` — never a hand-rolled family sort (epic #1020, #1032). +# A leaf crate (no transitive DIG deps), so the crates.io release is used directly. +dig-ip = "0.1.2" +# The canonical dig-app ⇄ dig-node IPC session/signing contract (engine role-half + wire types + +# domain builders + frame bounds). SSOT shared with dig-app so the two halves can never drift (#1080). +# Pinned EXACTLY: 0.3.0 is the first release on this line and the caret range would silently accept +# a future 0.3.x. The crate reaches this one through the IPC surface, so an unreviewed minor is a +# wire-shape change arriving without a PR (dig-node#369 gate). +dig-ipc-protocol = "=0.3.0" +# On-chain DID resolution (WU3, #778): the honest chain-authenticated read path the production +# DidSigningKeyResolver delegates to. +# +# NOW A CRATES.IO DEP (dig_ecosystem#901). It was git-pinned at `tag = "v0.4.0"` because the crate was +# unpublished; it is published now, so the git source is retired and NC-7 is satisfied for this dep. +# +# 0.4.2 IS THE CORRECT TARGET, AND IT IS NOT THE LATEST (0.7.1). Two separate constraints pick it, and +# both are "one line per family" (CLAUDE.md §2.4b), not staleness: +# +# 1. CHIA LINE. This crate re-exports `Bytes32` and `Coin` FROM dig_identity (`seams/dig_peer/ +# session.rs`), so dig-identity's chia-protocol MUST be this workspace's line. Published lines: +# 0.4.1 / 0.4.2 / 0.5.0 -> chia-protocol 0.26, chia-sdk-utils 0.30 <- this workspace +# 0.6.0 / 0.7.0 / 0.7.1 -> chia-protocol 0.36.1, chia-sdk-utils 0.34+ +# Taking 0.7.1 links a SECOND chia-protocol and stops every `Bytes32` crossing the resolver seam +# from unifying — the same split the dig-gossip and dig-constants notes below describe. The 0.6+ +# uplift is downstream of the chia-0.36 cascade (dig_ecosystem#2761), not an edit here. +# +# 2. DEDUPLICATION. A 0.x minor is a semver-INCOMPATIBLE boundary, so this pin must match whatever +# the rest of the graph already resolves or the tree carries TWO dig-identity copies — which for +# an identity/key crate is the whole point of pinning it at all. +# +# 0.7.1 is that value on the chia-0.36.1 ceiling (dig_ecosystem#3161): `dig-nat 0.20` and +# `dig-message 0.7` both declare `dig-identity ^0.7`, and 0.7.1 is the only line whose `chia-bls` is +# 0.36.1. The previous pin here was 0.4.2, chosen by the same rule against the then-current +# `dig-message 0.5` / `dig-nat 0.18`; it is the RULE that is stable, not the number. Re-derive it +# from `Cargo.lock` rather than trusting this comment. +dig-identity = "0.7.1" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "net", "io-util", "time", "signal"] } +# Object-safe async trait for the injectable anchored-root resolver (the +# mandatory read-path chain pin, #127): production walks coinset, tests inject a +# deterministic resolver. +async-trait = "0.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +# The canonical node<->node JSON-RPC contract: the Method catalogue (wire names + +# per-method tier) and the mTLS peer-reachability allowlist. dig-node-core reads its +# method names + the peer allowlist from HERE (never hand-rolled) so the contract +# cannot drift from the other node implementation or the discovery document (#1075). +dig-rpc-protocol = "0.10.2" +# The directed-message base protocol (epic #793/#796): the e2e seal/open pipeline + the typed envelope +# the chat subsystem seals into. dig-node is the TRANSPORT — it seals an app-supplied opaque DIGCHAT1 +# envelope to the recipient's 0x0010 BLS identity key and dig-gossip directed-sends the sealed bytes. +dig-message = "0.7" +# The chat message-TYPE layer: the five chat payloads on dig-message's dig-chat band + the registry +# wiring. A crypto-free, content-blind type layer — the DIGCHAT1 seal is carried as opaque bytes. +dig-chat-protocol = { path = "../dig-chat-protocol", version = "0.2" } +base64 = "0.22" +hex = "0.4" +# The whole-module pull's one content-addressing primitive (#1576): the module descriptor's per-chunk + +# whole-blob hashes on the SERVE side, and the pre-announce re-hash that proves the artifact about to be +# cached + announced is byte-identical to the one the anchor gate admitted. Production, not test-only — +# this hash is what stands between a promoted artifact and a promoted-but-unverified one. +sha2 = "0.10" +filetime = "0.2" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +fs4 = { version = "1.1.0", features = ["sync"] } +directories = "6.0.0" +# OS CSPRNG for the per-stage content-authoring BLS key (#95 Pass C dig.stage). +getrandom = "0.2" +# -- dig_ecosystem#2168: the node MACHINE key at rest -------------------------------------- +# The canonical DIG keystore. Consumed with `custody` OFF (it is off by default) because this +# crate seals only the node's OWN machine identity seed and must never be able to name the +# user-custody API -- the #908 boundary expressed in the dependency graph rather than in prose +# (dig-keystore SPEC.md 18.2, which names this engine as the intended opaque-only consumer). +# 0.10 rather than 0.9: 0.10 is a BREAKING release that moves the crate's re-exported chia-bls +# onto the ecosystem 0.36.1 line. That line is ALREADY in this lock (six chia-bls lines resolve +# here today), and the `opaque` surface this crate uses is bytes + Password with no BLS type on +# it at all, so nothing crosses between lines -- and adopting 0.10 now avoids routing the node's +# identity onto a type identity that was about to change under it. +# +# 0.13 is the FLOOR for taking hardware binding live, and the reason is not the providers. +# dig-keystore#18 made `create_with_rng` an atomic check-then-write in 0.13.0; 0.12 will happily +# overwrite an existing record. On a SOFTWARE tier an overwritten machine key is recoverable from +# a backup of two plaintext-sealed halves. Hardware wrapping binds the wrapping key to a +# non-exportable component, so the same overwrite becomes PERMANENT -- there is nothing to restore +# from. Sealing therefore upgrades that bug from recoverable to unrecoverable, and 0.12 is not a +# version this seam may be enabled on. +dig-keystore = "0.13" +# The platform trusted-component providers (Windows TPM 2.0 via CNG, Apple Secure Enclave, Linux +# TPM 2.0). A SEPARATE crate because `dig-keystore` forbids `unsafe` and every one of these is FFI; +# it is the workspace member dig_ecosystem#1693 asked for, and it landed in dig-keystore v0.12.0. +# Without it the node's hardware seam -- already fully composed at `machine_key.rs` -- passes a +# hardcoded `None` and can never report anything but `Software(NotRequested)`. +# 0.2 pairs with dig-keystore 0.13: it adds the macOS Secure Enclave and Linux TPM 2.0 providers, +# and reclassifies a TPM AUTHORIZATION refusal (owner password set, dictionary-attack lockout) as +# `Absent` rather than `Indeterminate`, so an ordinary locked-down Windows box degrades instead of +# refusing. Only a genuinely uninspectable host is indeterminate now. +dig-keystore-hardware = "0.2" +# The machine key lives in the SAME user-global identity dir digstore_remote used for the +# plaintext seed (`/dig`), so the #2168 migration finds it in place and the node +# keeps its peer_id. digstore keeps its own `identity_dir` private, so the path is reproduced. +dirs = "5" +# Keeps the unsealed seed off the heap after use, matching what dig-keystore hands back. +zeroize = "1" + +# -- L7 DIG Node peer network (PHASE-2b, #162) ------------------------------------------------------- +# The NAT-traversal + connected-peer-pool layer. The bespoke in-node relay client (formerly relay.rs) +# is RETIRED: the relay connection now lives inside dig-nat (the `connect()` ladder's last-resort tier +# + the persistent reservation) and dig-gossip (the introducer-backed connected peer pool). dig-node +# consumes both to auto-discover peers via relay.dig.net, maintain a pool, and serve/issue the L7 peer +# RPC over mTLS (peer_id = SHA256(TLS SPKI DER)). +# +# Sourcing (#1280/#1393/#1494 crates.io cascade): dig-nat 0.11 is on crates.io — the whole peer stack +# (dig-gossip 0.15.0, dig-dht 0.5.1, dig-download 0.7.1, dig-peer-selector 0.5.2) now depends on dig-nat +# ">=0.10, <0.12" from crates.io, so consuming it here as a plain crates.io version unifies the ENTIRE +# graph on ONE dig-nat 0.11 instance (the old bare-git unification patch is retired). 0.11 adds the +# relayed RESPONDER path — `RelayStatus::enable_accept` surfaces an introduced circuit as a +# `RelayTunnel`, and `RelayAcceptor` runs the mTLS SERVER half over it (#1536, Leg B) — on top of +# 0.10's SPKI-pinned dialer (#1422) and the STUN happy-eyeballs reflexive discovery, and auto-composes +# the FULL NAT ladder via `connect_with_runtime` + `NatRuntime`, taking a CA-signed NodeCert. +# 0.11.2 is REQUIRED (#1586): before it, `RangeFrame::bytes` was read with `serde_bytes`, which over +# the JSON frame wire takes the served base64 string as its literal characters — so every +# `dig.fetchRange` window arrived as its own base64 text, the reassembler rejected the frame, and the +# read leg aborted before reading a byte. 0.11.2 reads/writes `bytes` as base64 (the canonical +# `dig_rpc_protocol::types::RangeFrame` wire the node's own serve path emits). +# dig-nat 0.13 is the release that introduced the FALLIBLE, payload-capped `RangeFrame::encode` plus +# the paged-prologue sender API (`with_identity` carrying `chunk_count`, `with_chunk_lens_page` / +# `chunk_lens_offset`, `with_chunk_index`, `RangeRequest::skip_layout`). Everything 0.14 adds is the +# RECEIVER (`ChunkLensAssembler`) plus the `split_chunk_lens_pages` helper, so 0.13 carries the whole +# serve-side fix for #1640/#1668. +# +# The dig-nat MAJOR must match the whole peer stack's, and it moves as ONE step (#1771). This crate +# builds its own `dig_nat` `NodeCert` / `NatConfig` / `NatRuntime` / `RelayStatus` / `TraversalKind` +# values and passes them INTO dig-download, dig-gossip and dig-peer-selector, so a second dig-nat +# instance does not merely bloat the tree -- those calls stop typechecking (verified: E0308 `expected +# dig_nat::relay::RelayStatus, found dig_nat::RelayStatus`). So dig-nat cannot be bumped alone, and +# dig-gossip/dig-dht/dig-download/dig-peer/dig-peer-selector cannot be bumped past it: the dig-nat +# 0.18 line (dig-dht 0.11, dig-download 0.19, dig-peer 0.11, dig-peer-selector 0.9, dig-gossip on the +# pinned rev below) is a single atomic cascade, every member of which is published. +# `tests/dependency_tree.rs` asserts the single-instance invariant against the resolved LOCK so a +# partial cascade cannot quietly reintroduce the duplicate. +# +# This line carries the SafeText peer-text cascade (#1674/#1675): error text of remote origin is +# held as `dig_nat::SafeText` rather than `String`, so a hostile peer's control characters cannot forge +# a line in an operator's log. On a partial bump the failure is now LOUDER than the RelayStatus mismatch +# above -- `SafeText` crosses dig-dht's AND dig-peer's public error surfaces, so two dig-nat versions is +# an outright compile error on those seams rather than merely two mTLS stacks. See +# `seams/dig_peer/dht.rs::stage_failed` for this crate's side of the adoption. +dig-nat = "0.21" +# dig-gossip is the ONE peer-stack exception: not yet published to crates.io (its publish is guarded +# pending dig-peer-protocol, #681), so it stays a git dependency PINNED to a release commit — here +# v0.17.12 (rev 3e352d91). +# +# v0.17.12 is REQUIRED, not merely current (#1771): it carries all THREE duplicate-connection fixes, +# and until it landed here the node's pool refused connections that would have worked. Every pool-add +# path now supersedes a stale slot for an already-authenticated `peer_id` instead of refusing the newer +# session — inbound (#1691), `connect_to` (#1703), and `adopt_nat_connection` (#1762), the last being +# the path EVERY dig-nat connection is adopted through, where a dead relay circuit's leftover slot +# refused the direct dial while the peer reported zero connections. `DuplicateConnection` still exists +# as an error variant but is no longer raised on those paths. Consequence for this crate: `PeerAdded` is +# REPUBLISHED on a supersede, so every consumer of it must be keyed by `peer_id` and idempotent (the DHT +# routing feed, the selector registry, and the download-side connected pool all are — see +# `download.rs::on_pool_event`), and `GossipStats::total_connections` is a LIFETIME counter that a +# supersede increments, so it must never be read as a count of unique peers (this crate reads +# `connected_peers` / `pool_stats().connected`, which are the live keyed-map sizes). +# +# Also on 0.17: the outbound /16+AS diversity caps (#1710/#1716) apply to the adoption path, so a +# test fixture adopting two peers must place them in DIFFERENT /16 groups. +# Pulled rustls-only (no native-tls) -- note `--all-features` is NOT a runnable configuration for +# dig-gossip: with both `ring` and `aws-lc-rs` reachable rustls cannot pick a provider. Its +# dig-nat/dig-tls/dig-constants come from crates.io (same versions as here), so the graph still +# unifies to ONE of each. +# +# There are no vendored chia-protocol/chia-sdk-client forks to patch any more: dig-gossip 0.23.0 +# deleted both (dig_ecosystem#2228) and the workspace `[patch.crates-io]` section is RETIRED (see the +# root manifest). The #1529 three-rev lockstep is retired with it. An earlier version of this comment +# still described those patches as live; it was wrong, and a false manifest comment is how the next +# lane reasons wrongly about the version line. +# +# THIS DEP WAS THE CHIA-0.36 MIGRATION BLOCKER, AND IS NO LONGER (dig_ecosystem#3161, 2026-08-26). +# +# The old block, recorded because a future lane will otherwise re-derive it: the pinned rev was +# v0.25.0, which took `dig-peer-protocol = "0.6"`, whose only published 0.6.x declares +# `chia-protocol 0.26`. That put 0.26 into the graph through a path no dig-node manifest could edit, +# so no assignment of versions HERE could remove it. +# +# What cleared it was upstream moving, exactly as that comment predicted: dig-gossip `main` (v0.30.0, +# the rev pinned below) is on `dig-peer-protocol 0.7` and declares chia-protocol/traits/sha2/bls at +# 0.36.1. Repointing the rev is what let this workspace take the ceiling; it was never a local edit. +# +# The consequence that mattered is now discharged rather than merely avoided: `Bytes32` reaches the +# store-melt authorization path in `seams/dig_peer/store_melted.rs` from dig_gossip and from +# `digstore-chain` as the SAME type, so that path is no longer split across two chia-protocol +# versions. No conversion shim was added, and none may be — bridging two lines is the byte-drift +# class this migration exists to remove. +# +# THE dig-nat WALL IS CLEARED (dig-node#412 step 7). The rev pinned below is dig-gossip v0.32.0, +# which declares `dig-nat = "0.21"`, so the peer stack takes its ^0.21 tier with ONE dig-nat in the +# graph. The prediction the old note made -- "the fix is a dig-gossip release against `dig-nat +# ^0.21`, upstream, never a version edit here" -- is what happened; measured from the resolved LOCK +# rather than from the caret, because a caret-compatible transitive is invisible to a plain +# re-resolve (CLAUDE.md §2.4b). +# NC-7 exception: dig-gossip is git-pinned PERMANENTLY, not pending a publish — dig_ecosystem#2647. +# It cannot be published while its `native-tls` [patch.crates-io] fork stands, because `cargo publish` +# STRIPS [patch.crates-io] from the published metadata. A published dig-gossip would therefore build +# against UPSTREAM native-tls, compile cleanly, and silently accept inbound peers presenting no client +# certificate at all — a mutual-TLS bypass that no consumer could see from its own manifest. Its +# publish.yml guard is unconditional and deliberate: do not weaken it, do not `--allow-dirty` past it. +# dig_ecosystem#900 ("publish dig-gossip") was closed as superseded by #2647. +dig-gossip = { git = "https://github.com/DIG-Network/dig-gossip", rev = "1a3391662ecce1a3cbe8b74122a52bbb1b28d3ee", default-features = false, features = ["rustls", "relay"] } +# The canonical DIG network id (genesis challenge) for the gossip handshake / introducer scope. +# +# 0.9 is the TIP OF DIG-NODE'S CHIA LINE, and that — not 0.10 — is the correct target here (#2072). +# dig-constants 0.10.0 moved to chia-protocol 0.36.1 / chia-wallet-sdk 0.34, while this workspace +# builds against 0.26 / 0.30, which dig-gossip pins there via dig-peer-protocol 0.6 (see the +# dig-gossip entry above). Depending on 0.10 links a SECOND chia_protocol and every `Bytes32` this +# crate reads off `DIG_MAINNET` stops unifying with the rest of the workspace. Being current on +# dig-constants is therefore downstream of the chia 0.36 migration, not a version-string edit. +# +# Values are identical from 0.4.0 through 0.10.0 — the L2 genesis challenge and its six AGG_SIG +# domains were finalized in 0.4.0 and have not moved since — so this bump is value-neutral and buys +# the additive surface (DIG_ASSET_ID, the treasury hash/address, `dig.local`, `rpc.dig.net`). +# +# The graph still carries 0.4.0/0.5.1/0.8.0 because upstream crates hold them down through their +# PUBLISHED ranges (dig-gossip `>=0.2, <0.5`, dig-nat `>=0.4, <0.6`, dig-download `^0.8`), which a +# consumer cannot edit. Collapsing to a single copy needs each of those to release against 0.9 — +# the release-first cascade on #2072. Do NOT pin this BACK to widen a window: the fix is upstream +# moving forward, never dig-node drifting backward. +dig-constants = "0.13.0" +# -- Portable profile body format (DPB) — epic #3008 W6 ----------------------------------------------- +# +# `default-features = false` deliberately: the default `chia` feature pulls dig-did/dig-store/ +# chia-protocol 0.36.1, which is a DIFFERENT chia line from the 0.26 this workspace builds against, so +# the default surface would put a SECOND chia-protocol into the always-running service. Without the +# feature the crate is a pure BYTE CORE (`dig-smt + sha2 + thiserror`) exposing exactly what the node +# needs: the DPB container, `AnchoredRoot` and `VerifiedBody`. +# +# Consequence to keep in mind: `dig_social_profile::resolve` is NOT available here. The node resolves +# a store's on-chain root through its OWN `AnchoredRootResolver` (the coinset lineage walk), which is +# the same authority, reached by the node's existing seam. +dig-social-profile = { version = "0.7", default-features = false } +# -- L7 content-location DHT (PHASE-B, #163) --------------------------------------------------------- +# The Kademlia DHT that maps DIG content (store / capsule / root / resource) to the peer_ids holding +# it. The node LOCATES content it lacks via `find_providers`, and keeps its OWN held-inventory provider +# records CURRENT in the DHT (`announce_provider` / `withdraw_provider` / `republish`). It rides the +# SAME dig-nat mTLS transport as the rest of the peer network via a `DhtTransport` adapter (`dht.rs`). +# +# Sourcing: the crates.io release (NC-7). dig-download and dig-peer-selector below resolve dig-dht +# from crates.io too, so cargo unifies to ONE dig-dht instance — its `PeerId` / `Contact` / +# `DhtTransport` are then the exact types `dig_nat::connect` returns, rather than look-alikes. +# +# NOT bumped to 0.15, and this is the tier's REMAINING blocker rather than an oversight: dig-download +# 0.21.0 and dig-peer-selector 0.10.0 — the LATEST published of each — both require `dig-dht ^0.13`, +# read from the crates.io index. Declaring "0.15" resolves TWO dig-dht lines while cargo prints +# success, and dig-dht values cross from this crate into both consumers. dig-dht 0.15 is where +# `ProviderRecord::unverified_mirror_coin_id` lives (dig-dht#23), so the §25 mirror-coin pointer is +# unreachable here until dig-download AND dig-peer-selector release against `dig-dht ^0.15` — +# upstream, never a version edit here. Bridging two lines with a shim is the §4.1 byte-drift class. +dig-dht = "0.13" +# -- L7 Peer Exchange (PEX, #166) -------------------------------------------------------------------- +# The node<->node peer-sharing protocol: over each established mTLS peer connection, a dedicated PEX +# logical stream exchanges delta-based first-hand known-peer sets (handshake -> snapshot -> deltas), +# feeding discovered peers into the pool as dial candidates (hints, verified by dialing) and formalizing +# what the ad-hoc `dig.getPeers` polling did. dig-pex ships a transport-agnostic sans-IO `PexEngine` +# this crate embeds; `pex.rs` is the dig-nat mux I/O adapter (SPEC §10.1, Appendix A). +# +# Sourcing: bare git form (no `rev`, like dig-nat/dig-dht) so cargo unifies any shared transitive deps +# to ONE copy across the graph; resolved to the tracked rev via the lockfile. dig-pex keeps a minimal +# dependency surface (serde/serde_json/tokio/rand — it mirrors the address shape rather than importing +# the transport crates), so there is no cross-crate type-unification concern here. +dig-pex = "0.1.1" +# -- Multi-source content fetch (#164) + redirect-on-miss (#165) -------------------------------------- +# The node-side download orchestrator: locate holders via dig-dht `find_providers`, confirm with +# `dig.getAvailability`, fan byte ranges across MULTIPLE providers simultaneously (`dig.fetchRange` +# over dig-nat), verify each range + the whole resource against the chain-anchored root, and +# reassemble — with pause/resume + `.download.tmp` GC. dig-node wires the real seams per the crate's +# implementers' note (see `download.rs`): DhtProviderLocator over our DhtService, NatRangeTransport +# over our mTLS identity, MerkleVerifier bound to the digstore proof format, FileSink/FileStateStore +# under the node's cache dir. +# +# Sourcing: the crates.io release (NC-7), matching dig-dht above. dig-download 0.6 deps dig-dht "^0.5" + +# dig-nat "^0.10" + dig-tls "^0.3" from crates.io, so cargo unifies its dig-dht + dig-nat + dig-tls with +# ours — ONE crate instance each across the graph (its re-exported `ContentId`/`ProviderRecord` ARE our +# dig-dht types). 0.6 keeps the injectable `SourceSelector` seam (dig-node implements it over +# dig-peer-selector, see `seams/dig_peer/selector_adapter.rs`), records range outcomes internally, and the +# fetch leg composes the FULL NAT ladder from an injected shared `NatRuntime` (`NatRangeTransport::new_with_runtime`). +# +# 0.8.1 adds the `ModuleDownloader` — the whole-`.dig`-module peer pull that turns a node which READ a +# resource into a complete resharer of the capsule (#1576, the reshare leg of the MVP flywheel). Its two +# seams are supplied here: `seams/dig_peer/module_transport.rs` (`NatModuleTransport`) and +# `seams/dig_peer/module_anchor.rs` (`ChainAnchoredModuleVerifier`). NOTE the `testkit` feature is +# deliberately NOT enabled: it is what makes the fail-OPEN `AcceptAnyModuleAnchor` nameable, and this +# crate's anchor gate is the reshare path's ONLY root of trust. +# +# HELD on the 0.19 line, resolving 0.19.1 (dig_ecosystem#3128). 0.19.1 is a deliberate BACKPORT of the +# capsule-checkpoint filename fix (dig-download#38): `FileStateStore` hex-encoded the download key into +# the filename, so a module key `module:<64hex>:<64hex>` (136 bytes) became a 277-character name and +# EVERY capsule checkpoint write on Linux failed with `File name too long (os error 36)`. The same fix +# also shipped as 0.20.1, but 0.20 requires dig-dht ^0.12 / dig-nat ^0.20 / dig-peer ^0.12 / +# dig-tls ^0.4 — the whole dig_ecosystem#3152 cascade — which this crate cannot take yet, so the +# backport keeps the 0.19-line pins intact. Regression-tested here through the node's OWN production +# checkpoint store (`download.rs::capturing_state_store_checkpoints_a_real_module_download_key`), +# because dig-download's own suite missed it: every `module.rs` test used `InMemoryStateStore` (no +# filename at all) and the one `FileStateStore` test used a 3-character key. +dig-download = "0.21" +# -- The shared peer client (#1283/#1576) ------------------------------------------------------------- +# `DigPeer` — the ONE DIG Network peer client: peer_id-pinned mTLS over the full NAT ladder plus typed +# RPC. Depended on DIRECTLY (not only transitively through dig-download) because dig-node supplies the +# `ModuleTransport` half of dig-download's module pull, which speaks `dig.getModuleInfo` / +# `dig.fetchModuleRange` — the two client methods dig-peer 0.5 added for exactly this leg. +# +# 0.5 is REQUIRED, not merely current: dig-peer 0.4 pulled dig-rpc-protocol 0.3, so a tree containing +# both it and dig-download's dig-rpc-protocol 0.5 carried TWO `ModuleInfo` types either side of the +# module pull's trust boundary — on the fields that drive the whole pull plan. dig-download 0.8.1 is on +# dig-peer 0.5 too, so exactly ONE dig-rpc-protocol + ONE dig-peer resolve here (asserted by +# `crates/dig-node-core/tests/dependency_tree.rs`). +dig-peer = "0.13" +# -- Self-optimizing peer selection (#178) ------------------------------------------------------------ +# The decision + learning layer between dig-dht discovery and dig-download execution: it ranks the +# providers `find_providers` returns (learning throughput/rtt/reliability + a per-class saturation +# point + an adaptive relayed penalty from the REAL measured outcome of every range) and hands +# dig-download the ranked subset + per-peer concurrency, instead of dig-download picking sources +# blindly. Every completed/failed range streams back via `record_outcome` in real time, and a dropped +# source drives a `rebalance`. dig-node bridges dig-download's `SourceSelector` seam to this crate via +# `seams/dig_peer/selector_adapter.rs` (the @30↔@30 composition-root bridge — dig-download does NOT +# depend on dig-peer-selector; dig-node, one level up, wires them together). +# +# Sourcing: the crates.io release (NC-7), matching dig-dht/dig-download above. It deps dig-dht "^0.5" + +# dig-nat "^0.10" from crates.io, so cargo unifies the selector's `dig-nat` + `dig-dht` with ours — ONE +# crate instance each (its re-used `PeerId`/`ContentId`/`ProviderRecord`/`CandidateAddr` ARE our +# dig-nat/dig-dht types). The selector mirrors `dig_gossip::PoolEvent` LOCALLY (it does not depend on +# dig-gossip, which lags as a git dep), so the node maps `dig_gossip::PoolEvent` → the selector's local +# `PoolEvent` 1:1 (see `download.rs::pool_event_to_selector`). +# 0.9.2 IS the chia-0.36 cascade landing (dig_ecosystem#3161). The old note here said "NOT bumped to +# 0.9.1: that release moves onto dig-tls 0.4, and this crate still resolves 0.3 through +# dig-nat/dig-gossip, so the graph would carry TWO `dig_tls::PeerId` types that do not unify". Both +# halves of that split are gone — dig-nat 0.20 and the dig-gossip rev both take `dig-tls ^0.4`, and +# the lock now resolves dig-tls ONCE at 0.4.0. +# +# 0.10 IS takeable as of dig-gossip v0.32.0, which declares `dig-nat = "0.21"` — the release the old +# note here was waiting on. It carries `dig-dht ^0.13`, which is what holds dig-dht at 0.13; see the +# dig-dht entry above. +dig-peer-selector = "0.10" +# The canonical DIG mTLS certificate crate (L00, crates.io). The node's PERSISTENT machine identity +# is a CA-signed `dig_tls::NodeCert` minted from the node's own BLS identity key and persisted 0600 in +# the data dir (#908 identity boundary: this is the MACHINE key, never a user key). Replaces the +# legacy self-signed `rcgen` cert as the node dials/serves over the dig-nat CA-signed mTLS model +# (#1280). `peer_id = SHA256(SPKI DER)` is stable across restarts. dig-tls re-exports chia-bls's +# `SecretKey`, unifying with this crate's chia-bls 0.36.1. 0.4 is the chia-bls 0.36.1 uplift and is +# now TAKEN (dig_ecosystem#3161): the old hold — "taking it here would split `dig_tls::PeerId` +# against the 0.3 that dig-nat/dig-gossip still resolve" — expired when dig-nat 0.20 and the +# dig-gossip rev both moved onto `dig-tls ^0.4`. The lock resolves dig-tls ONCE, at 0.4.0. +dig-tls = "0.4" +# -- The store-exchange DECISION layer (dig_ecosystem#3130) ------------------------------------------- +# `dig-sex` owns every cache/tier/relevance/selection DECISION this node used to make in-tree: what to +# keep, what to sacrifice first, and whether a fresh candidate is worth displacing an incumbent. It is +# PURE — no clock, no socket, no filesystem — so this crate keeps owning all of the I/O and hands it +# only the facts. The in-tree `relevance` module and `tier0_prefetch::effective_tier` moved there +# verbatim and are now consumed as `dig_sex::*`. +# +# It also re-exports the ONE `CapsuleIdentity` the ecosystem speaks, so this crate never declares its +# own and never pulls a second major of `dig-store` into the graph. +dig-sex = "0.5" +# The cache/eviction SEAM `dig_sex::TieredPolicy` implements. Declared DIRECTLY rather than reached +# through `dig-sex`, which re-exports `CapsuleIdentity` alone — naming `EvictionEntry`, +# `EvictionContext` and the `EvictionPolicy` trait (whose `select_evictions` must be in scope to call) +# requires the crate itself. The version is pinned to the one `dig-sex` resolves, so the `CapsuleIdentity` +# this crate builds and the one `TieredPolicy` matches on are the SAME type; a split would silently +# fail to unify. `dig_sex::TieredPolicy` is the first non-LRU implementation of the seam, and the +# module sweep here is its first consumer. +dig-store-cache = "0.1.1" +# The mTLS peer-RPC LISTENER (server side): present the node cert + REQUIRE a client cert (peer_id +# enforced). rustls/tokio-rustls with ring only (no OpenSSL/aws-lc for OUR usage), matching dig-nat. +rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +# IPv6-first, IPv4-fallback (ecosystem HARD RULE): bind the peer-RPC listener on the IPv6 unspecified +# address `[::]` as a DUAL-STACK socket (IPV6_V6ONLY cleared) so ONE socket serves both native IPv6 +# and IPv4-mapped peers. Mirrors dig-relay's `net.rs` / dig-gossip's own dual-stack bind. +socket2 = "0.5" +# -- Engine-side identity session (NODE-1, #910) ----------------------------------------------------- +# The engine is IDENTITY-AGNOSTIC: it holds NO user signing key. `control.session.attach` proves the +# attaching dig-app holds the profile's slot-0x0010 identity key by VERIFYING an Ed25519 signature over +# the byte-identical `DIGNET-SESSION-v1 ‖ nonce ‖ profile_did` challenge (session.rs). Ed25519 +# verification rides `ring` — ALREADY in the locked graph (the rustls/tokio-rustls transport uses it), +# so it adds no new crate. VERIFY-ONLY: the engine never holds a signing key. (dig-app signs with +# ed25519-dalek; ring verifies RFC-8032 signatures + is strict against malleable/small-order inputs.) +ring = "0.17" +tracing = "0.1" +# Best-effort CONCURRENT fan-out of the multi-source provider union (`UnionLocator`, #1443): query +# dig-dht + PEX + relay-introducer sources at once via `future::join_all` (order-preserving, so the +# dedup keeps DHT-first precedence). rustls/tokio already pull futures transitively; declaring it here +# makes it a first-class prod dep of the union seam. Minimal feature set (no executor). +futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } + +[dev-dependencies] +tempfile = "3" +# `dig_keystore::hardware::double::FakeDevice` -- the only way to exercise a SPECIFIC hardware +# outcome from a test. The real providers ship now (dig-keystore-hardware above), but which one a +# host answers with is a property of that host: no CI runner can be made to produce an +# indeterminate TPM probe on demand, and that is the outcome the refusal path turns on. +# Test-only: `testing` is never enabled for the library build. +dig-keystore = { version = "0.13", features = ["testing"] } +# Same git rev as the normal dependency above, with dig-gossip's `test-util` feature added. That +# feature gates `__connect_stub_peer_with_direction`, which registers a connected peer without real +# TLS — the only way to assert what a broadcast ACTUALLY put on the wire from a test. Declared here +# rather than on the normal dependency so the shipped binary never carries the test hook: cargo +# unifies the feature into test builds only, since dev-dependencies are not built for `cargo build`. +# NC-7 exception: same permanent git pin as the runtime dep above (dig_ecosystem#2647). +dig-gossip = { git = "https://github.com/DIG-Network/dig-gossip", rev = "1a3391662ecce1a3cbe8b74122a52bbb1b28d3ee", default-features = false, features = ["rustls", "relay", "test-util"] } +# Generates the P-256 leaf key pairs the opcode-222 holdings tests sign with: `public_key_der()` IS the +# leaf `SubjectPublicKeyInfo` DER the wire carries as `provider_spki`, and whose SHA-256 is the +# announcing peer_id — so a test signer is a real §5.2 identity rather than a stand-in that could hide +# an attribution bug (#1429). +rcgen = "0.13" +# dig-download's in-memory harness (`MockRangeTransport`, `MockContent`, …), which the download-path +# tests drive the real orchestrator over. +# +# It is a DEV dependency and MUST STAY ONE (#1576). The `testkit` feature is also what makes +# `AcceptAnyModuleAnchor` — a fail-OPEN module anchor verifier — nameable at all; the crate compiles it +# out of a default consumer build precisely so a production wiring cannot reach it. The reshare path's +# anchor gate is its ONLY root of trust, so enabling this feature on the PRODUCTION `dig-download` entry +# above would put a bypass of that root of trust one `use` away. Dev-dependency features do not +# propagate to a consumer's build, so the node binaries never see it. +# +# Pinned by the `the_fail_open_anchor_verifier_is_not_reachable_from_a_production_build` test, which +# fails if `testkit` ever appears on the production entry. +dig-download = { version = "0.21", features = ["testkit"] } +# Captures the peer-facing serve's real emitted tracing records into an in-memory buffer, so the +# serve-observability tests (#1595) assert what an operator would actually see in the node log — +# and that no payload byte or proof ever reaches it. +tracing-subscriber = { version = "0.3", features = ["fmt"] } +# Builds canonical `did:chia:` strings for the on-chain DID-resolver tests (matches dig-identity's codec). +chia-sdk-utils = "0.36.0" +# The session attach tests sign challenges with the BLS12-381 G2 AugScheme (the app's slot-0x0010 +# identity key), matching dig-ipc-protocol's signing contract and dig-identity's published G1 key. +chia-bls = "0.36.1" +# axum is a TEST-ONLY dependency of the engine library: the node no longer serves its +# own HTTP (the `dig-node-service` shell owns the axum transport, `dig-runtime` owns the +# in-process FFI path — dispatch is `handle_rpc`, which is transport-agnostic). The +# integration/unit tests still spawn small in-process axum mock servers (the §21 remote +# + a module server) to exercise the fetch/cache path hermetically. +axum = "0.7" +# In-process loopback mTLS peer drives the peer-network tests with NO real network (the CA-signed +# `dig_tls::NodeCert` minted by `load_or_generate_node_cert` is the test client's mTLS identity). +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "net", "io-util", "time", "test-util"] } +futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } +# The peer_network.rs integration test implements PeerRpcResponder (an async trait). +async-trait = "0.1" From 71317f39874c6d7741cde0510a9d3075d51212db Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sun, 30 Aug 2026 05:18:46 -0700 Subject: [PATCH 3/6] feat(mirror): observe bond states from four readings (#412 step 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `mirror::observe` — a pure function over values that turns the capsules on disk, the mirror coins on chain, this node's open creates, and its spendable $DIG into the `BondObservation` the §25.8 surface pages. It takes no `MirrorEffects`, so it holds no `create` and no `reclaim`: the read surface cannot reach a spend because no spend capability is constructed on its path. States come from `pass::decide`, the same pure decision a real pass takes, rather than a second derivation that would drift. Supporting reads, both keyless: - `dig_wallet::operator_puzzle_hash` derives this node's public puzzle hash without ever producing a `WalletSigner`. - `WalletBackend::dig_balance_base_units` reads spendable $DIG at that hash. `None` is UNKNOWN, never zero — the two render as opposite claims. Constructing or scheduling a pass, and wiring a `MirrorSigner`, are step 8. Refs #412 --- crates/dig-node-service/src/mirror/mod.rs | 1 + crates/dig-node-service/src/mirror/observe.rs | 322 ++++++++++++++++++ crates/dig-node-service/src/mirror/runner.rs | 2 +- crates/dig-wallet/src/operator_wallet.rs | 31 ++ crates/dig-wallet/src/sage/rpc.rs | 27 ++ 5 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 crates/dig-node-service/src/mirror/observe.rs diff --git a/crates/dig-node-service/src/mirror/mod.rs b/crates/dig-node-service/src/mirror/mod.rs index abe0ac77..e8afa63f 100644 --- a/crates/dig-node-service/src/mirror/mod.rs +++ b/crates/dig-node-service/src/mirror/mod.rs @@ -77,6 +77,7 @@ //! that confusion is exactly how a money bug ships. Fees, which genuinely are XCH mojos, are named //! `*_mojos` and come from separate coins so a fee can never shave collateral. +pub mod observe; pub mod pass; pub mod plan; pub mod presence; diff --git a/crates/dig-node-service/src/mirror/observe.rs b/crates/dig-node-service/src/mirror/observe.rs new file mode 100644 index 00000000..1a77891f --- /dev/null +++ b/crates/dig-node-service/src/mirror/observe.rs @@ -0,0 +1,322 @@ +//! §25.8's OBSERVATION: what this node's bonds are, without doing anything about them +//! (dig-node#412 step 7). +//! +//! [`super::states`] renders an answer and [`super::runner`] acts on one. This is the step between: +//! it turns four already-gathered readings — the capsules on disk, the mirror coins on chain, this +//! node's own open creates, and the $DIG it can spend — into the +//! [`BondObservation`](super::states::BondObservation) the surface pages. +//! +//! # It cannot spend, and that is a property of the module rather than a rule about it +//! +//! [`super::runner::MirrorEffects`] holds both halves of the lifecycle: three reads and two spends. +//! A read surface built over it would hold a `create` and a `reclaim` it merely promised never to +//! call, and §25.8 is reachable from the token-gated control plane. So nothing here takes that +//! trait. [`observe`] is a **pure function over values**: it has no wallet, no signer, no chain +//! handle and no `&self`, so there is no unattended spend for a later edit to reach by accident. +//! +//! The same reasoning decides where the I/O lives. The caller performs the four reads and hands the +//! results in, which is what lets every property below be a fixture and a literal rather than a run +//! against a chain. +//! +//! # Nothing here recomputes what the decision already knows +//! +//! The states come from [`super::pass::decide`] — the same pure decision a real pass takes — rather +//! than from a second derivation written for the read path. A second derivation is a second answer +//! to "is this bond covered", and the two would drift in the direction nobody tests: the surface +//! would say `bonded` about a pair the pass was about to create a coin for, or `unfunded` about one +//! it had already covered. `decide`'s plan half is discarded here precisely because taking it is +//! step 8's act, not this one's. +//! +//! # The locked total is summed over the WHOLE chain observation +//! +//! Over every owned coin, before the plan splits it into keeps and reclaims, and including coins +//! being reclaimed — a broadcast reclaim has not confirmed, and the collateral is locked until it +//! does. It is read from each coin's own amount rather than from this epoch's requirement, because a +//! coin created under a previous requirement locks the previous amount. Summing the plan instead +//! would omit every coin the plan leaves alone, which is most of them on a healthy node, and would +//! report locked money as free — the one direction a money figure must never be wrong in. +//! +//! # The disk set is NOT presence-debounced, deliberately +//! +//! [`super::presence`]'s settling window exists to stop the node SPENDING on a capsule that may +//! vanish (§25.5). It is a spend suppressor, and a read surface is not a spend. Applying it here +//! would make a freshly-arrived capsule absent from its own node's bond list, which reads as a +//! missing capsule rather than as a deliberate wait — and a fresh tracker, which is all a stateless +//! read can build, suppresses everything it has ever seen exactly once. So this reports what is on +//! disk now. The cost is bounded and states itself: a capsule that arrived moments ago is reported +//! with the state it has right now, which for an uncovered pair is `unfunded` or `deferred` until +//! the next pass covers it. + +use dig_mirror_coin::MirrorInventory; +use num_bigint::BigInt; + +use dig_node_control_interface::results::CollateralRequirementResult; + +use super::pass::{self, PassInputs}; +use super::plan::{Bond, HeldMirror}; +use super::runner::ObservedCapsule; +use super::states::BondObservation; + +/// What an observation consults that it does not read for itself. +/// +/// The epoch, the requirement, the margin and the switch are parameters for the same reason +/// [`pass::decide`] takes them: one observation must see ONE epoch and ONE requirement throughout, +/// and a value re-read partway through could change underneath it — producing a page whose rows +/// were priced against two different requirements while claiming to describe one moment. +#[derive(Debug, Clone)] +pub struct ObserveContext { + /// The epoch in force. + pub current_epoch: i64, + /// This epoch's requirement, or the named reason it is unknown (§24.2). + pub requirement: CollateralRequirementResult, + /// The local safety margin, in basis points. + pub margin_bp: u64, + /// §25.7's switch. Reported because a node with creates OFF describes its bonds differently. + pub creates_enabled: bool, +} + +/// The whole §25.8 answer before paging, from four readings and a context. +/// +/// `dig_balance_base_units` is `Option` and `None` is **not zero**: a wallet that could not be read +/// is UNKNOWN, and reading it as zero reports every uncovered bond as `unfunded` — an out-of-funds +/// alarm about a wallet nobody read, which is the dig-app#300 conflation this surface exists to +/// remove. `None` yields `deferred{balance_unreadable}` on the affected rows and leaves every +/// `bonded`, `withheld` and `reclaiming` row untouched, because none of those three depends on the +/// balance. +pub fn observe( + observed: &[ObservedCapsule], + on_chain: &[HeldMirror], + in_flight: &[Bond], + dig_balance_base_units: Option, + ctx: &ObserveContext, +) -> BondObservation { + let (held, relayed) = super::runner::split_by_provenance(observed); + + // Over the WHOLE observation, not over the plan: see the module doc. `saturating_add` rather + // than a wrapping sum, because a total that wrapped would report a large locked figure as a + // small one. + let locked_dig_base_units = on_chain + .iter() + .map(|coin| coin.collateral_dig_base_units) + .fold(0u64, u64::saturating_add); + + let decision = pass::decide(&PassInputs { + held: &held, + relayed: &relayed, + on_chain, + in_flight, + current_epoch: ctx.current_epoch, + requirement: &ctx.requirement, + margin_bp: ctx.margin_bp, + dig_balance_base_units, + creates_enabled: ctx.creates_enabled, + }); + + BondObservation { + states: decision.states, + locked_dig_base_units, + epoch: ctx.current_epoch, + } +} + +/// The mirror coins an inventory says this node owns, in the planner's vocabulary. +/// +/// A coin whose declared epoch does not fit an `i64` is DROPPED rather than clamped. The epoch is a +/// `BigInt` on the wire because the hint morph is arithmetic over unbounded integers, so a stranger +/// can place a coin declaring any epoch at all for the price of a dust coin. Clamping such a value +/// would make that coin claim to bond the current epoch — a stranger choosing what this node reports +/// about its own bonds. Dropping it costs nothing: `dig_mirror_coin::list` already authenticates +/// ownership from the lineage proof, so a coin here is one this node controls, and one it controls +/// with an unrepresentable epoch is one no pass could ever act on anyway. +/// +/// [`MirrorInventory::skipped`] and [`MirrorInventory::complete`] are the caller's to inspect; this +/// maps only the coins the scan resolved. +pub fn held_mirrors(inventory: &MirrorInventory) -> Vec { + inventory + .coins() + .iter() + .filter_map(|coin| { + Some(HeldMirror { + coin_id: hex::encode(coin.coin().coin_id()), + store_id: hex::encode(coin.store_launcher_id()), + root: hex::encode(coin.root_hash()), + epoch: epoch_as_i64(coin.epoch())?, + collateral_dig_base_units: coin.collateral(), + }) + }) + .collect() +} + +/// A declared epoch as an `i64`, or `None` when it does not fit. +/// +/// Separated so the drop-rather-than-clamp rule above is a named, testable step rather than a +/// `try_into` buried in a closure. +fn epoch_as_i64(epoch: &BigInt) -> Option { + i64::try_from(epoch).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use dig_node_core::CapsuleProvenance; + + use super::super::pass::BondState; + + /// A store/root pair spelled out to 64 hex, so the canonicalisation downstream has real input. + fn bond(store: &str, root: &str) -> Bond { + Bond::new(store.repeat(32), root.repeat(32)) + } + + fn capsule(store: &str, root: &str, provenance: CapsuleProvenance) -> ObservedCapsule { + ObservedCapsule { + bond: bond(store, root), + provenance, + } + } + + fn coin(store: &str, root: &str, epoch: i64, collateral: u64) -> HeldMirror { + HeldMirror { + coin_id: "cc".repeat(32), + store_id: store.repeat(32), + root: root.repeat(32), + epoch, + collateral_dig_base_units: collateral, + } + } + + const REQUIRED: u64 = 1_000; + + fn ctx() -> ObserveContext { + ObserveContext { + current_epoch: 7, + requirement: CollateralRequirementResult::Known { + epoch: 7, + required_per_store_dig_base_units: REQUIRED, + stores: 10, + owners: 3, + census_height: 100, + }, + margin_bp: 0, + creates_enabled: true, + } + } + + /// The locked total spans EVERY owned coin, including one the plan will reclaim. + /// + /// The fixture is deliberately not "all coins are keepers": the second coin bonds a capsule that + /// is NOT on disk, so the plan reclaims it, and a total summed from the plan's keep list — or + /// from the page — would report `600` rather than `1000`. Money reported as free while it is on + /// chain is the one direction this figure must never be wrong in, and a fixture where every coin + /// is kept cannot tell the two implementations apart. + #[test] + fn the_locked_total_includes_a_coin_the_plan_is_about_to_reclaim() { + let observed = [capsule("aa", "11", CapsuleProvenance::Held)]; + let on_chain = [coin("aa", "11", 7, 600), coin("bb", "22", 7, 400)]; + + let observation = observe(&observed, &on_chain, &[], Some(10_000), &ctx()); + + assert_eq!( + observation.locked_dig_base_units, 1_000, + "both owned coins are locked; the reclaimed one has not confirmed" + ); + assert!( + observation + .states + .iter() + .any(|(b, s)| *b == bond("aa", "11") && *s == BondState::Bonded), + "the covered capsule is bonded" + ); + } + + /// A `Relayed` capsule is reported as `Withheld` rather than omitted. + /// + /// Two capsules with DIFFERENT provenance and neither on chain, so the assertion distinguishes + /// "the relayed half reached the states list" from "everything reached it". A fixture with only + /// a relayed capsule would pass against an implementation that reported every capsule as + /// withheld, which is the nearest wrong thing this split can do. + #[test] + fn a_relayed_capsule_is_withheld_and_a_held_one_is_not() { + let observed = [ + capsule("aa", "11", CapsuleProvenance::Held), + capsule("bb", "22", CapsuleProvenance::Relayed), + ]; + + let observation = observe(&observed, &[], &[], Some(10_000), &ctx()); + + let state_of = |b: Bond| { + observation + .states + .iter() + .find(|(k, _)| *k == b) + .map(|(_, s)| s.clone()) + }; + assert_eq!(state_of(bond("bb", "22")), Some(BondState::Withheld)); + assert_ne!( + state_of(bond("aa", "11")), + Some(BondState::Withheld), + "the held capsule is this node's own bond, not a withheld one" + ); + } + + /// An unreadable balance defers the rows it prices and leaves the rest alone. + /// + /// The fixture carries one COVERED bond beside one uncovered bond, so a `None` balance that + /// wrongly blanked the whole answer — or wrongly reported the uncovered row as `unfunded` — is + /// visible. An all-uncovered fixture could not tell `FundsUnknown` from a call-level failure. + #[test] + fn an_unreadable_balance_defers_only_the_rows_it_prices() { + let observed = [ + capsule("aa", "11", CapsuleProvenance::Held), + capsule("bb", "22", CapsuleProvenance::Held), + ]; + let on_chain = [coin("aa", "11", 7, REQUIRED)]; + + let observation = observe(&observed, &on_chain, &[], None, &ctx()); + + let state_of = |b: Bond| { + observation + .states + .iter() + .find(|(k, _)| *k == b) + .map(|(_, s)| s.clone()) + }; + assert_eq!( + state_of(bond("aa", "11")), + Some(BondState::Bonded), + "a covered bond does not depend on the balance" + ); + assert_eq!( + state_of(bond("bb", "22")), + Some(BondState::FundsUnknown), + "an uncovered bond is UNKNOWN, never a fabricated shortfall" + ); + } + + /// The epoch reported is the context's, not one derived from the coins. + /// + /// A node whose coins are all from a previous epoch still describes the epoch in force; deriving + /// it from the inventory would make a node with no coins have no epoch, and a node holding stale + /// coins report a past one as current. + #[test] + fn the_reported_epoch_is_the_one_in_force_not_the_coins() { + let on_chain = [coin("aa", "11", 3, 500)]; + + let observation = observe(&[], &on_chain, &[], Some(0), &ctx()); + + assert_eq!(observation.epoch, 7); + } + + /// An epoch that does not fit an `i64` is DROPPED, never clamped. + /// + /// Asserted on the conversion directly, because the value cannot be built through `HeldMirror`: + /// its field is already an `i64`, so the only place the decision is observable is here. Both + /// bounds are checked from BOTH sides — `i64::MAX` must convert and `i64::MAX + 1` must not — + /// since a conversion tested only from below would confirm itself. + #[test] + fn an_out_of_range_declared_epoch_does_not_become_a_valid_one() { + assert_eq!(epoch_as_i64(&BigInt::from(i64::MAX)), Some(i64::MAX)); + assert_eq!(epoch_as_i64(&BigInt::from(i64::MIN)), Some(i64::MIN)); + assert_eq!(epoch_as_i64(&(BigInt::from(i64::MAX) + 1)), None); + assert_eq!(epoch_as_i64(&(BigInt::from(i64::MIN) - 1)), None); + } +} diff --git a/crates/dig-node-service/src/mirror/runner.rs b/crates/dig-node-service/src/mirror/runner.rs index 210ef771..6199e747 100644 --- a/crates/dig-node-service/src/mirror/runner.rs +++ b/crates/dig-node-service/src/mirror/runner.rs @@ -336,7 +336,7 @@ impl PassRunner { /// than a step inside a [`MirrorEffects`] implementation, so that there is exactly one of it: a /// second implementation of this rule is a second answer to "what may this node spend its own money /// advertising", and the two would not stay equal. -fn split_by_provenance(observed: &[ObservedCapsule]) -> (Vec, Vec) { +pub(super) fn split_by_provenance(observed: &[ObservedCapsule]) -> (Vec, Vec) { let mut held = Vec::new(); let mut relayed = Vec::new(); for capsule in observed { diff --git a/crates/dig-wallet/src/operator_wallet.rs b/crates/dig-wallet/src/operator_wallet.rs index 8659798a..a230bd78 100644 --- a/crates/dig-wallet/src/operator_wallet.rs +++ b/crates/dig-wallet/src/operator_wallet.rs @@ -101,6 +101,37 @@ impl OperatorWallet { } } +/// This node's operator PUZZLE HASH, derived without ever producing a signer. +/// +/// `None` for exactly the cases [`OperatorWallet::open`] returns `None` for: no seed, no device key, +/// a seal that will not open, or a phrase that does not derive. A caller MUST report the capability +/// as unavailable rather than substitute any other address — a balance or a coin list read for the +/// wrong puzzle hash is a confident number about somebody else's money. +/// +/// # Why this exists beside [`OperatorWallet::open`] rather than being a call to it +/// +/// The puzzle hash is a **public** value: this node's receive address, the address the dig-app +/// deposit flow funds, and the address reclaims return to. A [`WalletSigner`] is not. §25.8's bond +/// surface (dig-node#412) is a token-gated READ that needs the first and has no business +/// materialising the second, so this function's return type is a [`Bytes32`] and there is no signer +/// value anywhere on its path. +/// +/// That distinction is held by the **type**, not by a convention: a control-plane read built on this +/// cannot reach a signing capability, because no such capability is ever constructed for it to +/// reach. `OperatorWallet::open` remains the only way to obtain one, and the mirror lifecycle +/// remains its only caller. +/// +/// The secret key derived along the way is dropped at the end of this function and never leaves it; +/// the phrase lives in a zeroizing wrapper for the length of the derivation, exactly as in +/// [`OperatorWallet::open`], and nothing here is logged. +/// +/// §908 is untouched. This is the §16.4 machine-custody wallet — the node's own money — and no user +/// seed reaches this process. +pub fn operator_puzzle_hash(paths: &WalletPaths) -> Option { + let phrase = autoseed::open_operator_phrase(paths)?; + Some(digstore_chain::keys::derive_wallet_keys(&phrase).ok()?.owner_puzzle_hash) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/dig-wallet/src/sage/rpc.rs b/crates/dig-wallet/src/sage/rpc.rs index cbf1dcd1..9eb7b963 100644 --- a/crates/dig-wallet/src/sage/rpc.rs +++ b/crates/dig-wallet/src/sage/rpc.rs @@ -1213,6 +1213,33 @@ impl WalletBackend { Self::SUPPORTED_METHODS.contains(&method) } + /// The spendable **$DIG** at this node's own operator puzzle hash, in DIG CAT base units. + /// + /// `None` when the balance could not be read — an unreachable chain source, an unsynced replica + /// that cannot answer for this address, a figure too large for a `u64`. `None` is **not zero**: + /// §25's bond surface reports an uncovered bond as `deferred{balance_unreadable}` on `None` and + /// as `unfunded` on `Some(0)`, and those are opposite claims — the first says the node does not + /// know, the second raises an out-of-funds alarm. Substituting zero for an unreadable balance is + /// the dig-app#300 conflation. + /// + /// Takes the puzzle hash rather than an address so no caller has to spell an address, pick a + /// prefix, or name the asset: the encoding and the canonical `$DIG` asset id both stay inside + /// this crate, where the one definition of each already lives. A caller that assembled its own + /// address string could read the right amount of the wrong asset at the wrong network's prefix, + /// and every one of those returns a confident number. + /// + /// This is a READ. It confers no custody and touches no key: the puzzle hash is a public value. + pub async fn dig_balance_base_units(&self, owner_puzzle_hash: Bytes32) -> Option { + let address = self.address_of(&hex::encode(owner_puzzle_hash)); + let read = self + .balance_for_address(&address, BalanceAsset::DIG) + .await + .ok()?; + // Narrowed rather than saturated. A saturating cast would report a balance above `u64::MAX` + // as exactly `u64::MAX` — the largest possible confident wrong number on a funding decision. + u64::try_from(read.balance).ok() + } + // ---- address helpers -------------------------------------------------- fn address_of(&self, puzzle_hash_hex: &str) -> String { From 5a1f3d4d30fad82b6c4c5150ad7492ff81f24e2b Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sun, 30 Aug 2026 05:22:49 -0700 Subject: [PATCH 4/6] fix(deps): restore LF on dig-node-core's manifest and make the section lookup fail loudly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build(deps)` 2614d07c saved crates/dig-node-core/Cargo.toml with CRLF line endings. Not one declaration changed — 1147 of its 1175 changed lines were line-ending churn — but `dependency_tree.rs` reads that manifest through `include_str!` and searches it for "\n[dependencies]\n", which CRLF bytes never match. The lookup's `unwrap_or(0)` then turned a failed search into a silent fallback to offset 0, so the guard searched `[package]` instead, found no `dig-download` there, and panicked claiming it was absent from the production tree. `dig-download = "0.21"` had never moved: it is on line 448 of the `[dependencies]` section, exactly where it was before. Two changes, both at the cause: - the manifest is normalised to LF before any search, so these assertions stay about DECLARATIONS rather than about how a file happened to be saved; - the lookup panics naming the section it could not read. A lookup that FAILED is not evidence about any dependency, and offset 0 makes every "X is a production dependency" assertion report absence with total confidence. The assertion itself is unchanged and no expect was loosened. Also bumps the workspace to 0.173.0: main took 0.172.0 while this branch was open, and step 7 adds a capability. Refs #412 --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/dig-node-core/Cargo.toml | 1182 ++++++++--------- crates/dig-node-core/tests/dependency_tree.rs | 26 +- 4 files changed, 615 insertions(+), 597 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e867b9e..24e835fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3031,7 +3031,7 @@ dependencies = [ [[package]] name = "dig-node-service" -version = "0.172.0" +version = "0.173.0" dependencies = [ "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index 0bdc5d42..45668d1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ edition = "2021" # the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a # release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet) # keep their own independent versions — only the released binary tracks the workspace version. -version = "0.172.0" +version = "0.173.0" # Release hardening, matching digstore: keep integer-overflow checks ON in release. # The node parses untrusted serialized input and does offset/length arithmetic over diff --git a/crates/dig-node-core/Cargo.toml b/crates/dig-node-core/Cargo.toml index 00169914..e9266f91 100644 --- a/crates/dig-node-core/Cargo.toml +++ b/crates/dig-node-core/Cargo.toml @@ -1,591 +1,591 @@ -[package] -name = "dig-node-core" -# The engine library's OWN version, independent of the released binary's (`[workspace.package].version` -# in the root manifest, which only dig-node-service tracks). It moves when this crate's PUBLIC surface -# moves: 0.24.0 acknowledges that the surface now exposes dig-nat 0.14 / dig-dht 0.8 types, a -# compatibility-relevant change even though the crate is consumed in-workspace and is not published to -# crates.io. Holding it at 0.23.0 would have overstated stability to anyone reading the manifest for -# what the exported types are (dig_ecosystem#1785c). 0.26.0 does the same for the 0.15 line: the -# exported dig-nat / dig-dht types rotate identity again, and `DhtError` — reachable through this -# crate's dig-dht re-exports — now carries `SafeText` instead of `String` (dig_ecosystem#1674/#1675). -# 0.27.0 adds the pure `relevance` module (CacheTier / relevance() / evict_key / should_displace) — -# a new, backwards-compatible public export (dig_ecosystem#1986), hence a MINOR bump. -# 0.28.0 adds the pure `tier0_selector` module (Candidate / select_within_budget / tier0_budget_bytes / -# should_displace_tier0) built on 0.27's relevance module — a new, backwards-compatible public export -# (dig_ecosystem#1988), hence another MINOR bump. -# 0.31.0 adds cache observability to `cache.stats` (§7.10e) — `refetch_count` + per-tier `tiers` -# occupancy fields, and makes `InboundDemand::entry_count` a real (no-longer-`#[cfg(test)]`) public -# API — a new, backwards-compatible surface (dig_ecosystem#1991), hence a MINOR bump. -# 0.47.0 adopts dig-gossip 0.25.0 and splits the announce seams by ORIGIN: `ProfileTransport` gains a -# required `announce_root_local` and `MeltBroadcast` a required `broadcast_local`, so a locally -# originated announce takes dig-gossip's dedup-exempt path and a re-announce of an unchanged root is -# no longer suppressed forever (dig_ecosystem#3061). Adding a required trait method is BREAKING for an -# out-of-workspace implementor; this crate is consumed in-workspace only and is pre-1.0, so it is a -# MINOR bump under SemVer's 0.x rule -- recorded here rather than letting the number imply the trait -# surface held still. -# 0.53.0 adds the module-granularity capsule relay leg (`seams/dig_peer/module_relay.rs`, gated OFF -# by default) and makes the provider/candidate locator readers absence-safe: `NodeContent::find_providers` -# and `NatModuleTransport::discovered_candidates` now return `BestEffort` rather than a bare `Vec`, -# so a source that could not be consulted can no longer be read as a proven absence (dig_ecosystem#3128, -# dig-node#276/#296). Changing a public return type is BREAKING for an out-of-workspace implementor; -# this crate is consumed in-workspace only and is pre-1.0, so it is a MINOR bump under SemVer's 0.x -# rule -- recorded here rather than letting the number imply the locator surface held still. -version = "0.62.0" -edition = "2021" -license = "GPL-2.0-only" -description = "The canonical DIG node ENGINE library (crate `dig_node_core`): the JSON-RPC dispatch (`handle_rpc`, the same contract as rpc.dig.net), local-first content serve/fetch/redirect from LOCAL .dig store modules (via digstore_host::serve_blind), chain-anchored-root resolution, chain-watch + subscriptions + generation gap-fill, the LRU cache, and the full P2P stack. Shared UNCHANGED by both host shells: the `dig-node` OS-service binary (dig-node-service) and the DIG Browser's in-process cdylib (dig-runtime). Native Rust so the compiled-module serve path works." - -[lib] -name = "dig_node_core" -path = "src/lib.rs" - -[dependencies] -# Bounds the whole-module descriptor memo (`seams/dig_peer/module_serve.rs`, #1615/G2): a per-process -# cache of `dig.getModuleInfo` answers keyed by (store, root), capped so a long-running node cannot -# accumulate one entry per module it has EVER described (each entry is tens of KB — up to 512 -# chunk_hashes + chunk_lens). Already a transitive dependency (pulled in via the workspace's other -# crates); promoted to a direct one now that this crate uses it itself. -lru = "0.12" -# The `.dig` STORE-FORMAT LIBRARY crates. These live in the digstore repo (the store -# CLI + format/read libraries) and are consumed here as GIT dependencies: the node -# depends on the store libs, NEVER the reverse (digstore is only ever an RPC client of -# a node). Pinned to a single digstore rev so the whole store-format surface the node -# reads/verifies/decrypts against is one coherent version. Bump all seven together. -# -# digstore-host — instantiate a compiled module + `serve_blind` (ciphertext + proof). -# digstore-core — wire types (ContentResponse), codec, Bytes32/48, the PublicManifest -# type + `datasection::read_public_manifest` (#176 Phase C). -# digstore-remote — the §21 remote client + persistent identity signer (§21.9). -# digstore-chain — CHIP-0035 singleton lineage walk (chain-anchored root). -# digstore-stage — the shared stage→compile engine for the `dig.stage` RPC (turn a -# folder into a capsule in-process, #95 Pass C). Its build script -# embeds the guest wasm (BINDING contract D6) via the DIGSTORE_GUEST_WASM -# override this repo sets in .cargo/config.toml (see vendor/). -# digstore-crypto — read-path crypto primitives. -# digstore-compiler — `extract_data_section_blob` (#176 Phase C): pulls the embedded DIGS -# data-section blob out of a compiled `.dig` module's wasm binary so -# `dig.getManifest` can read section 13 (the PublicManifest) WITHOUT a -# wasmtime instantiation (the manifest is PUBLIC, unencrypted data — no -# `serve_blind` decrypt needed). Already a transitive dependency via -# digstore-stage; pinned directly at the SAME rev so its public API is -# usable here too. -# -# Bumped to a rev past digstore's "Add normalized public manifest to the .dig format" -# commit (08f5d39) so `PublicManifest`/`read_public_manifest`/`extract_data_section_blob` -# exist. -# -# Bumped again to 5872240f (digstore v0.19.2, fix #1473) to adopt the HARDENED, -# unforgeable `digstore_chain::singleton::verify_pinned_root`. The rev before that (d5e52fb) -# anchored store identity on the attacker-controllable curried `SingletonStruct.launcher_id`, -# so a forged singleton currying `launcher_id == store_id` (hinted to store_id) could pass as -# chain-verified. 5872240f anchors identity on the UNFORGEABLE launcher coin (`coin_id == -# store_id`) via a bounded backward `parent_coin_info` walk, making the read-root anti-rollback -# check (#747/#841/#852-node) SOUND. -# -# Bumped again to the CURRENT rev, 161c2a3108cb4bb6c8791e96a26588ec99afb029 (digs#59), which is a -# descendant of 5872240f — so the launcher-coin anchoring above still holds — and additionally -# carries digstore-chain onto `chia-wallet-sdk` 0.36. See the SDK note below for why that bump is -# a security fix rather than housekeeping. -# -# Both bumps leave the public signature unchanged (a pure rev bump). Bump all seven together. -# NC-7 exception: dig_ecosystem#901 (the crates.io repoint) leaves the seven `digstore-*` deps below -# git-pinned ON PURPOSE. Three independent facts each block the repoint on their own, and all three are -# upstream of this workspace: -# -# 1. THE SET IS ATOMIC, so `digstore-core` cannot move alone even though it IS published (0.28.0). -# Inside the digstore workspace every sibling declares `digstore-core = { path = "../digstore-core" }`, -# so a git-sourced `digstore-host` always carries the git `digstore-core`. Repointing only the -# published members would put TWO `digstore-core` in the graph — a crates.io one for this crate's -# direct imports and a git one inside its siblings — and `ContentResponse`/`Bytes32` would stop -# unifying across the serve seam. They move together or not at all. -# -# 2. MOST OF THE SET IS UNPUBLISHED, and the closure is wider than it looks. Absent from crates.io: -# `digstore-host`, `digstore-remote`, `digstore-stage`, `digstore-crypto`, `digstore-compiler` — -# plus `digstore-prover`, `digstore-store`, `digstore-chunker` and `digstore-cli`, which the five -# pull in transitively and which no checklist had named. Each declares its siblings as a BARE -# `path = "../x"` with no `version` key, which `cargo publish` refuses outright, so publishing them -# is real work in DIG-Network/digs rather than a release button. -# -# 3. (CLEARED 2026-08-26, dig_ecosystem#3161.) This used to read "the published members are on the -# OTHER chia line", because digstore declared the primitives at 0.36.1 while this workspace was -# held at 0.26 by dig-gossip -> dig-peer-protocol 0.6. Both halves moved: the dig-gossip rev now -# carries 0.36.1, and the digstore rev below is digstore's own ceiling fix. The primitives are -# one line across the boundary, so nothing is split. -# -# The SDK family has now collapsed too (2026-08-26, dig-node#374). This used to read that -# digstore's rev took `chia-wallet-sdk 0.34` against dig-wallet's 0.36.0, and that closing the -# gap needed a digstore release — upstream, not here. That release happened: DIG-Network/digs#59 -# moved `digstore-chain` onto `chia-wallet-sdk 0.36` (and `datalayer-driver` 5, which is what -# required the SDK bump), and the rev below is that commit. -# -# Repointing was a SECURITY fix, not tidiness. `chia-sdk-driver` 0.30 and 0.34 decode the oracle -# fee out of an attacker-authored CREATE_COIN memo as -# `BigInt::from_signed_bytes_be(&memo).to_u64_digits().1[0]`, and an empty or explicit-zero memo -# makes that index an empty vec — a panic reachable from any spend dig-node walks through -# `Datastore::from_spend` (the lineage walk, `verify_pinned_root`, `sync_datastore`). 0.36 guards -# the empty case and uses `try_into`, so both become `DriverError::InvalidMemo`; it also stops -# silently discarding a negative memo's sign and truncating an over-`u64` fee to its low digit. -# -# So this row is still downstream of publishing the digstore workspace. Do NOT repoint these -# piecemeal, and never let the pin regress to a floating git BRANCH: the single `rev` is what makes -# the whole store-format surface reproducible. -digstore-host = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-core = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-remote = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-chain = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-stage = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-crypto = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -digstore-compiler = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } -chia-protocol = "0.36.1" -# The `Streamable` to_bytes/from_bytes trait, used to serialize the chat `ChatMessage` payload before -# the dig-message seal. Same 0.36.1 as chia-protocol so the byte layout agrees across the workspace. -chia-traits = "0.36.1" -# The ecosystem's canonical address-family / IPv6-first primitive (CLAUDE.md §5.2). The advertise -# path keys + orders candidate addresses via `dig_ip::Family` and aggregates them source-tagged + -# de-duplicated via `dig_ip::PeerCandidates` — never a hand-rolled family sort (epic #1020, #1032). -# A leaf crate (no transitive DIG deps), so the crates.io release is used directly. -dig-ip = "0.1.2" -# The canonical dig-app ⇄ dig-node IPC session/signing contract (engine role-half + wire types + -# domain builders + frame bounds). SSOT shared with dig-app so the two halves can never drift (#1080). -# Pinned EXACTLY: 0.3.0 is the first release on this line and the caret range would silently accept -# a future 0.3.x. The crate reaches this one through the IPC surface, so an unreviewed minor is a -# wire-shape change arriving without a PR (dig-node#369 gate). -dig-ipc-protocol = "=0.3.0" -# On-chain DID resolution (WU3, #778): the honest chain-authenticated read path the production -# DidSigningKeyResolver delegates to. -# -# NOW A CRATES.IO DEP (dig_ecosystem#901). It was git-pinned at `tag = "v0.4.0"` because the crate was -# unpublished; it is published now, so the git source is retired and NC-7 is satisfied for this dep. -# -# 0.4.2 IS THE CORRECT TARGET, AND IT IS NOT THE LATEST (0.7.1). Two separate constraints pick it, and -# both are "one line per family" (CLAUDE.md §2.4b), not staleness: -# -# 1. CHIA LINE. This crate re-exports `Bytes32` and `Coin` FROM dig_identity (`seams/dig_peer/ -# session.rs`), so dig-identity's chia-protocol MUST be this workspace's line. Published lines: -# 0.4.1 / 0.4.2 / 0.5.0 -> chia-protocol 0.26, chia-sdk-utils 0.30 <- this workspace -# 0.6.0 / 0.7.0 / 0.7.1 -> chia-protocol 0.36.1, chia-sdk-utils 0.34+ -# Taking 0.7.1 links a SECOND chia-protocol and stops every `Bytes32` crossing the resolver seam -# from unifying — the same split the dig-gossip and dig-constants notes below describe. The 0.6+ -# uplift is downstream of the chia-0.36 cascade (dig_ecosystem#2761), not an edit here. -# -# 2. DEDUPLICATION. A 0.x minor is a semver-INCOMPATIBLE boundary, so this pin must match whatever -# the rest of the graph already resolves or the tree carries TWO dig-identity copies — which for -# an identity/key crate is the whole point of pinning it at all. -# -# 0.7.1 is that value on the chia-0.36.1 ceiling (dig_ecosystem#3161): `dig-nat 0.20` and -# `dig-message 0.7` both declare `dig-identity ^0.7`, and 0.7.1 is the only line whose `chia-bls` is -# 0.36.1. The previous pin here was 0.4.2, chosen by the same rule against the then-current -# `dig-message 0.5` / `dig-nat 0.18`; it is the RULE that is stable, not the number. Re-derive it -# from `Cargo.lock` rather than trusting this comment. -dig-identity = "0.7.1" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "net", "io-util", "time", "signal"] } -# Object-safe async trait for the injectable anchored-root resolver (the -# mandatory read-path chain pin, #127): production walks coinset, tests inject a -# deterministic resolver. -async-trait = "0.1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -# The canonical node<->node JSON-RPC contract: the Method catalogue (wire names + -# per-method tier) and the mTLS peer-reachability allowlist. dig-node-core reads its -# method names + the peer allowlist from HERE (never hand-rolled) so the contract -# cannot drift from the other node implementation or the discovery document (#1075). -dig-rpc-protocol = "0.10.2" -# The directed-message base protocol (epic #793/#796): the e2e seal/open pipeline + the typed envelope -# the chat subsystem seals into. dig-node is the TRANSPORT — it seals an app-supplied opaque DIGCHAT1 -# envelope to the recipient's 0x0010 BLS identity key and dig-gossip directed-sends the sealed bytes. -dig-message = "0.7" -# The chat message-TYPE layer: the five chat payloads on dig-message's dig-chat band + the registry -# wiring. A crypto-free, content-blind type layer — the DIGCHAT1 seal is carried as opaque bytes. -dig-chat-protocol = { path = "../dig-chat-protocol", version = "0.2" } -base64 = "0.22" -hex = "0.4" -# The whole-module pull's one content-addressing primitive (#1576): the module descriptor's per-chunk + -# whole-blob hashes on the SERVE side, and the pre-announce re-hash that proves the artifact about to be -# cached + announced is byte-identical to the one the anchor gate admitted. Production, not test-only — -# this hash is what stands between a promoted artifact and a promoted-but-unverified one. -sha2 = "0.10" -filetime = "0.2" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -fs4 = { version = "1.1.0", features = ["sync"] } -directories = "6.0.0" -# OS CSPRNG for the per-stage content-authoring BLS key (#95 Pass C dig.stage). -getrandom = "0.2" -# -- dig_ecosystem#2168: the node MACHINE key at rest -------------------------------------- -# The canonical DIG keystore. Consumed with `custody` OFF (it is off by default) because this -# crate seals only the node's OWN machine identity seed and must never be able to name the -# user-custody API -- the #908 boundary expressed in the dependency graph rather than in prose -# (dig-keystore SPEC.md 18.2, which names this engine as the intended opaque-only consumer). -# 0.10 rather than 0.9: 0.10 is a BREAKING release that moves the crate's re-exported chia-bls -# onto the ecosystem 0.36.1 line. That line is ALREADY in this lock (six chia-bls lines resolve -# here today), and the `opaque` surface this crate uses is bytes + Password with no BLS type on -# it at all, so nothing crosses between lines -- and adopting 0.10 now avoids routing the node's -# identity onto a type identity that was about to change under it. -# -# 0.13 is the FLOOR for taking hardware binding live, and the reason is not the providers. -# dig-keystore#18 made `create_with_rng` an atomic check-then-write in 0.13.0; 0.12 will happily -# overwrite an existing record. On a SOFTWARE tier an overwritten machine key is recoverable from -# a backup of two plaintext-sealed halves. Hardware wrapping binds the wrapping key to a -# non-exportable component, so the same overwrite becomes PERMANENT -- there is nothing to restore -# from. Sealing therefore upgrades that bug from recoverable to unrecoverable, and 0.12 is not a -# version this seam may be enabled on. -dig-keystore = "0.13" -# The platform trusted-component providers (Windows TPM 2.0 via CNG, Apple Secure Enclave, Linux -# TPM 2.0). A SEPARATE crate because `dig-keystore` forbids `unsafe` and every one of these is FFI; -# it is the workspace member dig_ecosystem#1693 asked for, and it landed in dig-keystore v0.12.0. -# Without it the node's hardware seam -- already fully composed at `machine_key.rs` -- passes a -# hardcoded `None` and can never report anything but `Software(NotRequested)`. -# 0.2 pairs with dig-keystore 0.13: it adds the macOS Secure Enclave and Linux TPM 2.0 providers, -# and reclassifies a TPM AUTHORIZATION refusal (owner password set, dictionary-attack lockout) as -# `Absent` rather than `Indeterminate`, so an ordinary locked-down Windows box degrades instead of -# refusing. Only a genuinely uninspectable host is indeterminate now. -dig-keystore-hardware = "0.2" -# The machine key lives in the SAME user-global identity dir digstore_remote used for the -# plaintext seed (`/dig`), so the #2168 migration finds it in place and the node -# keeps its peer_id. digstore keeps its own `identity_dir` private, so the path is reproduced. -dirs = "5" -# Keeps the unsealed seed off the heap after use, matching what dig-keystore hands back. -zeroize = "1" - -# -- L7 DIG Node peer network (PHASE-2b, #162) ------------------------------------------------------- -# The NAT-traversal + connected-peer-pool layer. The bespoke in-node relay client (formerly relay.rs) -# is RETIRED: the relay connection now lives inside dig-nat (the `connect()` ladder's last-resort tier -# + the persistent reservation) and dig-gossip (the introducer-backed connected peer pool). dig-node -# consumes both to auto-discover peers via relay.dig.net, maintain a pool, and serve/issue the L7 peer -# RPC over mTLS (peer_id = SHA256(TLS SPKI DER)). -# -# Sourcing (#1280/#1393/#1494 crates.io cascade): dig-nat 0.11 is on crates.io — the whole peer stack -# (dig-gossip 0.15.0, dig-dht 0.5.1, dig-download 0.7.1, dig-peer-selector 0.5.2) now depends on dig-nat -# ">=0.10, <0.12" from crates.io, so consuming it here as a plain crates.io version unifies the ENTIRE -# graph on ONE dig-nat 0.11 instance (the old bare-git unification patch is retired). 0.11 adds the -# relayed RESPONDER path — `RelayStatus::enable_accept` surfaces an introduced circuit as a -# `RelayTunnel`, and `RelayAcceptor` runs the mTLS SERVER half over it (#1536, Leg B) — on top of -# 0.10's SPKI-pinned dialer (#1422) and the STUN happy-eyeballs reflexive discovery, and auto-composes -# the FULL NAT ladder via `connect_with_runtime` + `NatRuntime`, taking a CA-signed NodeCert. -# 0.11.2 is REQUIRED (#1586): before it, `RangeFrame::bytes` was read with `serde_bytes`, which over -# the JSON frame wire takes the served base64 string as its literal characters — so every -# `dig.fetchRange` window arrived as its own base64 text, the reassembler rejected the frame, and the -# read leg aborted before reading a byte. 0.11.2 reads/writes `bytes` as base64 (the canonical -# `dig_rpc_protocol::types::RangeFrame` wire the node's own serve path emits). -# dig-nat 0.13 is the release that introduced the FALLIBLE, payload-capped `RangeFrame::encode` plus -# the paged-prologue sender API (`with_identity` carrying `chunk_count`, `with_chunk_lens_page` / -# `chunk_lens_offset`, `with_chunk_index`, `RangeRequest::skip_layout`). Everything 0.14 adds is the -# RECEIVER (`ChunkLensAssembler`) plus the `split_chunk_lens_pages` helper, so 0.13 carries the whole -# serve-side fix for #1640/#1668. -# -# The dig-nat MAJOR must match the whole peer stack's, and it moves as ONE step (#1771). This crate -# builds its own `dig_nat` `NodeCert` / `NatConfig` / `NatRuntime` / `RelayStatus` / `TraversalKind` -# values and passes them INTO dig-download, dig-gossip and dig-peer-selector, so a second dig-nat -# instance does not merely bloat the tree -- those calls stop typechecking (verified: E0308 `expected -# dig_nat::relay::RelayStatus, found dig_nat::RelayStatus`). So dig-nat cannot be bumped alone, and -# dig-gossip/dig-dht/dig-download/dig-peer/dig-peer-selector cannot be bumped past it: the dig-nat -# 0.18 line (dig-dht 0.11, dig-download 0.19, dig-peer 0.11, dig-peer-selector 0.9, dig-gossip on the -# pinned rev below) is a single atomic cascade, every member of which is published. -# `tests/dependency_tree.rs` asserts the single-instance invariant against the resolved LOCK so a -# partial cascade cannot quietly reintroduce the duplicate. -# -# This line carries the SafeText peer-text cascade (#1674/#1675): error text of remote origin is -# held as `dig_nat::SafeText` rather than `String`, so a hostile peer's control characters cannot forge -# a line in an operator's log. On a partial bump the failure is now LOUDER than the RelayStatus mismatch -# above -- `SafeText` crosses dig-dht's AND dig-peer's public error surfaces, so two dig-nat versions is -# an outright compile error on those seams rather than merely two mTLS stacks. See -# `seams/dig_peer/dht.rs::stage_failed` for this crate's side of the adoption. -dig-nat = "0.21" -# dig-gossip is the ONE peer-stack exception: not yet published to crates.io (its publish is guarded -# pending dig-peer-protocol, #681), so it stays a git dependency PINNED to a release commit — here -# v0.17.12 (rev 3e352d91). -# -# v0.17.12 is REQUIRED, not merely current (#1771): it carries all THREE duplicate-connection fixes, -# and until it landed here the node's pool refused connections that would have worked. Every pool-add -# path now supersedes a stale slot for an already-authenticated `peer_id` instead of refusing the newer -# session — inbound (#1691), `connect_to` (#1703), and `adopt_nat_connection` (#1762), the last being -# the path EVERY dig-nat connection is adopted through, where a dead relay circuit's leftover slot -# refused the direct dial while the peer reported zero connections. `DuplicateConnection` still exists -# as an error variant but is no longer raised on those paths. Consequence for this crate: `PeerAdded` is -# REPUBLISHED on a supersede, so every consumer of it must be keyed by `peer_id` and idempotent (the DHT -# routing feed, the selector registry, and the download-side connected pool all are — see -# `download.rs::on_pool_event`), and `GossipStats::total_connections` is a LIFETIME counter that a -# supersede increments, so it must never be read as a count of unique peers (this crate reads -# `connected_peers` / `pool_stats().connected`, which are the live keyed-map sizes). -# -# Also on 0.17: the outbound /16+AS diversity caps (#1710/#1716) apply to the adoption path, so a -# test fixture adopting two peers must place them in DIFFERENT /16 groups. -# Pulled rustls-only (no native-tls) -- note `--all-features` is NOT a runnable configuration for -# dig-gossip: with both `ring` and `aws-lc-rs` reachable rustls cannot pick a provider. Its -# dig-nat/dig-tls/dig-constants come from crates.io (same versions as here), so the graph still -# unifies to ONE of each. -# -# There are no vendored chia-protocol/chia-sdk-client forks to patch any more: dig-gossip 0.23.0 -# deleted both (dig_ecosystem#2228) and the workspace `[patch.crates-io]` section is RETIRED (see the -# root manifest). The #1529 three-rev lockstep is retired with it. An earlier version of this comment -# still described those patches as live; it was wrong, and a false manifest comment is how the next -# lane reasons wrongly about the version line. -# -# THIS DEP WAS THE CHIA-0.36 MIGRATION BLOCKER, AND IS NO LONGER (dig_ecosystem#3161, 2026-08-26). -# -# The old block, recorded because a future lane will otherwise re-derive it: the pinned rev was -# v0.25.0, which took `dig-peer-protocol = "0.6"`, whose only published 0.6.x declares -# `chia-protocol 0.26`. That put 0.26 into the graph through a path no dig-node manifest could edit, -# so no assignment of versions HERE could remove it. -# -# What cleared it was upstream moving, exactly as that comment predicted: dig-gossip `main` (v0.30.0, -# the rev pinned below) is on `dig-peer-protocol 0.7` and declares chia-protocol/traits/sha2/bls at -# 0.36.1. Repointing the rev is what let this workspace take the ceiling; it was never a local edit. -# -# The consequence that mattered is now discharged rather than merely avoided: `Bytes32` reaches the -# store-melt authorization path in `seams/dig_peer/store_melted.rs` from dig_gossip and from -# `digstore-chain` as the SAME type, so that path is no longer split across two chia-protocol -# versions. No conversion shim was added, and none may be — bridging two lines is the byte-drift -# class this migration exists to remove. -# -# THE dig-nat WALL IS CLEARED (dig-node#412 step 7). The rev pinned below is dig-gossip v0.32.0, -# which declares `dig-nat = "0.21"`, so the peer stack takes its ^0.21 tier with ONE dig-nat in the -# graph. The prediction the old note made -- "the fix is a dig-gossip release against `dig-nat -# ^0.21`, upstream, never a version edit here" -- is what happened; measured from the resolved LOCK -# rather than from the caret, because a caret-compatible transitive is invisible to a plain -# re-resolve (CLAUDE.md §2.4b). -# NC-7 exception: dig-gossip is git-pinned PERMANENTLY, not pending a publish — dig_ecosystem#2647. -# It cannot be published while its `native-tls` [patch.crates-io] fork stands, because `cargo publish` -# STRIPS [patch.crates-io] from the published metadata. A published dig-gossip would therefore build -# against UPSTREAM native-tls, compile cleanly, and silently accept inbound peers presenting no client -# certificate at all — a mutual-TLS bypass that no consumer could see from its own manifest. Its -# publish.yml guard is unconditional and deliberate: do not weaken it, do not `--allow-dirty` past it. -# dig_ecosystem#900 ("publish dig-gossip") was closed as superseded by #2647. -dig-gossip = { git = "https://github.com/DIG-Network/dig-gossip", rev = "1a3391662ecce1a3cbe8b74122a52bbb1b28d3ee", default-features = false, features = ["rustls", "relay"] } -# The canonical DIG network id (genesis challenge) for the gossip handshake / introducer scope. -# -# 0.9 is the TIP OF DIG-NODE'S CHIA LINE, and that — not 0.10 — is the correct target here (#2072). -# dig-constants 0.10.0 moved to chia-protocol 0.36.1 / chia-wallet-sdk 0.34, while this workspace -# builds against 0.26 / 0.30, which dig-gossip pins there via dig-peer-protocol 0.6 (see the -# dig-gossip entry above). Depending on 0.10 links a SECOND chia_protocol and every `Bytes32` this -# crate reads off `DIG_MAINNET` stops unifying with the rest of the workspace. Being current on -# dig-constants is therefore downstream of the chia 0.36 migration, not a version-string edit. -# -# Values are identical from 0.4.0 through 0.10.0 — the L2 genesis challenge and its six AGG_SIG -# domains were finalized in 0.4.0 and have not moved since — so this bump is value-neutral and buys -# the additive surface (DIG_ASSET_ID, the treasury hash/address, `dig.local`, `rpc.dig.net`). -# -# The graph still carries 0.4.0/0.5.1/0.8.0 because upstream crates hold them down through their -# PUBLISHED ranges (dig-gossip `>=0.2, <0.5`, dig-nat `>=0.4, <0.6`, dig-download `^0.8`), which a -# consumer cannot edit. Collapsing to a single copy needs each of those to release against 0.9 — -# the release-first cascade on #2072. Do NOT pin this BACK to widen a window: the fix is upstream -# moving forward, never dig-node drifting backward. -dig-constants = "0.13.0" -# -- Portable profile body format (DPB) — epic #3008 W6 ----------------------------------------------- -# -# `default-features = false` deliberately: the default `chia` feature pulls dig-did/dig-store/ -# chia-protocol 0.36.1, which is a DIFFERENT chia line from the 0.26 this workspace builds against, so -# the default surface would put a SECOND chia-protocol into the always-running service. Without the -# feature the crate is a pure BYTE CORE (`dig-smt + sha2 + thiserror`) exposing exactly what the node -# needs: the DPB container, `AnchoredRoot` and `VerifiedBody`. -# -# Consequence to keep in mind: `dig_social_profile::resolve` is NOT available here. The node resolves -# a store's on-chain root through its OWN `AnchoredRootResolver` (the coinset lineage walk), which is -# the same authority, reached by the node's existing seam. -dig-social-profile = { version = "0.7", default-features = false } -# -- L7 content-location DHT (PHASE-B, #163) --------------------------------------------------------- -# The Kademlia DHT that maps DIG content (store / capsule / root / resource) to the peer_ids holding -# it. The node LOCATES content it lacks via `find_providers`, and keeps its OWN held-inventory provider -# records CURRENT in the DHT (`announce_provider` / `withdraw_provider` / `republish`). It rides the -# SAME dig-nat mTLS transport as the rest of the peer network via a `DhtTransport` adapter (`dht.rs`). -# -# Sourcing: the crates.io release (NC-7). dig-download and dig-peer-selector below resolve dig-dht -# from crates.io too, so cargo unifies to ONE dig-dht instance — its `PeerId` / `Contact` / -# `DhtTransport` are then the exact types `dig_nat::connect` returns, rather than look-alikes. -# -# NOT bumped to 0.15, and this is the tier's REMAINING blocker rather than an oversight: dig-download -# 0.21.0 and dig-peer-selector 0.10.0 — the LATEST published of each — both require `dig-dht ^0.13`, -# read from the crates.io index. Declaring "0.15" resolves TWO dig-dht lines while cargo prints -# success, and dig-dht values cross from this crate into both consumers. dig-dht 0.15 is where -# `ProviderRecord::unverified_mirror_coin_id` lives (dig-dht#23), so the §25 mirror-coin pointer is -# unreachable here until dig-download AND dig-peer-selector release against `dig-dht ^0.15` — -# upstream, never a version edit here. Bridging two lines with a shim is the §4.1 byte-drift class. -dig-dht = "0.13" -# -- L7 Peer Exchange (PEX, #166) -------------------------------------------------------------------- -# The node<->node peer-sharing protocol: over each established mTLS peer connection, a dedicated PEX -# logical stream exchanges delta-based first-hand known-peer sets (handshake -> snapshot -> deltas), -# feeding discovered peers into the pool as dial candidates (hints, verified by dialing) and formalizing -# what the ad-hoc `dig.getPeers` polling did. dig-pex ships a transport-agnostic sans-IO `PexEngine` -# this crate embeds; `pex.rs` is the dig-nat mux I/O adapter (SPEC §10.1, Appendix A). -# -# Sourcing: bare git form (no `rev`, like dig-nat/dig-dht) so cargo unifies any shared transitive deps -# to ONE copy across the graph; resolved to the tracked rev via the lockfile. dig-pex keeps a minimal -# dependency surface (serde/serde_json/tokio/rand — it mirrors the address shape rather than importing -# the transport crates), so there is no cross-crate type-unification concern here. -dig-pex = "0.1.1" -# -- Multi-source content fetch (#164) + redirect-on-miss (#165) -------------------------------------- -# The node-side download orchestrator: locate holders via dig-dht `find_providers`, confirm with -# `dig.getAvailability`, fan byte ranges across MULTIPLE providers simultaneously (`dig.fetchRange` -# over dig-nat), verify each range + the whole resource against the chain-anchored root, and -# reassemble — with pause/resume + `.download.tmp` GC. dig-node wires the real seams per the crate's -# implementers' note (see `download.rs`): DhtProviderLocator over our DhtService, NatRangeTransport -# over our mTLS identity, MerkleVerifier bound to the digstore proof format, FileSink/FileStateStore -# under the node's cache dir. -# -# Sourcing: the crates.io release (NC-7), matching dig-dht above. dig-download 0.6 deps dig-dht "^0.5" + -# dig-nat "^0.10" + dig-tls "^0.3" from crates.io, so cargo unifies its dig-dht + dig-nat + dig-tls with -# ours — ONE crate instance each across the graph (its re-exported `ContentId`/`ProviderRecord` ARE our -# dig-dht types). 0.6 keeps the injectable `SourceSelector` seam (dig-node implements it over -# dig-peer-selector, see `seams/dig_peer/selector_adapter.rs`), records range outcomes internally, and the -# fetch leg composes the FULL NAT ladder from an injected shared `NatRuntime` (`NatRangeTransport::new_with_runtime`). -# -# 0.8.1 adds the `ModuleDownloader` — the whole-`.dig`-module peer pull that turns a node which READ a -# resource into a complete resharer of the capsule (#1576, the reshare leg of the MVP flywheel). Its two -# seams are supplied here: `seams/dig_peer/module_transport.rs` (`NatModuleTransport`) and -# `seams/dig_peer/module_anchor.rs` (`ChainAnchoredModuleVerifier`). NOTE the `testkit` feature is -# deliberately NOT enabled: it is what makes the fail-OPEN `AcceptAnyModuleAnchor` nameable, and this -# crate's anchor gate is the reshare path's ONLY root of trust. -# -# HELD on the 0.19 line, resolving 0.19.1 (dig_ecosystem#3128). 0.19.1 is a deliberate BACKPORT of the -# capsule-checkpoint filename fix (dig-download#38): `FileStateStore` hex-encoded the download key into -# the filename, so a module key `module:<64hex>:<64hex>` (136 bytes) became a 277-character name and -# EVERY capsule checkpoint write on Linux failed with `File name too long (os error 36)`. The same fix -# also shipped as 0.20.1, but 0.20 requires dig-dht ^0.12 / dig-nat ^0.20 / dig-peer ^0.12 / -# dig-tls ^0.4 — the whole dig_ecosystem#3152 cascade — which this crate cannot take yet, so the -# backport keeps the 0.19-line pins intact. Regression-tested here through the node's OWN production -# checkpoint store (`download.rs::capturing_state_store_checkpoints_a_real_module_download_key`), -# because dig-download's own suite missed it: every `module.rs` test used `InMemoryStateStore` (no -# filename at all) and the one `FileStateStore` test used a 3-character key. -dig-download = "0.21" -# -- The shared peer client (#1283/#1576) ------------------------------------------------------------- -# `DigPeer` — the ONE DIG Network peer client: peer_id-pinned mTLS over the full NAT ladder plus typed -# RPC. Depended on DIRECTLY (not only transitively through dig-download) because dig-node supplies the -# `ModuleTransport` half of dig-download's module pull, which speaks `dig.getModuleInfo` / -# `dig.fetchModuleRange` — the two client methods dig-peer 0.5 added for exactly this leg. -# -# 0.5 is REQUIRED, not merely current: dig-peer 0.4 pulled dig-rpc-protocol 0.3, so a tree containing -# both it and dig-download's dig-rpc-protocol 0.5 carried TWO `ModuleInfo` types either side of the -# module pull's trust boundary — on the fields that drive the whole pull plan. dig-download 0.8.1 is on -# dig-peer 0.5 too, so exactly ONE dig-rpc-protocol + ONE dig-peer resolve here (asserted by -# `crates/dig-node-core/tests/dependency_tree.rs`). -dig-peer = "0.13" -# -- Self-optimizing peer selection (#178) ------------------------------------------------------------ -# The decision + learning layer between dig-dht discovery and dig-download execution: it ranks the -# providers `find_providers` returns (learning throughput/rtt/reliability + a per-class saturation -# point + an adaptive relayed penalty from the REAL measured outcome of every range) and hands -# dig-download the ranked subset + per-peer concurrency, instead of dig-download picking sources -# blindly. Every completed/failed range streams back via `record_outcome` in real time, and a dropped -# source drives a `rebalance`. dig-node bridges dig-download's `SourceSelector` seam to this crate via -# `seams/dig_peer/selector_adapter.rs` (the @30↔@30 composition-root bridge — dig-download does NOT -# depend on dig-peer-selector; dig-node, one level up, wires them together). -# -# Sourcing: the crates.io release (NC-7), matching dig-dht/dig-download above. It deps dig-dht "^0.5" + -# dig-nat "^0.10" from crates.io, so cargo unifies the selector's `dig-nat` + `dig-dht` with ours — ONE -# crate instance each (its re-used `PeerId`/`ContentId`/`ProviderRecord`/`CandidateAddr` ARE our -# dig-nat/dig-dht types). The selector mirrors `dig_gossip::PoolEvent` LOCALLY (it does not depend on -# dig-gossip, which lags as a git dep), so the node maps `dig_gossip::PoolEvent` → the selector's local -# `PoolEvent` 1:1 (see `download.rs::pool_event_to_selector`). -# 0.9.2 IS the chia-0.36 cascade landing (dig_ecosystem#3161). The old note here said "NOT bumped to -# 0.9.1: that release moves onto dig-tls 0.4, and this crate still resolves 0.3 through -# dig-nat/dig-gossip, so the graph would carry TWO `dig_tls::PeerId` types that do not unify". Both -# halves of that split are gone — dig-nat 0.20 and the dig-gossip rev both take `dig-tls ^0.4`, and -# the lock now resolves dig-tls ONCE at 0.4.0. -# -# 0.10 IS takeable as of dig-gossip v0.32.0, which declares `dig-nat = "0.21"` — the release the old -# note here was waiting on. It carries `dig-dht ^0.13`, which is what holds dig-dht at 0.13; see the -# dig-dht entry above. -dig-peer-selector = "0.10" -# The canonical DIG mTLS certificate crate (L00, crates.io). The node's PERSISTENT machine identity -# is a CA-signed `dig_tls::NodeCert` minted from the node's own BLS identity key and persisted 0600 in -# the data dir (#908 identity boundary: this is the MACHINE key, never a user key). Replaces the -# legacy self-signed `rcgen` cert as the node dials/serves over the dig-nat CA-signed mTLS model -# (#1280). `peer_id = SHA256(SPKI DER)` is stable across restarts. dig-tls re-exports chia-bls's -# `SecretKey`, unifying with this crate's chia-bls 0.36.1. 0.4 is the chia-bls 0.36.1 uplift and is -# now TAKEN (dig_ecosystem#3161): the old hold — "taking it here would split `dig_tls::PeerId` -# against the 0.3 that dig-nat/dig-gossip still resolve" — expired when dig-nat 0.20 and the -# dig-gossip rev both moved onto `dig-tls ^0.4`. The lock resolves dig-tls ONCE, at 0.4.0. -dig-tls = "0.4" -# -- The store-exchange DECISION layer (dig_ecosystem#3130) ------------------------------------------- -# `dig-sex` owns every cache/tier/relevance/selection DECISION this node used to make in-tree: what to -# keep, what to sacrifice first, and whether a fresh candidate is worth displacing an incumbent. It is -# PURE — no clock, no socket, no filesystem — so this crate keeps owning all of the I/O and hands it -# only the facts. The in-tree `relevance` module and `tier0_prefetch::effective_tier` moved there -# verbatim and are now consumed as `dig_sex::*`. -# -# It also re-exports the ONE `CapsuleIdentity` the ecosystem speaks, so this crate never declares its -# own and never pulls a second major of `dig-store` into the graph. -dig-sex = "0.5" -# The cache/eviction SEAM `dig_sex::TieredPolicy` implements. Declared DIRECTLY rather than reached -# through `dig-sex`, which re-exports `CapsuleIdentity` alone — naming `EvictionEntry`, -# `EvictionContext` and the `EvictionPolicy` trait (whose `select_evictions` must be in scope to call) -# requires the crate itself. The version is pinned to the one `dig-sex` resolves, so the `CapsuleIdentity` -# this crate builds and the one `TieredPolicy` matches on are the SAME type; a split would silently -# fail to unify. `dig_sex::TieredPolicy` is the first non-LRU implementation of the seam, and the -# module sweep here is its first consumer. -dig-store-cache = "0.1.1" -# The mTLS peer-RPC LISTENER (server side): present the node cert + REQUIRE a client cert (peer_id -# enforced). rustls/tokio-rustls with ring only (no OpenSSL/aws-lc for OUR usage), matching dig-nat. -rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } -# IPv6-first, IPv4-fallback (ecosystem HARD RULE): bind the peer-RPC listener on the IPv6 unspecified -# address `[::]` as a DUAL-STACK socket (IPV6_V6ONLY cleared) so ONE socket serves both native IPv6 -# and IPv4-mapped peers. Mirrors dig-relay's `net.rs` / dig-gossip's own dual-stack bind. -socket2 = "0.5" -# -- Engine-side identity session (NODE-1, #910) ----------------------------------------------------- -# The engine is IDENTITY-AGNOSTIC: it holds NO user signing key. `control.session.attach` proves the -# attaching dig-app holds the profile's slot-0x0010 identity key by VERIFYING an Ed25519 signature over -# the byte-identical `DIGNET-SESSION-v1 ‖ nonce ‖ profile_did` challenge (session.rs). Ed25519 -# verification rides `ring` — ALREADY in the locked graph (the rustls/tokio-rustls transport uses it), -# so it adds no new crate. VERIFY-ONLY: the engine never holds a signing key. (dig-app signs with -# ed25519-dalek; ring verifies RFC-8032 signatures + is strict against malleable/small-order inputs.) -ring = "0.17" -tracing = "0.1" -# Best-effort CONCURRENT fan-out of the multi-source provider union (`UnionLocator`, #1443): query -# dig-dht + PEX + relay-introducer sources at once via `future::join_all` (order-preserving, so the -# dedup keeps DHT-first precedence). rustls/tokio already pull futures transitively; declaring it here -# makes it a first-class prod dep of the union seam. Minimal feature set (no executor). -futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } - -[dev-dependencies] -tempfile = "3" -# `dig_keystore::hardware::double::FakeDevice` -- the only way to exercise a SPECIFIC hardware -# outcome from a test. The real providers ship now (dig-keystore-hardware above), but which one a -# host answers with is a property of that host: no CI runner can be made to produce an -# indeterminate TPM probe on demand, and that is the outcome the refusal path turns on. -# Test-only: `testing` is never enabled for the library build. -dig-keystore = { version = "0.13", features = ["testing"] } -# Same git rev as the normal dependency above, with dig-gossip's `test-util` feature added. That -# feature gates `__connect_stub_peer_with_direction`, which registers a connected peer without real -# TLS — the only way to assert what a broadcast ACTUALLY put on the wire from a test. Declared here -# rather than on the normal dependency so the shipped binary never carries the test hook: cargo -# unifies the feature into test builds only, since dev-dependencies are not built for `cargo build`. -# NC-7 exception: same permanent git pin as the runtime dep above (dig_ecosystem#2647). -dig-gossip = { git = "https://github.com/DIG-Network/dig-gossip", rev = "1a3391662ecce1a3cbe8b74122a52bbb1b28d3ee", default-features = false, features = ["rustls", "relay", "test-util"] } -# Generates the P-256 leaf key pairs the opcode-222 holdings tests sign with: `public_key_der()` IS the -# leaf `SubjectPublicKeyInfo` DER the wire carries as `provider_spki`, and whose SHA-256 is the -# announcing peer_id — so a test signer is a real §5.2 identity rather than a stand-in that could hide -# an attribution bug (#1429). -rcgen = "0.13" -# dig-download's in-memory harness (`MockRangeTransport`, `MockContent`, …), which the download-path -# tests drive the real orchestrator over. -# -# It is a DEV dependency and MUST STAY ONE (#1576). The `testkit` feature is also what makes -# `AcceptAnyModuleAnchor` — a fail-OPEN module anchor verifier — nameable at all; the crate compiles it -# out of a default consumer build precisely so a production wiring cannot reach it. The reshare path's -# anchor gate is its ONLY root of trust, so enabling this feature on the PRODUCTION `dig-download` entry -# above would put a bypass of that root of trust one `use` away. Dev-dependency features do not -# propagate to a consumer's build, so the node binaries never see it. -# -# Pinned by the `the_fail_open_anchor_verifier_is_not_reachable_from_a_production_build` test, which -# fails if `testkit` ever appears on the production entry. -dig-download = { version = "0.21", features = ["testkit"] } -# Captures the peer-facing serve's real emitted tracing records into an in-memory buffer, so the -# serve-observability tests (#1595) assert what an operator would actually see in the node log — -# and that no payload byte or proof ever reaches it. -tracing-subscriber = { version = "0.3", features = ["fmt"] } -# Builds canonical `did:chia:` strings for the on-chain DID-resolver tests (matches dig-identity's codec). -chia-sdk-utils = "0.36.0" -# The session attach tests sign challenges with the BLS12-381 G2 AugScheme (the app's slot-0x0010 -# identity key), matching dig-ipc-protocol's signing contract and dig-identity's published G1 key. -chia-bls = "0.36.1" -# axum is a TEST-ONLY dependency of the engine library: the node no longer serves its -# own HTTP (the `dig-node-service` shell owns the axum transport, `dig-runtime` owns the -# in-process FFI path — dispatch is `handle_rpc`, which is transport-agnostic). The -# integration/unit tests still spawn small in-process axum mock servers (the §21 remote -# + a module server) to exercise the fetch/cache path hermetically. -axum = "0.7" -# In-process loopback mTLS peer drives the peer-network tests with NO real network (the CA-signed -# `dig_tls::NodeCert` minted by `load_or_generate_node_cert` is the test client's mTLS identity). -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "net", "io-util", "time", "test-util"] } -futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } -# The peer_network.rs integration test implements PeerRpcResponder (an async trait). -async-trait = "0.1" +[package] +name = "dig-node-core" +# The engine library's OWN version, independent of the released binary's (`[workspace.package].version` +# in the root manifest, which only dig-node-service tracks). It moves when this crate's PUBLIC surface +# moves: 0.24.0 acknowledges that the surface now exposes dig-nat 0.14 / dig-dht 0.8 types, a +# compatibility-relevant change even though the crate is consumed in-workspace and is not published to +# crates.io. Holding it at 0.23.0 would have overstated stability to anyone reading the manifest for +# what the exported types are (dig_ecosystem#1785c). 0.26.0 does the same for the 0.15 line: the +# exported dig-nat / dig-dht types rotate identity again, and `DhtError` — reachable through this +# crate's dig-dht re-exports — now carries `SafeText` instead of `String` (dig_ecosystem#1674/#1675). +# 0.27.0 adds the pure `relevance` module (CacheTier / relevance() / evict_key / should_displace) — +# a new, backwards-compatible public export (dig_ecosystem#1986), hence a MINOR bump. +# 0.28.0 adds the pure `tier0_selector` module (Candidate / select_within_budget / tier0_budget_bytes / +# should_displace_tier0) built on 0.27's relevance module — a new, backwards-compatible public export +# (dig_ecosystem#1988), hence another MINOR bump. +# 0.31.0 adds cache observability to `cache.stats` (§7.10e) — `refetch_count` + per-tier `tiers` +# occupancy fields, and makes `InboundDemand::entry_count` a real (no-longer-`#[cfg(test)]`) public +# API — a new, backwards-compatible surface (dig_ecosystem#1991), hence a MINOR bump. +# 0.47.0 adopts dig-gossip 0.25.0 and splits the announce seams by ORIGIN: `ProfileTransport` gains a +# required `announce_root_local` and `MeltBroadcast` a required `broadcast_local`, so a locally +# originated announce takes dig-gossip's dedup-exempt path and a re-announce of an unchanged root is +# no longer suppressed forever (dig_ecosystem#3061). Adding a required trait method is BREAKING for an +# out-of-workspace implementor; this crate is consumed in-workspace only and is pre-1.0, so it is a +# MINOR bump under SemVer's 0.x rule -- recorded here rather than letting the number imply the trait +# surface held still. +# 0.53.0 adds the module-granularity capsule relay leg (`seams/dig_peer/module_relay.rs`, gated OFF +# by default) and makes the provider/candidate locator readers absence-safe: `NodeContent::find_providers` +# and `NatModuleTransport::discovered_candidates` now return `BestEffort` rather than a bare `Vec`, +# so a source that could not be consulted can no longer be read as a proven absence (dig_ecosystem#3128, +# dig-node#276/#296). Changing a public return type is BREAKING for an out-of-workspace implementor; +# this crate is consumed in-workspace only and is pre-1.0, so it is a MINOR bump under SemVer's 0.x +# rule -- recorded here rather than letting the number imply the locator surface held still. +version = "0.62.0" +edition = "2021" +license = "GPL-2.0-only" +description = "The canonical DIG node ENGINE library (crate `dig_node_core`): the JSON-RPC dispatch (`handle_rpc`, the same contract as rpc.dig.net), local-first content serve/fetch/redirect from LOCAL .dig store modules (via digstore_host::serve_blind), chain-anchored-root resolution, chain-watch + subscriptions + generation gap-fill, the LRU cache, and the full P2P stack. Shared UNCHANGED by both host shells: the `dig-node` OS-service binary (dig-node-service) and the DIG Browser's in-process cdylib (dig-runtime). Native Rust so the compiled-module serve path works." + +[lib] +name = "dig_node_core" +path = "src/lib.rs" + +[dependencies] +# Bounds the whole-module descriptor memo (`seams/dig_peer/module_serve.rs`, #1615/G2): a per-process +# cache of `dig.getModuleInfo` answers keyed by (store, root), capped so a long-running node cannot +# accumulate one entry per module it has EVER described (each entry is tens of KB — up to 512 +# chunk_hashes + chunk_lens). Already a transitive dependency (pulled in via the workspace's other +# crates); promoted to a direct one now that this crate uses it itself. +lru = "0.12" +# The `.dig` STORE-FORMAT LIBRARY crates. These live in the digstore repo (the store +# CLI + format/read libraries) and are consumed here as GIT dependencies: the node +# depends on the store libs, NEVER the reverse (digstore is only ever an RPC client of +# a node). Pinned to a single digstore rev so the whole store-format surface the node +# reads/verifies/decrypts against is one coherent version. Bump all seven together. +# +# digstore-host — instantiate a compiled module + `serve_blind` (ciphertext + proof). +# digstore-core — wire types (ContentResponse), codec, Bytes32/48, the PublicManifest +# type + `datasection::read_public_manifest` (#176 Phase C). +# digstore-remote — the §21 remote client + persistent identity signer (§21.9). +# digstore-chain — CHIP-0035 singleton lineage walk (chain-anchored root). +# digstore-stage — the shared stage→compile engine for the `dig.stage` RPC (turn a +# folder into a capsule in-process, #95 Pass C). Its build script +# embeds the guest wasm (BINDING contract D6) via the DIGSTORE_GUEST_WASM +# override this repo sets in .cargo/config.toml (see vendor/). +# digstore-crypto — read-path crypto primitives. +# digstore-compiler — `extract_data_section_blob` (#176 Phase C): pulls the embedded DIGS +# data-section blob out of a compiled `.dig` module's wasm binary so +# `dig.getManifest` can read section 13 (the PublicManifest) WITHOUT a +# wasmtime instantiation (the manifest is PUBLIC, unencrypted data — no +# `serve_blind` decrypt needed). Already a transitive dependency via +# digstore-stage; pinned directly at the SAME rev so its public API is +# usable here too. +# +# Bumped to a rev past digstore's "Add normalized public manifest to the .dig format" +# commit (08f5d39) so `PublicManifest`/`read_public_manifest`/`extract_data_section_blob` +# exist. +# +# Bumped again to 5872240f (digstore v0.19.2, fix #1473) to adopt the HARDENED, +# unforgeable `digstore_chain::singleton::verify_pinned_root`. The rev before that (d5e52fb) +# anchored store identity on the attacker-controllable curried `SingletonStruct.launcher_id`, +# so a forged singleton currying `launcher_id == store_id` (hinted to store_id) could pass as +# chain-verified. 5872240f anchors identity on the UNFORGEABLE launcher coin (`coin_id == +# store_id`) via a bounded backward `parent_coin_info` walk, making the read-root anti-rollback +# check (#747/#841/#852-node) SOUND. +# +# Bumped again to the CURRENT rev, 161c2a3108cb4bb6c8791e96a26588ec99afb029 (digs#59), which is a +# descendant of 5872240f — so the launcher-coin anchoring above still holds — and additionally +# carries digstore-chain onto `chia-wallet-sdk` 0.36. See the SDK note below for why that bump is +# a security fix rather than housekeeping. +# +# Both bumps leave the public signature unchanged (a pure rev bump). Bump all seven together. +# NC-7 exception: dig_ecosystem#901 (the crates.io repoint) leaves the seven `digstore-*` deps below +# git-pinned ON PURPOSE. Three independent facts each block the repoint on their own, and all three are +# upstream of this workspace: +# +# 1. THE SET IS ATOMIC, so `digstore-core` cannot move alone even though it IS published (0.28.0). +# Inside the digstore workspace every sibling declares `digstore-core = { path = "../digstore-core" }`, +# so a git-sourced `digstore-host` always carries the git `digstore-core`. Repointing only the +# published members would put TWO `digstore-core` in the graph — a crates.io one for this crate's +# direct imports and a git one inside its siblings — and `ContentResponse`/`Bytes32` would stop +# unifying across the serve seam. They move together or not at all. +# +# 2. MOST OF THE SET IS UNPUBLISHED, and the closure is wider than it looks. Absent from crates.io: +# `digstore-host`, `digstore-remote`, `digstore-stage`, `digstore-crypto`, `digstore-compiler` — +# plus `digstore-prover`, `digstore-store`, `digstore-chunker` and `digstore-cli`, which the five +# pull in transitively and which no checklist had named. Each declares its siblings as a BARE +# `path = "../x"` with no `version` key, which `cargo publish` refuses outright, so publishing them +# is real work in DIG-Network/digs rather than a release button. +# +# 3. (CLEARED 2026-08-26, dig_ecosystem#3161.) This used to read "the published members are on the +# OTHER chia line", because digstore declared the primitives at 0.36.1 while this workspace was +# held at 0.26 by dig-gossip -> dig-peer-protocol 0.6. Both halves moved: the dig-gossip rev now +# carries 0.36.1, and the digstore rev below is digstore's own ceiling fix. The primitives are +# one line across the boundary, so nothing is split. +# +# The SDK family has now collapsed too (2026-08-26, dig-node#374). This used to read that +# digstore's rev took `chia-wallet-sdk 0.34` against dig-wallet's 0.36.0, and that closing the +# gap needed a digstore release — upstream, not here. That release happened: DIG-Network/digs#59 +# moved `digstore-chain` onto `chia-wallet-sdk 0.36` (and `datalayer-driver` 5, which is what +# required the SDK bump), and the rev below is that commit. +# +# Repointing was a SECURITY fix, not tidiness. `chia-sdk-driver` 0.30 and 0.34 decode the oracle +# fee out of an attacker-authored CREATE_COIN memo as +# `BigInt::from_signed_bytes_be(&memo).to_u64_digits().1[0]`, and an empty or explicit-zero memo +# makes that index an empty vec — a panic reachable from any spend dig-node walks through +# `Datastore::from_spend` (the lineage walk, `verify_pinned_root`, `sync_datastore`). 0.36 guards +# the empty case and uses `try_into`, so both become `DriverError::InvalidMemo`; it also stops +# silently discarding a negative memo's sign and truncating an over-`u64` fee to its low digit. +# +# So this row is still downstream of publishing the digstore workspace. Do NOT repoint these +# piecemeal, and never let the pin regress to a floating git BRANCH: the single `rev` is what makes +# the whole store-format surface reproducible. +digstore-host = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-core = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-remote = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-chain = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-stage = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-crypto = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +digstore-compiler = { git = "https://github.com/DIG-Network/digstore.git", rev = "161c2a3108cb4bb6c8791e96a26588ec99afb029" } +chia-protocol = "0.36.1" +# The `Streamable` to_bytes/from_bytes trait, used to serialize the chat `ChatMessage` payload before +# the dig-message seal. Same 0.36.1 as chia-protocol so the byte layout agrees across the workspace. +chia-traits = "0.36.1" +# The ecosystem's canonical address-family / IPv6-first primitive (CLAUDE.md §5.2). The advertise +# path keys + orders candidate addresses via `dig_ip::Family` and aggregates them source-tagged + +# de-duplicated via `dig_ip::PeerCandidates` — never a hand-rolled family sort (epic #1020, #1032). +# A leaf crate (no transitive DIG deps), so the crates.io release is used directly. +dig-ip = "0.1.2" +# The canonical dig-app ⇄ dig-node IPC session/signing contract (engine role-half + wire types + +# domain builders + frame bounds). SSOT shared with dig-app so the two halves can never drift (#1080). +# Pinned EXACTLY: 0.3.0 is the first release on this line and the caret range would silently accept +# a future 0.3.x. The crate reaches this one through the IPC surface, so an unreviewed minor is a +# wire-shape change arriving without a PR (dig-node#369 gate). +dig-ipc-protocol = "=0.3.0" +# On-chain DID resolution (WU3, #778): the honest chain-authenticated read path the production +# DidSigningKeyResolver delegates to. +# +# NOW A CRATES.IO DEP (dig_ecosystem#901). It was git-pinned at `tag = "v0.4.0"` because the crate was +# unpublished; it is published now, so the git source is retired and NC-7 is satisfied for this dep. +# +# 0.4.2 IS THE CORRECT TARGET, AND IT IS NOT THE LATEST (0.7.1). Two separate constraints pick it, and +# both are "one line per family" (CLAUDE.md §2.4b), not staleness: +# +# 1. CHIA LINE. This crate re-exports `Bytes32` and `Coin` FROM dig_identity (`seams/dig_peer/ +# session.rs`), so dig-identity's chia-protocol MUST be this workspace's line. Published lines: +# 0.4.1 / 0.4.2 / 0.5.0 -> chia-protocol 0.26, chia-sdk-utils 0.30 <- this workspace +# 0.6.0 / 0.7.0 / 0.7.1 -> chia-protocol 0.36.1, chia-sdk-utils 0.34+ +# Taking 0.7.1 links a SECOND chia-protocol and stops every `Bytes32` crossing the resolver seam +# from unifying — the same split the dig-gossip and dig-constants notes below describe. The 0.6+ +# uplift is downstream of the chia-0.36 cascade (dig_ecosystem#2761), not an edit here. +# +# 2. DEDUPLICATION. A 0.x minor is a semver-INCOMPATIBLE boundary, so this pin must match whatever +# the rest of the graph already resolves or the tree carries TWO dig-identity copies — which for +# an identity/key crate is the whole point of pinning it at all. +# +# 0.7.1 is that value on the chia-0.36.1 ceiling (dig_ecosystem#3161): `dig-nat 0.20` and +# `dig-message 0.7` both declare `dig-identity ^0.7`, and 0.7.1 is the only line whose `chia-bls` is +# 0.36.1. The previous pin here was 0.4.2, chosen by the same rule against the then-current +# `dig-message 0.5` / `dig-nat 0.18`; it is the RULE that is stable, not the number. Re-derive it +# from `Cargo.lock` rather than trusting this comment. +dig-identity = "0.7.1" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "net", "io-util", "time", "signal"] } +# Object-safe async trait for the injectable anchored-root resolver (the +# mandatory read-path chain pin, #127): production walks coinset, tests inject a +# deterministic resolver. +async-trait = "0.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +# The canonical node<->node JSON-RPC contract: the Method catalogue (wire names + +# per-method tier) and the mTLS peer-reachability allowlist. dig-node-core reads its +# method names + the peer allowlist from HERE (never hand-rolled) so the contract +# cannot drift from the other node implementation or the discovery document (#1075). +dig-rpc-protocol = "0.10.2" +# The directed-message base protocol (epic #793/#796): the e2e seal/open pipeline + the typed envelope +# the chat subsystem seals into. dig-node is the TRANSPORT — it seals an app-supplied opaque DIGCHAT1 +# envelope to the recipient's 0x0010 BLS identity key and dig-gossip directed-sends the sealed bytes. +dig-message = "0.7" +# The chat message-TYPE layer: the five chat payloads on dig-message's dig-chat band + the registry +# wiring. A crypto-free, content-blind type layer — the DIGCHAT1 seal is carried as opaque bytes. +dig-chat-protocol = { path = "../dig-chat-protocol", version = "0.2" } +base64 = "0.22" +hex = "0.4" +# The whole-module pull's one content-addressing primitive (#1576): the module descriptor's per-chunk + +# whole-blob hashes on the SERVE side, and the pre-announce re-hash that proves the artifact about to be +# cached + announced is byte-identical to the one the anchor gate admitted. Production, not test-only — +# this hash is what stands between a promoted artifact and a promoted-but-unverified one. +sha2 = "0.10" +filetime = "0.2" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +fs4 = { version = "1.1.0", features = ["sync"] } +directories = "6.0.0" +# OS CSPRNG for the per-stage content-authoring BLS key (#95 Pass C dig.stage). +getrandom = "0.2" +# -- dig_ecosystem#2168: the node MACHINE key at rest -------------------------------------- +# The canonical DIG keystore. Consumed with `custody` OFF (it is off by default) because this +# crate seals only the node's OWN machine identity seed and must never be able to name the +# user-custody API -- the #908 boundary expressed in the dependency graph rather than in prose +# (dig-keystore SPEC.md 18.2, which names this engine as the intended opaque-only consumer). +# 0.10 rather than 0.9: 0.10 is a BREAKING release that moves the crate's re-exported chia-bls +# onto the ecosystem 0.36.1 line. That line is ALREADY in this lock (six chia-bls lines resolve +# here today), and the `opaque` surface this crate uses is bytes + Password with no BLS type on +# it at all, so nothing crosses between lines -- and adopting 0.10 now avoids routing the node's +# identity onto a type identity that was about to change under it. +# +# 0.13 is the FLOOR for taking hardware binding live, and the reason is not the providers. +# dig-keystore#18 made `create_with_rng` an atomic check-then-write in 0.13.0; 0.12 will happily +# overwrite an existing record. On a SOFTWARE tier an overwritten machine key is recoverable from +# a backup of two plaintext-sealed halves. Hardware wrapping binds the wrapping key to a +# non-exportable component, so the same overwrite becomes PERMANENT -- there is nothing to restore +# from. Sealing therefore upgrades that bug from recoverable to unrecoverable, and 0.12 is not a +# version this seam may be enabled on. +dig-keystore = "0.13" +# The platform trusted-component providers (Windows TPM 2.0 via CNG, Apple Secure Enclave, Linux +# TPM 2.0). A SEPARATE crate because `dig-keystore` forbids `unsafe` and every one of these is FFI; +# it is the workspace member dig_ecosystem#1693 asked for, and it landed in dig-keystore v0.12.0. +# Without it the node's hardware seam -- already fully composed at `machine_key.rs` -- passes a +# hardcoded `None` and can never report anything but `Software(NotRequested)`. +# 0.2 pairs with dig-keystore 0.13: it adds the macOS Secure Enclave and Linux TPM 2.0 providers, +# and reclassifies a TPM AUTHORIZATION refusal (owner password set, dictionary-attack lockout) as +# `Absent` rather than `Indeterminate`, so an ordinary locked-down Windows box degrades instead of +# refusing. Only a genuinely uninspectable host is indeterminate now. +dig-keystore-hardware = "0.2" +# The machine key lives in the SAME user-global identity dir digstore_remote used for the +# plaintext seed (`/dig`), so the #2168 migration finds it in place and the node +# keeps its peer_id. digstore keeps its own `identity_dir` private, so the path is reproduced. +dirs = "5" +# Keeps the unsealed seed off the heap after use, matching what dig-keystore hands back. +zeroize = "1" + +# -- L7 DIG Node peer network (PHASE-2b, #162) ------------------------------------------------------- +# The NAT-traversal + connected-peer-pool layer. The bespoke in-node relay client (formerly relay.rs) +# is RETIRED: the relay connection now lives inside dig-nat (the `connect()` ladder's last-resort tier +# + the persistent reservation) and dig-gossip (the introducer-backed connected peer pool). dig-node +# consumes both to auto-discover peers via relay.dig.net, maintain a pool, and serve/issue the L7 peer +# RPC over mTLS (peer_id = SHA256(TLS SPKI DER)). +# +# Sourcing (#1280/#1393/#1494 crates.io cascade): dig-nat 0.11 is on crates.io — the whole peer stack +# (dig-gossip 0.15.0, dig-dht 0.5.1, dig-download 0.7.1, dig-peer-selector 0.5.2) now depends on dig-nat +# ">=0.10, <0.12" from crates.io, so consuming it here as a plain crates.io version unifies the ENTIRE +# graph on ONE dig-nat 0.11 instance (the old bare-git unification patch is retired). 0.11 adds the +# relayed RESPONDER path — `RelayStatus::enable_accept` surfaces an introduced circuit as a +# `RelayTunnel`, and `RelayAcceptor` runs the mTLS SERVER half over it (#1536, Leg B) — on top of +# 0.10's SPKI-pinned dialer (#1422) and the STUN happy-eyeballs reflexive discovery, and auto-composes +# the FULL NAT ladder via `connect_with_runtime` + `NatRuntime`, taking a CA-signed NodeCert. +# 0.11.2 is REQUIRED (#1586): before it, `RangeFrame::bytes` was read with `serde_bytes`, which over +# the JSON frame wire takes the served base64 string as its literal characters — so every +# `dig.fetchRange` window arrived as its own base64 text, the reassembler rejected the frame, and the +# read leg aborted before reading a byte. 0.11.2 reads/writes `bytes` as base64 (the canonical +# `dig_rpc_protocol::types::RangeFrame` wire the node's own serve path emits). +# dig-nat 0.13 is the release that introduced the FALLIBLE, payload-capped `RangeFrame::encode` plus +# the paged-prologue sender API (`with_identity` carrying `chunk_count`, `with_chunk_lens_page` / +# `chunk_lens_offset`, `with_chunk_index`, `RangeRequest::skip_layout`). Everything 0.14 adds is the +# RECEIVER (`ChunkLensAssembler`) plus the `split_chunk_lens_pages` helper, so 0.13 carries the whole +# serve-side fix for #1640/#1668. +# +# The dig-nat MAJOR must match the whole peer stack's, and it moves as ONE step (#1771). This crate +# builds its own `dig_nat` `NodeCert` / `NatConfig` / `NatRuntime` / `RelayStatus` / `TraversalKind` +# values and passes them INTO dig-download, dig-gossip and dig-peer-selector, so a second dig-nat +# instance does not merely bloat the tree -- those calls stop typechecking (verified: E0308 `expected +# dig_nat::relay::RelayStatus, found dig_nat::RelayStatus`). So dig-nat cannot be bumped alone, and +# dig-gossip/dig-dht/dig-download/dig-peer/dig-peer-selector cannot be bumped past it: the dig-nat +# 0.18 line (dig-dht 0.11, dig-download 0.19, dig-peer 0.11, dig-peer-selector 0.9, dig-gossip on the +# pinned rev below) is a single atomic cascade, every member of which is published. +# `tests/dependency_tree.rs` asserts the single-instance invariant against the resolved LOCK so a +# partial cascade cannot quietly reintroduce the duplicate. +# +# This line carries the SafeText peer-text cascade (#1674/#1675): error text of remote origin is +# held as `dig_nat::SafeText` rather than `String`, so a hostile peer's control characters cannot forge +# a line in an operator's log. On a partial bump the failure is now LOUDER than the RelayStatus mismatch +# above -- `SafeText` crosses dig-dht's AND dig-peer's public error surfaces, so two dig-nat versions is +# an outright compile error on those seams rather than merely two mTLS stacks. See +# `seams/dig_peer/dht.rs::stage_failed` for this crate's side of the adoption. +dig-nat = "0.21" +# dig-gossip is the ONE peer-stack exception: not yet published to crates.io (its publish is guarded +# pending dig-peer-protocol, #681), so it stays a git dependency PINNED to a release commit — here +# v0.17.12 (rev 3e352d91). +# +# v0.17.12 is REQUIRED, not merely current (#1771): it carries all THREE duplicate-connection fixes, +# and until it landed here the node's pool refused connections that would have worked. Every pool-add +# path now supersedes a stale slot for an already-authenticated `peer_id` instead of refusing the newer +# session — inbound (#1691), `connect_to` (#1703), and `adopt_nat_connection` (#1762), the last being +# the path EVERY dig-nat connection is adopted through, where a dead relay circuit's leftover slot +# refused the direct dial while the peer reported zero connections. `DuplicateConnection` still exists +# as an error variant but is no longer raised on those paths. Consequence for this crate: `PeerAdded` is +# REPUBLISHED on a supersede, so every consumer of it must be keyed by `peer_id` and idempotent (the DHT +# routing feed, the selector registry, and the download-side connected pool all are — see +# `download.rs::on_pool_event`), and `GossipStats::total_connections` is a LIFETIME counter that a +# supersede increments, so it must never be read as a count of unique peers (this crate reads +# `connected_peers` / `pool_stats().connected`, which are the live keyed-map sizes). +# +# Also on 0.17: the outbound /16+AS diversity caps (#1710/#1716) apply to the adoption path, so a +# test fixture adopting two peers must place them in DIFFERENT /16 groups. +# Pulled rustls-only (no native-tls) -- note `--all-features` is NOT a runnable configuration for +# dig-gossip: with both `ring` and `aws-lc-rs` reachable rustls cannot pick a provider. Its +# dig-nat/dig-tls/dig-constants come from crates.io (same versions as here), so the graph still +# unifies to ONE of each. +# +# There are no vendored chia-protocol/chia-sdk-client forks to patch any more: dig-gossip 0.23.0 +# deleted both (dig_ecosystem#2228) and the workspace `[patch.crates-io]` section is RETIRED (see the +# root manifest). The #1529 three-rev lockstep is retired with it. An earlier version of this comment +# still described those patches as live; it was wrong, and a false manifest comment is how the next +# lane reasons wrongly about the version line. +# +# THIS DEP WAS THE CHIA-0.36 MIGRATION BLOCKER, AND IS NO LONGER (dig_ecosystem#3161, 2026-08-26). +# +# The old block, recorded because a future lane will otherwise re-derive it: the pinned rev was +# v0.25.0, which took `dig-peer-protocol = "0.6"`, whose only published 0.6.x declares +# `chia-protocol 0.26`. That put 0.26 into the graph through a path no dig-node manifest could edit, +# so no assignment of versions HERE could remove it. +# +# What cleared it was upstream moving, exactly as that comment predicted: dig-gossip `main` (v0.30.0, +# the rev pinned below) is on `dig-peer-protocol 0.7` and declares chia-protocol/traits/sha2/bls at +# 0.36.1. Repointing the rev is what let this workspace take the ceiling; it was never a local edit. +# +# The consequence that mattered is now discharged rather than merely avoided: `Bytes32` reaches the +# store-melt authorization path in `seams/dig_peer/store_melted.rs` from dig_gossip and from +# `digstore-chain` as the SAME type, so that path is no longer split across two chia-protocol +# versions. No conversion shim was added, and none may be — bridging two lines is the byte-drift +# class this migration exists to remove. +# +# THE dig-nat WALL IS CLEARED (dig-node#412 step 7). The rev pinned below is dig-gossip v0.32.0, +# which declares `dig-nat = "0.21"`, so the peer stack takes its ^0.21 tier with ONE dig-nat in the +# graph. The prediction the old note made -- "the fix is a dig-gossip release against `dig-nat +# ^0.21`, upstream, never a version edit here" -- is what happened; measured from the resolved LOCK +# rather than from the caret, because a caret-compatible transitive is invisible to a plain +# re-resolve (CLAUDE.md §2.4b). +# NC-7 exception: dig-gossip is git-pinned PERMANENTLY, not pending a publish — dig_ecosystem#2647. +# It cannot be published while its `native-tls` [patch.crates-io] fork stands, because `cargo publish` +# STRIPS [patch.crates-io] from the published metadata. A published dig-gossip would therefore build +# against UPSTREAM native-tls, compile cleanly, and silently accept inbound peers presenting no client +# certificate at all — a mutual-TLS bypass that no consumer could see from its own manifest. Its +# publish.yml guard is unconditional and deliberate: do not weaken it, do not `--allow-dirty` past it. +# dig_ecosystem#900 ("publish dig-gossip") was closed as superseded by #2647. +dig-gossip = { git = "https://github.com/DIG-Network/dig-gossip", rev = "1a3391662ecce1a3cbe8b74122a52bbb1b28d3ee", default-features = false, features = ["rustls", "relay"] } +# The canonical DIG network id (genesis challenge) for the gossip handshake / introducer scope. +# +# 0.9 is the TIP OF DIG-NODE'S CHIA LINE, and that — not 0.10 — is the correct target here (#2072). +# dig-constants 0.10.0 moved to chia-protocol 0.36.1 / chia-wallet-sdk 0.34, while this workspace +# builds against 0.26 / 0.30, which dig-gossip pins there via dig-peer-protocol 0.6 (see the +# dig-gossip entry above). Depending on 0.10 links a SECOND chia_protocol and every `Bytes32` this +# crate reads off `DIG_MAINNET` stops unifying with the rest of the workspace. Being current on +# dig-constants is therefore downstream of the chia 0.36 migration, not a version-string edit. +# +# Values are identical from 0.4.0 through 0.10.0 — the L2 genesis challenge and its six AGG_SIG +# domains were finalized in 0.4.0 and have not moved since — so this bump is value-neutral and buys +# the additive surface (DIG_ASSET_ID, the treasury hash/address, `dig.local`, `rpc.dig.net`). +# +# The graph still carries 0.4.0/0.5.1/0.8.0 because upstream crates hold them down through their +# PUBLISHED ranges (dig-gossip `>=0.2, <0.5`, dig-nat `>=0.4, <0.6`, dig-download `^0.8`), which a +# consumer cannot edit. Collapsing to a single copy needs each of those to release against 0.9 — +# the release-first cascade on #2072. Do NOT pin this BACK to widen a window: the fix is upstream +# moving forward, never dig-node drifting backward. +dig-constants = "0.13.0" +# -- Portable profile body format (DPB) — epic #3008 W6 ----------------------------------------------- +# +# `default-features = false` deliberately: the default `chia` feature pulls dig-did/dig-store/ +# chia-protocol 0.36.1, which is a DIFFERENT chia line from the 0.26 this workspace builds against, so +# the default surface would put a SECOND chia-protocol into the always-running service. Without the +# feature the crate is a pure BYTE CORE (`dig-smt + sha2 + thiserror`) exposing exactly what the node +# needs: the DPB container, `AnchoredRoot` and `VerifiedBody`. +# +# Consequence to keep in mind: `dig_social_profile::resolve` is NOT available here. The node resolves +# a store's on-chain root through its OWN `AnchoredRootResolver` (the coinset lineage walk), which is +# the same authority, reached by the node's existing seam. +dig-social-profile = { version = "0.7", default-features = false } +# -- L7 content-location DHT (PHASE-B, #163) --------------------------------------------------------- +# The Kademlia DHT that maps DIG content (store / capsule / root / resource) to the peer_ids holding +# it. The node LOCATES content it lacks via `find_providers`, and keeps its OWN held-inventory provider +# records CURRENT in the DHT (`announce_provider` / `withdraw_provider` / `republish`). It rides the +# SAME dig-nat mTLS transport as the rest of the peer network via a `DhtTransport` adapter (`dht.rs`). +# +# Sourcing: the crates.io release (NC-7). dig-download and dig-peer-selector below resolve dig-dht +# from crates.io too, so cargo unifies to ONE dig-dht instance — its `PeerId` / `Contact` / +# `DhtTransport` are then the exact types `dig_nat::connect` returns, rather than look-alikes. +# +# NOT bumped to 0.15, and this is the tier's REMAINING blocker rather than an oversight: dig-download +# 0.21.0 and dig-peer-selector 0.10.0 — the LATEST published of each — both require `dig-dht ^0.13`, +# read from the crates.io index. Declaring "0.15" resolves TWO dig-dht lines while cargo prints +# success, and dig-dht values cross from this crate into both consumers. dig-dht 0.15 is where +# `ProviderRecord::unverified_mirror_coin_id` lives (dig-dht#23), so the §25 mirror-coin pointer is +# unreachable here until dig-download AND dig-peer-selector release against `dig-dht ^0.15` — +# upstream, never a version edit here. Bridging two lines with a shim is the §4.1 byte-drift class. +dig-dht = "0.13" +# -- L7 Peer Exchange (PEX, #166) -------------------------------------------------------------------- +# The node<->node peer-sharing protocol: over each established mTLS peer connection, a dedicated PEX +# logical stream exchanges delta-based first-hand known-peer sets (handshake -> snapshot -> deltas), +# feeding discovered peers into the pool as dial candidates (hints, verified by dialing) and formalizing +# what the ad-hoc `dig.getPeers` polling did. dig-pex ships a transport-agnostic sans-IO `PexEngine` +# this crate embeds; `pex.rs` is the dig-nat mux I/O adapter (SPEC §10.1, Appendix A). +# +# Sourcing: bare git form (no `rev`, like dig-nat/dig-dht) so cargo unifies any shared transitive deps +# to ONE copy across the graph; resolved to the tracked rev via the lockfile. dig-pex keeps a minimal +# dependency surface (serde/serde_json/tokio/rand — it mirrors the address shape rather than importing +# the transport crates), so there is no cross-crate type-unification concern here. +dig-pex = "0.1.1" +# -- Multi-source content fetch (#164) + redirect-on-miss (#165) -------------------------------------- +# The node-side download orchestrator: locate holders via dig-dht `find_providers`, confirm with +# `dig.getAvailability`, fan byte ranges across MULTIPLE providers simultaneously (`dig.fetchRange` +# over dig-nat), verify each range + the whole resource against the chain-anchored root, and +# reassemble — with pause/resume + `.download.tmp` GC. dig-node wires the real seams per the crate's +# implementers' note (see `download.rs`): DhtProviderLocator over our DhtService, NatRangeTransport +# over our mTLS identity, MerkleVerifier bound to the digstore proof format, FileSink/FileStateStore +# under the node's cache dir. +# +# Sourcing: the crates.io release (NC-7), matching dig-dht above. dig-download 0.6 deps dig-dht "^0.5" + +# dig-nat "^0.10" + dig-tls "^0.3" from crates.io, so cargo unifies its dig-dht + dig-nat + dig-tls with +# ours — ONE crate instance each across the graph (its re-exported `ContentId`/`ProviderRecord` ARE our +# dig-dht types). 0.6 keeps the injectable `SourceSelector` seam (dig-node implements it over +# dig-peer-selector, see `seams/dig_peer/selector_adapter.rs`), records range outcomes internally, and the +# fetch leg composes the FULL NAT ladder from an injected shared `NatRuntime` (`NatRangeTransport::new_with_runtime`). +# +# 0.8.1 adds the `ModuleDownloader` — the whole-`.dig`-module peer pull that turns a node which READ a +# resource into a complete resharer of the capsule (#1576, the reshare leg of the MVP flywheel). Its two +# seams are supplied here: `seams/dig_peer/module_transport.rs` (`NatModuleTransport`) and +# `seams/dig_peer/module_anchor.rs` (`ChainAnchoredModuleVerifier`). NOTE the `testkit` feature is +# deliberately NOT enabled: it is what makes the fail-OPEN `AcceptAnyModuleAnchor` nameable, and this +# crate's anchor gate is the reshare path's ONLY root of trust. +# +# HELD on the 0.19 line, resolving 0.19.1 (dig_ecosystem#3128). 0.19.1 is a deliberate BACKPORT of the +# capsule-checkpoint filename fix (dig-download#38): `FileStateStore` hex-encoded the download key into +# the filename, so a module key `module:<64hex>:<64hex>` (136 bytes) became a 277-character name and +# EVERY capsule checkpoint write on Linux failed with `File name too long (os error 36)`. The same fix +# also shipped as 0.20.1, but 0.20 requires dig-dht ^0.12 / dig-nat ^0.20 / dig-peer ^0.12 / +# dig-tls ^0.4 — the whole dig_ecosystem#3152 cascade — which this crate cannot take yet, so the +# backport keeps the 0.19-line pins intact. Regression-tested here through the node's OWN production +# checkpoint store (`download.rs::capturing_state_store_checkpoints_a_real_module_download_key`), +# because dig-download's own suite missed it: every `module.rs` test used `InMemoryStateStore` (no +# filename at all) and the one `FileStateStore` test used a 3-character key. +dig-download = "0.21" +# -- The shared peer client (#1283/#1576) ------------------------------------------------------------- +# `DigPeer` — the ONE DIG Network peer client: peer_id-pinned mTLS over the full NAT ladder plus typed +# RPC. Depended on DIRECTLY (not only transitively through dig-download) because dig-node supplies the +# `ModuleTransport` half of dig-download's module pull, which speaks `dig.getModuleInfo` / +# `dig.fetchModuleRange` — the two client methods dig-peer 0.5 added for exactly this leg. +# +# 0.5 is REQUIRED, not merely current: dig-peer 0.4 pulled dig-rpc-protocol 0.3, so a tree containing +# both it and dig-download's dig-rpc-protocol 0.5 carried TWO `ModuleInfo` types either side of the +# module pull's trust boundary — on the fields that drive the whole pull plan. dig-download 0.8.1 is on +# dig-peer 0.5 too, so exactly ONE dig-rpc-protocol + ONE dig-peer resolve here (asserted by +# `crates/dig-node-core/tests/dependency_tree.rs`). +dig-peer = "0.13" +# -- Self-optimizing peer selection (#178) ------------------------------------------------------------ +# The decision + learning layer between dig-dht discovery and dig-download execution: it ranks the +# providers `find_providers` returns (learning throughput/rtt/reliability + a per-class saturation +# point + an adaptive relayed penalty from the REAL measured outcome of every range) and hands +# dig-download the ranked subset + per-peer concurrency, instead of dig-download picking sources +# blindly. Every completed/failed range streams back via `record_outcome` in real time, and a dropped +# source drives a `rebalance`. dig-node bridges dig-download's `SourceSelector` seam to this crate via +# `seams/dig_peer/selector_adapter.rs` (the @30↔@30 composition-root bridge — dig-download does NOT +# depend on dig-peer-selector; dig-node, one level up, wires them together). +# +# Sourcing: the crates.io release (NC-7), matching dig-dht/dig-download above. It deps dig-dht "^0.5" + +# dig-nat "^0.10" from crates.io, so cargo unifies the selector's `dig-nat` + `dig-dht` with ours — ONE +# crate instance each (its re-used `PeerId`/`ContentId`/`ProviderRecord`/`CandidateAddr` ARE our +# dig-nat/dig-dht types). The selector mirrors `dig_gossip::PoolEvent` LOCALLY (it does not depend on +# dig-gossip, which lags as a git dep), so the node maps `dig_gossip::PoolEvent` → the selector's local +# `PoolEvent` 1:1 (see `download.rs::pool_event_to_selector`). +# 0.9.2 IS the chia-0.36 cascade landing (dig_ecosystem#3161). The old note here said "NOT bumped to +# 0.9.1: that release moves onto dig-tls 0.4, and this crate still resolves 0.3 through +# dig-nat/dig-gossip, so the graph would carry TWO `dig_tls::PeerId` types that do not unify". Both +# halves of that split are gone — dig-nat 0.20 and the dig-gossip rev both take `dig-tls ^0.4`, and +# the lock now resolves dig-tls ONCE at 0.4.0. +# +# 0.10 IS takeable as of dig-gossip v0.32.0, which declares `dig-nat = "0.21"` — the release the old +# note here was waiting on. It carries `dig-dht ^0.13`, which is what holds dig-dht at 0.13; see the +# dig-dht entry above. +dig-peer-selector = "0.10" +# The canonical DIG mTLS certificate crate (L00, crates.io). The node's PERSISTENT machine identity +# is a CA-signed `dig_tls::NodeCert` minted from the node's own BLS identity key and persisted 0600 in +# the data dir (#908 identity boundary: this is the MACHINE key, never a user key). Replaces the +# legacy self-signed `rcgen` cert as the node dials/serves over the dig-nat CA-signed mTLS model +# (#1280). `peer_id = SHA256(SPKI DER)` is stable across restarts. dig-tls re-exports chia-bls's +# `SecretKey`, unifying with this crate's chia-bls 0.36.1. 0.4 is the chia-bls 0.36.1 uplift and is +# now TAKEN (dig_ecosystem#3161): the old hold — "taking it here would split `dig_tls::PeerId` +# against the 0.3 that dig-nat/dig-gossip still resolve" — expired when dig-nat 0.20 and the +# dig-gossip rev both moved onto `dig-tls ^0.4`. The lock resolves dig-tls ONCE, at 0.4.0. +dig-tls = "0.4" +# -- The store-exchange DECISION layer (dig_ecosystem#3130) ------------------------------------------- +# `dig-sex` owns every cache/tier/relevance/selection DECISION this node used to make in-tree: what to +# keep, what to sacrifice first, and whether a fresh candidate is worth displacing an incumbent. It is +# PURE — no clock, no socket, no filesystem — so this crate keeps owning all of the I/O and hands it +# only the facts. The in-tree `relevance` module and `tier0_prefetch::effective_tier` moved there +# verbatim and are now consumed as `dig_sex::*`. +# +# It also re-exports the ONE `CapsuleIdentity` the ecosystem speaks, so this crate never declares its +# own and never pulls a second major of `dig-store` into the graph. +dig-sex = "0.5" +# The cache/eviction SEAM `dig_sex::TieredPolicy` implements. Declared DIRECTLY rather than reached +# through `dig-sex`, which re-exports `CapsuleIdentity` alone — naming `EvictionEntry`, +# `EvictionContext` and the `EvictionPolicy` trait (whose `select_evictions` must be in scope to call) +# requires the crate itself. The version is pinned to the one `dig-sex` resolves, so the `CapsuleIdentity` +# this crate builds and the one `TieredPolicy` matches on are the SAME type; a split would silently +# fail to unify. `dig_sex::TieredPolicy` is the first non-LRU implementation of the seam, and the +# module sweep here is its first consumer. +dig-store-cache = "0.1.1" +# The mTLS peer-RPC LISTENER (server side): present the node cert + REQUIRE a client cert (peer_id +# enforced). rustls/tokio-rustls with ring only (no OpenSSL/aws-lc for OUR usage), matching dig-nat. +rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +# IPv6-first, IPv4-fallback (ecosystem HARD RULE): bind the peer-RPC listener on the IPv6 unspecified +# address `[::]` as a DUAL-STACK socket (IPV6_V6ONLY cleared) so ONE socket serves both native IPv6 +# and IPv4-mapped peers. Mirrors dig-relay's `net.rs` / dig-gossip's own dual-stack bind. +socket2 = "0.5" +# -- Engine-side identity session (NODE-1, #910) ----------------------------------------------------- +# The engine is IDENTITY-AGNOSTIC: it holds NO user signing key. `control.session.attach` proves the +# attaching dig-app holds the profile's slot-0x0010 identity key by VERIFYING an Ed25519 signature over +# the byte-identical `DIGNET-SESSION-v1 ‖ nonce ‖ profile_did` challenge (session.rs). Ed25519 +# verification rides `ring` — ALREADY in the locked graph (the rustls/tokio-rustls transport uses it), +# so it adds no new crate. VERIFY-ONLY: the engine never holds a signing key. (dig-app signs with +# ed25519-dalek; ring verifies RFC-8032 signatures + is strict against malleable/small-order inputs.) +ring = "0.17" +tracing = "0.1" +# Best-effort CONCURRENT fan-out of the multi-source provider union (`UnionLocator`, #1443): query +# dig-dht + PEX + relay-introducer sources at once via `future::join_all` (order-preserving, so the +# dedup keeps DHT-first precedence). rustls/tokio already pull futures transitively; declaring it here +# makes it a first-class prod dep of the union seam. Minimal feature set (no executor). +futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } + +[dev-dependencies] +tempfile = "3" +# `dig_keystore::hardware::double::FakeDevice` -- the only way to exercise a SPECIFIC hardware +# outcome from a test. The real providers ship now (dig-keystore-hardware above), but which one a +# host answers with is a property of that host: no CI runner can be made to produce an +# indeterminate TPM probe on demand, and that is the outcome the refusal path turns on. +# Test-only: `testing` is never enabled for the library build. +dig-keystore = { version = "0.13", features = ["testing"] } +# Same git rev as the normal dependency above, with dig-gossip's `test-util` feature added. That +# feature gates `__connect_stub_peer_with_direction`, which registers a connected peer without real +# TLS — the only way to assert what a broadcast ACTUALLY put on the wire from a test. Declared here +# rather than on the normal dependency so the shipped binary never carries the test hook: cargo +# unifies the feature into test builds only, since dev-dependencies are not built for `cargo build`. +# NC-7 exception: same permanent git pin as the runtime dep above (dig_ecosystem#2647). +dig-gossip = { git = "https://github.com/DIG-Network/dig-gossip", rev = "1a3391662ecce1a3cbe8b74122a52bbb1b28d3ee", default-features = false, features = ["rustls", "relay", "test-util"] } +# Generates the P-256 leaf key pairs the opcode-222 holdings tests sign with: `public_key_der()` IS the +# leaf `SubjectPublicKeyInfo` DER the wire carries as `provider_spki`, and whose SHA-256 is the +# announcing peer_id — so a test signer is a real §5.2 identity rather than a stand-in that could hide +# an attribution bug (#1429). +rcgen = "0.13" +# dig-download's in-memory harness (`MockRangeTransport`, `MockContent`, …), which the download-path +# tests drive the real orchestrator over. +# +# It is a DEV dependency and MUST STAY ONE (#1576). The `testkit` feature is also what makes +# `AcceptAnyModuleAnchor` — a fail-OPEN module anchor verifier — nameable at all; the crate compiles it +# out of a default consumer build precisely so a production wiring cannot reach it. The reshare path's +# anchor gate is its ONLY root of trust, so enabling this feature on the PRODUCTION `dig-download` entry +# above would put a bypass of that root of trust one `use` away. Dev-dependency features do not +# propagate to a consumer's build, so the node binaries never see it. +# +# Pinned by the `the_fail_open_anchor_verifier_is_not_reachable_from_a_production_build` test, which +# fails if `testkit` ever appears on the production entry. +dig-download = { version = "0.21", features = ["testkit"] } +# Captures the peer-facing serve's real emitted tracing records into an in-memory buffer, so the +# serve-observability tests (#1595) assert what an operator would actually see in the node log — +# and that no payload byte or proof ever reaches it. +tracing-subscriber = { version = "0.3", features = ["fmt"] } +# Builds canonical `did:chia:` strings for the on-chain DID-resolver tests (matches dig-identity's codec). +chia-sdk-utils = "0.36.0" +# The session attach tests sign challenges with the BLS12-381 G2 AugScheme (the app's slot-0x0010 +# identity key), matching dig-ipc-protocol's signing contract and dig-identity's published G1 key. +chia-bls = "0.36.1" +# axum is a TEST-ONLY dependency of the engine library: the node no longer serves its +# own HTTP (the `dig-node-service` shell owns the axum transport, `dig-runtime` owns the +# in-process FFI path — dispatch is `handle_rpc`, which is transport-agnostic). The +# integration/unit tests still spawn small in-process axum mock servers (the §21 remote +# + a module server) to exercise the fetch/cache path hermetically. +axum = "0.7" +# In-process loopback mTLS peer drives the peer-network tests with NO real network (the CA-signed +# `dig_tls::NodeCert` minted by `load_or_generate_node_cert` is the test client's mTLS identity). +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "net", "io-util", "time", "test-util"] } +futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } +# The peer_network.rs integration test implements PeerRpcResponder (an async trait). +async-trait = "0.1" diff --git a/crates/dig-node-core/tests/dependency_tree.rs b/crates/dig-node-core/tests/dependency_tree.rs index 082c8375..82418b16 100644 --- a/crates/dig-node-core/tests/dependency_tree.rs +++ b/crates/dig-node-core/tests/dependency_tree.rs @@ -16,18 +16,36 @@ //! So both are asserted where they are actually decided: the manifest and the lock. /// This crate's manifest, read at compile time so the assertion cannot drift from the build. -const MANIFEST: &str = include_str!("../Cargo.toml"); +const MANIFEST_RAW: &str = include_str!("../Cargo.toml"); + +/// The manifest with its line endings normalised to `\n`. +/// +/// An editor on Windows rewrites a manifest with CRLF without changing a single declaration. +/// Searching those bytes for `"\n[section]\n"` then finds nothing — which is a fact about the file's +/// line endings, not about its dependencies. dig-node#412 hit exactly that: a dependency-tier commit +/// saved this file as CRLF, and the section lookup below went on to report `dig-download` as absent +/// from a production tree it had never left. Normalising first keeps every assertion in this file +/// about DECLARATIONS, which is the only thing any of them means to be about. +fn manifest() -> &'static str { + static NORMALISED: std::sync::OnceLock = std::sync::OnceLock::new(); + NORMALISED.get_or_init(|| MANIFEST_RAW.replace("\r\n", "\n")) +} /// The workspace lock (two levels up from this crate). const LOCK: &str = include_str!("../../../Cargo.lock"); /// The body of `section` in the manifest (up to the next `[` at column 0). fn manifest_section(section: &str) -> &'static str { - let start = MANIFEST + let manifest = manifest(); + // Panics rather than falling back to offset 0. A lookup that FAILED is not evidence about any + // dependency, and offset 0 silently searches `[package]`, where nothing is declared — so every + // "X is a production dependency" assertion below would then report absence with total + // confidence. Failing here names the section that could not be read instead. + let start = manifest .find(&format!("\n[{section}]\n")) .map(|i| i + section.len() + 4) - .unwrap_or(0); - let rest = &MANIFEST[start..]; + .unwrap_or_else(|| panic!("this manifest has no `[{section}]` section to read")); + let rest = &manifest[start..]; let end = rest.find("\n[").unwrap_or(rest.len()); &rest[..end] } From 01bc66a40f1dd1cda31d2d1b998f5506942ed720 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sun, 30 Aug 2026 05:28:35 -0700 Subject: [PATCH 5/6] fix(mirror): make the salvaged observation tests compile against the real types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lane that wrote `mirror::observe` died before it ever built, so its test fixtures were written against remembered shapes rather than the published ones. Three errors, all in the test module and none in `observe` itself: - `CollateralRequirementResult::Known` has no `census_height`. Replaced with the real fields — `protocol_version`, `multiplier_micros` and `handicap_dig_base_units` — matching `pass::tests::known_at`, so the two fixtures describe the same requirement. - `BondState::Bonded` is a struct variant carrying the coin and the amount, not a unit value, so it cannot be compared with `==`. Both `Bonded` assertions were rewritten to match rather than weakened. One now pins the PAYLOAD — the coin id, the epoch, and the 600 that coin locks — because a bare variant check cannot see a row that names the other coin or that reports this epoch's requirement instead of the coin's own amount, and both are the plausible wrong answers here. That assertion needed a fixture change to be able to fail: every fixture coin shared the id `"cc" * 32`, so a claim about "the coin bonding aa/11" held equally against the coin bonding bb/22. Coin ids are now distinct per `(store, root)`. Also applies rustfmt to `operator_puzzle_hash`. Refs #412 --- crates/dig-node-service/src/mirror/observe.rs | 37 +++++++++++++++---- crates/dig-wallet/src/operator_wallet.rs | 6 ++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/crates/dig-node-service/src/mirror/observe.rs b/crates/dig-node-service/src/mirror/observe.rs index 1a77891f..6f867766 100644 --- a/crates/dig-node-service/src/mirror/observe.rs +++ b/crates/dig-node-service/src/mirror/observe.rs @@ -174,9 +174,14 @@ mod tests { } } + /// A coin whose id is DISTINCT per `(store, root)`. + /// + /// One shared id across every fixture coin would make `coin_id` useless as a discriminator: + /// an assertion about "the coin bonding aa/11" would hold just as well against the coin bonding + /// bb/22, so a `Bonded` row that named the wrong coin would still pass. fn coin(store: &str, root: &str, epoch: i64, collateral: u64) -> HeldMirror { HeldMirror { - coin_id: "cc".repeat(32), + coin_id: format!("{store}{root}").repeat(16), store_id: store.repeat(32), root: root.repeat(32), epoch, @@ -191,10 +196,12 @@ mod tests { current_epoch: 7, requirement: CollateralRequirementResult::Known { epoch: 7, + protocol_version: 1, required_per_store_dig_base_units: REQUIRED, stores: 10, owners: 3, - census_height: 100, + multiplier_micros: 1_000_000, + handicap_dig_base_units: 0, }, margin_bp: 0, creates_enabled: true, @@ -219,12 +226,25 @@ mod tests { observation.locked_dig_base_units, 1_000, "both owned coins are locked; the reclaimed one has not confirmed" ); + // Asserted on the PAYLOAD, not merely on the variant. `Bonded` carries the coin a person + // looks up and the amount that coin locks, and a row naming the other coin — or this + // epoch's requirement instead of the coin's own 600 — is exactly the plausible wrong answer + // a bare variant check cannot see. assert!( observation .states .iter() - .any(|(b, s)| *b == bond("aa", "11") && *s == BondState::Bonded), - "the covered capsule is bonded" + .any(|(b, s)| *b == bond("aa", "11") + && matches!( + s, + BondState::Bonded { coin_id, epoch, amount_dig_base_units } + if *coin_id == format!("{}{}", "aa", "11").repeat(16) + && *epoch == 7 + && *amount_dig_base_units == 600 + )), + "the covered capsule is bonded by its OWN coin, for the amount that coin locks: \ + {:?}", + observation.states ); } @@ -280,10 +300,11 @@ mod tests { .find(|(k, _)| *k == b) .map(|(_, s)| s.clone()) }; - assert_eq!( - state_of(bond("aa", "11")), - Some(BondState::Bonded), - "a covered bond does not depend on the balance" + assert!( + matches!(state_of(bond("aa", "11")), Some(BondState::Bonded { .. })), + "a covered bond does not depend on the balance, so an unreadable wallet must not \ + disturb it: {:?}", + state_of(bond("aa", "11")) ); assert_eq!( state_of(bond("bb", "22")), diff --git a/crates/dig-wallet/src/operator_wallet.rs b/crates/dig-wallet/src/operator_wallet.rs index a230bd78..705d1d53 100644 --- a/crates/dig-wallet/src/operator_wallet.rs +++ b/crates/dig-wallet/src/operator_wallet.rs @@ -129,7 +129,11 @@ impl OperatorWallet { /// seed reaches this process. pub fn operator_puzzle_hash(paths: &WalletPaths) -> Option { let phrase = autoseed::open_operator_phrase(paths)?; - Some(digstore_chain::keys::derive_wallet_keys(&phrase).ok()?.owner_puzzle_hash) + Some( + digstore_chain::keys::derive_wallet_keys(&phrase) + .ok()? + .owner_puzzle_hash, + ) } #[cfg(test)] From c649ce90e9113476edb7d33aa8cc6f8dccf75136 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sun, 30 Aug 2026 06:47:47 -0700 Subject: [PATCH 6/6] docs(wallet): state what dig_balance_base_units actually guarantees The doc promised `None` for "an unsynced replica that cannot answer for this address". `balance_for_address` returns `Err(NotSynced)` only from the `Source::Fallback` arm with no live fallback; the ordinary in-scope-but-behind case returns `Ok(WalletBalanceResult { synced: false, .. })`, so this returns `Some` of a possibly-stale figure. Authority for an address and currency with the chain are independent questions and only the first can fail the read, so staleness was never covered by `None`. Step 8 will call this on a funding decision because this sentence said the case was handled; correcting the claim now costs nothing and removes a born-false guarantee from a money path. Notes the residue rather than silencing it: discriminating on `synced` alone would report UNKNOWN for every fallback-served answer, since that arm hard-codes it false. Also drops the tracked `.step7-wip` scratch marker, which a squash would otherwise land on main. Co-Authored-By: Claude --- .step7-wip | 1 - crates/dig-wallet/src/sage/rpc.rs | 13 +++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 .step7-wip diff --git a/.step7-wip b/.step7-wip deleted file mode 100644 index a5ff0e78..00000000 --- a/.step7-wip +++ /dev/null @@ -1 +0,0 @@ -step 7 lane: mirror bond observation + DHT pointer. WIP. diff --git a/crates/dig-wallet/src/sage/rpc.rs b/crates/dig-wallet/src/sage/rpc.rs index 9eb7b963..be737215 100644 --- a/crates/dig-wallet/src/sage/rpc.rs +++ b/crates/dig-wallet/src/sage/rpc.rs @@ -1215,8 +1215,8 @@ impl WalletBackend { /// The spendable **$DIG** at this node's own operator puzzle hash, in DIG CAT base units. /// - /// `None` when the balance could not be read — an unreachable chain source, an unsynced replica - /// that cannot answer for this address, a figure too large for a `u64`. `None` is **not zero**: + /// `None` when the balance could not be read — an unreachable chain source, a replica that is + /// not authoritative for this address, a figure too large for a `u64`. `None` is **not zero**: /// §25's bond surface reports an uncovered bond as `deferred{balance_unreadable}` on `None` and /// as `unfunded` on `Some(0)`, and those are opposite claims — the first says the node does not /// know, the second raises an out-of-funds alarm. Substituting zero for an unreadable balance is @@ -1228,6 +1228,15 @@ impl WalletBackend { /// address string could read the right amount of the wrong asset at the wrong network's prefix, /// and every one of those returns a confident number. /// + /// **Staleness is NOT covered by `None`, and a caller must not read it as freshness.** Being + /// authoritative for an address and being current with the chain are independent questions, and + /// only the first can fail the read: a replica that is in scope but behind answers `Ok` with + /// `synced: false`, so this returns `Some` of a figure that may lag the chain. A caller that + /// needs currency must ask for it — `balance_for_address` returns `source`, `synced` and + /// `peak_height`, and this narrowing keeps only `balance`. Discriminating on `synced` alone + /// would be wrong in the other direction, because the fallback arm hard-codes it false for + /// answers that are perfectly good. + /// /// This is a READ. It confers no custody and touches no key: the puzzle hash is a public value. pub async fn dig_balance_base_units(&self, owner_puzzle_hash: Bytes32) -> Option { let address = self.address_of(&hex::encode(owner_puzzle_hash));