Skip to main content

sidestr_core/
block.rs

1//! Blocks on a sidestr chain (SPEC 4): building, the block data that is
2//! signed, the virtual transactions the challenge is evaluated against, the
3//! solution's place in the coinbase, and the height a stock header does not
4//! carry. A port of `siding/lib/block.mjs`.
5//!
6//! A block is a header of the parent's family followed by Bitcoin
7//! transactions: Bitcoin's transaction rules are the parent's, so
8//! [`bitcoin::Transaction`] serves for every family, and what differs — the
9//! header's layout, its proof-of-work hash, whether it carries the height —
10//! is behind [`HeaderFamily`]. Beside a stock parent the block *is*
11//! [`bitcoin::Block`] ([`Stock`]); beside a BLAKE2b parent it is
12//! [`FamilyBlock`] over the 164-byte v2 header that `sidestr-header`
13//! implements. Every function here is generic over the family and never asks
14//! which one it has.
15//!
16//! What sidestr adds is in the coinbase: after the witness commitment output's
17//! commitment, one push of `ecc7daa2` followed by a serialised script witness
18//! that satisfies the chain's `challenge` for the block's *signet hash*,
19//! computed as BIP 325 computes it over this chain's header serialisation.
20//!
21//! ```
22//! use sidestr_core::block::{coinbase_height, height_push};
23//! use bitcoin::{Transaction, TxIn, ScriptBuf, OutPoint, Sequence, Witness, transaction::Version, absolute::LockTime};
24//!
25//! let coinbase = |sig: Vec<u8>| Transaction { version: Version::TWO, lock_time: LockTime::ZERO,
26//!     input: vec![TxIn { previous_output: OutPoint::null(), script_sig: ScriptBuf::from_bytes(sig), sequence: Sequence::MAX, witness: Witness::new() }],
27//!     output: vec![] };
28//!
29//! // the height push is BIP 34's: little-endian, a padding byte only after a high bit
30//! assert_eq!(height_push(0), vec![0x00]);
31//! assert_eq!(height_push(128), vec![0x02, 0x80, 0x00]);
32//! assert_eq!(coinbase_height(&coinbase([height_push(70_000), vec![0xff]].concat())).unwrap(), 70_000);
33//! // and anything that is not a height push is refused, never read as height 0
34//! assert!(coinbase_height(&coinbase(vec![])).is_err());
35//! assert!(coinbase_height(&coinbase(vec![0x02, 0x05, 0x00])).unwrap_err().to_string().contains("not minimal"));
36//! ```
37
38use std::sync::OnceLock;
39
40use bitcoin::block::{Header, Version as HeaderVersion};
41use bitcoin::consensus::encode::{deserialize, serialize};
42use bitcoin::hashes::{sha256, sha256d, Hash, HashEngine};
43use bitcoin::key::TweakedPublicKey;
44use bitcoin::secp256k1::{
45    schnorr::Signature, All, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey,
46};
47use bitcoin::sighash::{Annex, Prevouts, SighashCache, TapSighashType};
48use bitcoin::taproot::TapLeafHash;
49use bitcoin::transaction::Version as TxVersion;
50use bitcoin::{
51    absolute::LockTime, merkle_tree, Amount, BlockHash, CompactTarget, OutPoint, Script, ScriptBuf,
52    Sequence, Target, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness, Wtxid,
53};
54
55use crate::error::{Error, Result};
56use crate::federation::{verify_multi_a_input, MultiA, ScriptPathError};
57use crate::parents::Family;
58use crate::rules::RuleResult;
59use crate::sighash::{verify_taproot_key_path, SighashRules};
60
61/// The four bytes that open the solution push: BIP 325's signet header.
62pub const SIGNET_HEADER: [u8; 4] = [0xec, 0xc7, 0xda, 0xa2];
63/// The tag every non-genesis coinbase pushes after its height.
64pub const MARKER: &str = "sidestr";
65/// The six bytes that open a witness commitment output.
66const COMMITMENT_PREFIX: [u8; 6] = [0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
67/// `OP_RETURN 0x24 aa21a9ed <32-byte commitment>`: the part of the
68/// commitment output that is not the solution.
69const COMMITMENT_LEN: usize = 38;
70/// The most witness items a solution may carry: BIP 341's control block
71/// allows 128 branches, so no honest witness comes near this.
72const MAX_WITNESS_ITEMS: usize = 256;
73
74/// One shared secp256k1 context for signing and verification.
75pub fn secp() -> &'static Secp256k1<All> {
76    static SECP: OnceLock<Secp256k1<All>> = OnceLock::new();
77    SECP.get_or_init(Secp256k1::new)
78}
79
80// --- the header family boundary (SPEC 3, 3.2) ------------------------------------
81
82/// What differs between header families, and nothing else: the header type
83/// and its wire codec, the proof-of-work hash, the bytes the block signature
84/// commits to, whether the header carries the height, what an unsigned
85/// header looks like, the rules the parent's fork adds, and whether Knots'
86/// unified sighash applies. The rules, the state and the chain are generic
87/// over `F: HeaderFamily` and never ask which one they have.
88///
89/// `sidestr-core` ships [`Stock`] (parents `btc`, `tbtc4`), whose header is
90/// [`bitcoin::block::Header`] and whose block is [`bitcoin::Block`].
91/// `sidestr-header` implements this trait for Knots' 164-byte v2 header
92/// (parents `xbt`, `txbt4`) with [`FamilyBlock`] as its block; this crate
93/// keeps no edge to it.
94pub trait HeaderFamily:
95    core::fmt::Debug + Copy + Default + PartialEq + Eq + Send + Sync + 'static
96{
97    /// The header: the parent's layout, decoded.
98    type Header: Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync + 'static;
99    /// The block: this header followed by the transactions. [`bitcoin::Block`]
100    /// for the stock family; [`FamilyBlock`] otherwise.
101    type Block: SidestrBlock<Header = Self::Header>;
102    /// Which family this is.
103    const FAMILY: Family;
104    /// The serialised header length: 80 or 164.
105    const HEADER_LEN: usize;
106
107    /// Which family this is.
108    fn family(&self) -> Family {
109        Self::FAMILY
110    }
111    /// The serialised header length.
112    fn header_len(&self) -> usize {
113        Self::HEADER_LEN
114    }
115    /// The header's wire bytes.
116    fn encode_header(&self, header: &Self::Header) -> Vec<u8>;
117    /// A header from exactly [`Self::HEADER_LEN`] wire bytes.
118    fn decode_header(&self, bytes: &[u8]) -> Result<Self::Header>;
119    /// The block hash: the parent's proof-of-work hash over the header.
120    fn block_hash(&self, header: &Self::Header) -> BlockHash;
121    /// The bytes the block signature commits to (SPEC 4): the header's first
122    /// 72 bytes, version, prev, merkle root and time on wire — never the nonce,
123    /// which is found after signing.
124    fn signed_prefix(&self, header: &Self::Header) -> Vec<u8>;
125    /// The height the header itself carries, when the family writes it there.
126    fn header_height(&self, header: &Self::Header) -> Option<u32>;
127    /// An unsigned header on `prev` with this family's version and zero nonce
128    /// (`siding/lib/block.mjs buildBlock`). `height` and `tx_count` are for
129    /// the families whose header commits to them; the stock header ignores both.
130    fn new_header(
131        &self,
132        prev: BlockHash,
133        merkle_root: TxMerkleNode,
134        time: u32,
135        bits: CompactTarget,
136        height: u32,
137        tx_count: usize,
138    ) -> Self::Header;
139    /// The raw wire version word, unsigned.
140    fn version(&self, header: &Self::Header) -> u32;
141    /// The version as the reference kernel's codec *types* it, which is what
142    /// `btc:rule-header-version` compares against the minimum: a stock header's
143    /// `version` is `i32le` (`btc:BlockHeader` in `schema/core.jsonld`), so a
144    /// word with bit 31 set is negative and fails `version >= 1`; a Knots v2
145    /// header's is `u32le` (`schema/overlays/knots-blake2b.jsonld`), so its
146    /// mandatory bit 31 does not. Every family states this itself — there is
147    /// no default — so a typed header cannot reach the rule with the wrong
148    /// sign.
149    fn version_number(&self, header: &Self::Header) -> i64;
150    /// The previous block's hash.
151    fn prev(&self, header: &Self::Header) -> BlockHash;
152    /// The merkle root.
153    fn merkle_root(&self, header: &Self::Header) -> TxMerkleNode;
154    /// The consensus time: for a v2 header, the wire time with its offset applied.
155    fn time(&self, header: &Self::Header) -> u32;
156    /// The compact target.
157    fn bits(&self, header: &Self::Header) -> CompactTarget;
158    /// The (first) nonce.
159    fn nonce(&self, header: &Self::Header) -> u32;
160    /// Replace the merkle root; [`seal_block`] does so once the solution is in.
161    fn set_merkle_root(&self, header: &mut Self::Header, root: TxMerkleNode);
162    /// Replace the nonce; [`seal_block`] grinds it.
163    fn set_nonce(&self, header: &mut Self::Header, nonce: u32);
164    /// The header rules the parent's fork adds beyond Bitcoin's (the Knots
165    /// overlay's `knots:rule-header-*`), judged for a header at `height`. None
166    /// for the stock family.
167    fn header_rules(&self, header: &Self::Header, height: u32) -> Vec<RuleResult> {
168        let _ = (header, height);
169        Vec::new()
170    }
171    /// The block rules the parent's fork adds (`knots:rule-block-txcount`),
172    /// given the header and the block's transaction count.
173    fn block_rules(&self, header: &Self::Header, tx_count: usize) -> Vec<RuleResult> {
174        let _ = (header, tx_count);
175        Vec::new()
176    }
177    /// The signature-hash rules a spend at `height` is judged by: BIP 341 on a
178    /// stock chain; Knots' unified opt-in sighash from the fork height on a
179    /// BLAKE2b chain, which for a sidestr chain is height 0
180    /// (`siding/lib/overlay.mjs`: `unifiedSighashParam: 'blake2bHeight'`,
181    /// `blake2bHeight: 0`).
182    fn sighash_rules(&self, height: u32) -> SighashRules {
183        let _ = height;
184        SighashRules::Bip341
185    }
186}
187
188/// What the rules need of a block, whatever its header: the header, the
189/// transactions and the wire codec. Implemented by [`bitcoin::Block`] for the
190/// stock family and by [`FamilyBlock`] for any other. The functions that do
191/// not touch the header — the solution, the commitment output — are generic
192/// over this trait alone, so `&bitcoin::Block` needs no annotation.
193pub trait SidestrBlock:
194    Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync + Sized + 'static
195{
196    /// The header type.
197    type Header;
198    /// A block from its parts.
199    fn from_parts(header: Self::Header, txdata: Vec<Transaction>) -> Self;
200    /// The header.
201    fn header(&self) -> &Self::Header;
202    /// The header, to seal.
203    fn header_mut(&mut self) -> &mut Self::Header;
204    /// The transactions, coinbase first.
205    fn txdata(&self) -> &[Transaction];
206    /// The transactions, to build.
207    fn txdata_mut(&mut self) -> &mut Vec<Transaction>;
208    /// The consensus bytes: header, then the transaction vector.
209    fn encode(&self) -> Vec<u8>;
210    /// A block from its consensus bytes, all of them.
211    fn decode(bytes: &[u8]) -> Result<Self>;
212}
213
214/// Version bit 31: the kernel's `VERSION_HEADER_V2_FLAG`
215/// (`codec/pow/knots-header-v2.js`), set on every Knots v2 header and never
216/// on a stock one. A stock header carrying it is not a stock header:
217/// [`Stock::decode_header`] refuses it, and on the typed path
218/// `btc:rule-header-version` does, because the kernel reads a stock version
219/// as `i32le` and the word is negative.
220pub const VERSION_HEADER_V2_FLAG: u32 = 0x8000_0000;
221
222/// The stock 80-byte Bitcoin header, hashed with double SHA-256 (parents
223/// `btc` and `tbtc4`). Version `0x20000000`, bit 31 clear, so a Knots node
224/// never mistakes it for a v2 header. The stock header does not carry the
225/// height, so beside a stock parent the coinbase is the only place the
226/// height is written ([`coinbase_height`]).
227#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
228pub struct Stock;
229
230/// [`Error::Encoding`] for a stock header whose version has bit 31 set.
231fn stock_bit31(header: &Header) -> Result<()> {
232    if header.version.to_consensus() as u32 & VERSION_HEADER_V2_FLAG != 0 {
233        return Err(Error::Encoding(format!(
234            "stock header: version {:#010x} has bit 31 set (VERSION_HEADER_V2_FLAG): not a stock header",
235            header.version.to_consensus() as u32
236        )));
237    }
238    Ok(())
239}
240
241impl HeaderFamily for Stock {
242    type Header = Header;
243    type Block = bitcoin::Block;
244    const FAMILY: Family = Family::Stock;
245    const HEADER_LEN: usize = 80;
246
247    fn encode_header(&self, header: &Header) -> Vec<u8> {
248        serialize(header)
249    }
250    /// Exactly 80 bytes with bit 31 of the version clear; a set bit 31 is
251    /// refused as the kernel's `structVariants` would select the v2 layout
252    /// for it (and as `sidestr-header`'s `StockHeader::decode` refuses it).
253    fn decode_header(&self, bytes: &[u8]) -> Result<Header> {
254        let header: Header =
255            deserialize(bytes).map_err(|e| Error::Encoding(format!("stock header: {e}")))?;
256        stock_bit31(&header)?;
257        Ok(header)
258    }
259    fn block_hash(&self, header: &Header) -> BlockHash {
260        header.block_hash()
261    }
262    fn signed_prefix(&self, header: &Header) -> Vec<u8> {
263        serialize(header)[..72].to_vec()
264    }
265    fn header_height(&self, _header: &Header) -> Option<u32> {
266        None
267    }
268    fn new_header(
269        &self,
270        prev: BlockHash,
271        merkle_root: TxMerkleNode,
272        time: u32,
273        bits: CompactTarget,
274        _height: u32,
275        _tx_count: usize,
276    ) -> Header {
277        Header {
278            version: HeaderVersion::from_consensus(0x2000_0000),
279            prev_blockhash: prev,
280            merkle_root,
281            time,
282            bits,
283            nonce: 0,
284        }
285    }
286    fn version(&self, header: &Header) -> u32 {
287        header.version.to_consensus() as u32
288    }
289    /// `i32le`: bit 31 set reads as a negative version.
290    fn version_number(&self, header: &Header) -> i64 {
291        i64::from(header.version.to_consensus())
292    }
293    fn prev(&self, header: &Header) -> BlockHash {
294        header.prev_blockhash
295    }
296    fn merkle_root(&self, header: &Header) -> TxMerkleNode {
297        header.merkle_root
298    }
299    fn time(&self, header: &Header) -> u32 {
300        header.time
301    }
302    fn bits(&self, header: &Header) -> CompactTarget {
303        header.bits
304    }
305    fn nonce(&self, header: &Header) -> u32 {
306        header.nonce
307    }
308    fn set_merkle_root(&self, header: &mut Header, root: TxMerkleNode) {
309        header.merkle_root = root;
310    }
311    fn set_nonce(&self, header: &mut Header, nonce: u32) {
312        header.nonce = nonce;
313    }
314}
315
316impl SidestrBlock for bitcoin::Block {
317    type Header = Header;
318    fn from_parts(header: Header, txdata: Vec<Transaction>) -> Self {
319        bitcoin::Block { header, txdata }
320    }
321    fn header(&self) -> &Header {
322        &self.header
323    }
324    fn header_mut(&mut self) -> &mut Header {
325        &mut self.header
326    }
327    fn txdata(&self) -> &[Transaction] {
328        &self.txdata
329    }
330    fn txdata_mut(&mut self) -> &mut Vec<Transaction> {
331        &mut self.txdata
332    }
333    fn encode(&self) -> Vec<u8> {
334        serialize(self)
335    }
336    /// The consensus bytes of a stock block; a header with bit 31 set is
337    /// refused here as [`Stock::decode_header`] refuses it.
338    fn decode(bytes: &[u8]) -> Result<Self> {
339        let block: bitcoin::Block =
340            deserialize(bytes).map_err(|e| Error::Encoding(e.to_string()))?;
341        stock_bit31(&block.header)?;
342        Ok(block)
343    }
344}
345
346/// The stock block: [`bitcoin::Block`].
347pub type Block = <Stock as HeaderFamily>::Block;
348
349/// A block of any family: its header followed by the transactions, on the
350/// wire as `header ‖ CompactSize(n) ‖ tx…`, exactly as a Bitcoin block is
351/// laid out with the family's header in place of the 80-byte one. The block
352/// type of every family but [`Stock`].
353#[derive(Clone, Debug, PartialEq, Eq)]
354pub struct FamilyBlock<F: HeaderFamily> {
355    /// The header.
356    pub header: F::Header,
357    /// The transactions, coinbase first.
358    pub txdata: Vec<Transaction>,
359    /// The family marker, so the derives bound `F` and not only `F::Header`.
360    family: F,
361}
362
363impl<F: HeaderFamily> SidestrBlock for FamilyBlock<F> {
364    type Header = F::Header;
365    fn from_parts(header: F::Header, txdata: Vec<Transaction>) -> Self {
366        FamilyBlock {
367            header,
368            txdata,
369            family: F::default(),
370        }
371    }
372    fn header(&self) -> &F::Header {
373        &self.header
374    }
375    fn header_mut(&mut self) -> &mut F::Header {
376        &mut self.header
377    }
378    fn txdata(&self) -> &[Transaction] {
379        &self.txdata
380    }
381    fn txdata_mut(&mut self) -> &mut Vec<Transaction> {
382        &mut self.txdata
383    }
384    fn encode(&self) -> Vec<u8> {
385        let mut out = F::default().encode_header(&self.header);
386        out.extend(serialize(&self.txdata));
387        out
388    }
389    fn decode(bytes: &[u8]) -> Result<Self> {
390        if bytes.len() < F::HEADER_LEN {
391            return Err(Error::Encoding(format!(
392                "block of {} bytes is shorter than a {} header",
393                bytes.len(),
394                F::HEADER_LEN
395            )));
396        }
397        let header = F::default().decode_header(&bytes[..F::HEADER_LEN])?;
398        let txdata: Vec<Transaction> =
399            deserialize(&bytes[F::HEADER_LEN..]).map_err(|e| Error::Encoding(e.to_string()))?;
400        Ok(FamilyBlock {
401            header,
402            txdata,
403            family: F::default(),
404        })
405    }
406}
407
408/// The block's weight as the reference kernel computes it (`blocks.js
409/// blockWeight`): three times the legacy size plus the total size, the
410/// family's header length counted in both. Equals [`bitcoin::Block::weight`]
411/// for the stock family.
412pub fn block_weight<F: HeaderFamily>(family: &F, block: &F::Block) -> u64 {
413    let n = block.txdata().len();
414    let varint = match n {
415        0..=0xfc => 1,
416        0xfd..=0xffff => 3,
417        _ => 5,
418    };
419    let fixed = (family.header_len() as u64).saturating_add(varint);
420    let legacy = block
421        .txdata()
422        .iter()
423        .fold(fixed, |n, tx| n.saturating_add(tx.base_size() as u64));
424    let total = block
425        .txdata()
426        .iter()
427        .fold(fixed, |n, tx| n.saturating_add(tx.total_size() as u64));
428    legacy.saturating_mul(3).saturating_add(total)
429}
430
431/// The merkle root over these transactions' txids; all zeros for none.
432pub fn merkle_root_of_txs(txdata: &[Transaction]) -> TxMerkleNode {
433    merkle_root_of(txdata.iter().map(Transaction::compute_txid))
434}
435
436/// The witness merkle root: the coinbase's wtxid taken as all zeros, then
437/// every other transaction's wtxid. What the witness commitment hashes.
438pub fn witness_root_of_txs(txdata: &[Transaction]) -> [u8; 32] {
439    merkle_tree::calculate_root(
440        std::iter::once(Wtxid::all_zeros())
441            .chain(txdata.iter().skip(1).map(Transaction::compute_wtxid)),
442    )
443    .map(|h| h.to_byte_array())
444    .unwrap_or([0u8; 32])
445}
446
447// --- the witness carried in the solution --------------------------------------
448
449fn compact_size(n: usize) -> Vec<u8> {
450    match n {
451        0..=0xfc => vec![n as u8],
452        0xfd..=0xffff => vec![0xfd, (n & 0xff) as u8, (n >> 8) as u8],
453        _ => vec![
454            0xfe,
455            (n & 0xff) as u8,
456            ((n >> 8) & 0xff) as u8,
457            ((n >> 16) & 0xff) as u8,
458            ((n >> 24) & 0xff) as u8,
459        ],
460    }
461}
462
463/// A CompactSize at `i`, minimal: `0xfd` must encode at least 0xfd, `0xfe` at
464/// least 0x10000. `None` when the bytes run out, are not minimal, or use the
465/// 8-byte form no witness item needs.
466fn read_compact(b: &[u8], i: usize) -> Option<(usize, usize)> {
467    let first = *b.get(i)?;
468    match first {
469        0..=0xfc => Some((usize::from(first), i + 1)),
470        0xfd => {
471            let n = usize::from(*b.get(i + 1)?) | usize::from(*b.get(i + 2)?) << 8;
472            (n >= 0xfd).then_some((n, i + 3))
473        }
474        0xfe => {
475            let n = usize::from(*b.get(i + 1)?)
476                | usize::from(*b.get(i + 2)?) << 8
477                | usize::from(*b.get(i + 3)?) << 16
478                | usize::from(*b.get(i + 4)?) << 24;
479            (n >= 0x1_0000).then_some((n, i + 5))
480        }
481        _ => None,
482    }
483}
484
485/// A serialised script witness: the item count, then each item length-prefixed
486/// (`siding/lib/block.mjs encodeWitness`).
487pub fn encode_witness(items: &[Vec<u8>]) -> Vec<u8> {
488    let mut out = compact_size(items.len());
489    for it in items {
490        out.extend(compact_size(it.len()));
491        out.extend_from_slice(it);
492    }
493    out
494}
495
496/// The inverse of [`encode_witness`], strictly: `None` when the bytes run
497/// out, when a CompactSize is not minimal, when more than 256 items are
498/// announced, or when bytes remain after the last item. The reference
499/// decoder is lenient on all four; a solution is consensus data, so this
500/// one is not.
501pub fn decode_witness(bytes: &[u8]) -> Option<Vec<Vec<u8>>> {
502    let (n, mut i) = read_compact(bytes, 0)?;
503    if n > MAX_WITNESS_ITEMS {
504        return None;
505    }
506    let mut items = Vec::with_capacity(n);
507    for _ in 0..n {
508        let (len, at) = read_compact(bytes, i)?;
509        let end = at.checked_add(len)?;
510        items.push(bytes.get(at..end)?.to_vec());
511        i = end;
512    }
513    (i == bytes.len()).then_some(items)
514}
515
516/// The coinbase output carrying the witness commitment: the last one whose
517/// script starts `OP_RETURN 0x24 aa21a9ed` and is at least 38 bytes
518/// (`siding/lib/block.mjs commitmentOutput`). The solution push follows it.
519pub fn commitment_output<B: SidestrBlock>(block: &B) -> Option<(usize, &Script)> {
520    commitment_output_of(block.txdata().first()?)
521}
522
523fn commitment_output_of(cb: &Transaction) -> Option<(usize, &Script)> {
524    cb.output
525        .iter()
526        .enumerate()
527        .rev()
528        .find(|(_, o)| {
529            o.script_pubkey.len() >= COMMITMENT_LEN
530                && o.script_pubkey.as_bytes().starts_with(&COMMITMENT_PREFIX)
531        })
532        .map(|(i, o)| (i, o.script_pubkey.as_script()))
533}
534
535/// The solution a block carries: the witness items after `ecc7daa2`, and where.
536#[derive(Debug, Clone, PartialEq, Eq)]
537pub struct Solution {
538    /// The script witness that must satisfy the challenge.
539    pub witness: Vec<Vec<u8>>,
540    /// Index of the commitment output in the coinbase.
541    pub index: usize,
542}
543
544/// The block's solution, or `None` when the commitment output carries no
545/// well-formed one (`siding/lib/block.mjs solutionOf`). One push, direct
546/// (≤ 75 bytes), `OP_PUSHDATA1` (≤ 255) or `OP_PUSHDATA2` (≤ 65535) as the
547/// size needs (SPEC 4); it must start with the signet header.
548pub fn solution_of<B: SidestrBlock>(block: &B) -> Option<Solution> {
549    let (index, spk) = commitment_output(block)?;
550    let rest = &spk.as_bytes()[COMMITMENT_LEN..];
551    if rest.is_empty() {
552        return None;
553    }
554    let (n, at) = match rest[0] {
555        1..=75 => (usize::from(rest[0]), 1),
556        0x4c if rest.len() >= 2 => (usize::from(rest[1]), 2),
557        0x4d if rest.len() >= 3 => (usize::from(rest[1]) | usize::from(rest[2]) << 8, 3),
558        _ => return None,
559    };
560    if n == 0 || rest.len() != at + n {
561        return None;
562    }
563    let push = &rest[at..];
564    let body = push.strip_prefix(&SIGNET_HEADER)?;
565    Some(Solution {
566        witness: decode_witness(body)?,
567        index,
568    })
569}
570
571/// The block with this witness as its solution (`siding/lib/block.mjs
572/// withSolution`): the commitment output's script becomes the 38-byte
573/// commitment, then one push of `ecc7daa2` and the serialised witness. The
574/// merkle root is not recomputed here; [`seal_block`] does that.
575pub fn with_solution<B: SidestrBlock>(block: &B, witness_items: &[Vec<u8>]) -> Result<B> {
576    let (index, spk) = commitment_output(block)
577        .ok_or_else(|| Error::Block("no witness commitment output to carry the solution".into()))?;
578    let mut push = SIGNET_HEADER.to_vec();
579    push.extend(encode_witness(witness_items));
580    let len = push.len();
581    let op: Vec<u8> = match len {
582        0..=75 => vec![len as u8],
583        76..=255 => vec![0x4c, len as u8],
584        256..=65535 => vec![0x4d, (len & 0xff) as u8, (len >> 8) as u8],
585        _ => return Err(Error::Block("solution too long for one push".into())),
586    };
587    let mut script = spk.as_bytes()[..COMMITMENT_LEN].to_vec();
588    script.extend(op);
589    script.extend(push);
590    let mut out = block.clone();
591    out.txdata_mut()[0].output[index].script_pubkey = ScriptBuf::from_bytes(script);
592    Ok(out)
593}
594
595/// The coinbase with the solution stripped: the commitment output cut back
596/// to its 38 bytes. What the merkle root is computed over for signing.
597fn stripped_coinbase(txdata: &[Transaction]) -> Transaction {
598    let mut cb = txdata[0].clone();
599    if let Some((i, spk)) = commitment_output_of(&txdata[0]) {
600        cb.output[i].script_pubkey =
601            ScriptBuf::from_bytes(spk.as_bytes()[..COMMITMENT_LEN].to_vec());
602    }
603    cb
604}
605
606fn merkle_root_of(txids: impl Iterator<Item = Txid>) -> TxMerkleNode {
607    merkle_tree::calculate_root(txids)
608        .map(TxMerkleNode::from)
609        .unwrap_or_else(TxMerkleNode::all_zeros)
610}
611
612/// SPEC 4: the block data is SHA-256 of the header's first 72 bytes (version,
613/// prev, merkle root, time on wire) with the merkle root recomputed over the
614/// coinbase stripped of its solution (`siding/lib/block.mjs blockData`).
615///
616/// This is what a block signature commits to, and so what sealing may not
617/// change: [`seal_block`] rewrites the coinbase's solution push (stripped
618/// here), the merkle root (recomputed here) and the nonce (outside the
619/// first 72 bytes). Every other header field — and on a v2 header the
620/// committed height, transaction count and the rest of the 164 bytes — is
621/// fixed before signing and only reachable through the merkle root.
622pub fn block_data<F: HeaderFamily>(family: &F, block: &F::Block) -> [u8; 32] {
623    let txdata = block.txdata();
624    let cb = stripped_coinbase(txdata);
625    let root = merkle_root_of(
626        std::iter::once(cb.compute_txid())
627            .chain(txdata.iter().skip(1).map(Transaction::compute_txid)),
628    );
629    let mut header = block.header().clone();
630    family.set_merkle_root(&mut header, root);
631    sha256::Hash::hash(&family.signed_prefix(&header)).to_byte_array()
632}
633
634/// BIP 325's shape (`siding/lib/block.mjs virtualTxs`): a virtual output
635/// paying the challenge, spent by a virtual transaction whose input carries
636/// the solution; the block data sits in `to_spend`'s scriptSig.
637#[derive(Debug, Clone)]
638pub struct VirtualTxs {
639    /// Version 0, one null input whose scriptSig is `OP_0 <block data>`, one output paying the challenge.
640    pub to_spend: Transaction,
641    /// Version 0, spends `to_spend:0` with the solution as its witness, one `OP_RETURN` output.
642    pub to_sign: Transaction,
643    /// The output `to_sign` spends: value 0, the challenge.
644    pub prevout: TxOut,
645}
646
647/// The virtual transactions for block data `data`, with `witness` as the
648/// solution (empty for signing).
649pub fn virtual_txs(data: &[u8; 32], challenge: &Script, witness: &[Vec<u8>]) -> VirtualTxs {
650    let mut script_sig = vec![0x00, 0x20];
651    script_sig.extend_from_slice(data);
652    let to_spend = Transaction {
653        version: TxVersion(0),
654        lock_time: LockTime::ZERO,
655        input: vec![TxIn {
656            previous_output: OutPoint::null(),
657            script_sig: ScriptBuf::from_bytes(script_sig),
658            sequence: Sequence::ZERO,
659            witness: Witness::new(),
660        }],
661        output: vec![TxOut {
662            value: Amount::ZERO,
663            script_pubkey: challenge.to_owned(),
664        }],
665    };
666    let prevout = TxOut {
667        value: Amount::ZERO,
668        script_pubkey: challenge.to_owned(),
669    };
670    let to_sign = Transaction {
671        version: TxVersion(0),
672        lock_time: LockTime::ZERO,
673        input: vec![TxIn {
674            previous_output: OutPoint {
675                txid: to_spend.compute_txid(),
676                vout: 0,
677            },
678            script_sig: ScriptBuf::new(),
679            sequence: Sequence::ZERO,
680            witness: Witness::from_slice(witness),
681        }],
682        output: vec![TxOut {
683            value: Amount::ZERO,
684            script_pubkey: ScriptBuf::from_bytes(vec![0x6a]),
685        }],
686    };
687    VirtualTxs {
688        to_spend,
689        to_sign,
690        prevout,
691    }
692}
693
694// --- height -------------------------------------------------------------------
695
696/// The BIP 34 height push for a coinbase (`siding/lib/block.mjs heightPush`):
697/// the height little-endian with a padding byte only after a high bit, as one
698/// length-prefixed push; `OP_0` for height 0.
699pub fn height_push(height: u32) -> Vec<u8> {
700    let mut out = Vec::new();
701    let mut n = height;
702    while n > 0 {
703        out.push((n & 0xff) as u8);
704        n >>= 8;
705    }
706    if out.last().is_some_and(|b| b & 0x80 != 0) {
707        out.push(0);
708    }
709    if out.is_empty() {
710        return vec![0x00];
711    }
712    let mut push = vec![out.len() as u8];
713    push.extend(out);
714    push
715}
716
717/// The height a coinbase scriptSig pushes first (BIP 34): the inverse of
718/// [`height_push`] (`siding/lib/block.mjs coinbaseHeight`). A scriptSig that
719/// does not start with a height push is refused, never read as height 0:
720/// beside a stock parent the coinbase is the only place the height is
721/// written. `OP_0` and `OP_1`..`OP_16` are read as 0 and 1..16; a
722/// length-prefixed push must be minimal and non-negative.
723pub fn coinbase_height(coinbase: &Transaction) -> Result<u32> {
724    let sig = coinbase
725        .input
726        .first()
727        .map(|i| i.script_sig.as_bytes())
728        .unwrap_or(&[]);
729    let bad = |m: &str| Err(Error::CoinbaseHeight(m.into()));
730    if sig.is_empty() {
731        return bad("coinbase scriptSig is not a hex script");
732    }
733    let n = usize::from(sig[0]);
734    if n == 0 {
735        return Ok(0);
736    }
737    if (0x51..=0x60).contains(&n) {
738        return Ok((n - 0x50) as u32);
739    }
740    if n > 75 || sig.len() < 1 + n {
741        return bad("coinbase scriptSig does not start with a height push");
742    }
743    if n > 1 && sig[n] == 0 && sig[n - 1] & 0x80 == 0 {
744        return bad("coinbase height push is not minimal");
745    }
746    if sig[n] & 0x80 != 0 {
747        return bad("coinbase height push is negative");
748    }
749    if n > 5 {
750        return bad("coinbase height push is too long for a height");
751    }
752    let h = (1..=n).rev().fold(0u64, |h, i| h * 256 + u64::from(sig[i]));
753    u32::try_from(h)
754        .map_err(|_| Error::CoinbaseHeight("coinbase height push is too long for a height".into()))
755}
756
757/// A block's height: the v2 header carries it; the stock header does not, so
758/// the coinbase says (`siding/lib/block.mjs blockHeight`).
759pub fn block_height<F: HeaderFamily>(family: &F, block: &F::Block) -> Result<u32> {
760    match family.header_height(block.header()) {
761        Some(h) => Ok(h),
762        None => coinbase_height(
763            block
764                .txdata()
765                .first()
766                .ok_or_else(|| Error::CoinbaseHeight("block has no coinbase".into()))?,
767        ),
768    }
769}
770
771// --- building ---------------------------------------------------------------------
772
773/// What [`build_block`] needs: the unsigned block's inputs.
774#[derive(Debug, Clone)]
775pub struct BlockTemplate {
776    /// The block's height; the coinbase pushes it, and a v2 header commits to it.
777    pub height: u32,
778    /// The previous block's hash (all zeros for the genesis).
779    pub prev: BlockHash,
780    /// The header time.
781    pub time: u32,
782    /// The transactions after the coinbase, in order.
783    pub transactions: Vec<Transaction>,
784    /// The coinbase's outputs before the witness commitment: fees, claims, pegs.
785    pub outputs: Vec<TxOut>,
786    /// The compact target every block carries (`powLimit`, SPEC 3).
787    pub bits: CompactTarget,
788    /// The tag pushed after the height: [`MARKER`], or `sidestr genesis <id>` for block 0.
789    pub marker: String,
790}
791
792/// The witness commitment for these non-coinbase transactions: SHA256d of
793/// the witness merkle root (coinbase wtxid all zeros) and a 32-zero reserved
794/// value. The coinbase output carrying it is `OP_RETURN 0x24 aa21a9ed` + this.
795pub fn witness_commitment(transactions: &[Transaction]) -> [u8; 32] {
796    let root = merkle_tree::calculate_root(
797        std::iter::once(Wtxid::all_zeros())
798            .chain(transactions.iter().map(Transaction::compute_wtxid)),
799    )
800    .map(|h| h.to_byte_array())
801    .unwrap_or([0u8; 32]);
802    let mut cat = [0u8; 64];
803    cat[..32].copy_from_slice(&root);
804    sha256d::Hash::hash(&cat).to_byte_array()
805}
806
807/// An unsigned block on `prev` with these transactions (`siding/lib/block.mjs
808/// buildBlock`). Outputs: the template's, then the witness commitment; the
809/// solution is appended by [`sign_block`] or [`seal_block`]. The header's
810/// shape follows the parent's family (SPEC 3.2): the stock 80-byte header,
811/// version with bit 31 clear, beside stock Bitcoin; the 164-byte v2 header
812/// with its height and transaction count beside a BLAKE2b parent.
813pub fn build_block<F: HeaderFamily>(family: &F, t: &BlockTemplate) -> F::Block {
814    let commitment = witness_commitment(&t.transactions);
815    let mut commitment_spk = COMMITMENT_PREFIX.to_vec();
816    commitment_spk.extend_from_slice(&commitment);
817    let tag = t.marker.as_bytes();
818    let mut script_sig = height_push(t.height);
819    script_sig.push(tag.len() as u8);
820    script_sig.extend_from_slice(tag);
821    let mut outputs = t.outputs.clone();
822    outputs.push(TxOut {
823        value: Amount::ZERO,
824        script_pubkey: ScriptBuf::from_bytes(commitment_spk),
825    });
826    let coinbase = Transaction {
827        version: TxVersion::TWO,
828        lock_time: LockTime::ZERO,
829        input: vec![TxIn {
830            previous_output: OutPoint::null(),
831            script_sig: ScriptBuf::from_bytes(script_sig),
832            sequence: Sequence::MAX,
833            witness: Witness::from_slice(&[[0u8; 32]]),
834        }],
835        output: outputs,
836    };
837    let mut txdata = Vec::with_capacity(t.transactions.len() + 1);
838    txdata.push(coinbase);
839    txdata.extend(t.transactions.iter().cloned());
840    let merkle_root = merkle_root_of_txs(&txdata);
841    let header = family.new_header(t.prev, merkle_root, t.time, t.bits, t.height, txdata.len());
842    F::Block::from_parts(header, txdata)
843}
844
845// --- signing and sealing ----------------------------------------------------------
846
847/// Which taproot path a block signature is for: the key path (level 1, one
848/// signer whose key *is* the output key) or a leaf of the challenge (level 2,
849/// the federation's `multi_a` leaf). The review of ADR-2101 asked for this to
850/// be typed rather than an optional leaf hash.
851#[derive(Debug, Clone, Copy, PartialEq, Eq)]
852pub enum SpendPath<'a> {
853    /// BIP 341 key path: no leaf, no annex.
854    KeyPath,
855    /// BIP 342 script path: the leaf being executed, the annex if any, and
856    /// the position of the last executed `OP_CODESEPARATOR` (`0xffffffff`
857    /// for none).
858    ScriptPath {
859        /// The TapLeaf hash.
860        leaf_hash: TapLeafHash,
861        /// The annex, without its `0x50` prefix stripped: the whole item.
862        annex: Option<&'a [u8]>,
863        /// The code-separator position.
864        codesep_pos: u32,
865    },
866}
867
868/// What a block signature signs (`siding/lib/block.mjs blockSigHash`): the
869/// taproot sighash of the virtual transaction, `SIGHASH_DEFAULT`, for the
870/// key path (level 1) or for a leaf of the challenge (level 2).
871pub fn block_sighash_for<F: HeaderFamily>(
872    family: &F,
873    block: &F::Block,
874    challenge: &Script,
875    path: &SpendPath,
876) -> Result<[u8; 32]> {
877    let data = block_data(family, block);
878    let v = virtual_txs(&data, challenge, &[]);
879    let mut cache = SighashCache::new(&v.to_sign);
880    let prevouts = [v.prevout];
881    let msg = match *path {
882        SpendPath::KeyPath => cache
883            .taproot_key_spend_signature_hash(0, &Prevouts::All(&prevouts), TapSighashType::Default)
884            .map_err(|e| Error::Block(e.to_string()))?,
885        SpendPath::ScriptPath {
886            leaf_hash,
887            annex,
888            codesep_pos,
889        } => {
890            let annex = annex
891                .map(Annex::new)
892                .transpose()
893                .map_err(|_| Error::Block("bad annex".into()))?;
894            cache
895                .taproot_signature_hash(
896                    0,
897                    &Prevouts::All(&prevouts),
898                    annex,
899                    Some((leaf_hash, codesep_pos)),
900                    TapSighashType::Default,
901                )
902                .map_err(|e| Error::Block(e.to_string()))?
903        }
904    };
905    Ok(msg.to_byte_array())
906}
907
908/// [`block_sighash_for`] on the key path: what a level-1 signer signs.
909pub fn block_sighash<F: HeaderFamily>(
910    family: &F,
911    block: &F::Block,
912    challenge: &Script,
913) -> Result<[u8; 32]> {
914    block_sighash_for(family, block, challenge, &SpendPath::KeyPath)
915}
916
917/// The identity of a block *template* — what a federation's signers
918/// authorise — as distinct from the sealed block's hash. A tagged hash
919/// (`sidestr/template-id`) over the chain scope (id and genesis hash), the
920/// height, the previous hash and SHA-256 of the block encoded **with its
921/// solution stripped and its nonce zeroed**, so the same value comes out
922/// before and after sealing.
923///
924/// Why the two identities differ: [`seal_block`] rewrites the coinbase's
925/// solution push, recomputes the merkle root and grinds the nonce, so the
926/// sealed hash depends on *which* `k` signatures went in and on the nonce
927/// found; the same template sealed by two valid subsets has two hashes.
928/// Consensus above the signature therefore decides the template first (this
929/// id) and the exact sealed hash second (ADR-2101, review §4).
930pub fn template_id<F: HeaderFamily>(
931    family: &F,
932    block: &F::Block,
933    chain_id: &str,
934    genesis_hash: Option<BlockHash>,
935) -> Result<[u8; 32]> {
936    let mut t = block.clone();
937    if let Some((i, spk)) = commitment_output(&t).map(|(i, s)| (i, s.to_owned())) {
938        t.txdata_mut()[0].output[i].script_pubkey =
939            ScriptBuf::from_bytes(spk.as_bytes()[..COMMITMENT_LEN].to_vec());
940    }
941    let root = merkle_root_of_txs(t.txdata());
942    family.set_merkle_root(t.header_mut(), root);
943    family.set_nonce(t.header_mut(), 0);
944    let height = block_height(family, block)?;
945    let tag = sha256::Hash::hash(b"sidestr/template-id").to_byte_array();
946    let mut e = sha256::Hash::engine();
947    e.input(&tag);
948    e.input(&tag);
949    e.input(&compact_size(chain_id.len()));
950    e.input(chain_id.as_bytes());
951    e.input(
952        &genesis_hash
953            .unwrap_or_else(BlockHash::all_zeros)
954            .to_byte_array(),
955    );
956    e.input(&height.to_le_bytes());
957    e.input(&family.prev(block.header()).to_byte_array());
958    e.input(&sha256::Hash::hash(&t.encode()).to_byte_array());
959    Ok(sha256::Hash::from_engine(e).to_byte_array())
960}
961
962/// The block with its witness in place (`siding/lib/block.mjs sealBlock`):
963/// the solution appended, the merkle root recomputed, the header nonce found
964/// for the block's `bits`. Any path that produced the witness — one key, a
965/// federation's round — ends here.
966pub fn seal_block<F: HeaderFamily>(
967    family: &F,
968    block: &F::Block,
969    witness_items: &[Vec<u8>],
970) -> Result<F::Block> {
971    let mut sealed = with_solution(block, witness_items)?;
972    let root = merkle_root_of_txs(sealed.txdata());
973    family.set_merkle_root(sealed.header_mut(), root);
974    let target = Target::from_compact(family.bits(sealed.header()));
975    for nonce in 0..=u32::MAX {
976        family.set_nonce(sealed.header_mut(), nonce);
977        if target.is_met_by(family.block_hash(sealed.header())) {
978            return Ok(sealed);
979        }
980    }
981    Err(Error::Block("no nonce meets the target".into()))
982}
983
984/// Sign with the challenge key (`siding/lib/block.mjs signBlock`): key path,
985/// no tweak, the challenge is `5120‖pubkey`; then satisfy the proof of work.
986/// `aux` is BIP 340's auxiliary randomness: siding passes 32 zero bytes for
987/// the genesis so it is reproducible from the document, and this crate
988/// passes zeros everywhere, so a block is a pure function of its inputs and
989/// the key. The key must be the one the challenge names.
990pub fn sign_block<F: HeaderFamily>(
991    family: &F,
992    block: &F::Block,
993    challenge: &Script,
994    key: &SecretKey,
995    aux: &[u8; 32],
996) -> Result<F::Block> {
997    let keypair = Keypair::from_secret_key(secp(), key);
998    let (xonly, _) = keypair.x_only_public_key();
999    let expected = [&[0x51, 0x20][..], &xonly.serialize()].concat();
1000    if challenge.as_bytes() != expected.as_slice() {
1001        return Err(Error::Block(
1002            "the key is not the chain's signer: the challenge names another key".into(),
1003        ));
1004    }
1005    let msg = block_sighash(family, block, challenge)?;
1006    let sig = secp().sign_schnorr_with_aux_rand(&Message::from_digest(msg), &keypair, aux);
1007    seal_block(family, block, &[sig.serialize().to_vec()])
1008}
1009
1010/// How a block's solution satisfied the challenge.
1011#[derive(Debug, Clone, PartialEq, Eq)]
1012pub enum BlockSolution {
1013    /// One key-path signature: level 1.
1014    KeyPath,
1015    /// The federation's `multi_a` leaf, with which slots signed: level 2.
1016    ScriptPath(MultiA),
1017}
1018
1019/// Why a block's solution was refused.
1020#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
1021pub enum SolutionError {
1022    /// No well-formed solution push in the commitment output.
1023    #[error("no solution")]
1024    NoSolution,
1025    /// One witness item: judged as a key-path spend, and refused.
1026    #[error("key path: {0}")]
1027    KeyPath(&'static str),
1028    /// Several witness items: judged as the `multi_a` script path, and refused.
1029    #[error("script path: {0}")]
1030    ScriptPath(#[from] ScriptPathError),
1031}
1032
1033/// The block's solution judged against the challenge for its block data
1034/// (SPEC 4, `sidestr:rule-block-signature` in `siding/lib/overlay.mjs`): the
1035/// solution is read, the virtual transaction built with it as witness, and
1036/// its one input verified as a taproot spend — the key path when the witness
1037/// is one signature (plus an annex), the `multi_a` script path when it is
1038/// slots, a leaf and a control block ([`crate::federation::verify_multi_a_input`]).
1039/// Any other shape is refused by name; there is no general interpreter. The
1040/// block signature is judged under BIP 341 on every family: the reference's
1041/// rule verifies it without the unified-sighash option.
1042pub fn verify_block_solution<F: HeaderFamily>(
1043    family: &F,
1044    block: &F::Block,
1045    challenge: &Script,
1046) -> core::result::Result<BlockSolution, SolutionError> {
1047    let sol = solution_of(block).ok_or(SolutionError::NoSolution)?;
1048    let data = block_data(family, block);
1049    let v = virtual_txs(&data, challenge, &sol.witness);
1050    let prevouts = [v.prevout];
1051    let items = sol.witness.len();
1052    let has_annex = items >= 2 && sol.witness[items - 1].first() == Some(&0x50);
1053    if items - usize::from(has_annex) <= 1 {
1054        verify_key_path_input(&v.to_sign, 0, &prevouts)
1055            .map(|()| BlockSolution::KeyPath)
1056            .map_err(SolutionError::KeyPath)
1057    } else {
1058        verify_multi_a_input(&v.to_sign, 0, &prevouts)
1059            .map(BlockSolution::ScriptPath)
1060            .map_err(SolutionError::ScriptPath)
1061    }
1062}
1063
1064/// Whether the block's solution satisfies the challenge: [`verify_block_solution`] as a bool.
1065pub fn verify_block_signature<F: HeaderFamily>(
1066    family: &F,
1067    block: &F::Block,
1068    challenge: &Script,
1069) -> bool {
1070    verify_block_solution(family, block, challenge).is_ok()
1071}
1072
1073/// Verify one input as a BIP 341 taproot key-path spend: one Schnorr
1074/// signature over the taproot sighash, 64 bytes for `SIGHASH_DEFAULT` or 65
1075/// with an explicit type, an annex allowed. Any other script type, and the
1076/// taproot script path, is refused rather than skipped — where the reference
1077/// kernel reports "unverifiable" and lets the block through, this crate fails
1078/// closed. This is the stock-chain reading; a spend on a BLAKE2b chain goes
1079/// through [`verify_taproot_key_path`] with [`SighashRules::KnotsUnified`].
1080pub fn verify_key_path_input(
1081    tx: &Transaction,
1082    index: usize,
1083    prevouts: &[TxOut],
1084) -> core::result::Result<(), &'static str> {
1085    verify_taproot_key_path(tx, index, prevouts, SighashRules::Bip341)
1086}
1087
1088/// The x-only public key of a secret key, as the document's `signer` field
1089/// and the challenge `5120‖pubkey` carry it.
1090pub fn pubkey_of(key: &SecretKey) -> XOnlyPublicKey {
1091    Keypair::from_secret_key(secp(), key).x_only_public_key().0
1092}
1093
1094/// The single-key challenge for a signer: `OP_1 <32-byte x-only key>`, the
1095/// signer's key used **untweaked** as the output key (level 1, SPEC 4:
1096/// "key path, no tweak"). This is not BIP 86: the key here is not a
1097/// descriptor's internal key, and no script path exists. For a challenge
1098/// whose output key is a tweaked internal key — a federation's — use
1099/// [`challenge_for_output_key`] with the tweaked key, never this with the
1100/// internal one.
1101pub fn challenge_for(pubkey: &XOnlyPublicKey) -> ScriptBuf {
1102    ScriptBuf::from_bytes([&[0x51, 0x20][..], &pubkey.serialize()].concat())
1103}
1104
1105/// The challenge for an already-tweaked output key: `OP_1 <output key>`.
1106/// The type says the tweak has been applied, which is what distinguishes it
1107/// from [`challenge_for`]'s raw signer key (review §9).
1108pub fn challenge_for_output_key(output_key: &TweakedPublicKey) -> ScriptBuf {
1109    ScriptBuf::from_bytes([&[0x51, 0x20][..], &output_key.serialize()].concat())
1110}
1111
1112/// A secret key from the 32-byte hex a siding key file holds (`~/.sidestr/<name>.key`,
1113/// `siding/lib/sign.mjs`). Keys are files, never arguments: this takes the
1114/// file's *text*, trimmed, so the caller reads the file and nothing logs it.
1115pub fn key_from_hex(text: &str) -> Result<SecretKey> {
1116    let bytes = hex::decode(text.trim()).map_err(|e| Error::Encoding(e.to_string()))?;
1117    Ok(SecretKey::from_slice(&bytes)?)
1118}
1119
1120pub(crate) fn schnorr_verify(msg: &[u8; 32], sig: &[u8], pk: &[u8]) -> bool {
1121    let (Ok(sig), Ok(pk)) = (Signature::from_slice(sig), XOnlyPublicKey::from_slice(pk)) else {
1122        return false;
1123    };
1124    secp()
1125        .verify_schnorr(&sig, &Message::from_digest(*msg), &pk)
1126        .is_ok()
1127}
1128
1129pub(crate) fn annex_of<'a>(
1130    items: &mut Vec<&'a [u8]>,
1131) -> core::result::Result<Option<Annex<'a>>, &'static str> {
1132    if items.len() >= 2 && items.last().is_some_and(|a| a.first() == Some(&0x50)) {
1133        let raw = items.pop().expect("checked");
1134        return Annex::new(raw).map(Some).map_err(|_| "bad annex");
1135    }
1136    Ok(None)
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141    use super::*;
1142
1143    fn cb(bytes: Vec<u8>) -> Transaction {
1144        Transaction {
1145            version: TxVersion::TWO,
1146            lock_time: LockTime::ZERO,
1147            input: vec![TxIn {
1148                previous_output: OutPoint::null(),
1149                script_sig: ScriptBuf::from_bytes(bytes),
1150                sequence: Sequence::MAX,
1151                witness: Witness::new(),
1152            }],
1153            output: vec![],
1154        }
1155    }
1156
1157    // siding/test/stock-header-test.mjs: coinbaseHeight is the inverse of the height push
1158    #[test]
1159    fn coinbase_height_inverts_the_push() {
1160        for h in [
1161            0u32,
1162            1,
1163            16,
1164            17,
1165            127,
1166            128,
1167            255,
1168            256,
1169            65535,
1170            70000,
1171            8_388_608,
1172            u32::MAX,
1173        ] {
1174            assert_eq!(
1175                coinbase_height(&cb([height_push(h), vec![0xff]].concat())).unwrap(),
1176                h,
1177                "height {h}"
1178            );
1179        }
1180        let err = |b: Vec<u8>| coinbase_height(&cb(b)).unwrap_err().to_string();
1181        assert!(err(vec![]).contains("not a hex script"));
1182        assert!(coinbase_height(&Transaction {
1183            input: vec![],
1184            ..cb(vec![])
1185        })
1186        .unwrap_err()
1187        .to_string()
1188        .contains("not a hex script"));
1189        assert!(err(vec![0x4c, 0x01, 0x05]).contains("height push"));
1190        assert!(err(vec![0x03, 0x01]).contains("height push"));
1191        assert!(err(vec![0x01, 0x80]).contains("negative"));
1192        assert!(err(vec![0x04, 0x00, 0x00, 0x00, 0x80]).contains("negative"));
1193        assert_eq!(
1194            coinbase_height(&cb(vec![0x03, 0xff, 0xff, 0x7f])).unwrap(),
1195            8_388_607
1196        );
1197        assert!(err(vec![0x02, 0x05, 0x00]).contains("not minimal"));
1198        assert_eq!(coinbase_height(&cb(vec![0x02, 0x80, 0x00])).unwrap(), 128);
1199        assert_eq!(coinbase_height(&cb(vec![0x51])).unwrap(), 1);
1200        assert_eq!(coinbase_height(&cb(vec![0x60])).unwrap(), 16);
1201        assert!(err(vec![0x06, 1, 1, 1, 1, 1, 1]).contains("too long"));
1202    }
1203
1204    #[test]
1205    fn witness_round_trip_and_solution_push_sizes() {
1206        let items = vec![vec![1u8; 64], vec![], vec![7u8; 300]];
1207        assert_eq!(decode_witness(&encode_witness(&items)).unwrap(), items);
1208        assert_eq!(decode_witness(&[2, 1, 9]), None);
1209        let empty = witness_commitment(&[]);
1210        assert_eq!(
1211            hex::encode(empty),
1212            "e2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf9"
1213        );
1214        let block = build_block(
1215            &Stock,
1216            &BlockTemplate {
1217                height: 3,
1218                prev: BlockHash::all_zeros(),
1219                time: 1,
1220                transactions: vec![],
1221                outputs: vec![],
1222                bits: CompactTarget::from_consensus(0x207f_ffff),
1223                marker: MARKER.into(),
1224            },
1225        );
1226        assert!(solution_of(&block).is_none());
1227        for n in [64usize, 80, 300] {
1228            let sealed = seal_block(&Stock, &block, &[vec![0xabu8; n]]).unwrap();
1229            let sol = solution_of(&sealed).unwrap();
1230            assert_eq!(sol.witness, vec![vec![0xabu8; n]]);
1231            assert!(Target::from_compact(sealed.header.bits).is_met_by(sealed.header.block_hash()));
1232            assert_eq!(coinbase_height(&sealed.txdata[0]).unwrap(), 3);
1233            assert_eq!(
1234                sealed.compute_merkle_root(),
1235                Some(sealed.header.merkle_root)
1236            );
1237            assert_eq!(block_weight(&Stock, &sealed), sealed.weight().to_wu());
1238        }
1239        assert!(with_solution(&block, &[vec![0u8; 70_000]]).is_err());
1240    }
1241
1242    // the review's witness-decoder bounds: truncation, trailing bytes, non-minimal sizes, counts
1243    #[test]
1244    fn witness_decoder_is_strict() {
1245        let one = encode_witness(&[vec![9u8; 3]]);
1246        assert!(decode_witness(&one).is_some());
1247        assert_eq!(decode_witness(&[&one[..], &[0u8][..]].concat()), None); // trailing byte
1248        assert_eq!(decode_witness(&one[..one.len() - 1]), None); // truncated item
1249        assert_eq!(
1250            decode_witness(&[0xfd, 0x03, 0x00, 0x01, 0x09, 0x01, 0x09, 0x01, 0x09]),
1251            None
1252        ); // non-minimal count
1253        assert_eq!(decode_witness(&[0xfd, 0xff, 0xff]), None); // 65535 items announced, none present
1254        assert_eq!(decode_witness(&[0xff, 0, 0, 0, 0, 0, 0, 0, 0]), None); // 8-byte form refused
1255        assert_eq!(decode_witness(&[]), None);
1256        assert_eq!(decode_witness(&[0]), Some(vec![]));
1257        let big = encode_witness(&[vec![0u8; 300]]);
1258        assert_eq!(decode_witness(&big).unwrap()[0].len(), 300);
1259    }
1260
1261    #[test]
1262    fn a_family_block_round_trips_and_weighs_like_bitcoins() {
1263        // FamilyBlock<Stock> is not Stock's block type, but the codec is the same shape
1264        let b = build_block(
1265            &Stock,
1266            &BlockTemplate {
1267                height: 1,
1268                prev: BlockHash::all_zeros(),
1269                time: 7,
1270                transactions: vec![],
1271                outputs: vec![],
1272                bits: CompactTarget::from_consensus(0x207f_ffff),
1273                marker: MARKER.into(),
1274            },
1275        );
1276        let fb = FamilyBlock::<Stock>::from_parts(b.header, b.txdata.clone());
1277        assert_eq!(fb.encode(), serialize(&b));
1278        assert_eq!(FamilyBlock::<Stock>::decode(&serialize(&b)).unwrap(), fb);
1279        assert!(FamilyBlock::<Stock>::decode(&serialize(&b)[..90]).is_err());
1280        assert!(FamilyBlock::<Stock>::decode(&[serialize(&b), vec![0]].concat()).is_err());
1281        assert_eq!(hex::encode(witness_root_of_txs(&b.txdata)), "00".repeat(32));
1282    }
1283}