Skip to main content

dig_constants/
lib.rs

1//! DIG Network Constants
2//!
3//! Defines network parameters for the DIG L2 blockchain. This crate exists
4//! separately so that any DIG crate can import network constants without
5//! pulling in the full CLVM engine or other heavy dependencies.
6//!
7//! The core type is [`NetworkConstants`], which wraps `chia-consensus`'s
8//! `ConsensusConstants` with DIG-specific values (genesis challenge,
9//! AGG_SIG additional data, cost limits, etc.).
10//!
11//! # Chia L1 vs DIG L2 (do not mix)
12//!
13//! [`DIG_MAINNET`] / [`DIG_TESTNET`] describe the DIG **L2** network. Separately,
14//! [`CHIA_L1_MAINNET_AGG_SIG_ME`] / [`CHIA_L1_TESTNET11_AGG_SIG_ME`] hold the
15//! **Chia L1 (foreign chain)** genesis challenge that DIG wallet code needs as
16//! AGG_SIG_ME additional data when signing L1 spends. They live here as the
17//! ecosystem's single source of truth, but are DELIBERATELY distinct from the DIG
18//! L2 genesis — signing an L1 spend with the DIG L2 genesis produces an invalid
19//! signature. The `CHIA_L1_` prefix is the anti-mixup guard.
20//!
21//! # Usage
22//!
23//! ```rust,ignore
24//! use dig_constants::DIG_MAINNET;
25//!
26//! let genesis = DIG_MAINNET.genesis_challenge();
27//! let consensus = DIG_MAINNET.consensus();
28//! ```
29
30use chia_consensus::consensus_constants::ConsensusConstants;
31use chia_protocol::Bytes32;
32use hex_literal::hex;
33
34/// DIG network constants.
35///
36/// Wraps `chia-consensus::ConsensusConstants` with accessors for the fields
37/// that DIG validators and wallet code commonly need. The underlying
38/// `ConsensusConstants` is available via [`consensus()`](Self::consensus)
39/// for direct use with `chia-consensus` functions like `run_spendbundle()`.
40#[derive(Debug, Clone)]
41pub struct NetworkConstants {
42    inner: ConsensusConstants,
43}
44
45impl NetworkConstants {
46    /// The underlying `chia-consensus` constants, for passing directly to
47    /// `run_spendbundle()`, `validate_clvm_and_signature()`, etc.
48    pub fn consensus(&self) -> &ConsensusConstants {
49        &self.inner
50    }
51
52    /// DIG genesis challenge.
53    pub fn genesis_challenge(&self) -> Bytes32 {
54        self.inner.genesis_challenge
55    }
56
57    /// AGG_SIG_ME additional data (== genesis_challenge on Chia L1).
58    pub fn agg_sig_me_additional_data(&self) -> Bytes32 {
59        self.inner.agg_sig_me_additional_data
60    }
61
62    /// Maximum CLVM cost per block.
63    pub fn max_block_cost_clvm(&self) -> u64 {
64        self.inner.max_block_cost_clvm
65    }
66
67    /// Cost per byte of generator program.
68    pub fn cost_per_byte(&self) -> u64 {
69        self.inner.cost_per_byte
70    }
71
72    /// Maximum coin amount (u64::MAX).
73    pub fn max_coin_amount(&self) -> u64 {
74        self.inner.max_coin_amount
75    }
76}
77
78// =============================================================================
79// AGG_SIG additional data derivation
80//
81// On Chia L1, each AGG_SIG_* variant's additional_data is:
82//   sha256(genesis_challenge || opcode_byte)
83// except AGG_SIG_ME which uses genesis_challenge directly.
84//
85// See: condition_tools.py:58-71
86//   https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L58
87// =============================================================================
88
89// ---------------------------------------------------------------------------
90// DIG Mainnet
91//
92// The genesis challenge is the 32-byte consensus anchor for the DIG L2 network.
93// It doubles as the gossip `network_id` gate: `dig-gossip` REJECTS an all-zero
94// network_id, so this value MUST be non-zero for the node's gossip pool / DHT /
95// PEX to start.
96//
97// DIG_MAINNET L2 genesis = the Chia mainnet header hash @ height 9,021,277
98//   (0af981...1abf), pinned 2026-07-17 — anchors the DIG L2 genesis to a real,
99//   verifiable Chia block (captured via coinset.org get_blockchain_state).
100//
101//   DIG_MAINNET_GENESIS_CHALLENGE
102//     = 0af981862a4df51f51ec59c312315d959931d917c375730b89b9e2b0854d1abf
103//
104// This is the PRE-LAUNCH canonical DIG mainnet genesis. Per CLAUDE.md §3.7 the
105// ecosystem is pre-release with no live users, so this value is revisable at
106// true mainnet launch — re-anchor to the launch-time Chia header hash and
107// recompute every derived value below if it is ever changed.
108//
109// All `agg_sig_*_additional_data` values are derived from this genesis as
110// `sha256(genesis_challenge || opcode_byte)` (AGG_SIG_ME = genesis directly),
111// so they were all recomputed for this genesis.
112// ---------------------------------------------------------------------------
113
114/// Canonical DIG mainnet genesis challenge.
115///
116/// The Chia mainnet header hash at block height 9,021,277 (`0af981…1abf`),
117/// pinned 2026-07-17 — a real, verifiable, fixed 32-byte value anchoring the
118/// DIG L2 genesis to a real Chia block. This is the pre-launch canonical value;
119/// per §3.7 it is revisable at true mainnet launch. All
120/// `agg_sig_*_additional_data` fields are derived from this.
121const DIG_MAINNET_GENESIS_CHALLENGE: [u8; 32] =
122    hex!("0af981862a4df51f51ec59c312315d959931d917c375730b89b9e2b0854d1abf");
123
124/// DIG mainnet constants.
125///
126/// Uses DIG's own genesis challenge and AGG_SIG domain separation.
127/// Proof-of-space and VDF fields are set to neutral values since DIG L2
128/// does not use Chia's proof-of-space consensus.
129pub const DIG_MAINNET: NetworkConstants = NetworkConstants {
130    inner: ConsensusConstants {
131        // -- DIG-specific values --
132        genesis_challenge: Bytes32::new(DIG_MAINNET_GENESIS_CHALLENGE),
133
134        // AGG_SIG additional data: derived from genesis_challenge.
135        // AGG_SIG_ME = genesis_challenge directly.
136        // Others = sha256(genesis_challenge || opcode_byte).
137        // Derivation: condition_tools.py:58-71
138        //   https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L58
139        // Opcode bytes: AGG_SIG_PARENT=43, PUZZLE=44, AMOUNT=45,
140        //   PUZZLE_AMOUNT=46, PARENT_AMOUNT=47, PARENT_PUZZLE=48
141        // NOTE: Recompute ALL values when genesis_challenge is finalized.
142        agg_sig_me_additional_data: Bytes32::new(DIG_MAINNET_GENESIS_CHALLENGE),
143        agg_sig_parent_additional_data: Bytes32::new(hex!(
144            "196d63b6dfbd4440656f9c1eadc686cacfaae771c565762a8cd6e51c892a0077"
145        )),
146        agg_sig_puzzle_additional_data: Bytes32::new(hex!(
147            "9ca719659b5e2355a91ff330c8612cb58c74f1063eaff99e507602d450b1f71f"
148        )),
149        agg_sig_amount_additional_data: Bytes32::new(hex!(
150            "d13767da4a8bd9520dbd9e039e68b3eb4b16fdcbb7e7755b5064840eaeb553ce"
151        )),
152        agg_sig_puzzle_amount_additional_data: Bytes32::new(hex!(
153            "73eea3473bd0daa28793d4bcd218ade462b634b53af97f9a01a91f3059ac75df"
154        )),
155        agg_sig_parent_amount_additional_data: Bytes32::new(hex!(
156            "eb7302224e77c0f269d0c8b105d4cc786775ae012ed2db49751c33c244c3f647"
157        )),
158        agg_sig_parent_puzzle_additional_data: Bytes32::new(hex!(
159            "ccac5983685257d50ee7b439bbb502128ddb262813dde4e4a11ac6cdfc66fa8e"
160        )),
161
162        // DIG L2 cost limits
163        max_block_cost_clvm: 11_000_000_000, // per-spend limit, same as Chia L1
164        cost_per_byte: 12_000,
165        max_coin_amount: u64::MAX,
166
167        // Block generator limits
168        max_generator_ref_list_size: 512,
169
170        // Hard fork heights — set to 0 to always use latest consensus rules.
171        // DIG L2 starts with all features enabled from block 0.
172        hard_fork_height: 0,
173        hard_fork2_height: 0,
174
175        // Pre-farm puzzle hashes — not used by DIG L2, set to zero.
176        genesis_pre_farm_pool_puzzle_hash: Bytes32::new([0u8; 32]),
177        genesis_pre_farm_farmer_puzzle_hash: Bytes32::new([0u8; 32]),
178
179        // -- Proof-of-space / VDF fields (not used by DIG L2) --
180        // These must be valid values since ConsensusConstants is passed to
181        // chia-consensus functions, but DIG does not use PoS consensus.
182        slot_blocks_target: 32,
183        min_blocks_per_challenge_block: 16,
184        max_sub_slot_blocks: 128,
185        num_sps_sub_slot: 64,
186        sub_slot_iters_starting: 1 << 27,
187        difficulty_constant_factor: 1 << 67,
188        difficulty_starting: 7,
189        difficulty_change_max_factor: 3,
190        sub_epoch_blocks: 384,
191        epoch_blocks: 4608,
192        significant_bits: 8,
193        discriminant_size_bits: 1024,
194        number_zero_bits_plot_filter_v1: 9,
195        number_zero_bits_plot_filter_v2: 9,
196        min_plot_size_v1: 32,
197        max_plot_size_v1: 50,
198        plot_size_v2: 28,
199        sub_slot_time_target: 600,
200        num_sp_intervals_extra: 3,
201        max_future_time2: 120,
202        number_of_timestamps: 11,
203        max_vdf_witness_size: 64,
204        mempool_block_buffer: 10,
205        weight_proof_threshold: 2,
206        blocks_cache_size: 4608 + (128 * 4),
207        weight_proof_recent_blocks: 1000,
208        max_block_count_per_requests: 32,
209        pool_sub_slot_iters: 37_600_000_000,
210        plot_filter_128_height: 0xffff_ffff,
211        plot_filter_64_height: 0xffff_ffff,
212        plot_filter_32_height: 0xffff_ffff,
213        plot_v1_phase_out_epoch_bits: 8,
214        min_plot_strength: 2,
215        max_plot_strength: 32,
216        plot_filter_v2_first_adjustment_height: 0xffff_ffff,
217        plot_filter_v2_second_adjustment_height: 0xffff_ffff,
218        plot_filter_v2_third_adjustment_height: 0xffff_ffff,
219    },
220};
221
222// =============================================================================
223// NAT-traversal relay endpoint
224//
225// A DIG Node behind NAT cannot accept inbound dials, so it holds a constant
226// reservation with a publicly-reachable relay to stay discoverable. The
227// canonical public relay is `relay.dig.net`, serving the `RelayMessage`
228// WebSocket wire (RLY-001..RLY-007) on port 9450.
229//
230// This constant is the single source of truth for that endpoint so consumers
231// (`dig-node`, `dig-gossip`) don't each hardcode it. It MUST stay byte-identical
232// to `dig-node`'s `relay::DEFAULT_RELAY_URL` (the string a node actually dials
233// when `DIG_RELAY_URL` is unset) and to the `dig-relay` server's documented
234// client endpoint.
235//
236// Port 443: the live `relay.dig.net` NLB exposes its public TLS listener on the
237// standard HTTPS port 443 (the earlier :9450 listener is closed). Using 443 also
238// maximizes reachability from restrictive networks that only allow outbound 443.
239// =============================================================================
240
241/// Canonical DIG NAT-traversal relay endpoint.
242///
243/// This is the WebSocket URL a DIG Node dials by default to obtain a relay
244/// reservation (so NAT'd peers stay reachable). It is the value used unless an
245/// operator overrides it via the `DIG_RELAY_URL` environment variable (or
246/// disables the reservation with `DIG_RELAY_URL=off`).
247///
248/// Format: `wss://<host>:<port>` — the relay protocol (`RelayMessage`,
249/// RLY-001..RLY-007) is JSON over a secure WebSocket. Mainnet uses the canonical
250/// public deployment `relay.dig.net` on port 443 (the live NLB public TLS
251/// listener; the earlier :9450 listener is closed).
252///
253/// Kept byte-identical to `dig-node`'s `relay::DEFAULT_RELAY_URL` and the
254/// `dig-relay` server's documented client endpoint.
255pub const DIG_RELAY_URL: &str = "wss://relay.dig.net:443";
256
257// =============================================================================
258// DIG Node localhost endpoint
259//
260// A client connecting to a local DIG node (§5.3 client→node connection order)
261// resolves `dig.local` or `localhost` to reach the node via localhost TCP on
262// port 9778. This constant is the single source of truth for that port so
263// consumers (dig-node, dig-dns, dig-installer, SDK, CLI) don't each hardcode it.
264// =============================================================================
265
266/// The default localhost port a client uses to reach the local DIG node.
267///
268/// This is used to implement §5.3 client→node connection order: when a client
269/// needs to connect to a DIG node, it tries `dig.local` and `localhost` on this
270/// port before falling back to the public `rpc.dig.net` gateway. This constant
271/// ensures all consumers (dig-node, dig-dns, dig-installer, dig-sdk, digstore CLI)
272/// use an identical port, preventing port-mismatch bugs. It MUST stay byte-identical
273/// to `dig-node`'s documented localhost serve port and the installer's registered
274/// `dig.local` address.
275pub const DIG_NODE_PORT: u16 = 9778;
276
277/// The mDNS/local hostname the installed DIG node registers.
278///
279/// This is the FIRST tier of the §5.3 client→node connection order: a client
280/// tries `dig.local` (on [`DIG_NODE_PORT`]) before falling back to `localhost`
281/// and finally the public [`RPC_DIG_NET_URL`] gateway. This constant ensures
282/// all consumers (dig-node, dig-dns, dig-installer, dig-sdk, digstore CLI) use
283/// an identical hostname, preventing drift between the address the installer
284/// registers and the address clients probe.
285pub const DIG_LOCAL_HOST: &str = "dig.local";
286
287/// The public DIG read gateway.
288///
289/// This is the FINAL-FALLBACK tier of the §5.3 client→node connection order:
290/// a client falls through to this plain-HTTPS public read tier only when
291/// neither `dig.local` nor `localhost` (both on [`DIG_NODE_PORT`]) responds.
292/// This constant ensures all consumers (dig-download, digstore CLI, dig-sdk,
293/// dig-node) reference an identical gateway URL instead of each hardcoding
294/// their own copy of `rpc.dig.net`.
295pub const RPC_DIG_NET_URL: &str = "https://rpc.dig.net";
296
297// =============================================================================
298// DIG CAT asset id ($DIG token)
299//
300// $DIG is a Chia CAT (CHIP-0004); its asset id is the TAIL program's hash,
301// fixed for the token's lifetime. This is the single canonical home for that
302// value — `chip35_dl_coin`, `dig-cat-decoder`, and any DIG-aware wallet/
303// balance/spend code import it from HERE rather than each hardcoding a copy.
304// =============================================================================
305
306/// Canonical $DIG CAT asset id (TAIL hash) on Chia mainnet.
307///
308/// The single token every capsule (commit) payment is denominated in
309/// (`chip35_dl_coin::build_dig_store_payment`) and the value a wallet/decoder
310/// checks a CAT coin's `asset_id` against to recognize $DIG.
311///
312/// CONTRACT: byte-identical to `chip35_dl_coin::DIG_ASSET_ID`, digstore-chain's
313/// `DIG_ASSET_ID`, and DataLayer-Driver's. Do not change without changing every
314/// consumer in lockstep (SYSTEM.md → Shared contracts → DIG CAT payment).
315pub const DIG_ASSET_ID: Bytes32 = Bytes32::new(hex!(
316    "a406d3a9de984d03c9591c10d917593b434d5263cabe2b42f6b367df16832f81"
317));
318
319// =============================================================================
320// DIG treasury recipient (destination of $DIG payments + dev-tips)
321//
322// Every $DIG capsule/commit payment and dev-tip is created-coin'd to the DIG
323// treasury. This section is the single canonical home for that recipient in two
324// equivalent forms: the on-chain inner (standard) puzzle hash and its bech32m
325// address. A WRONG value here silently MISDIRECTS funds to an attacker/void —
326// a custody break — so both forms are pinned byte-for-byte by tests, and a KAT
327// proves the address decodes to the puzzle hash (they cannot drift apart).
328//
329// CONTRACT: dig-constants is the intended canonical LOWEST-level home for this
330// value. The existing higher-level copies (`digstore_chain::dig`,
331// `chip35_dl_coin`, `dighub-core`) SHOULD later converge to re-export from HERE.
332// That convergence is a SEPARATE follow-up — this change only introduces the
333// canonical constants; it does not touch those crates. Until convergence, this
334// value stays byte-identical to `digstore_chain::dig` (the current source of
335// truth: `TREASURY_ADDRESS` at `crates/digstore-chain/src/dig.rs:41`, from which
336// it derives `treasury_inner_puzzle_hash()`, pinned by its test at dig.rs:206-209).
337// =============================================================================
338
339/// Canonical DIG treasury inner (standard) puzzle hash.
340///
341/// The on-chain recipient every $DIG capsule/commit payment and dev-tip is
342/// created-coin'd to. A wrong value silently misdirects treasury funds (a
343/// custody break), so it is pinned byte-for-byte by a test.
344///
345/// CONTRACT: byte-identical to what `digstore_chain::dig::treasury_inner_puzzle_hash()`
346/// decodes to (pinned by that crate's test at `crates/digstore-chain/src/dig.rs:206-209`).
347/// dig-constants is the intended canonical lowest-level home; higher copies
348/// (`digstore_chain::dig`, `chip35_dl_coin`, `dighub-core`) should later
349/// re-export from here (a separate follow-up — see the section note above).
350pub const DIG_TREASURY_INNER_PUZZLE_HASH: Bytes32 = Bytes32::new(hex!(
351    "ec7c304708c7d59c078d5ae098d0dea004decf47fa1cafebb266c10ad6466ce8"
352));
353
354/// Canonical DIG treasury address (bech32m form of [`DIG_TREASURY_INNER_PUZZLE_HASH`]).
355///
356/// The human-readable `xch1…` form of the same treasury recipient — the
357/// destination of $DIG payments and dev-tips. A wrong value misdirects funds
358/// (a custody break), so it is pinned by a test AND a KAT proves it decodes to
359/// [`DIG_TREASURY_INNER_PUZZLE_HASH`] (the two forms cannot silently drift).
360///
361/// CONTRACT: digstore-chain's source-of-truth form (`digstore_chain::dig::TREASURY_ADDRESS`,
362/// `crates/digstore-chain/src/dig.rs:41`), from which it derives the puzzle hash
363/// at runtime. dig-constants is the intended canonical lowest-level home; higher
364/// copies should later re-export from here (a separate follow-up).
365pub const DIG_TREASURY_ADDRESS: &str =
366    "xch1a37rq3cgcl2ecpudttsf35x75qzdan68lgw2l6ajvmqs44jxdn5qv6pk3y";
367
368// =============================================================================
369// Chia L1 (foreign chain) AGG_SIG_ME additional data
370//
371// The DIG wallet signs and validates spends on the Chia L1 chain. On Chia L1 the
372// AGG_SIG_ME additional data IS the network genesis challenge, so every L1 spend
373// signature is bound to it. This is a FOREIGN chain's value — completely distinct
374// from the DIG L2 genesis (`DIG_MAINNET_GENESIS_CHALLENGE`, 0af98186…).
375//
376// Both the wallet's signer seam AND the engine's message-binding seam MUST read
377// the SAME 32 bytes from here, or a spend the engine builds is signed with a
378// different domain than it binds — a custody break (invalid, unspendable
379// signatures on mainnet). This crate is the single source of truth for those
380// bytes; the `[u8; 32]` shape matches the signer field directly (the engine wraps
381// it once via `Bytes32::new(...)`).
382//
383// The value is invariant-forced: it is exactly Chia's well-known mainnet genesis
384// (ccd5bb71…) / testnet11 genesis (37a90eb5…), the same values
385// `chia-wallet-sdk`'s `MAINNET_CONSTANTS` / `TESTNET11_CONSTANTS` carry (asserted
386// by an anti-drift dev-dependency test).
387// =============================================================================
388
389/// Chia **L1 mainnet** genesis challenge, used as AGG_SIG_ME additional data.
390///
391/// The 32-byte domain every Chia L1 mainnet spend signature is bound to. This is
392/// the foreign-chain (Chia) value — DISTINCT from the DIG L2 genesis
393/// ([`DIG_MAINNET`]); signing an L1 spend with the DIG L2 genesis yields an
394/// invalid signature.
395///
396/// CONTRACT: DIG wallet consumers (the client signer AND the engine's
397/// message-binding path) MUST both use this constant so signer == engine,
398/// producing byte-identical, valid signatures. Equals Chia's canonical mainnet
399/// genesis `ccd5bb71…` (== `chia_sdk_types::MAINNET_CONSTANTS.agg_sig_me_additional_data`).
400pub const CHIA_L1_MAINNET_AGG_SIG_ME: [u8; 32] =
401    hex!("ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb");
402
403/// Chia **L1 testnet11** genesis challenge, used as AGG_SIG_ME additional data.
404///
405/// The 32-byte domain every Chia L1 testnet11 spend signature is bound to. As
406/// with [`CHIA_L1_MAINNET_AGG_SIG_ME`], this is the foreign-chain (Chia) value,
407/// DISTINCT from the DIG L2 genesis ([`DIG_TESTNET`]).
408///
409/// CONTRACT: DIG wallet consumers (signer AND engine) MUST both use this constant
410/// so signer == engine on testnet11. Equals Chia's canonical testnet11 genesis
411/// `37a90eb5…` (== `chia_sdk_types::TESTNET11_CONSTANTS.agg_sig_me_additional_data`).
412pub const CHIA_L1_TESTNET11_AGG_SIG_ME: [u8; 32] =
413    hex!("37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615");
414
415// ---------------------------------------------------------------------------
416// DIG Testnet
417// ---------------------------------------------------------------------------
418
419/// Canonical DIG testnet genesis challenge.
420///
421/// Deterministically derived as `sha256(b"DIG_TESTNET:genesis:v1")` — distinct
422/// from mainnet so the two networks never share a `network_id`. Non-zero so the
423/// gossip network_id gate accepts it. Pre-launch canonical value (§3.7),
424/// revisable at true launch; all derived agg_sig data below follows it.
425///   = 088c18d6b7859d885dc2f03166e862c958f74b63b6353c3df71d103b9b806c3b
426const DIG_TESTNET_GENESIS_CHALLENGE: [u8; 32] =
427    hex!("088c18d6b7859d885dc2f03166e862c958f74b63b6353c3df71d103b9b806c3b");
428
429/// DIG testnet constants.
430///
431/// Same structure as mainnet but with a different genesis challenge.
432/// Useful for testing without risking mainnet state.
433pub const DIG_TESTNET: NetworkConstants = NetworkConstants {
434    inner: ConsensusConstants {
435        genesis_challenge: Bytes32::new(DIG_TESTNET_GENESIS_CHALLENGE),
436        // AGG_SIG_ME = genesis_challenge. Others = sha256(genesis || opcode_byte).
437        agg_sig_me_additional_data: Bytes32::new(DIG_TESTNET_GENESIS_CHALLENGE),
438        agg_sig_parent_additional_data: Bytes32::new(hex!(
439            "85b3963bdeb9848af970a9bbd1d36809ae41491ffd67aee7f27e8883936d495c"
440        )),
441        agg_sig_puzzle_additional_data: Bytes32::new(hex!(
442            "66aba1939e128e1465d58fde414325630e891747c1428d76ebce193cbe966301"
443        )),
444        agg_sig_amount_additional_data: Bytes32::new(hex!(
445            "eccab86920a6d982a68898b2dcb7c150383529fcd532fe84c693fb4592c38ae3"
446        )),
447        agg_sig_puzzle_amount_additional_data: Bytes32::new(hex!(
448            "eb088fad0d4caba66e29130fb07407e60a7545d035d19a188fef0855c874084e"
449        )),
450        agg_sig_parent_amount_additional_data: Bytes32::new(hex!(
451            "232aec0a351ba4936b04920e074aebcc621a458f6b1461c4b28c658552f2f35d"
452        )),
453        agg_sig_parent_puzzle_additional_data: Bytes32::new(hex!(
454            "96263ac395703ab9b3b0f0587e79185f4a9898574a28b4491015ddcf9d321873"
455        )),
456        // All other fields same as mainnet
457        max_block_cost_clvm: 11_000_000_000,
458        cost_per_byte: 12_000,
459        max_coin_amount: u64::MAX,
460        max_generator_ref_list_size: 512,
461        hard_fork_height: 0,
462        hard_fork2_height: 0,
463        genesis_pre_farm_pool_puzzle_hash: Bytes32::new([0u8; 32]),
464        genesis_pre_farm_farmer_puzzle_hash: Bytes32::new([0u8; 32]),
465        slot_blocks_target: 32,
466        min_blocks_per_challenge_block: 16,
467        max_sub_slot_blocks: 128,
468        num_sps_sub_slot: 64,
469        sub_slot_iters_starting: 1 << 27,
470        difficulty_constant_factor: 1 << 67,
471        difficulty_starting: 7,
472        difficulty_change_max_factor: 3,
473        sub_epoch_blocks: 384,
474        epoch_blocks: 4608,
475        significant_bits: 8,
476        discriminant_size_bits: 1024,
477        number_zero_bits_plot_filter_v1: 9,
478        number_zero_bits_plot_filter_v2: 9,
479        min_plot_size_v1: 32,
480        max_plot_size_v1: 50,
481        plot_size_v2: 28,
482        sub_slot_time_target: 600,
483        num_sp_intervals_extra: 3,
484        max_future_time2: 120,
485        number_of_timestamps: 11,
486        max_vdf_witness_size: 64,
487        mempool_block_buffer: 10,
488        weight_proof_threshold: 2,
489        blocks_cache_size: 4608 + (128 * 4),
490        weight_proof_recent_blocks: 1000,
491        max_block_count_per_requests: 32,
492        pool_sub_slot_iters: 37_600_000_000,
493        plot_filter_128_height: 0xffff_ffff,
494        plot_filter_64_height: 0xffff_ffff,
495        plot_filter_32_height: 0xffff_ffff,
496        plot_v1_phase_out_epoch_bits: 8,
497        min_plot_strength: 2,
498        max_plot_strength: 32,
499        plot_filter_v2_first_adjustment_height: 0xffff_ffff,
500        plot_filter_v2_second_adjustment_height: 0xffff_ffff,
501        plot_filter_v2_third_adjustment_height: 0xffff_ffff,
502    },
503};
504
505// =============================================================================
506// Profile DEK at-rest byte contract
507//
508// A DIG user profile's data-encryption-key (DEK) is derived, never stored, from
509// the user's identity scalar via HKDF-SHA256:
510//
511//   HKDF-SHA256(salt = DEK_SALT,
512//               ikm  = IDENTITY_IKM_VERSION || identity_scalar_32,
513//               info = PROFILE_DEK_LABEL)
514//     -> SYMMETRIC_KEY_LEN bytes
515//
516// These four values are a PERMANENT at-rest byte-identical contract (§4.1/§5.1/
517// NC-5): every sealed profile on disk was encrypted with a DEK derived from
518// EXACTLY these bytes. Changing any one of them re-derives a different key and
519// makes every already-sealed profile permanently unreadable — there is no
520// migration path for a derived (never-stored) key. Treat this section as
521// frozen; only ever ADD a new version-scoped label/version alongside it.
522//
523// Consumers (this crate is their single source of truth — do not duplicate the
524// literals locally):
525//   - dig-app:    crates/dig-app-core/src/keystore/secrets.rs
526//   - dig-session: src/unlocked.rs (derive_symmetric_key)
527// =============================================================================
528
529/// HKDF salt for the per-profile DEK derivation.
530///
531/// Part of the frozen [profile DEK byte contract](self#profile-dek-at-rest-byte-contract)
532/// — see the section comment above. Consumed by dig-app's
533/// `keystore/secrets.rs` and dig-session's `derive_symmetric_key`.
534pub const DEK_SALT: &[u8] = b"dig-app:dek-salt:v1";
535
536/// Version byte prefixed to the 32-byte identity scalar to form the DEK's HKDF
537/// input key material (`IDENTITY_IKM_VERSION || identity_scalar_32`).
538///
539/// Part of the frozen [profile DEK byte contract](self#profile-dek-at-rest-byte-contract).
540/// Consumed by dig-app's `keystore/secrets.rs` and dig-session's
541/// `derive_symmetric_key`.
542pub const IDENTITY_IKM_VERSION: u8 = 2;
543
544/// HKDF info/label for the per-profile DEK derivation.
545///
546/// Part of the frozen [profile DEK byte contract](self#profile-dek-at-rest-byte-contract).
547/// Consumed by dig-app's `keystore/secrets.rs` and dig-session's
548/// `derive_symmetric_key`.
549pub const PROFILE_DEK_LABEL: &[u8] = b"dig-app:profile-dek:v2";
550
551/// Output length, in bytes, of the derived per-profile DEK (HKDF-SHA256's
552/// natural output for a symmetric AEAD key).
553///
554/// Part of the frozen [profile DEK byte contract](self#profile-dek-at-rest-byte-contract).
555/// Consumed by dig-app's `keystore/secrets.rs` and dig-session's
556/// `derive_symmetric_key`.
557pub const SYMMETRIC_KEY_LEN: usize = 32;
558
559// =============================================================================
560// Profile sealing X25519 byte contract
561//
562// A DIG user profile's per-profile X25519 *sealing* keypair (used by the DIG
563// App to seal/unseal `DIGCHAT1` messages for dig-chat, §NC-1 end-to-end
564// encryption) is derived, never stored, from the same identity scalar as the
565// DEK — but under a DISTINCT HKDF `info` label, which is the sole thing that
566// domain-separates the sealing key from the at-rest DEK:
567//
568//   HKDF-SHA256(salt = DEK_SALT,
569//               ikm  = IDENTITY_IKM_VERSION || identity_scalar_32,
570//               info = PROFILE_SEALING_X25519_LABEL)
571//     -> SYMMETRIC_KEY_LEN (32) bytes, then CLAMPED to an X25519 scalar
572//
573// This label reuses the already-frozen DEK_SALT + IDENTITY_IKM_VERSION on
574// purpose; only the `info` label differs. The 32-byte HKDF output is clamped
575// to a valid X25519 secret scalar by the CONSUMER (dig-account) — this crate
576// owns ONLY the frozen label bytes, not the clamp/derivation.
577//
578// The label is a PERMANENT byte-identical contract (§4.1/§5.1/NC-1): every
579// message already sealed on the network was encrypted under a keypair derived
580// from EXACTLY these bytes. Changing it re-derives a different keypair and
581// makes every already-sealed message permanently unopenable — there is no
582// migration path for a derived (never-stored) key. Treat it as frozen; only
583// ever ADD a new version-scoped label (`…:v2`) alongside it, never mutate it.
584//
585// Consumers (this crate is their single source of truth — do not duplicate the
586// literal locally):
587//   - dig-account: derives the sealing keypair via
588//     `seed.profile_derive_symmetric_key(ix, PROFILE_SEALING_X25519_LABEL)`
589//     then clamps the output to an X25519 scalar.
590// =============================================================================
591
592/// HKDF info/label for deriving a profile's per-profile X25519 **sealing**
593/// keypair — the key the DIG App uses to seal/unseal `DIGCHAT1` messages.
594///
595/// Part of the frozen [profile sealing X25519 byte
596/// contract](self#profile-sealing-x25519-byte-contract) — see the section
597/// comment above. Reuses [`DEK_SALT`] + [`IDENTITY_IKM_VERSION`]; this distinct
598/// `info` label is what domain-separates the sealing key from [`PROFILE_DEK_LABEL`].
599/// The 32-byte HKDF output is clamped to an X25519 scalar by the consumer
600/// (dig-account), not here.
601pub const PROFILE_SEALING_X25519_LABEL: &[u8] = b"dig-app:profile-sealing-x25519:v1";
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    /// The canonical relay endpoint must equal exactly what a DIG Node dials by
608    /// default. This pins the value byte-for-byte against `dig-node`'s
609    /// `relay::DEFAULT_RELAY_URL` (`wss://relay.dig.net:9450`) and the
610    /// `dig-relay` server's documented client endpoint. If either side ever
611    /// changes the scheme, host, or port, this guard fails so the shared
612    /// contract can't silently drift.
613    #[test]
614    fn dig_relay_url_is_canonical_endpoint() {
615        assert_eq!(DIG_RELAY_URL, "wss://relay.dig.net:443");
616    }
617
618    /// The relay endpoint is a secure-WebSocket URL pointing at the canonical
619    /// public host on the relay protocol port.
620    #[test]
621    fn dig_relay_url_is_well_formed() {
622        assert!(
623            DIG_RELAY_URL.starts_with("wss://"),
624            "relay must use secure WebSocket"
625        );
626        assert!(
627            DIG_RELAY_URL.contains("relay.dig.net"),
628            "relay must point at the canonical host"
629        );
630        assert!(
631            DIG_RELAY_URL.ends_with(":443"),
632            "relay must use the live NLB public TLS port 443"
633        );
634    }
635
636    /// The DIG node localhost port must equal the expected default.
637    ///
638    /// This guards against accidental mutations and ensures all consumers
639    /// (dig-node, dig-dns, dig-installer, dig-sdk, digstore) use a consistent
640    /// port when connecting to the local node on `dig.local` or `localhost`.
641    #[test]
642    fn dig_node_port_is_canonical() {
643        assert_eq!(DIG_NODE_PORT, 9778);
644    }
645
646    /// The local-node hostname must equal the expected default.
647    ///
648    /// This guards the first tier of the §5.3 client→node connection order —
649    /// a drift here would desync the installer's registered address from what
650    /// clients probe.
651    #[test]
652    fn dig_local_host_is_canonical() {
653        assert_eq!(DIG_LOCAL_HOST, "dig.local");
654    }
655
656    /// The public read gateway must equal the expected default.
657    ///
658    /// This guards the final-fallback tier of the §5.3 client→node connection
659    /// order — the gateway every consumer falls through to when no local node
660    /// responds.
661    #[test]
662    fn rpc_dig_net_url_is_canonical() {
663        assert_eq!(RPC_DIG_NET_URL, "https://rpc.dig.net");
664    }
665
666    /// The public read gateway is a plain-HTTPS URL (the public read tier,
667    /// distinct from the mTLS transport node-class clients use, §5.3).
668    #[test]
669    fn rpc_dig_net_url_is_well_formed() {
670        assert!(
671            RPC_DIG_NET_URL.starts_with("https://"),
672            "the public read gateway must use HTTPS"
673        );
674    }
675
676    // -- Genesis challenge canonical-value guards --------------------------
677    //
678    // These pin the pre-launch canonical genesis challenges byte-for-byte AND
679    // prove they are reproducible from their documented preimages, so the
680    // values can never silently drift (a drift changes every derived signature
681    // domain + the gossip network_id — a cross-repo breaking event).
682
683    use sha2::{Digest, Sha256};
684
685    /// AGG_SIG opcode bytes, per §4.2 of `SPEC.md` (Chia L1 `condition_tools`).
686    const AGG_SIG_OPCODES: [u8; 6] = [43, 44, 45, 46, 47, 48];
687
688    fn sha256(bytes: &[u8]) -> [u8; 32] {
689        let mut hasher = Sha256::new();
690        hasher.update(bytes);
691        hasher.finalize().into()
692    }
693
694    /// The genesis MUST be non-zero: `dig-gossip` rejects an all-zero
695    /// `network_id`, so a zero genesis would stop the node's gossip pool / DHT /
696    /// PEX from ever starting. This is the connect-enabler invariant.
697    #[test]
698    fn genesis_challenges_are_non_zero() {
699        assert_ne!(DIG_MAINNET.genesis_challenge(), Bytes32::new([0u8; 32]));
700        assert_ne!(DIG_TESTNET.genesis_challenge(), Bytes32::new([0u8; 32]));
701    }
702
703    /// The mainnet genesis is pinned to the Chia mainnet header hash @ height
704    /// 9,021,277 (a real anchored value), and the testnet genesis is the
705    /// reproducible `sha256` of its documented preimage. These pin both values
706    /// byte-for-byte so neither can silently drift.
707    #[test]
708    fn genesis_challenges_are_the_pinned_values() {
709        assert_eq!(
710            DIG_MAINNET_GENESIS_CHALLENGE,
711            hex_literal::hex!("0af981862a4df51f51ec59c312315d959931d917c375730b89b9e2b0854d1abf"),
712        );
713        assert_eq!(
714            DIG_TESTNET_GENESIS_CHALLENGE,
715            sha256(b"DIG_TESTNET:genesis:v1"),
716        );
717    }
718
719    /// Mainnet and testnet MUST NOT share a genesis (no cross-network replay).
720    #[test]
721    fn mainnet_and_testnet_genesis_differ() {
722        assert_ne!(
723            DIG_MAINNET.genesis_challenge(),
724            DIG_TESTNET.genesis_challenge(),
725        );
726    }
727
728    /// Pins the $DIG CAT asset id byte-for-byte against the value shipped in
729    /// `chip35_dl_coin::DIG_ASSET_ID` — a drift here silently breaks $DIG
730    /// recognition across every consumer (wallets, decoders, payment builders).
731    #[test]
732    fn dig_asset_id_is_canonical() {
733        assert_eq!(
734            DIG_ASSET_ID,
735            Bytes32::new(hex_literal::hex!(
736                "a406d3a9de984d03c9591c10d917593b434d5263cabe2b42f6b367df16832f81"
737            )),
738        );
739    }
740
741    // -- Chia L1 AGG_SIG_ME anti-drift guards ------------------------------
742
743    /// Literal pin: the Chia L1 AGG_SIG_ME constants equal Chia's well-known
744    /// mainnet / testnet11 genesis challenges byte-for-byte. This catches any
745    /// accidental mutation independently of any external crate.
746    #[test]
747    fn chia_l1_agg_sig_me_constants_are_the_pinned_values() {
748        assert_eq!(
749            CHIA_L1_MAINNET_AGG_SIG_ME,
750            hex_literal::hex!("ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb"),
751        );
752        assert_eq!(
753            CHIA_L1_TESTNET11_AGG_SIG_ME,
754            hex_literal::hex!("37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615"),
755        );
756    }
757
758    /// Source KAT: the Chia L1 constants MUST equal the values `chia-wallet-sdk`
759    /// (via `chia-sdk-types`) uses in its `MAINNET_CONSTANTS` / `TESTNET11_CONSTANTS`.
760    /// This is the primary anti-drift guard — the wallet engine binds spends with
761    /// those SDK constants, so if a future SDK version ever changed the value, this
762    /// fails and forces a deliberate re-pin instead of a silent custody break.
763    #[test]
764    fn chia_l1_agg_sig_me_matches_chia_sdk_types() {
765        use chia_sdk_types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS};
766        assert_eq!(
767            CHIA_L1_MAINNET_AGG_SIG_ME.as_slice(),
768            MAINNET_CONSTANTS.agg_sig_me_additional_data.as_ref(),
769        );
770        assert_eq!(
771            CHIA_L1_TESTNET11_AGG_SIG_ME.as_slice(),
772            TESTNET11_CONSTANTS.agg_sig_me_additional_data.as_ref(),
773        );
774    }
775
776    /// The Chia L1 (foreign chain) AGG_SIG_ME MUST NOT equal the DIG L2 genesis —
777    /// this is the whole reason the constants exist. Signing an L1 spend with the
778    /// DIG L2 genesis would be a custody break.
779    #[test]
780    fn chia_l1_agg_sig_me_differs_from_dig_l2_genesis() {
781        assert_ne!(
782            Bytes32::new(CHIA_L1_MAINNET_AGG_SIG_ME),
783            DIG_MAINNET.genesis_challenge(),
784        );
785        assert_ne!(
786            Bytes32::new(CHIA_L1_TESTNET11_AGG_SIG_ME),
787            DIG_TESTNET.genesis_challenge(),
788        );
789    }
790
791    // -- DIG treasury recipient anti-drift guards --------------------------
792
793    /// Literal pin: the treasury inner puzzle hash equals the value
794    /// `digstore_chain::dig::treasury_inner_puzzle_hash()` decodes to
795    /// (byte-identical, pinned by that crate's own test at
796    /// `crates/digstore-chain/src/dig.rs:206-209`). A drift here silently
797    /// MISDIRECTS every $DIG capsule/commit payment and dev-tip to the wrong
798    /// on-chain recipient — a custody break.
799    #[test]
800    fn dig_treasury_inner_puzzle_hash_is_canonical() {
801        assert_eq!(
802            DIG_TREASURY_INNER_PUZZLE_HASH,
803            Bytes32::new(hex_literal::hex!(
804                "ec7c304708c7d59c078d5ae098d0dea004decf47fa1cafebb266c10ad6466ce8"
805            )),
806        );
807    }
808
809    /// Literal pin: the treasury address equals digstore-chain's
810    /// source-of-truth bech32m form (`digstore_chain::dig::TREASURY_ADDRESS`,
811    /// `crates/digstore-chain/src/dig.rs:41`). A drift misdirects funds.
812    #[test]
813    fn dig_treasury_address_is_canonical() {
814        assert_eq!(
815            DIG_TREASURY_ADDRESS,
816            "xch1a37rq3cgcl2ecpudttsf35x75qzdan68lgw2l6ajvmqs44jxdn5qv6pk3y",
817        );
818    }
819
820    /// KAT: the bech32m address and the inner puzzle hash cannot silently drift
821    /// apart. Decodes `DIG_TREASURY_ADDRESS` (HRP `xch`, bech32m) and asserts
822    /// the 32 decoded bytes equal `DIG_TREASURY_INNER_PUZZLE_HASH`, proving the
823    /// two constants encode the SAME on-chain recipient.
824    #[test]
825    fn dig_treasury_address_decodes_to_inner_puzzle_hash() {
826        use bech32::Hrp;
827        let (hrp, data) = bech32::decode(DIG_TREASURY_ADDRESS).expect("valid bech32m");
828        assert_eq!(hrp, Hrp::parse("xch").unwrap(), "HRP must be xch");
829        assert_eq!(
830            data.as_slice(),
831            DIG_TREASURY_INNER_PUZZLE_HASH.to_bytes(),
832            "address must decode to the pinned inner puzzle hash",
833        );
834    }
835
836    // -- Profile DEK at-rest byte-contract guards ---------------------------
837    //
838    // These pin every DEK-derivation constant literally so a future edit can't
839    // silently drift the contract (which would make every already-sealed
840    // profile permanently unreadable, §5.1).
841
842    #[test]
843    fn dek_salt_is_the_pinned_value() {
844        assert_eq!(DEK_SALT, b"dig-app:dek-salt:v1");
845    }
846
847    #[test]
848    fn identity_ikm_version_is_the_pinned_value() {
849        assert_eq!(IDENTITY_IKM_VERSION, 2);
850    }
851
852    #[test]
853    fn profile_dek_label_is_the_pinned_value() {
854        assert_eq!(PROFILE_DEK_LABEL, b"dig-app:profile-dek:v2");
855    }
856
857    #[test]
858    fn symmetric_key_len_is_the_pinned_value() {
859        assert_eq!(SYMMETRIC_KEY_LEN, 32);
860    }
861
862    /// The per-profile X25519 sealing label is a PERMANENT crypto byte contract
863    /// (§5.1): every `DIGCHAT1` message a DIG user has ever sealed was encrypted
864    /// under a sealing key derived from EXACTLY these bytes. A drift here would
865    /// re-derive a different keypair and make every already-sealed message
866    /// permanently unopenable. This pins the label literally so no future edit
867    /// can silently change it.
868    #[test]
869    fn profile_sealing_x25519_label_is_the_pinned_value() {
870        assert_eq!(
871            PROFILE_SEALING_X25519_LABEL,
872            b"dig-app:profile-sealing-x25519:v1"
873        );
874    }
875
876    /// The sealing label MUST be distinct from the DEK label — a shared `info`
877    /// would derive the same 32 bytes for both the at-rest DEK and the X25519
878    /// sealing key, collapsing the domain separation the two labels exist to
879    /// provide. This guards that domain separation directly.
880    #[test]
881    fn profile_sealing_label_is_domain_separated_from_dek_label() {
882        assert_ne!(PROFILE_SEALING_X25519_LABEL, PROFILE_DEK_LABEL);
883    }
884
885    /// Every baked-in AGG_SIG additional-data value MUST equal the §4.1 rule
886    /// applied to the network's genesis: AGG_SIG_ME == genesis, and each other
887    /// variant == `sha256(genesis || opcode_byte)`. This regenerates the values
888    /// independently and asserts the constants match — so a genesis change that
889    /// forgets to recompute a derived value is caught.
890    #[test]
891    fn agg_sig_additional_data_matches_derivation_rule() {
892        for net in [&DIG_MAINNET, &DIG_TESTNET] {
893            let genesis = net.genesis_challenge();
894            assert_eq!(net.agg_sig_me_additional_data(), genesis);
895
896            let c = net.consensus();
897            let derived: Vec<Bytes32> = AGG_SIG_OPCODES
898                .iter()
899                .map(|&op| {
900                    let mut preimage = genesis.as_ref().to_vec();
901                    preimage.push(op);
902                    Bytes32::new(sha256(&preimage))
903                })
904                .collect();
905            assert_eq!(c.agg_sig_parent_additional_data, derived[0]);
906            assert_eq!(c.agg_sig_puzzle_additional_data, derived[1]);
907            assert_eq!(c.agg_sig_amount_additional_data, derived[2]);
908            assert_eq!(c.agg_sig_puzzle_amount_additional_data, derived[3]);
909            assert_eq!(c.agg_sig_parent_amount_additional_data, derived[4]);
910            assert_eq!(c.agg_sig_parent_puzzle_additional_data, derived[5]);
911        }
912    }
913}