Skip to main content

sidestr_header/
lib.rs

1//! `sidestr-header` — block headers for sidestr sidechains, in both families
2//! the parent table allows.
3//!
4//! A [sidestr](https://github.com/sidestr/spec) chain is an overlay on a
5//! Bitcoin-family parent, and SPEC 3 is explicit that the overlay names no
6//! header format and no hash: "everything the overlay does not set is
7//! inherited from the parent: header format and proof-of-work hash …
8//! Nothing in the document names a header format or a hash; the parent
9//! decides both." SPEC 3.2's parents table then fixes two families:
10//!
11//! | parent alias | chain | headers, proof of work | this crate |
12//! |---|---|---|---|
13//! | `btc`, `tbtc4` | Bitcoin mainnet, testnet4 | stock 80 bytes, SHA-256d | [`StockHeader`] |
14//! | `xbt`, `txbt4` | Bitcoin Knots' BLAKE2b fork of each | 164-byte v2, BLAKE2b | [`Blake2bV2Header`] |
15//!
16//! `ltc` and `vtc` are reserved upstream and absent here. The crate is a port
17//! of the reference JavaScript — the bitcoin-desktop/schema kernel's header
18//! codec, Knots proof of work and Knots overlay rules, and Melvin Carvalho's
19//! `siding` for how a sidestr block shapes its header and what its signature
20//! covers — under the same AGPL-3.0 licence (agentbox ADR-2106). Without the
21//! `core` feature it is `#![no_std]`, allocates nothing, and takes every
22//! primitive from RustCrypto ([`sha2`], [`blake2`]) with no `bitcoin` crate
23//! dependency (ADR-2096 D2): the header and its proof of work are the part of
24//! consensus that the parent's serialisation library does not own.
25//!
26//! With `core` (default) the [`family`] module implements
27//! `sidestr_core::HeaderFamily` for both header types, so
28//! `sidestr_core::StateOf<Blake2bV2>` and `ChainOf<Blake2bV2>` validate and
29//! produce a chain beside `xbt` or `txbt4` end to end — the v2 header, the
30//! BLAKE2b proof of work, the Knots overlay's rules and Knots' unified sighash
31//! on every spend — proven by replaying the live `sidestr:txbt4-siding` chain.
32//! The edge points from this crate to `sidestr-core`, never back.
33//!
34//! # What it gives a validator
35//!
36//! Per family, through [`HeaderFamily`] and the enum [`Header`] or the two
37//! structs directly:
38//!
39//! - the wire size and a strict `decode` / `encode` pair;
40//! - `hash()`, the block id, which in both families **is** the proof-of-work
41//!   hash (SHA-256d of the 80 bytes; the v2 pipeline for Knots) — so there is
42//!   no separate `pow_hash`;
43//! - [`Target`] with compact `bits` decoding, and [`check_pow`] with the
44//!   `powLimit` semantics siding uses — `bits` is pinned to `powLimit`'s
45//!   compact form and never retargets (SPEC 3, SPEC 4 step 1);
46//! - the BIP-325 block-data preimage over that family's serialisation
47//!   ([`signet`]), so `sidestr-core` signs and verifies blocks without knowing
48//!   the layout;
49//! - the version-bit-31 rule: bit 31 selects the family, so a stock header
50//!   with it set and a v2 header without it are both refused at decode;
51//! - the fork activation constants of SPEC 3.2 ([`fork`]);
52//! - with `core`, the two families as `sidestr-core` sees them
53//!   ([`Blake2bV2`], [`family::Stock`]).
54//!
55//! # Where this port departs from the reference
56//!
57//! - **The genesis is judged under the family's rules.** The reference
58//!   applies block 0 on its hash; through `sidestr-core`'s
59//!   `StateOf::from_genesis` a typed v2 genesis is held to
60//!   `knots:rule-header-v2-from-fork`, `-height` and `-flags-reserved`, the
61//!   proof of work and the signature before its hash is compared to the
62//!   document, so a header the decoder would refuse cannot enter as a struct
63//!   either (`tests/audit_regressions.rs`).
64//! - **Version bit 31 is refused on every stock path.** [`StockHeader::decode`]
65//!   refuses the bytes; [`family::Stock`] reports the version as the kernel
66//!   types it (`i32le`, so bit 31 is negative) and `btc:rule-header-version`
67//!   refuses a typed header, as the kernel does on the same block.
68//! - **Compact targets Bitcoin Core rejects** (negative, overflow) are
69//!   rejected by [`Target::from_compact`] where the kernel is lenient —
70//!   unreachable on a valid sidestr chain, where `bits` is pinned to
71//!   `powLimit`.
72//! - **A mirror's record framing is checked** on replay (`sidestr-core`'s
73//!   `blockfile::read_block`): the `[u32 height][u32 size]` prefix of every
74//!   record must agree with the index entry, and the entry must lie within
75//!   the file.
76//!
77//! # The v2 header, field by field
78//!
79//! The 164-byte Knots v2 header keeps the classic 80-byte prefix (version with
80//! bit 31 set, previous hash, merkle root, time on wire, bits, nonce) and
81//! appends 84 bytes: two more nonces, a 128-bit extranonce, a time offset, the
82//! committed transaction count, a flags byte (ASIC profile in bits 0–1, time
83//! offset in bit 2, bits 6–7 reserved), the XOR-mask clear count, a 128-bit XOR
84//! key, the committed height, and a 32-byte merge-mining hook. The table with
85//! offsets is on [`Blake2bV2Header`]. Its hash is not a hash of the bytes but a
86//! commitment tree: two BIP-340 tagged-hash rounds over the fields, then two
87//! BLAKE2b-256 rounds whose second input layout the ASIC profile selects, then
88//! an XOR mask derived from the key (see [`Blake2bV2Header::hash`]).
89//!
90//! # The signet preimage
91//!
92//! Siding's `blockData` hashes the first 72 header bytes — version, previous
93//! hash, merkle root, time on wire — with the merkle root recomputed over the
94//! coinbase stripped of its solution. Those 72 bytes have the same layout in
95//! both families, so [`Header::block_data`] is one function.
96//!
97//! # Example
98//!
99//! ```
100//! use sidestr_header::{Header, HeaderFamily, Target};
101//!
102//! // sidestr:dreamlab block 0, beside tbtc4: stock family.
103//! let bytes = hex::decode(
104//!     "0000002000000000000000000000000000000000000000000000000000000000000000003a87d59ecf60ab58ee75948cc39d1bb44ac4285747e64b5a1e7a960d37764cb40e67b26affff7f2002000000",
105//! ).unwrap();
106//! let header = HeaderFamily::Stock.decode(&bytes).unwrap();
107//! assert_eq!(header.hash().to_string(),
108//!            "4db37517728bd509c0cb96ee5a2e3e2a77f9e965a092e9f67948b413d453dbc0");
109//!
110//! // SPEC 4 step 1 against the chain document's powLimit.
111//! let pow_limit = Target::from_hex(
112//!     "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
113//! assert!(header.check_pow(&pow_limit).is_ok());
114//!
115//! // The same 80 bytes are not a v2 header, and 81 bytes are not a header.
116//! assert!(HeaderFamily::Blake2bV2.decode(&bytes).is_err());
117//! assert!(HeaderFamily::Stock.decode(&bytes[..79]).is_err());
118//!
119//! // Re-encoding is the identity.
120//! assert_eq!(header.encode().as_ref(), bytes.as_slice());
121//! # let _: Header = header;
122//! ```
123//!
124//! # Provenance
125//!
126//! - `codec/pow/knots-header-v2.js`, `codec/pow/blake2b.js`, `codec/hash.js`,
127//!   `codec/codec.js`, `codec/headers.js`, `codec/overlays/knots-blake2b.js`
128//!   (the overlay's header and block checks), `schema/overlays/knots-blake2b.jsonld`
129//!   of [bitcoin-desktop/schema](https://github.com/bitcoin-desktop/schema)
130//!   (AGPL-3.0), at commit `b8cbf6337c7450fe14ddc5bce00c7280059aab5d`.
131//! - `siding/lib/block.mjs`, `siding/lib/parents.mjs`, `siding/lib/chain.mjs`,
132//!   `siding/lib/overlay.mjs` (`blake2bHeight: 0`, `unifiedSighashParam`),
133//!   `SPEC.md` §3, §3.2, §4 of [sidestr/spec](https://github.com/sidestr/spec)
134//!   (AGPL-3.0, Melvin Carvalho), at commit
135//!   `2de40bdac4cba01be0864156a553d8287c22e279`.
136//! - Test vectors: Knots' own `block_header_v2.json` and real fork headers
137//!   captured from a Knots 29.4.1 node on 2026-09-05, both carried by the
138//!   schema kernel; siding's `blockData` on the live `sidestr:dreamlab` block 0
139//!   and on kernel-hashed v2 headers, computed with the JavaScript engine as
140//!   the oracle; and the live `sidestr:txbt4-siding` chain (229 blocks as of
141//!   2026-09-22) as the oracle for the whole BLAKE2b family through
142//!   `sidestr-core` (`tests/core_family.rs`).
143
144#![cfg_attr(not(feature = "std"), no_std)]
145#![forbid(unsafe_code)]
146// The crate docs link `family`, `Blake2bV2` and `family::Stock`, which exist
147// only with `core`; without it those links have nowhere to point and are not
148// an error.
149#![cfg_attr(not(feature = "core"), allow(rustdoc::broken_intra_doc_links))]
150#![deny(missing_docs)]
151
152mod error;
153#[cfg(feature = "core")]
154pub mod family;
155pub mod fork;
156pub mod hash;
157pub mod signet;
158mod stock;
159mod target;
160mod v2;
161
162pub use error::Error;
163#[cfg(feature = "core")]
164pub use family::Blake2bV2;
165pub use stock::StockHeader;
166pub use target::Target;
167pub use v2::{
168    Blake2bV2Header, V2HashStages, FLAG_ASIC_PROFILE_MASK, FLAG_RESERVED_MASK,
169    FLAG_USE_TIME_OFFSET, POW_HASH_NAME as BLAKE2B_V2_POW_HASH_NAME,
170};
171
172use core::fmt;
173
174/// Version bit 31: set on every v2 header, clear on every stock header
175/// (`VERSION_HEADER_V2_FLAG` in `codec/pow/knots-header-v2.js`; the kernel's
176/// `structVariants` selects the v2 struct on it).
177pub const VERSION_HEADER_V2_FLAG: u32 = 0x8000_0000;
178
179/// The kernel's name for the stock family's proof-of-work hash.
180pub const SHA256D_POW_HASH_NAME: &str = "sha256d";
181
182/// A block hash in **display order** — the byte order bitcoind prints and a
183/// chain document's `genesisHash` uses; read as a big-endian number it
184/// compares directly against a [`Target`].
185///
186/// ```
187/// use sidestr_header::BlockHash;
188/// let h = BlockHash::from_hex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f").unwrap();
189/// assert_eq!(h.to_string(), "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f");
190/// assert_eq!(h.to_wire()[0], 0x6f); // wire order is the reverse
191/// assert_eq!(BlockHash::from_wire(h.to_wire()), h);
192/// ```
193#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
194pub struct BlockHash(pub [u8; 32]);
195
196impl BlockHash {
197    /// All zeros: a genesis header's `prev_block_hash`.
198    pub const ZERO: BlockHash = BlockHash([0u8; 32]);
199
200    /// From the 32 bytes as they sit in a header (wire / internal order).
201    pub fn from_wire(mut wire: [u8; 32]) -> Self {
202        wire.reverse();
203        BlockHash(wire)
204    }
205
206    /// The 32 bytes as they sit in a header (wire / internal order).
207    pub fn to_wire(self) -> [u8; 32] {
208        let mut b = self.0;
209        b.reverse();
210        b
211    }
212
213    /// From 64 hex characters in display order.
214    pub fn from_hex(s: &str) -> Result<Self, Error> {
215        hash::hex32(s).map(BlockHash).ok_or(Error::InvalidHex)
216    }
217
218    /// `hash ≤ target`, both read as big-endian 256-bit numbers
219    /// (`BigInt('0x' + blockHash) <= expandCompact(bits)` in the kernel).
220    pub fn meets(&self, target: &Target) -> bool {
221        self.0 <= target.to_be_bytes()
222    }
223}
224
225impl fmt::Debug for BlockHash {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        f.write_str("BlockHash(")?;
228        hash::fmt_hex(f, &self.0)?;
229        f.write_str(")")
230    }
231}
232
233impl fmt::Display for BlockHash {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        hash::fmt_hex(f, &self.0)
236    }
237}
238
239/// The two header families of SPEC 3.2, named after the header they carry.
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
241pub enum HeaderFamily {
242    /// Stock 80-byte header, SHA-256d: beside `btc` or `tbtc4`
243    /// (`family: 'stock'` in `siding/lib/parents.mjs`).
244    Stock,
245    /// Knots 164-byte v2 header, BLAKE2b: beside `xbt` or `txbt4`
246    /// (`family: 'blake2b'`).
247    Blake2bV2,
248}
249
250impl HeaderFamily {
251    /// The family's header size in bytes: 80 or 164.
252    pub const fn wire_size(self) -> usize {
253        match self {
254            HeaderFamily::Stock => StockHeader::WIRE_SIZE,
255            HeaderFamily::Blake2bV2 => Blake2bV2Header::WIRE_SIZE,
256        }
257    }
258
259    /// The kernel's name for the family's proof-of-work hash: `sha256d` or
260    /// `knots:blake2b-v2`.
261    pub const fn pow_hash_name(self) -> &'static str {
262        match self {
263            HeaderFamily::Stock => SHA256D_POW_HASH_NAME,
264            HeaderFamily::Blake2bV2 => v2::POW_HASH_NAME,
265        }
266    }
267
268    /// The family a version word claims: bit 31 set means v2. This is the
269    /// kernel's `structVariants` selector, useful when the bytes' provenance
270    /// is unknown; a validator knows its family from the chain document's
271    /// parent and should use [`HeaderFamily::decode`] with that.
272    pub const fn from_version(version: u32) -> HeaderFamily {
273        if version & VERSION_HEADER_V2_FLAG != 0 {
274            HeaderFamily::Blake2bV2
275        } else {
276            HeaderFamily::Stock
277        }
278    }
279
280    /// Decodes `bytes` as this family's header.
281    pub fn decode(self, bytes: &[u8]) -> Result<Header, Error> {
282        match self {
283            HeaderFamily::Stock => StockHeader::decode(bytes).map(Header::Stock),
284            HeaderFamily::Blake2bV2 => Blake2bV2Header::decode(bytes).map(Header::Blake2bV2),
285        }
286    }
287}
288
289impl fmt::Display for HeaderFamily {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        f.write_str(match self {
292            HeaderFamily::Stock => "stock",
293            HeaderFamily::Blake2bV2 => "blake2b-v2",
294        })
295    }
296}
297
298/// A header of either family, with the operations a sidestr validator needs
299/// dispatched to the right one.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum Header {
302    /// An 80-byte stock header.
303    Stock(StockHeader),
304    /// A 164-byte Knots v2 header.
305    Blake2bV2(Blake2bV2Header),
306}
307
308/// A header's wire bytes, sized for the larger family; `as_ref()` yields
309/// exactly the family's bytes.
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub struct EncodedHeader {
312    buf: [u8; 164],
313    len: usize,
314}
315
316impl AsRef<[u8]> for EncodedHeader {
317    fn as_ref(&self) -> &[u8] {
318        &self.buf[..self.len]
319    }
320}
321
322impl core::ops::Deref for EncodedHeader {
323    type Target = [u8];
324    fn deref(&self) -> &[u8] {
325        self.as_ref()
326    }
327}
328
329impl Header {
330    /// Which family this header belongs to.
331    pub const fn family(&self) -> HeaderFamily {
332        match self {
333            Header::Stock(_) => HeaderFamily::Stock,
334            Header::Blake2bV2(_) => HeaderFamily::Blake2bV2,
335        }
336    }
337
338    /// The raw wire version.
339    pub const fn version(&self) -> u32 {
340        match self {
341            Header::Stock(h) => h.version,
342            Header::Blake2bV2(h) => h.version,
343        }
344    }
345
346    /// The previous block's hash.
347    pub const fn prev_block_hash(&self) -> BlockHash {
348        match self {
349            Header::Stock(h) => h.prev_block_hash,
350            Header::Blake2bV2(h) => h.prev_block_hash,
351        }
352    }
353
354    /// The merkle root, wire order.
355    pub const fn merkle_root(&self) -> [u8; 32] {
356        match self {
357            Header::Stock(h) => h.merkle_root,
358            Header::Blake2bV2(h) => h.merkle_root,
359        }
360    }
361
362    /// The consensus block time (for v2, with the offset applied when
363    /// flagged).
364    pub fn time(&self) -> u32 {
365        match self {
366            Header::Stock(h) => h.time,
367            Header::Blake2bV2(h) => h.time(),
368        }
369    }
370
371    /// Compact target.
372    pub const fn bits(&self) -> u32 {
373        match self {
374            Header::Stock(h) => h.bits,
375            Header::Blake2bV2(h) => h.bits,
376        }
377    }
378
379    /// The (first) nonce.
380    pub const fn nonce(&self) -> u32 {
381        match self {
382            Header::Stock(h) => h.nonce,
383            Header::Blake2bV2(h) => h.nonce,
384        }
385    }
386
387    /// The committed height: a v2 header carries it, a stock header does not
388    /// (siding's `blockHeight` then reads the coinbase's BIP 34 push, which
389    /// is `sidestr-core`'s job).
390    pub const fn height(&self) -> Option<u32> {
391        match self {
392            Header::Stock(_) => None,
393            Header::Blake2bV2(h) => Some(h.height),
394        }
395    }
396
397    /// The block hash — the family's proof-of-work hash, display order.
398    pub fn hash(&self) -> BlockHash {
399        match self {
400            Header::Stock(h) => h.hash(),
401            Header::Blake2bV2(h) => h.hash(),
402        }
403    }
404
405    /// The wire bytes.
406    pub fn encode(&self) -> EncodedHeader {
407        let mut buf = [0u8; 164];
408        let len = match self {
409            Header::Stock(h) => {
410                buf[..80].copy_from_slice(&h.encode());
411                80
412            }
413            Header::Blake2bV2(h) => {
414                buf.copy_from_slice(&h.encode());
415                164
416            }
417        };
418        EncodedHeader { buf, len }
419    }
420
421    /// The target `bits` encodes.
422    pub fn target(&self) -> Result<Target, Error> {
423        Target::from_compact(self.bits())
424    }
425
426    /// `hash ≤ target(bits)`.
427    pub fn meets_target(&self) -> bool {
428        match self {
429            Header::Stock(h) => h.meets_target(),
430            Header::Blake2bV2(h) => h.meets_target(),
431        }
432    }
433
434    /// SPEC 4 step 1 against the chain document's `powLimit`; see
435    /// [`check_pow`].
436    pub fn check_pow(&self, pow_limit: &Target) -> Result<(), Error> {
437        check_pow(self.bits(), self.hash(), pow_limit)
438    }
439
440    /// The 72-byte BIP-325 preimage with the stripped merkle root in place.
441    pub fn signet_preimage(&self, stripped_merkle_root: [u8; 32]) -> [u8; signet::BLOCK_DATA_LEN] {
442        match self {
443            Header::Stock(h) => h.signet_preimage(stripped_merkle_root),
444            Header::Blake2bV2(h) => h.signet_preimage(stripped_merkle_root),
445        }
446    }
447
448    /// The BIP-325 block data the block signature commits to (SPEC 4 step 2).
449    pub fn block_data(&self, stripped_merkle_root: [u8; 32]) -> [u8; 32] {
450        signet::block_data(&self.signet_preimage(stripped_merkle_root))
451    }
452}
453
454/// SPEC 4 step 1, "the header meets `powLimit`. No difficulty adjustment, no
455/// minimum-difficulty window", as siding and the kernel enforce it together:
456///
457/// 1. `bits` equals `pow_limit.to_compact()` — siding writes that value into
458///    every block (`siding/lib/chain.mjs`) and the kernel's difficulty rule
459///    under `powNoRetargeting` requires each block's bits to equal the
460///    previous block's, so the value never moves from genesis;
461/// 2. `hash ≤ Target::from_compact(bits)` (`btc:rule-header-pow`).
462///
463/// Compact encoding keeps only three significant bytes, so the target a
464/// block is actually checked against is `powLimit` truncated to its compact
465/// form — for the usual `7fff…ff` limit that is `7fffff00…00`, exactly as
466/// the kernel's `expandCompact(bits)` computes it.
467///
468/// Errors are [`Error::BitsNotPowLimit`] and [`Error::TargetNotMet`] in that
469/// order; a `bits` that does not decode is reported by [`Target::from_compact`].
470///
471/// ```
472/// use sidestr_header::{check_pow, BlockHash, Target};
473/// let lim = Target::from_hex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
474/// let easy = BlockHash::from_hex("4db37517728bd509c0cb96ee5a2e3e2a77f9e965a092e9f67948b413d453dbc0").unwrap();
475/// assert!(check_pow(0x207f_ffff, easy, &lim).is_ok());
476/// // Wrong bits, even an easier target, is refused before the hash is looked at.
477/// assert!(check_pow(0x2100_ffff, easy, &lim).is_err());
478/// // A hash above the limit is refused.
479/// let high = BlockHash::from_hex("8000000000000000000000000000000000000000000000000000000000000000").unwrap();
480/// assert!(check_pow(0x207f_ffff, high, &lim).is_err());
481/// ```
482pub fn check_pow(bits: u32, hash: BlockHash, pow_limit: &Target) -> Result<(), Error> {
483    let expected = pow_limit.to_compact();
484    if bits != expected {
485        return Err(Error::BitsNotPowLimit { bits, expected });
486    }
487    let target = Target::from_compact(bits)?;
488    if hash.meets(&target) {
489        Ok(())
490    } else {
491        Err(Error::TargetNotMet)
492    }
493}