Skip to main content

sidestr_core/
sighash.rs

1//! The signature hashes a spend on a sidestr chain is judged by, and the
2//! taproot key-path verifier over them.
3//!
4//! On a stock chain a spend signs BIP 341's taproot sighash and nothing
5//! else. Beside a BLAKE2b parent the chain inherits Bitcoin Knots' **unified
6//! opt-in signature hash** (`doc/unified-sighash.md` in Knots
7//! v29.4.1.knots20260508): one message layout for every script type,
8//! selected per signature by bit `0x20` in the hash-type byte, active from
9//! the chain's `unifiedSighashParam` height — which `siding/lib/overlay.mjs`
10//! sets to `blake2bHeight: 0`, so on a sidestr chain beside `xbt` or
11//! `txbt4` it applies from the genesis. Every spend on the live
12//! `sidestr:txbt4-siding` chain carries hash type `0x21`
13//! (`SIGHASH_ALL | SIGHASH_UNIFIED`); the message is ported from the
14//! reference kernel's `codec/interpreter.js sighashUnified` (Melvin
15//! Carvalho, AGPL-3.0) and proven by replaying that chain.
16//!
17//! A signature *without* the bit reads as plain BIP 341 on both families; a
18//! signature *with* it on a stock chain is an invalid hash type, as BIP 341
19//! says. Which reading applies is [`HeaderFamily::sighash_rules`]'s answer.
20//!
21//! [`HeaderFamily::sighash_rules`]: crate::block::HeaderFamily::sighash_rules
22
23use bitcoin::consensus::encode::serialize;
24use bitcoin::hashes::{sha256, Hash, HashEngine};
25use bitcoin::sighash::{Annex, Prevouts, SighashCache, TapSighashType};
26use bitcoin::{Transaction, TxOut};
27
28use crate::block::{annex_of, schnorr_verify};
29use crate::parents::Family;
30
31/// Knots' opt-in bit in the hash-type byte (`SIGHASH_UNIFIED` in the
32/// reference interpreter).
33pub const SIGHASH_UNIFIED: u8 = 0x20;
34
35/// Which signature-hash rules a spend is judged by.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum SighashRules {
38    /// BIP 341 only: the stock family, and any family before its fork height.
39    #[default]
40    Bip341,
41    /// Knots' unified sighash is in force: a signature whose hash type carries
42    /// [`SIGHASH_UNIFIED`] is verified over the unified message; one without
43    /// it over BIP 341's, as before.
44    KnotsUnified,
45}
46
47/// The taproot half of a unified message: which spend path, and for the
48/// script path the leaf hash and code-separator position.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum UnifiedTaproot<'a> {
51    /// Script type 2: the key path.
52    KeyPath,
53    /// Script type 3: a tapscript leaf, key version 0.
54    ScriptPath {
55        /// The TapLeaf hash of the leaf being executed.
56        leaf_hash: &'a [u8; 32],
57        /// The position of the last executed `OP_CODESEPARATOR`, `0xffffffff` for none.
58        codesep_pos: u32,
59    },
60}
61
62fn sha(parts: &[&[u8]]) -> [u8; 32] {
63    let mut e = sha256::Hash::engine();
64    for p in parts {
65        e.input(p);
66    }
67    sha256::Hash::from_engine(e).to_byte_array()
68}
69
70fn compact_size(n: usize) -> Vec<u8> {
71    match n {
72        0..=0xfc => vec![n as u8],
73        0xfd..=0xffff => vec![0xfd, (n & 0xff) as u8, (n >> 8) as u8],
74        _ => {
75            let mut v = vec![0xfe];
76            v.extend((n as u32).to_le_bytes());
77            v
78        }
79    }
80}
81
82/// Knots' unified sighash for a taproot spend (`interpreter.js
83/// sighashUnified`, script types 2 and 3): BIP 341's layout with a script-type
84/// byte in place of the spend type, single SHA-256 aggregates, under
85/// `TaggedHash("UnifiedSighash")`. `hash_type` must carry
86/// [`SIGHASH_UNIFIED`], no undefined bits, and an output type of 1 to 3;
87/// `prevouts` is every input's, in order, since the message commits to all
88/// spent amounts and scripts.
89///
90/// ```text
91/// 0x00 ‖ hash_type ‖ version ‖ lock_time ‖ 0x00
92/// [sha_prevouts ‖ sha_amounts ‖ sha_scriptpubkeys ‖ sha_sequences]   unless ANYONECANPAY
93/// [sha_outputs]                                                      unless NONE or SINGLE
94/// script_type
95/// ANYONECANPAY ? outpoint ‖ prevout ‖ sequence : input index
96/// annex present ‖ [sha(compact(annex) ‖ annex)]
97/// [sha(this output)]                                                 SINGLE
98/// [leaf_hash ‖ 0x00 ‖ codesep_pos]                                   script path
99/// ```
100pub fn unified_taproot_sighash(
101    tx: &Transaction,
102    index: usize,
103    prevouts: &[TxOut],
104    hash_type: u8,
105    annex: Option<&[u8]>,
106    path: UnifiedTaproot,
107) -> Result<[u8; 32], &'static str> {
108    if hash_type & SIGHASH_UNIFIED == 0 {
109        return Err("unified sighash without SIGHASH_UNIFIED");
110    }
111    if hash_type & !(0x1f | 0x80 | SIGHASH_UNIFIED) != 0 {
112        return Err("invalid taproot sighash type");
113    }
114    let output_type = hash_type & 0x1f;
115    if !(1..=3).contains(&output_type) {
116        return Err("invalid taproot sighash type");
117    }
118    if prevouts.len() != tx.input.len() {
119        return Err("unified sighash needs every input prevout");
120    }
121    let input = tx.input.get(index).ok_or("no such input")?;
122    let anyone = hash_type & 0x80 != 0;
123    let outpoint = |i: &bitcoin::TxIn| serialize(&i.previous_output);
124    let mut msg: Vec<u8> = vec![0x00, hash_type];
125    msg.extend((tx.version.0 as u32).to_le_bytes());
126    msg.extend(tx.lock_time.to_consensus_u32().to_le_bytes());
127    msg.push(0x00);
128    if !anyone {
129        let prev: Vec<u8> = tx.input.iter().flat_map(outpoint).collect();
130        let amounts: Vec<u8> = prevouts
131            .iter()
132            .flat_map(|p| p.value.to_sat().to_le_bytes())
133            .collect();
134        let spks: Vec<u8> = prevouts
135            .iter()
136            .flat_map(|p| serialize(&p.script_pubkey))
137            .collect();
138        let seqs: Vec<u8> = tx
139            .input
140            .iter()
141            .flat_map(|i| i.sequence.0.to_le_bytes())
142            .collect();
143        msg.extend(sha(&[&prev]));
144        msg.extend(sha(&[&amounts]));
145        msg.extend(sha(&[&spks]));
146        msg.extend(sha(&[&seqs]));
147    }
148    if output_type != 2 && output_type != 3 {
149        let outs: Vec<u8> = tx.output.iter().flat_map(serialize).collect();
150        msg.extend(sha(&[&outs]));
151    }
152    msg.push(match path {
153        UnifiedTaproot::KeyPath => 2,
154        UnifiedTaproot::ScriptPath { .. } => 3,
155    });
156    if anyone {
157        msg.extend(outpoint(input));
158        msg.extend(serialize(&prevouts[index]));
159        msg.extend(input.sequence.0.to_le_bytes());
160    } else {
161        msg.extend((index as u32).to_le_bytes());
162    }
163    match annex {
164        Some(a) => {
165            msg.push(1);
166            msg.extend(sha(&[&compact_size(a.len()), a]));
167        }
168        None => msg.push(0),
169    }
170    if output_type == 3 {
171        let out = tx
172            .output
173            .get(index)
174            .ok_or("sighash single without matching output")?;
175        msg.extend(sha(&[&serialize(out)]));
176    }
177    if let UnifiedTaproot::ScriptPath {
178        leaf_hash,
179        codesep_pos,
180    } = path
181    {
182        msg.extend(leaf_hash);
183        msg.push(0x00);
184        msg.extend(codesep_pos.to_le_bytes());
185    }
186    let tag = sha256::Hash::hash(b"UnifiedSighash").to_byte_array();
187    Ok(sha(&[&tag, &tag, &msg]))
188}
189
190/// The non-taproot half of a unified message: which script type a
191/// signature in a legacy or segwit v0 input commits to (Knots
192/// `doc/unified-sighash.md`, script types 0 and 1).
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum UnifiedLegacy {
195    /// Script type 0: a bare or P2SH script.
196    Base,
197    /// Script type 1: a segwit v0 program (P2WPKH, P2WSH).
198    WitnessV0,
199}
200
201/// Knots' unified sighash for a legacy or segwit v0 input (`interpreter.js
202/// sighashUnified` and fidsigner's `unified.js unifiedSighash`, script types
203/// 0 and 1): the same layout as [`unified_taproot_sighash`] with the
204/// script-type byte 0 or 1 and, where a taproot message has the annex
205/// byte, the script code with its CompactSize length. Beside it rather than
206/// folded into it, so the taproot function's signature and behaviour stay
207/// fixed.
208///
209/// Only the [`SIGHASH_UNIFIED`] bit is required: as in the reference, the
210/// output type is the low five bits, outputs are committed unless it is
211/// `NONE` (2) or `SINGLE` (3), and no other bit is refused. `prevouts` is
212/// every input's, in order. sidestr-core verifies no legacy or segwit v0
213/// spend (see the crate docs); this is the message, held to Knots' 142
214/// non-taproot known answers in `tests/fidsigner.rs`.
215///
216/// ```text
217/// 0x00 ‖ hash_type ‖ version ‖ lock_time ‖ 0x00
218/// [sha_prevouts ‖ sha_amounts ‖ sha_scriptpubkeys ‖ sha_sequences]   unless ANYONECANPAY
219/// [sha_outputs]                                                      unless NONE or SINGLE
220/// script_type (0 or 1)
221/// ANYONECANPAY ? outpoint ‖ prevout ‖ sequence : input index
222/// compact(script_code) ‖ script_code
223/// [sha(this output)]                                                 SINGLE
224/// ```
225pub fn unified_legacy_sighash(
226    tx: &Transaction,
227    index: usize,
228    prevouts: &[TxOut],
229    hash_type: u8,
230    script_type: UnifiedLegacy,
231    script_code: &[u8],
232) -> Result<[u8; 32], &'static str> {
233    if hash_type & SIGHASH_UNIFIED == 0 {
234        return Err("unified sighash without SIGHASH_UNIFIED");
235    }
236    if prevouts.len() != tx.input.len() {
237        return Err("unified sighash needs every input prevout");
238    }
239    let input = tx.input.get(index).ok_or("no such input")?;
240    let output_type = hash_type & 0x1f;
241    let anyone = hash_type & 0x80 != 0;
242    let outpoint = |i: &bitcoin::TxIn| serialize(&i.previous_output);
243    let mut msg: Vec<u8> = vec![0x00, hash_type];
244    msg.extend((tx.version.0 as u32).to_le_bytes());
245    msg.extend(tx.lock_time.to_consensus_u32().to_le_bytes());
246    msg.push(0x00);
247    if !anyone {
248        let prev: Vec<u8> = tx.input.iter().flat_map(outpoint).collect();
249        let amounts: Vec<u8> = prevouts
250            .iter()
251            .flat_map(|p| p.value.to_sat().to_le_bytes())
252            .collect();
253        let spks: Vec<u8> = prevouts
254            .iter()
255            .flat_map(|p| serialize(&p.script_pubkey))
256            .collect();
257        let seqs: Vec<u8> = tx
258            .input
259            .iter()
260            .flat_map(|i| i.sequence.0.to_le_bytes())
261            .collect();
262        msg.extend(sha(&[&prev]));
263        msg.extend(sha(&[&amounts]));
264        msg.extend(sha(&[&spks]));
265        msg.extend(sha(&[&seqs]));
266    }
267    if output_type != 2 && output_type != 3 {
268        let outs: Vec<u8> = tx.output.iter().flat_map(serialize).collect();
269        msg.extend(sha(&[&outs]));
270    }
271    msg.push(match script_type {
272        UnifiedLegacy::Base => 0,
273        UnifiedLegacy::WitnessV0 => 1,
274    });
275    if anyone {
276        msg.extend(outpoint(input));
277        msg.extend(serialize(&prevouts[index]));
278        msg.extend(input.sequence.0.to_le_bytes());
279    } else {
280        msg.extend((index as u32).to_le_bytes());
281    }
282    msg.extend(compact_size(script_code.len()));
283    msg.extend(script_code);
284    if output_type == 3 {
285        let out = tx
286            .output
287            .get(index)
288            .ok_or("sighash single without matching output")?;
289        msg.extend(sha(&[&serialize(out)]));
290    }
291    let tag = sha256::Hash::hash(b"UnifiedSighash").to_byte_array();
292    Ok(sha(&[&tag, &tag, &msg]))
293}
294
295/// Verify one input as a taproot key-path spend under `rules`: one Schnorr
296/// signature, 64 bytes for `SIGHASH_DEFAULT` or 65 with an explicit type, an
297/// annex allowed, over BIP 341's sighash — or, under
298/// [`SighashRules::KnotsUnified`] with the [`SIGHASH_UNIFIED`] bit set,
299/// over the unified message. Any other script type and the script path are
300/// refused, never skipped (`interpreter.js #verifyTaproot`, key path).
301pub fn verify_taproot_key_path(
302    tx: &Transaction,
303    index: usize,
304    prevouts: &[TxOut],
305    rules: SighashRules,
306) -> Result<(), &'static str> {
307    let prevout = prevouts.get(index).ok_or("no prevout for the input")?;
308    let input = tx.input.get(index).ok_or("no such input")?;
309    if !prevout.script_pubkey.is_p2tr() {
310        return Err("unsupported script type: sidestr-core verifies taproot spends only");
311    }
312    if !input.script_sig.is_empty() {
313        return Err("WITNESS_MALLEATED");
314    }
315    let mut items: Vec<&[u8]> = input.witness.iter().collect();
316    if items.is_empty() {
317        return Err("empty taproot witness");
318    }
319    let annex = annex_of(&mut items)?;
320    if items.len() != 1 {
321        return Err("taproot script path: not a key-path spend");
322    }
323    let raw = items[0];
324    let (sig, hash_type) = match raw.len() {
325        64 => (raw, 0u8),
326        65 => {
327            if raw[64] == 0 {
328                return Err("explicit SIGHASH_DEFAULT in 65-byte signature");
329            }
330            (&raw[..64], raw[64])
331        }
332        _ => return Err("bad key-path signature size"),
333    };
334    let msg = if rules == SighashRules::KnotsUnified && hash_type & SIGHASH_UNIFIED != 0 {
335        unified_taproot_sighash(
336            tx,
337            index,
338            prevouts,
339            hash_type,
340            annex.as_ref().map(Annex::as_bytes),
341            UnifiedTaproot::KeyPath,
342        )?
343    } else {
344        let ty = TapSighashType::from_consensus_u8(hash_type)
345            .map_err(|_| "invalid taproot sighash type")?;
346        if prevouts.len() != tx.input.len() {
347            return Err("taproot sighash needs every input prevout");
348        }
349        SighashCache::new(tx)
350            .taproot_signature_hash(index, &Prevouts::All(prevouts), annex, None, ty)
351            .map_err(|_| "sighash failed")?
352            .to_byte_array()
353    };
354    if schnorr_verify(&msg, sig, &prevout.script_pubkey.as_bytes()[2..34]) {
355        Ok(())
356    } else {
357        Err("invalid key-path schnorr signature")
358    }
359}
360
361/// Verify one input under the script templates this crate carries.
362///
363/// Three forms, and nothing else:
364///
365/// - a taproot **key-path** spend ([`verify_taproot_key_path`]): one
366///   signature, plus an annex;
367/// - a taproot **script-path** spend of one of Hitch's seven channel leaves
368///   ([`crate::channel::verify_channel_input`]): BIP 341 says a witness is a
369///   script path when two or more items remain after the annex, and such a
370///   witness is judged as a channel leaf or refused;
371/// - the bare `OP_TRUE` collateral coin the pool and markets rules use,
372///   spent with an empty `scriptSig` and witness, as the reference writes it.
373///
374/// Every other script type and every other leaf fails closed.
375pub fn verify_supported_input(
376    tx: &Transaction,
377    index: usize,
378    prevouts: &[TxOut],
379    rules: SighashRules,
380) -> Result<(), String> {
381    let prevout = prevouts.get(index).ok_or("no prevout for the input")?;
382    if prevout.script_pubkey.as_bytes() == [0x51] {
383        let input = tx.input.get(index).ok_or("no input at that index")?;
384        return if input.script_sig.is_empty() && input.witness.is_empty() {
385            Ok(())
386        } else {
387            Err("an OP_TRUE collateral spend has empty scriptSig and witness".into())
388        };
389    }
390    if is_script_path(tx, index, prevout) {
391        return crate::channel::verify_channel_input(tx, index, prevouts, rules)
392            .map(|_| ())
393            .map_err(|e| format!("taproot script path: {e}"));
394    }
395    verify_taproot_key_path(tx, index, prevouts, rules).map_err(str::to_owned)
396}
397
398/// BIP 341's split: a taproot input whose witness keeps two or more items
399/// once an annex (a last item starting `0x50`, with at least two items) is
400/// set aside spends by script path.
401fn is_script_path(tx: &Transaction, index: usize, prevout: &TxOut) -> bool {
402    let Some(input) = tx.input.get(index) else {
403        return false;
404    };
405    if !prevout.script_pubkey.is_p2tr() {
406        return false;
407    }
408    let n = input.witness.len();
409    let annex = n >= 2
410        && input
411            .witness
412            .last()
413            .is_some_and(|a| a.first() == Some(&0x50));
414    n - usize::from(annex) >= 2
415}
416
417/// The signature-hash rules a chain inherits from its parent's family (SPEC
418/// 3, 0.0.3): Knots' unified sighash beside a BLAKE2b parent, BIP 341
419/// beside stock Bitcoin. The same answer [`HeaderFamily::sighash_rules`]
420/// gives for a sidestr chain of that family, whose fork height is 0, for a
421/// caller — a wallet — that holds a chain document and no family type
422/// (`siding/lib/txsign.mjs usesUnifiedSighash`).
423///
424/// ```
425/// use sidestr_core::parents::{resolve_parent, Family};
426/// use sidestr_core::sighash::{rules_for, SighashRules};
427///
428/// assert_eq!(rules_for(resolve_parent("tbtc4").unwrap().family), SighashRules::Bip341);
429/// assert_eq!(rules_for(Family::Blake2b), SighashRules::KnotsUnified);
430/// ```
431///
432/// [`HeaderFamily::sighash_rules`]: crate::block::HeaderFamily::sighash_rules
433pub fn rules_for(family: Family) -> SighashRules {
434    match family {
435        Family::Stock => SighashRules::Bip341,
436        Family::Blake2b => SighashRules::KnotsUnified,
437    }
438}
439
440/// The hash type a key-path signature carries under `rules`
441/// (`txsign.mjs keyPathSighash`): `0x21` (`SIGHASH_ALL | SIGHASH_UNIFIED`)
442/// where the unified sighash is in force, `0x01` (`SIGHASH_ALL`) under BIP
443/// 341. Always explicit, so every witness is 65 bytes and names its rule.
444pub fn key_path_hash_type(rules: SighashRules) -> u8 {
445    match rules {
446        SighashRules::KnotsUnified => 0x01 | SIGHASH_UNIFIED,
447        SighashRules::Bip341 => 0x01,
448    }
449}
450
451/// The message input `index`'s key-path signature commits to under `rules`,
452/// and the hash-type byte appended to that signature (`txsign.mjs
453/// keyPathSighash`). `prevouts` is every input's, in order. The signer's
454/// witness is the 64-byte BIP 340 signature over the message followed by
455/// the byte: exactly what [`verify_taproot_key_path`] checks under the same
456/// rules, and what the reference producer checks before a transaction enters
457/// its mempool.
458///
459/// ```
460/// use bitcoin::hashes::Hash;
461/// use bitcoin::secp256k1::{Keypair, Message, SecretKey};
462/// use bitcoin::{absolute::LockTime, transaction::Version, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness};
463/// use sidestr_core::block::{challenge_for, secp};
464/// use sidestr_core::sighash::{key_path_sighash, verify_taproot_key_path, SighashRules};
465///
466/// let kp = Keypair::from_secret_key(secp(), &SecretKey::from_slice(&[0x11; 32]).unwrap());
467/// let me = challenge_for(&kp.x_only_public_key().0);
468/// let prevouts = vec![TxOut { value: Amount::from_sat(50_000), script_pubkey: me.clone() }];
469/// let mut tx = Transaction { version: Version::TWO, lock_time: LockTime::ZERO,
470///     input: vec![TxIn { previous_output: OutPoint { txid: Txid::all_zeros(), vout: 0 }, script_sig: ScriptBuf::new(),
471///                        sequence: Sequence(0xffff_fffd), witness: Witness::new() }],
472///     output: vec![TxOut { value: Amount::from_sat(49_000), script_pubkey: me }] };
473///
474/// // beside a BLAKE2b parent: hash type 0x21, refused beside stock Bitcoin (txsign-test.mjs)
475/// let (msg, ht) = key_path_sighash(&tx, 0, &prevouts, SighashRules::KnotsUnified).unwrap();
476/// let sig = secp().sign_schnorr_with_aux_rand(&Message::from_digest(msg), &kp, &[0; 32]);
477/// tx.input[0].witness = Witness::from_slice(&[[sig.serialize().as_slice(), &[ht]].concat()]);
478/// assert_eq!(ht, 0x21);
479/// assert!(verify_taproot_key_path(&tx, 0, &prevouts, SighashRules::KnotsUnified).is_ok());
480/// assert!(verify_taproot_key_path(&tx, 0, &prevouts, SighashRules::Bip341).is_err());
481/// ```
482pub fn key_path_sighash(
483    tx: &Transaction,
484    index: usize,
485    prevouts: &[TxOut],
486    rules: SighashRules,
487) -> Result<([u8; 32], u8), &'static str> {
488    let hash_type = key_path_hash_type(rules);
489    let msg = match rules {
490        SighashRules::KnotsUnified => unified_taproot_sighash(
491            tx,
492            index,
493            prevouts,
494            hash_type,
495            None,
496            UnifiedTaproot::KeyPath,
497        )?,
498        SighashRules::Bip341 => {
499            if prevouts.len() != tx.input.len() {
500                return Err("taproot sighash needs every input prevout");
501            }
502            if index >= tx.input.len() {
503                return Err("no such input");
504            }
505            SighashCache::new(tx)
506                .taproot_key_spend_signature_hash(
507                    index,
508                    &Prevouts::All(prevouts),
509                    TapSighashType::All,
510                )
511                .map_err(|_| "sighash failed")?
512                .to_byte_array()
513        }
514    };
515    Ok((msg, hash_type))
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use bitcoin::secp256k1::{Keypair, Message, SecretKey};
522    use bitcoin::transaction::Version;
523    use bitcoin::{absolute::LockTime, Amount, OutPoint, ScriptBuf, Sequence, TxIn, Witness};
524
525    fn spend(hash_type: u8, rules: SighashRules) -> (Transaction, Vec<TxOut>) {
526        let key = SecretKey::from_slice(&[3u8; 32]).unwrap();
527        let kp = Keypair::from_secret_key(crate::block::secp(), &key);
528        let me = crate::block::challenge_for(&kp.x_only_public_key().0);
529        let prevouts = vec![TxOut {
530            value: Amount::from_sat(10_000),
531            script_pubkey: me.clone(),
532        }];
533        let mut tx = Transaction {
534            version: Version::TWO,
535            lock_time: LockTime::ZERO,
536            input: vec![TxIn {
537                previous_output: OutPoint {
538                    txid: bitcoin::Txid::all_zeros(),
539                    vout: 1,
540                },
541                script_sig: ScriptBuf::new(),
542                sequence: Sequence(0xffff_fffd),
543                witness: Witness::new(),
544            }],
545            output: vec![TxOut {
546                value: Amount::from_sat(9_000),
547                script_pubkey: me,
548            }],
549        };
550        let msg = match rules {
551            SighashRules::KnotsUnified if hash_type & SIGHASH_UNIFIED != 0 => {
552                unified_taproot_sighash(&tx, 0, &prevouts, hash_type, None, UnifiedTaproot::KeyPath)
553                    .unwrap()
554            }
555            _ => SighashCache::new(&tx)
556                .taproot_key_spend_signature_hash(
557                    0,
558                    &Prevouts::All(&prevouts),
559                    TapSighashType::from_consensus_u8(hash_type & !SIGHASH_UNIFIED).unwrap(),
560                )
561                .unwrap()
562                .to_byte_array(),
563        };
564        let sig = crate::block::secp()
565            .sign_schnorr_with_aux_rand(&Message::from_digest(msg), &kp, &[0u8; 32])
566            .serialize()
567            .to_vec();
568        let item = if hash_type == 0 {
569            sig
570        } else {
571            [sig, vec![hash_type]].concat()
572        };
573        tx.input[0].witness = Witness::from_slice(&[item]);
574        (tx, prevouts)
575    }
576
577    #[test]
578    fn unified_bit_is_read_only_where_the_rules_say() {
579        let (tx, p) = spend(0x21, SighashRules::KnotsUnified);
580        assert!(verify_taproot_key_path(&tx, 0, &p, SighashRules::KnotsUnified).is_ok());
581        assert_eq!(
582            verify_taproot_key_path(&tx, 0, &p, SighashRules::Bip341),
583            Err("invalid taproot sighash type")
584        );
585        let (tx, p) = spend(0x00, SighashRules::KnotsUnified);
586        assert!(verify_taproot_key_path(&tx, 0, &p, SighashRules::KnotsUnified).is_ok());
587        assert!(verify_taproot_key_path(&tx, 0, &p, SighashRules::Bip341).is_ok());
588        let (tx, p) = spend(0x01, SighashRules::Bip341);
589        assert!(verify_taproot_key_path(&tx, 0, &p, SighashRules::KnotsUnified).is_ok());
590        // a unified message is not a BIP 341 message
591        let (tx, p) = spend(0x21, SighashRules::Bip341);
592        assert!(verify_taproot_key_path(&tx, 0, &p, SighashRules::KnotsUnified).is_err());
593    }
594
595    #[test]
596    fn unified_message_refuses_undefined_types() {
597        let (tx, p) = spend(0x00, SighashRules::Bip341);
598        for bad in [0x20u8, 0x24, 0x60, 0xa0] {
599            assert!(
600                unified_taproot_sighash(&tx, 0, &p, bad, None, UnifiedTaproot::KeyPath).is_err(),
601                "{bad:#x}"
602            );
603        }
604        assert!(unified_taproot_sighash(&tx, 0, &p, 0x01, None, UnifiedTaproot::KeyPath).is_err());
605        assert!(unified_taproot_sighash(&tx, 0, &[], 0x21, None, UnifiedTaproot::KeyPath).is_err());
606        let a = unified_taproot_sighash(&tx, 0, &p, 0x21, None, UnifiedTaproot::KeyPath).unwrap();
607        let b = unified_taproot_sighash(&tx, 0, &p, 0x21, Some(&[0x50]), UnifiedTaproot::KeyPath)
608            .unwrap();
609        let c = unified_taproot_sighash(
610            &tx,
611            0,
612            &p,
613            0x21,
614            None,
615            UnifiedTaproot::ScriptPath {
616                leaf_hash: &[9u8; 32],
617                codesep_pos: 0xffff_ffff,
618            },
619        )
620        .unwrap();
621        let d = unified_taproot_sighash(&tx, 0, &p, 0xa1, None, UnifiedTaproot::KeyPath).unwrap();
622        assert!(a != b && a != c && a != d && b != c);
623    }
624}