sidestr_header/v2.rs
1//! Bitcoin Knots' 164-byte v2 header and its BLAKE2b proof of work: the
2//! family of a chain beside `xbt` or `txbt4` (SPEC 3.2).
3//!
4//! Ported from the kernel's `codec/pow/knots-header-v2.js` (the pipeline) and
5//! `schema/overlays/knots-blake2b.jsonld` (the layout), which follow Knots
6//! v29.4.1 `src/primitives/block.cpp` `CBlockHeader::GetHash`.
7
8use crate::hash::{blake2b_256, tagged_hash, Digest32};
9use crate::signet::{self, BLOCK_DATA_LEN};
10use crate::stock::{arr16, arr32, u16_at, u32_at};
11use crate::{BlockHash, Error, HeaderFamily, Target, VERSION_HEADER_V2_FLAG};
12
13/// `flags` bit 2: the consensus time is `time_on_wire + time_offset`
14/// (`FLAG_USE_TIME_OFFSET` in `codec/pow/knots-header-v2.js`).
15pub const FLAG_USE_TIME_OFFSET: u8 = 4;
16
17/// `flags` bits 0–1 select the ASIC input layout of the second BLAKE2b round.
18pub const FLAG_ASIC_PROFILE_MASK: u8 = 0x03;
19
20/// `flags` bits 6–7 are reserved for future hardforks and must be zero
21/// (`knots:rule-header-flags-reserved`).
22pub const FLAG_RESERVED_MASK: u8 = 0xc0;
23
24/// The kernel's name for this proof-of-work hash (`POW_HASH_NAME`).
25pub const POW_HASH_NAME: &str = "knots:blake2b-v2";
26
27/// The 164-byte header used from the BLAKE2b fork height — on a sidestr chain
28/// beside a BLAKE2b parent, from height 0 (`siding/lib/overlay.mjs`:
29/// `blake2bHeight: 0`).
30///
31/// The layout below is the kernel's `knots:BlockHeaderV2` struct with its
32/// field comments adapted. The first 80 bytes keep the classic layout
33/// (version with bit 31 set; the wire time may be offset); proof of work is
34/// the BLAKE2b construction over a commitment tree of these fields, not
35/// SHA-256d of the bytes.
36///
37/// | offset | size | field | wire |
38/// |---|---|---|---|
39/// | 0 | 4 | `version` | u32le, bit 31 set |
40/// | 4 | 32 | `prev_block_hash` | hash256 |
41/// | 36 | 32 | `merkle_root` | hash256 |
42/// | 68 | 4 | `time_on_wire` | u32le |
43/// | 72 | 4 | `bits` | u32le |
44/// | 76 | 4 | `nonce` | u32le |
45/// | 80 | 4 | `nonce2` | u32le |
46/// | 84 | 4 | `nonce3` | u32le |
47/// | 88 | 16 | `extranonce` | bytes, wire order |
48/// | 104 | 4 | `time_offset` | u32le |
49/// | 108 | 2 | `tx_count` | u16le |
50/// | 110 | 1 | `flags` | u8 |
51/// | 111 | 1 | `xor_key_mask_clear_bits` | u8 |
52/// | 112 | 16 | `xor_key` | bytes, wire order |
53/// | 128 | 4 | `height` | i32le |
54/// | 132 | 32 | `mm_rhs` | hash256 |
55///
56/// Beside a BLAKE2b parent siding's `buildBlock` sets `version =
57/// 0xa000_0000`, every nonce, the extranonce, the XOR key and `mm_rhs` to
58/// zero, `flags = 0`, `tx_count` to the block's transaction count and
59/// `height` to the block's height; only `nonce` moves while the block is
60/// sealed against `powLimit`.
61///
62/// ```
63/// use sidestr_header::{Blake2bV2Header, fork};
64/// // txbt4 block 150,308, the fork block: the first v2 / BLAKE2b header.
65/// // Captured from a Knots 29.4.1 node (bitcoin-desktop/schema test vectors).
66/// let bytes = hex::decode(
67/// "000000a0ccb157caa788400a667f6c19858ee913c701a42c8d1cd85122ec17000000000043d2e57990429ae581621ce01aa5fbf5e4c2723996be18660a4930b91e96d6c871b4946affff001dce0ac801d123881f71b4946a00000000b10cf00d0100000000000000000000008e00000000000000000000000000000000000000244b02000000000000000000000000000000000000000000000000000000000000000000",
68/// ).unwrap();
69/// let h = Blake2bV2Header::decode(&bytes).unwrap();
70/// assert_eq!(h.height, fork::TXBT4_FORK_HEIGHT);
71/// assert_eq!(h.hash(), fork::TXBT4_FORK_HASH);
72/// assert_eq!(h.tx_count, 142);
73/// assert_eq!(h.flags, 0); // ASIC profile 0, no time offset
74/// assert_eq!(h.encode().as_slice(), bytes.as_slice());
75/// ```
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct Blake2bV2Header {
78 /// Raw wire version: bit 31 set marks a v2 header; the low bits carry the
79 /// usual version.
80 pub version: u32,
81 /// The previous block's hash.
82 pub prev_block_hash: BlockHash,
83 /// The transaction merkle root in **wire (internal) order**.
84 pub merkle_root: [u8; 32],
85 /// Block time as serialised. The consensus time is `time_on_wire +
86 /// time_offset` when `flags` bit 2 is set; see [`Self::time`].
87 pub time_on_wire: u32,
88 /// Compact proof-of-work target.
89 pub bits: u32,
90 /// The first nonce.
91 pub nonce: u32,
92 /// The second nonce.
93 pub nonce2: u32,
94 /// The third nonce.
95 pub nonce3: u32,
96 /// 128-bit extranonce, wire order (bitcoind's RPC shows it
97 /// byte-reversed).
98 pub extranonce: [u8; 16],
99 /// The offset added to `time_on_wire` when `flags` bit 2 is set; also a
100 /// nonce input to the second BLAKE2b round.
101 pub time_offset: u32,
102 /// Committed transaction count; must equal the block's transaction count
103 /// (`knots:rule-block-txcount`).
104 pub tx_count: u16,
105 /// Bits 0–1: ASIC layout profile; bit 2: time offset in use; bits 6–7
106 /// reserved for future hardforks (must be 0).
107 pub flags: u8,
108 /// How many leading bits of the PoW XOR mask are cleared.
109 pub xor_key_mask_clear_bits: u8,
110 /// 128-bit PoW XOR key, wire order.
111 pub xor_key: [u8; 16],
112 /// Committed height; must equal the previous header's height + 1
113 /// (`knots:rule-header-height`). Serialised `i32le`; held unsigned.
114 pub height: u32,
115 /// Merge-mining hook right-hand side, wire order (reserved; all zeros so
116 /// far).
117 pub mm_rhs: [u8; 32],
118}
119
120/// Every intermediate of the v2 pipeline, for tests and debugging — the
121/// kernel's `hashHeaderV2Detailed`. Digests are in the byte order the
122/// pipeline feeds them onward; `hash` is the display-order block hash.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct V2HashStages {
125 /// `tagged("Bitcoin block hash PoW XOR key", xor_key)`.
126 pub xor_key_hash: Digest32,
127 /// Round one: the tagged hash over the committed fields.
128 pub h1: Digest32,
129 /// Round two: the merge-mining hook over `h1`.
130 pub h2: Digest32,
131 /// The first BLAKE2b-256, over `0 ‖ h2 ‖ extranonce`.
132 pub blake2b_1: Digest32,
133 /// The second BLAKE2b-256, over the profile-dependent ASIC input.
134 pub blake2b_2: Digest32,
135 /// The XOR mask applied to `blake2b_2` (zero when the key is zero).
136 pub mask: Digest32,
137 /// `flags & 3`.
138 pub asic_profile: u8,
139 /// `blake2b_2 XOR mask`: the block hash.
140 pub hash: BlockHash,
141}
142
143impl Blake2bV2Header {
144 /// Wire size in bytes.
145 pub const WIRE_SIZE: usize = 164;
146
147 /// Decodes exactly 164 bytes. Rejects any other length
148 /// ([`Error::WrongLength`]) and a version with bit 31 clear
149 /// ([`Error::VersionBit31Clear`]). Reserved flag bits are not checked
150 /// here — the kernel decodes them and its rule rejects them — see
151 /// [`Self::check_flags`].
152 pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
153 if bytes.len() != Self::WIRE_SIZE {
154 return Err(Error::WrongLength {
155 family: HeaderFamily::Blake2bV2,
156 expected: Self::WIRE_SIZE,
157 actual: bytes.len(),
158 });
159 }
160 let version = u32_at(bytes, 0);
161 if version & VERSION_HEADER_V2_FLAG == 0 {
162 return Err(Error::VersionBit31Clear);
163 }
164 Ok(Blake2bV2Header {
165 version,
166 prev_block_hash: BlockHash::from_wire(arr32(bytes, 4)),
167 merkle_root: arr32(bytes, 36),
168 time_on_wire: u32_at(bytes, 68),
169 bits: u32_at(bytes, 72),
170 nonce: u32_at(bytes, 76),
171 nonce2: u32_at(bytes, 80),
172 nonce3: u32_at(bytes, 84),
173 extranonce: arr16(bytes, 88),
174 time_offset: u32_at(bytes, 104),
175 tx_count: u16_at(bytes, 108),
176 flags: bytes[110],
177 xor_key_mask_clear_bits: bytes[111],
178 xor_key: arr16(bytes, 112),
179 height: u32_at(bytes, 128),
180 mm_rhs: arr32(bytes, 132),
181 })
182 }
183
184 /// The 164 wire bytes.
185 pub fn encode(&self) -> [u8; 164] {
186 let mut out = [0u8; 164];
187 out[..72].copy_from_slice(&self.signet_preimage(self.merkle_root));
188 out[72..76].copy_from_slice(&self.bits.to_le_bytes());
189 out[76..80].copy_from_slice(&self.nonce.to_le_bytes());
190 out[80..84].copy_from_slice(&self.nonce2.to_le_bytes());
191 out[84..88].copy_from_slice(&self.nonce3.to_le_bytes());
192 out[88..104].copy_from_slice(&self.extranonce);
193 out[104..108].copy_from_slice(&self.time_offset.to_le_bytes());
194 out[108..110].copy_from_slice(&self.tx_count.to_le_bytes());
195 out[110] = self.flags;
196 out[111] = self.xor_key_mask_clear_bits;
197 out[112..128].copy_from_slice(&self.xor_key);
198 out[128..132].copy_from_slice(&self.height.to_le_bytes());
199 out[132..].copy_from_slice(&self.mm_rhs);
200 out
201 }
202
203 /// The consensus block time: `time_on_wire + time_offset` (wrapping)
204 /// when `flags` bit 2 is set, else `time_on_wire` (`headerTime` in the
205 /// kernel).
206 pub fn time(&self) -> u32 {
207 if self.uses_time_offset() {
208 self.time_on_wire.wrapping_add(self.time_offset)
209 } else {
210 self.time_on_wire
211 }
212 }
213
214 /// Whether `flags` bit 2 is set.
215 pub fn uses_time_offset(&self) -> bool {
216 self.flags & FLAG_USE_TIME_OFFSET != 0
217 }
218
219 /// `flags & 3`: which ASIC input layout the second BLAKE2b round uses.
220 pub fn asic_profile(&self) -> u8 {
221 self.flags & FLAG_ASIC_PROFILE_MASK
222 }
223
224 /// `knots:rule-header-flags-reserved`: bits 6–7 of `flags` must be zero.
225 pub fn check_flags(&self) -> Result<(), Error> {
226 if self.flags & FLAG_RESERVED_MASK != 0 {
227 Err(Error::ReservedFlags(self.flags))
228 } else {
229 Ok(())
230 }
231 }
232
233 /// The block hash, which is the proof-of-work hash (`hashHeaderV2`):
234 ///
235 /// ```text
236 /// h1 = tagged("Bitcoin block header 1",
237 /// version ‖ prev(display) ‖ height ‖ merkle(wire) ‖ time_on_wire ‖ 0x00
238 /// ‖ bits ‖ tx_count as u32 ‖ flags ‖ clear_bits ‖ tagged("…XOR key", xor_key))
239 /// h2 = tagged("Merge-mining hook", h1 ‖ 0¹⁶ ‖ 0¹⁶ ‖ mm_rhs(wire))
240 /// b1 = blake2b-256(0u32 ‖ h2 ‖ extranonce)
241 /// b2 = blake2b-256(profile-dependent layout of prev_hidden / h2, nonces, b1)
242 /// hash = b2 XOR mask(xor_key, clear_bits) (display-order bytes)
243 /// ```
244 ///
245 /// Note the two byte orders inside round one: `prev_block_hash` enters
246 /// in display order (Knots' `hashPrevBlock.ReversedBytes()`) while the
247 /// merkle root enters in wire order.
248 pub fn hash(&self) -> BlockHash {
249 self.hash_stages().hash
250 }
251
252 /// The pipeline with every intermediate exposed.
253 pub fn hash_stages(&self) -> V2HashStages {
254 let prev_display = self.prev_block_hash.0;
255 let xor_key_hash = tagged_hash(b"Bitcoin block hash PoW XOR key", &[&self.xor_key]);
256
257 let mut mask = [0u8; 32];
258 if self.xor_key.iter().any(|&b| b != 0) {
259 mask = tagged_hash(b"Bitcoin block hash PoW XOR mask", &[&self.xor_key]);
260 let clear_bytes = usize::from(self.xor_key_mask_clear_bits >> 3);
261 let n = clear_bytes.min(32);
262 mask[..n].fill(0);
263 if clear_bytes < 32 {
264 mask[clear_bytes] &= 0xff >> (self.xor_key_mask_clear_bits & 7);
265 }
266 }
267
268 let prev_hidden = tagged_hash(b"Bitcoin prevblock header, hashed", &[&prev_display]);
269 let h1 = tagged_hash(
270 b"Bitcoin block header 1",
271 &[
272 &self.version.to_le_bytes(),
273 &prev_display,
274 &self.height.to_le_bytes(),
275 &self.merkle_root,
276 &self.time_on_wire.to_le_bytes(),
277 &[0u8],
278 &self.bits.to_le_bytes(),
279 &u32::from(self.tx_count).to_le_bytes(),
280 &[self.flags, self.xor_key_mask_clear_bits],
281 &xor_key_hash,
282 ],
283 );
284 let zeros16 = [0u8; 16];
285 let h2 = tagged_hash(
286 b"Merge-mining hook",
287 &[&h1, &zeros16, &zeros16, &self.mm_rhs],
288 );
289 let b1 = blake2b_256(&[&0u32.to_le_bytes(), &h2, &self.extranonce]);
290
291 let nonce = self.nonce.to_le_bytes();
292 let nonce2 = self.nonce2.to_le_bytes();
293 let nonce3 = self.nonce3.to_le_bytes();
294 let offset = self.time_offset.to_le_bytes();
295 let asic_profile = self.asic_profile();
296 let b2 = match asic_profile {
297 0 => {
298 let mut p = prev_hidden;
299 p[..6].fill(0);
300 blake2b_256(&[&p, &nonce, &nonce2, &offset, &nonce3, &b1])
301 }
302 1 => blake2b_256(&[&nonce, &nonce2, &nonce3, &offset, &b1, &h2]),
303 2 => blake2b_256(&[&[0u8; 48], &h2, &nonce, &nonce2, &offset, &nonce3, &b1]),
304 _ => blake2b_256(&[&[0u8; 80], &h2, &nonce, &nonce2, &offset, &nonce3, &b1]),
305 };
306
307 let mut hash = [0u8; 32];
308 for i in 0..32 {
309 hash[i] = b2[i] ^ mask[i];
310 }
311 V2HashStages {
312 xor_key_hash,
313 h1,
314 h2,
315 blake2b_1: b1,
316 blake2b_2: b2,
317 mask,
318 asic_profile,
319 hash: BlockHash(hash),
320 }
321 }
322
323 /// The target `bits` encodes.
324 pub fn target(&self) -> Result<Target, Error> {
325 Target::from_compact(self.bits)
326 }
327
328 /// `hash ≤ target(bits)`: the kernel's `checkProofOfWork`. False when
329 /// `bits` does not decode.
330 pub fn meets_target(&self) -> bool {
331 self.target().is_ok_and(|t| self.hash().meets(&t))
332 }
333
334 /// SPEC 4 step 1 as siding applies it: `bits` must be the compact form
335 /// of `pow_limit` and the hash must meet it.
336 pub fn check_pow(&self, pow_limit: &Target) -> Result<(), Error> {
337 crate::check_pow(self.bits, self.hash(), pow_limit)
338 }
339
340 /// The first 72 header bytes — `version ‖ prev ‖ merkle_root ‖
341 /// time_on_wire` — with `merkle_root` replaced by `stripped_merkle_root`.
342 /// Same shape as the stock family's, which is what makes the BIP-325
343 /// block data family-agnostic (`siding/lib/block.mjs` `blockData`).
344 pub fn signet_preimage(&self, stripped_merkle_root: [u8; 32]) -> [u8; BLOCK_DATA_LEN] {
345 let mut out = [0u8; BLOCK_DATA_LEN];
346 out[..4].copy_from_slice(&self.version.to_le_bytes());
347 out[4..36].copy_from_slice(&self.prev_block_hash.to_wire());
348 out[36..68].copy_from_slice(&stripped_merkle_root);
349 out[68..].copy_from_slice(&self.time_on_wire.to_le_bytes());
350 out
351 }
352
353 /// The BIP-325 block data, `SHA256(signet_preimage)` (SPEC 4 step 2).
354 pub fn block_data(&self, stripped_merkle_root: [u8; 32]) -> [u8; 32] {
355 signet::block_data(&self.signet_preimage(stripped_merkle_root))
356 }
357}