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 443 (see the port note below).
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/// The always-on peer anchors a node dials at startup, as `peer_id@host:port`.
298///
299/// # Why a fresh node needs this
300///
301/// Every other way a node learns peers already requires having one: peer exchange
302/// spreads the peers a live link's far end knows, the DHT answers queries routed
303/// through peers already in the table, and a relay reservation only makes a node
304/// *reachable*. A node installed onto a machine that has never run one therefore
305/// has nothing to dial. This set is the one input that does not presuppose its own
306/// output.
307///
308/// # Why the host is `node-rpc.dig.net` and NOT `rpc.dig.net`
309///
310/// These are different machines with different jobs, and confusing them ships a
311/// dial at a closed port. [`RPC_DIG_NET_URL`] is the §5.3 client→node READ gateway:
312/// a CloudFront distribution that terminates HTTPS and cannot carry the mTLS peer
313/// protocol — its peer ports are closed. `node-rpc.dig.net` is that distribution's
314/// ORIGIN, an instance that answers the peer protocol directly. Both names are
315/// legitimate and they MUST NOT be collapsed into one another.
316///
317/// # Why each entry carries an identity
318///
319/// The node↔node interface is mTLS with the peer's certificate pinned by
320/// `peer_id = SHA-256(TLS SPKI DER)`, so an address alone is not dialable. An entry
321/// without an identity could only be dialled unpinned — accepting whatever answered
322/// at that address, which is exactly what the pinning exists to deny.
323///
324/// # A bootstrap peer is NOT a trusted peer
325///
326/// Being well-known is not being trusted. An anchor gets no trust flag, bypasses no
327/// corroboration, and counts as exactly one voice — the same as any peer learned by
328/// exchange. Consumers MUST treat it as untrusted (NC-12) and MUST tolerate every
329/// entry here being unreachable: a node whose bootstrap dials all fail is still a
330/// working node, and a hard dependency on one host would make it a single point of
331/// failure for every fresh node in the network.
332pub const DIG_BOOTSTRAP_PEERS: &[&str] =
333    &["741592c0e1e1e9b1a02d3e0bb165bfe54b7adbb5878a3c5de59893949524b68f@node-rpc.dig.net:9444"];
334
335// =============================================================================
336// DIG CAT asset id ($DIG token)
337//
338// $DIG is a Chia CAT (CHIP-0004); its asset id is the TAIL program's hash,
339// fixed for the token's lifetime. This is the single canonical home for that
340// value — `chip35_dl_coin`, `dig-cat-decoder`, and any DIG-aware wallet/
341// balance/spend code import it from HERE rather than each hardcoding a copy.
342// =============================================================================
343
344/// Canonical $DIG CAT asset id (TAIL hash) on Chia mainnet.
345///
346/// The single token every capsule (commit) payment is denominated in
347/// (`chip35_dl_coin::build_dig_store_payment`) and the value a wallet/decoder
348/// checks a CAT coin's `asset_id` against to recognize $DIG.
349///
350/// CONTRACT: byte-identical to `chip35_dl_coin::DIG_ASSET_ID`, digstore-chain's
351/// `DIG_ASSET_ID`, and DataLayer-Driver's. Do not change without changing every
352/// consumer in lockstep (SYSTEM.md → Shared contracts → DIG CAT payment).
353pub const DIG_ASSET_ID: Bytes32 = Bytes32::new(hex!(
354    "a406d3a9de984d03c9591c10d917593b434d5263cabe2b42f6b367df16832f81"
355));
356
357// =============================================================================
358// DIG treasury recipient (destination of $DIG payments + dev-tips)
359//
360// Every $DIG capsule/commit payment and dev-tip is created-coin'd to the DIG
361// treasury. This section is the single canonical home for that recipient in two
362// equivalent forms: the on-chain inner (standard) puzzle hash and its bech32m
363// address. A WRONG value here silently MISDIRECTS funds to an attacker/void —
364// a custody break — so both forms are pinned byte-for-byte by tests, and a KAT
365// proves the address decodes to the puzzle hash (they cannot drift apart).
366//
367// CONTRACT: dig-constants is the intended canonical LOWEST-level home for this
368// value. The existing higher-level copies (`digstore_chain::dig`,
369// `chip35_dl_coin`, `dighub-core`) SHOULD later converge to re-export from HERE.
370// That convergence is a SEPARATE follow-up — this change only introduces the
371// canonical constants; it does not touch those crates. Until convergence, this
372// value stays byte-identical to `digstore_chain::dig` (the current source of
373// truth: `TREASURY_ADDRESS` at `crates/digstore-chain/src/dig.rs:41`, from which
374// it derives `treasury_inner_puzzle_hash()`, pinned by its test at dig.rs:206-209).
375// =============================================================================
376
377/// Canonical DIG treasury inner (standard) puzzle hash.
378///
379/// The on-chain recipient every $DIG capsule/commit payment and dev-tip is
380/// created-coin'd to. A wrong value silently misdirects treasury funds (a
381/// custody break), so it is pinned byte-for-byte by a test.
382///
383/// CONTRACT: byte-identical to what `digstore_chain::dig::treasury_inner_puzzle_hash()`
384/// decodes to (pinned by that crate's test at `crates/digstore-chain/src/dig.rs:206-209`).
385/// dig-constants is the intended canonical lowest-level home; higher copies
386/// (`digstore_chain::dig`, `chip35_dl_coin`, `dighub-core`) should later
387/// re-export from here (a separate follow-up — see the section note above).
388pub const DIG_TREASURY_INNER_PUZZLE_HASH: Bytes32 = Bytes32::new(hex!(
389    "ec7c304708c7d59c078d5ae098d0dea004decf47fa1cafebb266c10ad6466ce8"
390));
391
392/// Canonical DIG treasury address (bech32m form of [`DIG_TREASURY_INNER_PUZZLE_HASH`]).
393///
394/// The human-readable `xch1…` form of the same treasury recipient — the
395/// destination of $DIG payments and dev-tips. A wrong value misdirects funds
396/// (a custody break), so it is pinned by a test AND a KAT proves it decodes to
397/// [`DIG_TREASURY_INNER_PUZZLE_HASH`] (the two forms cannot silently drift).
398///
399/// CONTRACT: digstore-chain's source-of-truth form (`digstore_chain::dig::TREASURY_ADDRESS`,
400/// `crates/digstore-chain/src/dig.rs:41`), from which it derives the puzzle hash
401/// at runtime. dig-constants is the intended canonical lowest-level home; higher
402/// copies should later re-export from here (a separate follow-up).
403pub const DIG_TREASURY_ADDRESS: &str =
404    "xch1a37rq3cgcl2ecpudttsf35x75qzdan68lgw2l6ajvmqs44jxdn5qv6pk3y";
405
406// =============================================================================
407// Chia L1 (foreign chain) AGG_SIG_ME additional data
408//
409// The DIG wallet signs and validates spends on the Chia L1 chain. On Chia L1 the
410// AGG_SIG_ME additional data IS the network genesis challenge, so every L1 spend
411// signature is bound to it. This is a FOREIGN chain's value — completely distinct
412// from the DIG L2 genesis (`DIG_MAINNET_GENESIS_CHALLENGE`, 0af98186…).
413//
414// Both the wallet's signer seam AND the engine's message-binding seam MUST read
415// the SAME 32 bytes from here, or a spend the engine builds is signed with a
416// different domain than it binds — a custody break (invalid, unspendable
417// signatures on mainnet). This crate is the single source of truth for those
418// bytes; the `[u8; 32]` shape matches the signer field directly (the engine wraps
419// it once via `Bytes32::new(...)`).
420//
421// The value is invariant-forced: it is exactly Chia's well-known mainnet genesis
422// (ccd5bb71…) / testnet11 genesis (37a90eb5…), the same values
423// `chia-wallet-sdk`'s `MAINNET_CONSTANTS` / `TESTNET11_CONSTANTS` carry (asserted
424// by an anti-drift dev-dependency test).
425// =============================================================================
426
427/// Chia **L1 mainnet** genesis challenge, used as AGG_SIG_ME additional data.
428///
429/// The 32-byte domain every Chia L1 mainnet spend signature is bound to. This is
430/// the foreign-chain (Chia) value — DISTINCT from the DIG L2 genesis
431/// ([`DIG_MAINNET`]); signing an L1 spend with the DIG L2 genesis yields an
432/// invalid signature.
433///
434/// CONTRACT: DIG wallet consumers (the client signer AND the engine's
435/// message-binding path) MUST both use this constant so signer == engine,
436/// producing byte-identical, valid signatures. Equals Chia's canonical mainnet
437/// genesis `ccd5bb71…` (== `chia_sdk_types::MAINNET_CONSTANTS.agg_sig_me_additional_data`).
438pub const CHIA_L1_MAINNET_AGG_SIG_ME: [u8; 32] =
439    hex!("ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb");
440
441/// Chia **L1 testnet11** genesis challenge, used as AGG_SIG_ME additional data.
442///
443/// The 32-byte domain every Chia L1 testnet11 spend signature is bound to. As
444/// with [`CHIA_L1_MAINNET_AGG_SIG_ME`], this is the foreign-chain (Chia) value,
445/// DISTINCT from the DIG L2 genesis ([`DIG_TESTNET`]).
446///
447/// CONTRACT: DIG wallet consumers (signer AND engine) MUST both use this constant
448/// so signer == engine on testnet11. Equals Chia's canonical testnet11 genesis
449/// `37a90eb5…` (== `chia_sdk_types::TESTNET11_CONSTANTS.agg_sig_me_additional_data`).
450pub const CHIA_L1_TESTNET11_AGG_SIG_ME: [u8; 32] =
451    hex!("37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615");
452
453// ---------------------------------------------------------------------------
454// DIG Testnet
455// ---------------------------------------------------------------------------
456
457/// Canonical DIG testnet genesis challenge.
458///
459/// Deterministically derived as `sha256(b"DIG_TESTNET:genesis:v1")` — distinct
460/// from mainnet so the two networks never share a `network_id`. Non-zero so the
461/// gossip network_id gate accepts it. Pre-launch canonical value (§3.7),
462/// revisable at true launch; all derived agg_sig data below follows it.
463///   = 088c18d6b7859d885dc2f03166e862c958f74b63b6353c3df71d103b9b806c3b
464const DIG_TESTNET_GENESIS_CHALLENGE: [u8; 32] =
465    hex!("088c18d6b7859d885dc2f03166e862c958f74b63b6353c3df71d103b9b806c3b");
466
467/// DIG testnet constants.
468///
469/// Same structure as mainnet but with a different genesis challenge.
470/// Useful for testing without risking mainnet state.
471pub const DIG_TESTNET: NetworkConstants = NetworkConstants {
472    inner: ConsensusConstants {
473        genesis_challenge: Bytes32::new(DIG_TESTNET_GENESIS_CHALLENGE),
474        // AGG_SIG_ME = genesis_challenge. Others = sha256(genesis || opcode_byte).
475        agg_sig_me_additional_data: Bytes32::new(DIG_TESTNET_GENESIS_CHALLENGE),
476        agg_sig_parent_additional_data: Bytes32::new(hex!(
477            "85b3963bdeb9848af970a9bbd1d36809ae41491ffd67aee7f27e8883936d495c"
478        )),
479        agg_sig_puzzle_additional_data: Bytes32::new(hex!(
480            "66aba1939e128e1465d58fde414325630e891747c1428d76ebce193cbe966301"
481        )),
482        agg_sig_amount_additional_data: Bytes32::new(hex!(
483            "eccab86920a6d982a68898b2dcb7c150383529fcd532fe84c693fb4592c38ae3"
484        )),
485        agg_sig_puzzle_amount_additional_data: Bytes32::new(hex!(
486            "eb088fad0d4caba66e29130fb07407e60a7545d035d19a188fef0855c874084e"
487        )),
488        agg_sig_parent_amount_additional_data: Bytes32::new(hex!(
489            "232aec0a351ba4936b04920e074aebcc621a458f6b1461c4b28c658552f2f35d"
490        )),
491        agg_sig_parent_puzzle_additional_data: Bytes32::new(hex!(
492            "96263ac395703ab9b3b0f0587e79185f4a9898574a28b4491015ddcf9d321873"
493        )),
494        // All other fields same as mainnet
495        max_block_cost_clvm: 11_000_000_000,
496        cost_per_byte: 12_000,
497        max_coin_amount: u64::MAX,
498        max_generator_ref_list_size: 512,
499        hard_fork_height: 0,
500        hard_fork2_height: 0,
501        genesis_pre_farm_pool_puzzle_hash: Bytes32::new([0u8; 32]),
502        genesis_pre_farm_farmer_puzzle_hash: Bytes32::new([0u8; 32]),
503        slot_blocks_target: 32,
504        min_blocks_per_challenge_block: 16,
505        max_sub_slot_blocks: 128,
506        num_sps_sub_slot: 64,
507        sub_slot_iters_starting: 1 << 27,
508        difficulty_constant_factor: 1 << 67,
509        difficulty_starting: 7,
510        difficulty_change_max_factor: 3,
511        sub_epoch_blocks: 384,
512        epoch_blocks: 4608,
513        significant_bits: 8,
514        discriminant_size_bits: 1024,
515        number_zero_bits_plot_filter_v1: 9,
516        number_zero_bits_plot_filter_v2: 9,
517        min_plot_size_v1: 32,
518        max_plot_size_v1: 50,
519        plot_size_v2: 28,
520        sub_slot_time_target: 600,
521        num_sp_intervals_extra: 3,
522        max_future_time2: 120,
523        number_of_timestamps: 11,
524        max_vdf_witness_size: 64,
525        mempool_block_buffer: 10,
526        weight_proof_threshold: 2,
527        blocks_cache_size: 4608 + (128 * 4),
528        weight_proof_recent_blocks: 1000,
529        max_block_count_per_requests: 32,
530        pool_sub_slot_iters: 37_600_000_000,
531        plot_filter_128_height: 0xffff_ffff,
532        plot_filter_64_height: 0xffff_ffff,
533        plot_filter_32_height: 0xffff_ffff,
534        plot_v1_phase_out_epoch_bits: 8,
535        min_plot_strength: 2,
536        max_plot_strength: 32,
537        plot_filter_v2_first_adjustment_height: 0xffff_ffff,
538        plot_filter_v2_second_adjustment_height: 0xffff_ffff,
539        plot_filter_v2_third_adjustment_height: 0xffff_ffff,
540    },
541};
542
543// =============================================================================
544// Profile DEK at-rest byte contract
545//
546// A DIG user profile's data-encryption-key (DEK) is derived, never stored, from
547// the user's identity scalar via HKDF-SHA256:
548//
549//   HKDF-SHA256(salt = DEK_SALT,
550//               ikm  = IDENTITY_IKM_VERSION || identity_scalar_32,
551//               info = PROFILE_DEK_LABEL)
552//     -> SYMMETRIC_KEY_LEN bytes
553//
554// These four values are a PERMANENT at-rest byte-identical contract (§4.1/§5.1/
555// NC-5): every sealed profile on disk was encrypted with a DEK derived from
556// EXACTLY these bytes. Changing any one of them re-derives a different key and
557// makes every already-sealed profile permanently unreadable — there is no
558// migration path for a derived (never-stored) key. Treat this section as
559// frozen; only ever ADD a new version-scoped label/version alongside it.
560//
561// Consumers (this crate is their single source of truth — do not duplicate the
562// literals locally):
563//   - dig-app:    crates/dig-app-core/src/keystore/secrets.rs
564//   - dig-session: src/unlocked.rs (derive_symmetric_key)
565// =============================================================================
566
567/// HKDF salt for the per-profile DEK derivation.
568///
569/// Part of the frozen [profile DEK byte contract](self#profile-dek-at-rest-byte-contract)
570/// — see the section comment above. Consumed by dig-app's
571/// `keystore/secrets.rs` and dig-session's `derive_symmetric_key`.
572pub const DEK_SALT: &[u8] = b"dig-app:dek-salt:v1";
573
574/// Version byte prefixed to the 32-byte identity scalar to form the DEK's HKDF
575/// input key material (`IDENTITY_IKM_VERSION || identity_scalar_32`).
576///
577/// Part of the frozen [profile DEK byte contract](self#profile-dek-at-rest-byte-contract).
578/// Consumed by dig-app's `keystore/secrets.rs` and dig-session's
579/// `derive_symmetric_key`.
580pub const IDENTITY_IKM_VERSION: u8 = 2;
581
582/// HKDF info/label for the per-profile DEK derivation.
583///
584/// Part of the frozen [profile DEK byte contract](self#profile-dek-at-rest-byte-contract).
585/// Consumed by dig-app's `keystore/secrets.rs` and dig-session's
586/// `derive_symmetric_key`.
587pub const PROFILE_DEK_LABEL: &[u8] = b"dig-app:profile-dek:v2";
588
589/// Output length, in bytes, of the derived per-profile DEK (HKDF-SHA256's
590/// natural output for a symmetric AEAD key).
591///
592/// Part of the frozen [profile DEK byte contract](self#profile-dek-at-rest-byte-contract).
593/// Consumed by dig-app's `keystore/secrets.rs` and dig-session's
594/// `derive_symmetric_key`.
595pub const SYMMETRIC_KEY_LEN: usize = 32;
596
597// =============================================================================
598// Profile sealing X25519 byte contract
599//
600// A DIG user profile's per-profile X25519 *sealing* keypair (used by the DIG
601// App to seal/unseal `DIGCHAT1` messages for dig-chat, §NC-1 end-to-end
602// encryption) is derived, never stored, from the same identity scalar as the
603// DEK — but under a DISTINCT HKDF `info` label, which is the sole thing that
604// domain-separates the sealing key from the at-rest DEK:
605//
606//   HKDF-SHA256(salt = DEK_SALT,
607//               ikm  = IDENTITY_IKM_VERSION || identity_scalar_32,
608//               info = PROFILE_SEALING_X25519_LABEL)
609//     -> SYMMETRIC_KEY_LEN (32) bytes, then CLAMPED to an X25519 scalar
610//
611// This label reuses the already-frozen DEK_SALT + IDENTITY_IKM_VERSION on
612// purpose; only the `info` label differs. The 32-byte HKDF output is clamped
613// to a valid X25519 secret scalar by the CONSUMER (dig-account) — this crate
614// owns ONLY the frozen label bytes, not the clamp/derivation.
615//
616// The label is a PERMANENT byte-identical contract (§4.1/§5.1/NC-1): every
617// message already sealed on the network was encrypted under a keypair derived
618// from EXACTLY these bytes. Changing it re-derives a different keypair and
619// makes every already-sealed message permanently unopenable — there is no
620// migration path for a derived (never-stored) key. Treat it as frozen; only
621// ever ADD a new version-scoped label (`…:v2`) alongside it, never mutate it.
622//
623// Consumers (this crate is their single source of truth — do not duplicate the
624// literal locally):
625//   - dig-account: derives the sealing keypair via
626//     `seed.profile_derive_symmetric_key(ix, PROFILE_SEALING_X25519_LABEL)`
627//     then clamps the output to an X25519 scalar.
628// =============================================================================
629
630/// HKDF info/label for deriving a profile's per-profile X25519 **sealing**
631/// keypair — the key the DIG App uses to seal/unseal `DIGCHAT1` messages.
632///
633/// Part of the frozen [profile sealing X25519 byte
634/// contract](self#profile-sealing-x25519-byte-contract) — see the section
635/// comment above. Reuses [`DEK_SALT`] + [`IDENTITY_IKM_VERSION`]; this distinct
636/// `info` label is what domain-separates the sealing key from [`PROFILE_DEK_LABEL`].
637/// The 32-byte HKDF output is clamped to an X25519 scalar by the consumer
638/// (dig-account), not here.
639pub const PROFILE_SEALING_X25519_LABEL: &[u8] = b"dig-app:profile-sealing-x25519:v1";
640
641// =============================================================================
642// $DIG denomination
643//
644// $DIG is a CAT with THREE decimal places, so the smallest indivisible unit —
645// a "CAT mojo" — is one thousandth of a whole $DIG. Every amount that crosses
646// a wire, a coin, or a puzzle is expressed in CAT mojos; whole $DIG exists
647// only for display and for human-authored policy numbers.
648//
649// The two constants below exist so that no consumer ever writes a bare
650// `* 1000` next to a $DIG amount. A misplaced factor of a thousand in either
651// direction is a real-money bug, and the ecosystem has been bitten by an
652// amount whose unit was recorded nowhere near the literal (see the
653// mirror-coin collateral section below).
654// =============================================================================
655
656/// Number of decimal places $DIG carries as a CAT.
657///
658/// Whole $DIG × 10<sup>[`DIG_DECIMALS`]</sup> = CAT mojos. Pinned together with
659/// [`CAT_MOJOS_PER_DIG`] so the two can never disagree.
660pub const DIG_DECIMALS: u32 = 3;
661
662/// CAT mojos in one whole $DIG — i.e. 10<sup>[`DIG_DECIMALS`]</sup> = 1,000.
663///
664/// Multiply by this to convert whole $DIG to the CAT-mojo amounts that coins,
665/// puzzles, and wire messages carry; divide to render a whole-$DIG figure.
666pub const CAT_MOJOS_PER_DIG: u64 = 1_000;
667
668// =============================================================================
669// Mirror-coin collateral
670//
671// A DIG store mirror advertises itself on chain by locking collateral in a
672// mirror coin. The amount below is the ecosystem's CURRENT answer to "how much
673// is enough", read by three independent consumers that must agree:
674//
675//   - dig-node, which creates the mirror coin and must lock exactly this much
676//   - the DIG App, which displays the lock-up and computes a shortfall
677//   - the dig CLI, which audits existing mirror coins against it
678//
679// It is deliberately NOT a wire rule. `dig-mirror-coin` refuses to bake an
680// amount into the puzzle on the grounds that what amount is enough is an
681// economic question for the network; this constant is policy that can be
682// re-decided without a format change.
683//
684// The legacy system's equivalent was `const serverCoinCollateral = 300_000_000`
685// — 0.0003 XCH, drawn from an XCH wallet, with its unit stated nowhere near the
686// literal and the literal hand-copied into a second repo. The DIG figure below
687// differs from it in BOTH asset (CAT $DIG, not XCH) and magnitude, on purpose.
688// Do not "correct" it toward the legacy number.
689// =============================================================================
690
691/// Mirror-coin collateral per store, in **whole $DIG**: 20 $DIG.
692///
693/// The human-facing figure. Coins and wire messages carry
694/// [`MIRROR_COIN_COLLATERAL_CAT_MOJOS`]; the two are pinned to each other
695/// through [`CAT_MOJOS_PER_DIG`], so editing one without the other fails the
696/// crate's tests.
697pub const MIRROR_COIN_COLLATERAL_DIG: u64 = 20;
698
699/// Mirror-coin collateral per store, in **CAT mojos**: 20,000 mojos = 20 $DIG.
700///
701/// This is the amount a `MirrorAdvertisement`'s `collateral` field carries and
702/// the amount dig-node locks when it creates a mirror coin. The unit is CAT
703/// mojos — $DIG's smallest indivisible unit, one thousandth of a whole $DIG
704/// ([`CAT_MOJOS_PER_DIG`]) — NOT XCH mojos and NOT whole $DIG.
705pub const MIRROR_COIN_COLLATERAL_CAT_MOJOS: u64 = MIRROR_COIN_COLLATERAL_DIG * CAT_MOJOS_PER_DIG;
706
707// =============================================================================
708// Mirror-coin epoch clock
709//
710// Mirror coins are scoped to an EPOCH: their on-chain hint is derived by
711// `dig_mirror_coin::mirror_hint(store, root, owner_puzzle_hash, epoch)`, so the
712// epoch number is an INPUT TO COIN IDENTITY. A consumer that computes a
713// different epoch number than its peers does not merely display the wrong label
714// — it creates or looks up coins under a hint nobody else uses, silently
715// orphaning an entire epoch's coins.
716//
717// The epoch is one of FOUR terms there, not one of two. A two-term
718// `morph(store, epoch)` under the same namespace tag is the pre-0.5.0 shape and
719// yields a different hint; call `dig-mirror-coin` rather than recomputing the
720// morph, so a shape change surfaces as a compile error instead of an empty
721// query that looks like "no coins exist".
722//
723// The clock is therefore canonical here rather than re-derived per consumer.
724// Three readers must agree offline: dig-node (morphs the hint), the CLI
725// (audits by epoch), and the DIG App (displays "collateralised for epoch N").
726//
727// It is a pure WALL-CLOCK schedule with NO chain input — a fixed UTC genesis
728// and a fixed 7-day window — which is exactly what lets those three agree
729// without coordinating. The definitive properties, preserved byte-for-byte
730// from the legacy `calculateEpochAndRound`:
731//
732//   genesis      2024-09-03T00:00:00Z
733//   epoch length 7 days, wall-clock UTC, hard-coded
734//   epoch number floor((now - genesis) / 7d) + 1   <- ONE-BASED
735//   round length 10 minutes (hence 1008 rounds per epoch)
736//
737// NOT to be confused with the `dig-epoch` crate, which defines L2 epoch
738// geometry anchored to L1 block heights with BlockProduction / Checkpoint /
739// Finalization phases. That is a genuinely different notion that happens to
740// share the word; adopting it here would change both the cadence and the
741// boundaries.
742// =============================================================================
743
744/// Genesis instant of the mirror-coin epoch clock, as Unix milliseconds:
745/// `2024-09-03T00:00:00Z`.
746///
747/// Epoch 1 begins at exactly this instant — see
748/// [`mirror_epoch_at_unix_ms`].
749pub const MIRROR_EPOCH_GENESIS_UNIX_MS: i64 = 1_725_321_600_000;
750
751/// Length of one mirror-coin epoch, in milliseconds: 7 days of wall-clock UTC.
752///
753/// Hard-coded, not derived from block interval or height.
754pub const MIRROR_EPOCH_LENGTH_MS: i64 = 7 * 24 * 60 * 60 * 1_000;
755
756/// Length of one mirror-coin round, in milliseconds: 10 minutes.
757pub const MIRROR_ROUND_LENGTH_MS: i64 = 10 * 60 * 1_000;
758
759/// Rounds in one mirror-coin epoch: 1008 (7 days ÷ 10 minutes).
760///
761/// Pinned against [`MIRROR_EPOCH_LENGTH_MS`] / [`MIRROR_ROUND_LENGTH_MS`] so
762/// the three cannot drift apart.
763pub const MIRROR_ROUNDS_PER_EPOCH: i64 = MIRROR_EPOCH_LENGTH_MS / MIRROR_ROUND_LENGTH_MS;
764
765/// The mirror-coin epoch number containing `now_unix_ms`.
766///
767/// `floor((now - genesis) / 7 days) + 1`, so the epoch is **one-based**: the
768/// genesis instant itself is epoch **1**, not 0. The `+ 1` is not cosmetic —
769/// the epoch feeds `dig_mirror_coin::mirror_hint`, so an off-by-one puts every
770/// coin of an epoch under a hint nobody queries.
771///
772/// Instants before genesis yield zero or a negative number, matching the legacy
773/// JavaScript `Math.floor` semantics exactly (floored division, not truncated);
774/// they are not a meaningful epoch and callers should not create coins for one.
775///
776/// ```
777/// use dig_constants::{mirror_epoch_at_unix_ms, MIRROR_EPOCH_GENESIS_UNIX_MS};
778/// assert_eq!(mirror_epoch_at_unix_ms(MIRROR_EPOCH_GENESIS_UNIX_MS), 1);
779/// ```
780#[must_use]
781pub const fn mirror_epoch_at_unix_ms(now_unix_ms: i64) -> i64 {
782    (now_unix_ms - MIRROR_EPOCH_GENESIS_UNIX_MS).div_euclid(MIRROR_EPOCH_LENGTH_MS) + 1
783}
784
785/// The Unix-millisecond instant at which `epoch` begins.
786///
787/// Inverse of [`mirror_epoch_at_unix_ms`] on the one-based numbering: epoch 1
788/// starts at [`MIRROR_EPOCH_GENESIS_UNIX_MS`].
789#[must_use]
790pub const fn mirror_epoch_start_unix_ms(epoch: i64) -> i64 {
791    MIRROR_EPOCH_GENESIS_UNIX_MS + (epoch - 1) * MIRROR_EPOCH_LENGTH_MS
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797
798    /// The canonical relay endpoint must equal exactly what a DIG Node dials by
799    /// default. This pins the value byte-for-byte against `dig-node`'s
800    /// `relay::DEFAULT_RELAY_URL` (`wss://relay.dig.net:443`) and the
801    /// `dig-relay` server's documented client endpoint. If either side ever
802    /// changes the scheme, host, or port, this guard fails so the shared
803    /// contract can't silently drift.
804    #[test]
805    fn dig_relay_url_is_canonical_endpoint() {
806        assert_eq!(DIG_RELAY_URL, "wss://relay.dig.net:443");
807    }
808
809    /// The relay endpoint is a secure-WebSocket URL pointing at the canonical
810    /// public host on the relay protocol port.
811    #[test]
812    fn dig_relay_url_is_well_formed() {
813        assert!(
814            DIG_RELAY_URL.starts_with("wss://"),
815            "relay must use secure WebSocket"
816        );
817        assert!(
818            DIG_RELAY_URL.contains("relay.dig.net"),
819            "relay must point at the canonical host"
820        );
821        assert!(
822            DIG_RELAY_URL.ends_with(":443"),
823            "relay must use the live NLB public TLS port 443"
824        );
825    }
826
827    /// The DIG node localhost port must equal the expected default.
828    ///
829    /// This guards against accidental mutations and ensures all consumers
830    /// (dig-node, dig-dns, dig-installer, dig-sdk, digstore) use a consistent
831    /// port when connecting to the local node on `dig.local` or `localhost`.
832    #[test]
833    fn dig_node_port_is_canonical() {
834        assert_eq!(DIG_NODE_PORT, 9778);
835    }
836
837    /// The local-node hostname must equal the expected default.
838    ///
839    /// This guards the first tier of the §5.3 client→node connection order —
840    /// a drift here would desync the installer's registered address from what
841    /// clients probe.
842    #[test]
843    fn dig_local_host_is_canonical() {
844        assert_eq!(DIG_LOCAL_HOST, "dig.local");
845    }
846
847    /// The public read gateway must equal the expected default.
848    ///
849    /// This guards the final-fallback tier of the §5.3 client→node connection
850    /// order — the gateway every consumer falls through to when no local node
851    /// responds.
852    #[test]
853    fn rpc_dig_net_url_is_canonical() {
854        assert_eq!(RPC_DIG_NET_URL, "https://rpc.dig.net");
855    }
856
857    /// The public read gateway is a plain-HTTPS URL (the public read tier,
858    /// distinct from the mTLS transport node-class clients use, §5.3).
859    #[test]
860    fn rpc_dig_net_url_is_well_formed() {
861        assert!(
862            RPC_DIG_NET_URL.starts_with("https://"),
863            "the public read gateway must use HTTPS"
864        );
865    }
866
867    // -- Bootstrap anchor guards -------------------------------------------
868    //
869    // The bootstrap set is the one peer input that does not presuppose its own
870    // output, so a wrong value here strands every fresh node in the network.
871    // The two failure modes it can have are BOTH the value itself rather than
872    // the mechanism that consumes it, which is why they are pinned here.
873
874    /// The bootstrap set names the PEER interface host, never the read gateway.
875    ///
876    /// `rpc.dig.net` is CloudFront (distribution `E3L33T1REWMUIK`): it terminates
877    /// HTTPS and cannot carry the mTLS peer protocol, and its :9444/:9445 are
878    /// closed. The peer interface is live on `node-rpc.dig.net`, the CloudFront
879    /// ORIGIN. The two hosts differ by one label, so this asserts the gateway host
880    /// is ABSENT rather than merely asserting some host is present — a test that
881    /// only checked "a bootstrap entry exists" passes with the closed-port host in
882    /// it, which is the nearest wrong value and the one #923's own premise names.
883    #[test]
884    fn bootstrap_peers_name_the_peer_host_not_the_read_gateway() {
885        assert!(
886            !DIG_BOOTSTRAP_PEERS.is_empty(),
887            "a fresh node with no bootstrap anchor has nothing to dial"
888        );
889        for entry in DIG_BOOTSTRAP_PEERS {
890            let authority = entry.split_once('@').expect("pinned identity").1;
891            let host = authority.rsplit_once(':').expect("explicit port").0;
892            assert_ne!(
893                host, "rpc.dig.net",
894                "{entry} dials the CloudFront read gateway, whose peer ports are closed"
895            );
896            assert!(
897                host.ends_with(".dig.net"),
898                "{entry} must anchor on a DIG-operated host"
899            );
900        }
901    }
902
903    /// Every bootstrap entry is expressible in the operator override's own syntax:
904    /// a 64-hex pinned identity, then `@host:port`.
905    ///
906    /// The identity half is load-bearing rather than decorative. The peer transport
907    /// pins `peer_id = SHA-256(TLS SPKI DER)`, so an entry carrying no identity
908    /// would either be skipped (no anchor) or dialled unpinned (accepting whatever
909    /// answered at that address) — the exact outcome the pinning exists to deny.
910    #[test]
911    fn every_bootstrap_entry_carries_a_pinned_identity_and_an_explicit_port() {
912        for entry in DIG_BOOTSTRAP_PEERS {
913            let (peer_id, authority) = entry
914                .split_once('@')
915                .unwrap_or_else(|| panic!("{entry} carries no pinned identity"));
916            assert_eq!(peer_id.len(), 64, "{entry}: peer_id must be 64 hex chars");
917            assert!(
918                peer_id.chars().all(|c| c.is_ascii_hexdigit()),
919                "{entry}: peer_id must be hex"
920            );
921            let (host, port) = authority
922                .rsplit_once(':')
923                .unwrap_or_else(|| panic!("{entry} carries no explicit port"));
924            assert!(!host.is_empty(), "{entry}: empty host");
925            assert!(
926                port.parse::<u16>().is_ok(),
927                "{entry}: port must be a u16, got {port}"
928            );
929        }
930    }
931
932    /// The anchor is reachable on the peer port, not on the local-RPC port.
933    ///
934    /// `DIG_NODE_PORT` (9778) is the §5.3 client→node READ port and is a different
935    /// role entirely; an anchor published on it would be dialled by the peer stack
936    /// and never answer. Asserting the inequality keeps the two roles from
937    /// collapsing into one another the way the two hostnames already can.
938    #[test]
939    fn bootstrap_port_is_the_peer_port_not_the_local_rpc_port() {
940        for entry in DIG_BOOTSTRAP_PEERS {
941            let port: u16 = entry
942                .rsplit_once(':')
943                .expect("explicit port")
944                .1
945                .parse()
946                .expect("numeric port");
947            assert_ne!(
948                port, DIG_NODE_PORT,
949                "{entry} dials the local JSON-RPC port, not the peer port"
950            );
951            assert_eq!(port, 9444, "{entry} must dial the DIG peer port");
952        }
953    }
954
955    // -- Genesis challenge canonical-value guards --------------------------
956    //
957    // These pin the pre-launch canonical genesis challenges byte-for-byte AND
958    // prove they are reproducible from their documented preimages, so the
959    // values can never silently drift (a drift changes every derived signature
960    // domain + the gossip network_id — a cross-repo breaking event).
961
962    use sha2::{Digest, Sha256};
963
964    /// AGG_SIG opcode bytes, per §4.2 of `SPEC.md` (Chia L1 `condition_tools`).
965    const AGG_SIG_OPCODES: [u8; 6] = [43, 44, 45, 46, 47, 48];
966
967    fn sha256(bytes: &[u8]) -> [u8; 32] {
968        let mut hasher = Sha256::new();
969        hasher.update(bytes);
970        hasher.finalize().into()
971    }
972
973    /// The genesis MUST be non-zero: `dig-gossip` rejects an all-zero
974    /// `network_id`, so a zero genesis would stop the node's gossip pool / DHT /
975    /// PEX from ever starting. This is the connect-enabler invariant.
976    #[test]
977    fn genesis_challenges_are_non_zero() {
978        assert_ne!(DIG_MAINNET.genesis_challenge(), Bytes32::new([0u8; 32]));
979        assert_ne!(DIG_TESTNET.genesis_challenge(), Bytes32::new([0u8; 32]));
980    }
981
982    /// The mainnet genesis is pinned to the Chia mainnet header hash @ height
983    /// 9,021,277 (a real anchored value), and the testnet genesis is the
984    /// reproducible `sha256` of its documented preimage. These pin both values
985    /// byte-for-byte so neither can silently drift.
986    #[test]
987    fn genesis_challenges_are_the_pinned_values() {
988        assert_eq!(
989            DIG_MAINNET_GENESIS_CHALLENGE,
990            hex_literal::hex!("0af981862a4df51f51ec59c312315d959931d917c375730b89b9e2b0854d1abf"),
991        );
992        assert_eq!(
993            DIG_TESTNET_GENESIS_CHALLENGE,
994            sha256(b"DIG_TESTNET:genesis:v1"),
995        );
996    }
997
998    /// Mainnet and testnet MUST NOT share a genesis (no cross-network replay).
999    #[test]
1000    fn mainnet_and_testnet_genesis_differ() {
1001        assert_ne!(
1002            DIG_MAINNET.genesis_challenge(),
1003            DIG_TESTNET.genesis_challenge(),
1004        );
1005    }
1006
1007    /// Pins the $DIG CAT asset id byte-for-byte against the value shipped in
1008    /// `chip35_dl_coin::DIG_ASSET_ID` — a drift here silently breaks $DIG
1009    /// recognition across every consumer (wallets, decoders, payment builders).
1010    #[test]
1011    fn dig_asset_id_is_canonical() {
1012        assert_eq!(
1013            DIG_ASSET_ID,
1014            Bytes32::new(hex_literal::hex!(
1015                "a406d3a9de984d03c9591c10d917593b434d5263cabe2b42f6b367df16832f81"
1016            )),
1017        );
1018    }
1019
1020    // -- Independent genesis + AGG_SIG domain pins (#2316) -----------------
1021    //
1022    // These guard against the ORIGINAL defect class: dig-constants 0.1.0 shipped
1023    // an all-zeros PLACEHOLDER genesis and its six AGG_SIG additional-data
1024    // domains were CORRECTLY derived from that placeholder. Any test that
1025    // re-derives the domains from the crate's OWN genesis (like
1026    // `agg_sig_additional_data_matches_derivation_rule` below) passes on
1027    // placeholder data exactly as on real data — the values are
1028    // "self-consistent-wrong". The finalized value (`0af981…1abf`, 0.4.0) had no
1029    // independent pin, so it could silently regress to a placeholder again.
1030    //
1031    // The guards here break that self-consistency by pinning against a SECOND,
1032    // INDEPENDENT hardcoded copy of the genesis literal: the AGG_SIG domains are
1033    // derived FROM THAT LITERAL, not from `net.genesis_challenge()`. A placeholder
1034    // genesis with placeholder-derived domains (internally consistent) therefore
1035    // FAILS these tests even though it passes the derivation-rule test. Do NOT
1036    // change these expectations to read from the const under test — that would
1037    // reintroduce the self-consistency the pin exists to prevent.
1038
1039    /// Independent second copy of the finalized DIG mainnet genesis (`0af981…1abf`),
1040    /// deliberately NOT read from [`DIG_MAINNET_GENESIS_CHALLENGE`] — this is the
1041    /// external witness the const is checked against.
1042    const EXPECTED_MAINNET_GENESIS: [u8; 32] =
1043        hex_literal::hex!("0af981862a4df51f51ec59c312315d959931d917c375730b89b9e2b0854d1abf");
1044
1045    /// Independent second copy of the DIG testnet genesis (`088c18d6…6c3b`).
1046    const EXPECTED_TESTNET_GENESIS: [u8; 32] =
1047        hex_literal::hex!("088c18d6b7859d885dc2f03166e862c958f74b63b6353c3df71d103b9b806c3b");
1048
1049    /// The mainnet genesis MUST equal the finalized literal, checked at BOTH the
1050    /// raw const and the public accessor against an independent hardcoded copy.
1051    /// If someone edits the const back to a placeholder, this fails.
1052    #[test]
1053    fn mainnet_genesis_equals_independent_literal() {
1054        assert_eq!(DIG_MAINNET_GENESIS_CHALLENGE, EXPECTED_MAINNET_GENESIS);
1055        assert_eq!(
1056            DIG_MAINNET.genesis_challenge(),
1057            Bytes32::new(EXPECTED_MAINNET_GENESIS),
1058        );
1059    }
1060
1061    /// The testnet genesis MUST equal its finalized literal, at both the const
1062    /// and the accessor.
1063    #[test]
1064    fn testnet_genesis_equals_independent_literal() {
1065        assert_eq!(DIG_TESTNET_GENESIS_CHALLENGE, EXPECTED_TESTNET_GENESIS);
1066        assert_eq!(
1067            DIG_TESTNET.genesis_challenge(),
1068            Bytes32::new(EXPECTED_TESTNET_GENESIS),
1069        );
1070    }
1071
1072    /// Belt-and-suspenders: the genesis must not be all-zeros, all-0xFF, or a
1073    /// trivial counting pattern — the shapes a stub/placeholder tends to take.
1074    #[test]
1075    fn mainnet_genesis_is_not_an_obvious_placeholder() {
1076        let g = DIG_MAINNET_GENESIS_CHALLENGE;
1077        assert_ne!(g, [0u8; 32], "genesis must not be all-zeros (0.1.0 stub)");
1078        assert_ne!(g, [0xFFu8; 32], "genesis must not be all-0xFF");
1079        let counting: [u8; 32] = core::array::from_fn(|i| i as u8);
1080        assert_ne!(g, counting, "genesis must not be a counting pattern");
1081        // Not a single repeated byte (e.g. 0x01010101…).
1082        assert!(
1083            g.iter().any(|&b| b != g[0]),
1084            "genesis must not be a single repeated byte",
1085        );
1086    }
1087
1088    /// Independent AGG_SIG-domain pin (the core of the fix). Each of the six DIG
1089    /// mainnet AGG_SIG additional-data domains MUST equal `sha256(genesis_literal
1090    /// || opcode_byte)` computed from the INDEPENDENT [`EXPECTED_MAINNET_GENESIS`]
1091    /// literal (AGG_SIG_ME == the genesis literal directly). Because the expected
1092    /// values come from a hardcoded copy of the REAL genesis rather than from the
1093    /// crate's own const, a placeholder genesis whose domains were re-derived from
1094    /// the placeholder (self-consistent-wrong) FAILS here.
1095    #[test]
1096    fn mainnet_agg_sig_domains_equal_independent_literal_derivation() {
1097        let c = DIG_MAINNET.consensus();
1098        assert_eq!(
1099            c.agg_sig_me_additional_data,
1100            Bytes32::new(EXPECTED_MAINNET_GENESIS),
1101            "AGG_SIG_ME must equal the genesis literal directly",
1102        );
1103        let expected: Vec<Bytes32> = AGG_SIG_OPCODES
1104            .iter()
1105            .map(|&op| {
1106                let mut preimage = EXPECTED_MAINNET_GENESIS.to_vec();
1107                preimage.push(op);
1108                Bytes32::new(sha256(&preimage))
1109            })
1110            .collect();
1111        assert_eq!(c.agg_sig_parent_additional_data, expected[0]);
1112        assert_eq!(c.agg_sig_puzzle_additional_data, expected[1]);
1113        assert_eq!(c.agg_sig_amount_additional_data, expected[2]);
1114        assert_eq!(c.agg_sig_puzzle_amount_additional_data, expected[3]);
1115        assert_eq!(c.agg_sig_parent_amount_additional_data, expected[4]);
1116        assert_eq!(c.agg_sig_parent_puzzle_additional_data, expected[5]);
1117    }
1118
1119    /// The same independent AGG_SIG-domain pin for DIG testnet.
1120    #[test]
1121    fn testnet_agg_sig_domains_equal_independent_literal_derivation() {
1122        let c = DIG_TESTNET.consensus();
1123        assert_eq!(
1124            c.agg_sig_me_additional_data,
1125            Bytes32::new(EXPECTED_TESTNET_GENESIS),
1126        );
1127        let expected: Vec<Bytes32> = AGG_SIG_OPCODES
1128            .iter()
1129            .map(|&op| {
1130                let mut preimage = EXPECTED_TESTNET_GENESIS.to_vec();
1131                preimage.push(op);
1132                Bytes32::new(sha256(&preimage))
1133            })
1134            .collect();
1135        assert_eq!(c.agg_sig_parent_additional_data, expected[0]);
1136        assert_eq!(c.agg_sig_puzzle_additional_data, expected[1]);
1137        assert_eq!(c.agg_sig_amount_additional_data, expected[2]);
1138        assert_eq!(c.agg_sig_puzzle_amount_additional_data, expected[3]);
1139        assert_eq!(c.agg_sig_parent_amount_additional_data, expected[4]);
1140        assert_eq!(c.agg_sig_parent_puzzle_additional_data, expected[5]);
1141    }
1142
1143    // -- Chia L1 AGG_SIG_ME anti-drift guards ------------------------------
1144
1145    /// Literal pin: the Chia L1 AGG_SIG_ME constants equal Chia's well-known
1146    /// mainnet / testnet11 genesis challenges byte-for-byte. This catches any
1147    /// accidental mutation independently of any external crate.
1148    #[test]
1149    fn chia_l1_agg_sig_me_constants_are_the_pinned_values() {
1150        assert_eq!(
1151            CHIA_L1_MAINNET_AGG_SIG_ME,
1152            hex_literal::hex!("ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb"),
1153        );
1154        assert_eq!(
1155            CHIA_L1_TESTNET11_AGG_SIG_ME,
1156            hex_literal::hex!("37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615"),
1157        );
1158    }
1159
1160    /// Source KAT: the Chia L1 constants MUST equal the values `chia-wallet-sdk`
1161    /// (via `chia-sdk-types`) uses in its `MAINNET_CONSTANTS` / `TESTNET11_CONSTANTS`.
1162    /// This is the primary anti-drift guard — the wallet engine binds spends with
1163    /// those SDK constants, so if a future SDK version ever changed the value, this
1164    /// fails and forces a deliberate re-pin instead of a silent custody break.
1165    #[test]
1166    fn chia_l1_agg_sig_me_matches_chia_sdk_types() {
1167        use chia_sdk_types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS};
1168        assert_eq!(
1169            CHIA_L1_MAINNET_AGG_SIG_ME.as_slice(),
1170            MAINNET_CONSTANTS.agg_sig_me_additional_data.as_ref(),
1171        );
1172        assert_eq!(
1173            CHIA_L1_TESTNET11_AGG_SIG_ME.as_slice(),
1174            TESTNET11_CONSTANTS.agg_sig_me_additional_data.as_ref(),
1175        );
1176    }
1177
1178    /// The Chia L1 (foreign chain) AGG_SIG_ME MUST NOT equal the DIG L2 genesis —
1179    /// this is the whole reason the constants exist. Signing an L1 spend with the
1180    /// DIG L2 genesis would be a custody break.
1181    #[test]
1182    fn chia_l1_agg_sig_me_differs_from_dig_l2_genesis() {
1183        assert_ne!(
1184            Bytes32::new(CHIA_L1_MAINNET_AGG_SIG_ME),
1185            DIG_MAINNET.genesis_challenge(),
1186        );
1187        assert_ne!(
1188            Bytes32::new(CHIA_L1_TESTNET11_AGG_SIG_ME),
1189            DIG_TESTNET.genesis_challenge(),
1190        );
1191    }
1192
1193    // -- DIG treasury recipient anti-drift guards --------------------------
1194
1195    /// Literal pin: the treasury inner puzzle hash equals the value
1196    /// `digstore_chain::dig::treasury_inner_puzzle_hash()` decodes to
1197    /// (byte-identical, pinned by that crate's own test at
1198    /// `crates/digstore-chain/src/dig.rs:206-209`). A drift here silently
1199    /// MISDIRECTS every $DIG capsule/commit payment and dev-tip to the wrong
1200    /// on-chain recipient — a custody break.
1201    #[test]
1202    fn dig_treasury_inner_puzzle_hash_is_canonical() {
1203        assert_eq!(
1204            DIG_TREASURY_INNER_PUZZLE_HASH,
1205            Bytes32::new(hex_literal::hex!(
1206                "ec7c304708c7d59c078d5ae098d0dea004decf47fa1cafebb266c10ad6466ce8"
1207            )),
1208        );
1209    }
1210
1211    /// Literal pin: the treasury address equals digstore-chain's
1212    /// source-of-truth bech32m form (`digstore_chain::dig::TREASURY_ADDRESS`,
1213    /// `crates/digstore-chain/src/dig.rs:41`). A drift misdirects funds.
1214    #[test]
1215    fn dig_treasury_address_is_canonical() {
1216        assert_eq!(
1217            DIG_TREASURY_ADDRESS,
1218            "xch1a37rq3cgcl2ecpudttsf35x75qzdan68lgw2l6ajvmqs44jxdn5qv6pk3y",
1219        );
1220    }
1221
1222    /// KAT: the bech32m address and the inner puzzle hash cannot silently drift
1223    /// apart. Decodes `DIG_TREASURY_ADDRESS` (HRP `xch`, bech32m) and asserts
1224    /// the 32 decoded bytes equal `DIG_TREASURY_INNER_PUZZLE_HASH`, proving the
1225    /// two constants encode the SAME on-chain recipient.
1226    #[test]
1227    fn dig_treasury_address_decodes_to_inner_puzzle_hash() {
1228        use bech32::Hrp;
1229        let (hrp, data) = bech32::decode(DIG_TREASURY_ADDRESS).expect("valid bech32m");
1230        assert_eq!(hrp, Hrp::parse("xch").unwrap(), "HRP must be xch");
1231        assert_eq!(
1232            data.as_slice(),
1233            DIG_TREASURY_INNER_PUZZLE_HASH.to_bytes(),
1234            "address must decode to the pinned inner puzzle hash",
1235        );
1236    }
1237
1238    // -- Profile DEK at-rest byte-contract guards ---------------------------
1239    //
1240    // These pin every DEK-derivation constant literally so a future edit can't
1241    // silently drift the contract (which would make every already-sealed
1242    // profile permanently unreadable, §5.1).
1243
1244    #[test]
1245    fn dek_salt_is_the_pinned_value() {
1246        assert_eq!(DEK_SALT, b"dig-app:dek-salt:v1");
1247    }
1248
1249    #[test]
1250    fn identity_ikm_version_is_the_pinned_value() {
1251        assert_eq!(IDENTITY_IKM_VERSION, 2);
1252    }
1253
1254    #[test]
1255    fn profile_dek_label_is_the_pinned_value() {
1256        assert_eq!(PROFILE_DEK_LABEL, b"dig-app:profile-dek:v2");
1257    }
1258
1259    #[test]
1260    fn symmetric_key_len_is_the_pinned_value() {
1261        assert_eq!(SYMMETRIC_KEY_LEN, 32);
1262    }
1263
1264    /// The per-profile X25519 sealing label is a PERMANENT crypto byte contract
1265    /// (§5.1): every `DIGCHAT1` message a DIG user has ever sealed was encrypted
1266    /// under a sealing key derived from EXACTLY these bytes. A drift here would
1267    /// re-derive a different keypair and make every already-sealed message
1268    /// permanently unopenable. This pins the label literally so no future edit
1269    /// can silently change it.
1270    #[test]
1271    fn profile_sealing_x25519_label_is_the_pinned_value() {
1272        assert_eq!(
1273            PROFILE_SEALING_X25519_LABEL,
1274            b"dig-app:profile-sealing-x25519:v1"
1275        );
1276    }
1277
1278    /// The sealing label MUST be distinct from the DEK label — a shared `info`
1279    /// would derive the same 32 bytes for both the at-rest DEK and the X25519
1280    /// sealing key, collapsing the domain separation the two labels exist to
1281    /// provide. This guards that domain separation directly.
1282    #[test]
1283    fn profile_sealing_label_is_domain_separated_from_dek_label() {
1284        assert_ne!(PROFILE_SEALING_X25519_LABEL, PROFILE_DEK_LABEL);
1285    }
1286
1287    /// Every baked-in AGG_SIG additional-data value MUST equal the §4.1 rule
1288    /// applied to the network's genesis: AGG_SIG_ME == genesis, and each other
1289    /// variant == `sha256(genesis || opcode_byte)`. This regenerates the values
1290    /// independently and asserts the constants match — so a genesis change that
1291    /// forgets to recompute a derived value is caught.
1292    #[test]
1293    fn agg_sig_additional_data_matches_derivation_rule() {
1294        for net in [&DIG_MAINNET, &DIG_TESTNET] {
1295            let genesis = net.genesis_challenge();
1296            assert_eq!(net.agg_sig_me_additional_data(), genesis);
1297
1298            let c = net.consensus();
1299            let derived: Vec<Bytes32> = AGG_SIG_OPCODES
1300                .iter()
1301                .map(|&op| {
1302                    let mut preimage = genesis.as_ref().to_vec();
1303                    preimage.push(op);
1304                    Bytes32::new(sha256(&preimage))
1305                })
1306                .collect();
1307            assert_eq!(c.agg_sig_parent_additional_data, derived[0]);
1308            assert_eq!(c.agg_sig_puzzle_additional_data, derived[1]);
1309            assert_eq!(c.agg_sig_amount_additional_data, derived[2]);
1310            assert_eq!(c.agg_sig_puzzle_amount_additional_data, derived[3]);
1311            assert_eq!(c.agg_sig_parent_amount_additional_data, derived[4]);
1312            assert_eq!(c.agg_sig_parent_puzzle_additional_data, derived[5]);
1313        }
1314    }
1315
1316    /// Mirror-coin collateral: the whole-$DIG figure and the CAT-mojo figure
1317    /// must stay in lock-step through the $DIG denomination.
1318    ///
1319    /// Both sides are also pinned to their literals, so that editing ONE of the
1320    /// three (whole $DIG, mojos, or the decimal factor) without the others
1321    /// fails — an equality written only in terms of the other constants would
1322    /// survive scaling all of them together, which is precisely the
1323    /// factor-of-a-thousand mistake this guards.
1324    #[test]
1325    fn mirror_collateral_is_20_dig_and_20_000_cat_mojos() {
1326        assert_eq!(DIG_DECIMALS, 3, "$DIG is a 3-decimal CAT");
1327        assert_eq!(CAT_MOJOS_PER_DIG, 1_000, "10^3 mojos per whole $DIG");
1328        assert_eq!(CAT_MOJOS_PER_DIG, 10u64.pow(DIG_DECIMALS));
1329
1330        assert_eq!(MIRROR_COIN_COLLATERAL_DIG, 20, "20 whole $DIG per store");
1331        assert_eq!(
1332            MIRROR_COIN_COLLATERAL_CAT_MOJOS, 20_000,
1333            "= 20,000 CAT mojos"
1334        );
1335        assert_eq!(
1336            MIRROR_COIN_COLLATERAL_CAT_MOJOS,
1337            MIRROR_COIN_COLLATERAL_DIG * CAT_MOJOS_PER_DIG
1338        );
1339
1340        // The unit-confusion neighbours: the whole-$DIG figure is NOT the coin
1341        // amount, and the legacy XCH-mojo literal (0.0003 XCH) is neither.
1342        assert_ne!(MIRROR_COIN_COLLATERAL_CAT_MOJOS, MIRROR_COIN_COLLATERAL_DIG);
1343        assert_ne!(MIRROR_COIN_COLLATERAL_CAT_MOJOS, 300_000_000);
1344    }
1345
1346    /// The epoch genesis literal must be exactly `2024-09-03T00:00:00Z`.
1347    ///
1348    /// Recomputed from the Unix epoch rather than restated: 19,969 whole days
1349    /// elapse between 1970-01-01 and 2024-09-03, so the instant is
1350    /// 19_969 × 86_400 s = 1_725_321_600 s = 1_725_321_600_000 ms. A literal
1351    /// asserted against itself would prove nothing.
1352    #[test]
1353    fn epoch_genesis_is_2024_09_03t00_00_00z() {
1354        const DAYS_1970_TO_2024_09_03: i64 = 19_969;
1355        assert_eq!(
1356            MIRROR_EPOCH_GENESIS_UNIX_MS,
1357            DAYS_1970_TO_2024_09_03 * 86_400 * 1_000
1358        );
1359        assert_eq!(MIRROR_EPOCH_LENGTH_MS, 604_800_000, "7 days in ms");
1360        assert_eq!(MIRROR_ROUND_LENGTH_MS, 600_000, "10 minutes in ms");
1361        assert_eq!(
1362            MIRROR_ROUNDS_PER_EPOCH, 1_008,
1363            "1008 ten-minute rounds per 7 days"
1364        );
1365    }
1366
1367    /// The epoch is ONE-BASED and its boundaries are exact.
1368    ///
1369    /// Each assertion is chosen to fail against a specific nearest-wrong
1370    /// implementation:
1371    ///
1372    /// - genesis itself → **1**; a zero-based clock (no `+ 1`) returns 0 here.
1373    ///   The epoch feeds `dig_mirror_coin::mirror_hint`, so that off-by-one
1374    ///   hides an entire epoch's coins under a hint nobody queries.
1375    /// - one millisecond BEFORE genesis → **0**; an implementation using Rust's
1376    ///   truncating `/` instead of floored `div_euclid` returns 1 here (−1 / 7d
1377    ///   truncates to 0, +1 = 1), silently colliding with real epoch 1. This is
1378    ///   the assertion that pins JavaScript `Math.floor` parity.
1379    /// - the last millisecond of epoch 1 → still **1**, and the first
1380    ///   millisecond of epoch 2 → **2**; an inclusive/exclusive slip at the
1381    ///   rollover fails exactly one of these two.
1382    #[test]
1383    fn epoch_boundaries_are_one_based_and_floored() {
1384        let genesis = MIRROR_EPOCH_GENESIS_UNIX_MS;
1385
1386        assert_eq!(mirror_epoch_at_unix_ms(genesis), 1, "genesis is epoch 1");
1387        assert_eq!(
1388            mirror_epoch_at_unix_ms(genesis - 1),
1389            0,
1390            "pre-genesis floors down"
1391        );
1392        assert_eq!(
1393            mirror_epoch_at_unix_ms(genesis + MIRROR_EPOCH_LENGTH_MS - 1),
1394            1,
1395            "last ms of epoch 1"
1396        );
1397        assert_eq!(
1398            mirror_epoch_at_unix_ms(genesis + MIRROR_EPOCH_LENGTH_MS),
1399            2,
1400            "rollover instant belongs to epoch 2"
1401        );
1402    }
1403
1404    /// A known-good pair computed independently, by hand, against the legacy
1405    /// `calculateEpochAndRound` formula.
1406    ///
1407    /// Instant: `2026-08-26T00:00:00Z` = 1_787_702_400 s.
1408    ///   - 1_787_702_400 − 1_725_321_600 = 62_380_800 s elapsed since genesis
1409    ///   - 62_380_800 / 86_400 = 722 whole days
1410    ///   - 722 / 7 = 103.142… → `Math.floor` → 103
1411    ///   - 103 + 1 = **epoch 104**
1412    ///
1413    /// The offset is deliberately NOT a whole multiple of 7 days (722 = 7×103
1414    /// + 1), so the fractional part is real and a rounding error would show.
1415    #[test]
1416    fn epoch_matches_hand_computed_legacy_value() {
1417        const AUG_26_2026_UNIX_MS: i64 = 1_787_702_400_000;
1418        assert_eq!(
1419            AUG_26_2026_UNIX_MS - MIRROR_EPOCH_GENESIS_UNIX_MS,
1420            722 * 86_400 * 1_000,
1421            "722 whole days since genesis"
1422        );
1423        assert_eq!(mirror_epoch_at_unix_ms(AUG_26_2026_UNIX_MS), 104);
1424
1425        // 722 days is one day INTO epoch 104, so the epoch is stable across
1426        // that whole day but not across the following rollover.
1427        assert_eq!(
1428            mirror_epoch_at_unix_ms(AUG_26_2026_UNIX_MS - 86_400_000),
1429            104
1430        );
1431        assert_eq!(
1432            mirror_epoch_at_unix_ms(AUG_26_2026_UNIX_MS - 86_400_000 - 1),
1433            103
1434        );
1435    }
1436
1437    /// [`mirror_epoch_start_unix_ms`] must invert [`mirror_epoch_at_unix_ms`]
1438    /// on the one-based numbering: a start instant lands in its own epoch, and
1439    /// the millisecond before it lands in the previous one.
1440    #[test]
1441    fn epoch_start_inverts_the_epoch_clock() {
1442        assert_eq!(mirror_epoch_start_unix_ms(1), MIRROR_EPOCH_GENESIS_UNIX_MS);
1443        for epoch in [1i64, 2, 104, 1_000] {
1444            let start = mirror_epoch_start_unix_ms(epoch);
1445            assert_eq!(mirror_epoch_at_unix_ms(start), epoch);
1446            assert_eq!(mirror_epoch_at_unix_ms(start - 1), epoch - 1);
1447        }
1448    }
1449}