From 990997ac296e8f67b019cea8655e8301f4f254f2 Mon Sep 17 00:00:00 2001 From: woltx <94266259+w0xlt@users.noreply.github.com> Date: Sat, 30 May 2026 02:18:55 -0700 Subject: [PATCH 1/3] opcodes: add Inquisition BIP448 aliases Expose names for the BIP348, BIP349, and BIP446 opcode bytes used by Bitcoin Inquisition signet. They remain aliases for the current OP_SUCCESSx bytes so script construction can use the proposal names without changing TapScript classification. Add coverage for the alias byte values, builder output, and current OP_SUCCESSx classification behavior. --- bitcoin/src/blockdata/opcodes.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bitcoin/src/blockdata/opcodes.rs b/bitcoin/src/blockdata/opcodes.rs index ac8dfa026d..682dafd9fe 100644 --- a/bitcoin/src/blockdata/opcodes.rs +++ b/bitcoin/src/blockdata/opcodes.rs @@ -80,6 +80,13 @@ macro_rules! all_opcodes { /// Previously called OP_NOP3. pub const OP_NOP3: Opcode = OP_CSV; + /// Bitcoin Inquisition/BIP349 alias for `OP_SUCCESS203`. + pub const OP_INTERNALKEY: Opcode = OP_RETURN_203; + /// Bitcoin Inquisition/BIP348 alias for `OP_SUCCESS204`. + pub const OP_CHECKSIGFROMSTACK: Opcode = OP_RETURN_204; + /// Bitcoin Inquisition/BIP446 alias for `OP_SUCCESS206`. + pub const OP_TEMPLATEHASH: Opcode = OP_RETURN_206; + /// Push the array `0x81` onto the stack. #[deprecated(since = "TBD", note = "use OP_1NEGATE instead")] pub const OP_PUSHNUM_NEG1: Opcode = OP_1NEGATE; @@ -642,6 +649,28 @@ mod tests { assert_eq!(ordinary_op.to_u8(), 0x4C_u8); } + #[test] + fn inquisition_bip448_aliases() { + use crate::script::Builder; + + type Tag = primitives::script::TapScriptTag; + + assert_eq!(OP_INTERNALKEY.to_u8(), 0xcb); + assert_eq!(OP_CHECKSIGFROMSTACK.to_u8(), 0xcc); + assert_eq!(OP_TEMPLATEHASH.to_u8(), 0xce); + + let script = Builder::::new() + .push_opcode(OP_TEMPLATEHASH) + .push_opcode(OP_INTERNALKEY) + .push_opcode(OP_CHECKSIGFROMSTACK) + .into_script(); + assert_eq!(script.as_bytes(), &[0xce, 0xcb, 0xcc]); + + assert_eq!(OP_INTERNALKEY.classify(ClassifyContext::TapScript), Class::SuccessOp); + assert_eq!(OP_CHECKSIGFROMSTACK.classify(ClassifyContext::TapScript), Class::SuccessOp); + assert_eq!(OP_TEMPLATEHASH.classify(ClassifyContext::TapScript), Class::SuccessOp); + } + #[test] fn decode_pushnum() { // Test all possible opcodes From ad79aabea91331c0ba766a24ada97aba1ca0460b Mon Sep 17 00:00:00 2001 From: woltx <94266259+w0xlt@users.noreply.github.com> Date: Sat, 30 May 2026 02:19:33 -0700 Subject: [PATCH 2/3] sighash: add BIP446 TemplateHash helpers Add a TemplateHash tagged-hash type and SighashCache helpers for computing the BIP446 OP_TEMPLATEHASH message used by Bitcoin Inquisition signet scripts. The helper commits to version, lock time, sequences, outputs, input index, and this input's annex data while intentionally leaving prevouts and spent amounts out of scope. Share the BIP341 annex hashing logic, add raw BIP340 signing helpers for CHECKSIGFROMSTACK use, and cover the behavior with local BIP446 vectors plus length and input-index error tests. --- bitcoin/src/crypto/sighash.rs | 175 +++++++++++++++++++++-- bitcoin/tests/data/bip446/basics.json | 192 ++++++++++++++++++++++++++ 2 files changed, 355 insertions(+), 12 deletions(-) create mode 100644 bitcoin/tests/data/bip446/basics.json diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs index 5caf949351..33b1c88ab6 100644 --- a/bitcoin/src/crypto/sighash.rs +++ b/bitcoin/src/crypto/sighash.rs @@ -107,16 +107,27 @@ sha256t_tag! { pub struct TapSighashTag = hash_str("TapSighash"); } +sha256t_tag! { + pub struct TemplateHashTag = hash_str("TemplateHash"); +} + hash_newtype! { /// Taproot-tagged hash with tag \"TapSighash\". /// /// This hash type is used for computing Taproot signature hash. pub struct TapSighash(sha256t::Hash); + + /// Bitcoin Inquisition/BIP446 tagged hash with tag \"TemplateHash\". + /// + /// This hash type is the 32-byte message pushed by `OP_TEMPLATEHASH`. + pub struct TemplateHash(sha256t::Hash); } -hashes::impl_hex_for_newtype!(TapSighash); +hashes::impl_hex_for_newtype!(TapSighash, TemplateHash); #[cfg(feature = "serde")] -hashes::impl_serde_for_newtype!(TapSighash); +hashes::impl_serde_for_newtype!(TapSighash, TemplateHash); + +impl_message_from_hash!(TemplateHash); impl TapSighash { /// Signs the sighash for a P2TR key-path spending transaction with the @@ -150,6 +161,31 @@ impl TapSighash { } } +impl TemplateHash { + /// Signs this template hash for use with `OP_CHECKSIGFROMSTACK`. + /// + /// The returned BIP340 signature is the raw 64-byte Schnorr signature. Do not append a Taproot + /// sighash byte when placing it on the stack for `OP_CHECKSIGFROMSTACK`. + pub fn sign_checksigfromstack( + &self, + keypair: &UntweakedKeypair, + ) -> secp256k1::schnorr::Signature { + keypair.raw_bip340_sign(self.as_ref()) + } + + /// Signs this template hash for use with `OP_CHECKSIGFROMSTACK` using explicit aux randomness. + /// + /// The returned BIP340 signature is the raw 64-byte Schnorr signature. Do not append a Taproot + /// sighash byte when placing it on the stack for `OP_CHECKSIGFROMSTACK`. + pub fn sign_checksigfromstack_with_aux_randomness( + &self, + keypair: &UntweakedKeypair, + aux_rand: &[u8; 32], + ) -> secp256k1::schnorr::Signature { + keypair.raw_bip340_sign_with_aux_randomness(self.as_ref(), aux_rand) + } +} + /// Efficiently calculates signature hash message for legacy, SegWit and Taproot inputs. #[derive(Debug, Clone)] pub struct SighashCache> { @@ -433,16 +469,7 @@ impl> SighashCache { // sha_annex (32): the SHA256 of (compact_size(size of annex) || annex), where annex // includes the mandatory 0x50 prefix. if let Some(annex) = annex { - let mut enc = sha256::Hash::engine(); - // Use an ad-hoc encoder to write to the hash engine, since annex has no - // true consensus encoding format. - let mut encoder = encoding::Encoder2::new( - encoding::CompactSizeEncoder::new(annex.0.len()), - encoding::BytesEncoder::without_length_prefix(annex.0), - ); - io::drain_to_writer(&mut encoder, &mut enc)?; - let hash = sha256::Hash::from_engine(enc); - writer.write_all(&hash.to_byte_array())?; + writer.write_all(&sha256_annex(annex)?.to_byte_array())?; } // * Data about this output: @@ -501,6 +528,50 @@ impl> SighashCache { Ok(TapSighash::from_byte_array(inner.to_byte_array())) } + /// Encodes the BIP446 `OP_TEMPLATEHASH` data into a writer. + /// + /// The data encoded here is hashed with the [`TemplateHashTag`] tagged hash by + /// [`SighashCache::template_hash`]. It commits to transaction version, lock time, all input + /// sequences, all outputs, this input index, and this input's annex presence/hash. It does not + /// commit to prevouts, spent amounts, spent script pubkeys, scriptSigs, or other inputs' + /// annexes. + pub fn template_hash_encode_data_to( + &mut self, + mut writer: W, + input_index: usize, + annex: Option, + ) -> Result<(), SigningDataError> { + self.tx.borrow().tx_in(input_index).map_err(SigningDataError::sighash)?; + + // Transaction data. + io::encode_to_writer(&self.tx.borrow().version, &mut writer)?; + io::encode_to_writer(&self.tx.borrow().lock_time, &mut writer)?; + writer.write_all(&self.common_cache().sequences.to_byte_array())?; + writer.write_all(&self.common_cache().outputs.to_byte_array())?; + + // Data about this input. + writer.write_all(&[u8::from(annex.is_some())])?; + writer.write_all(&(input_index as u32).to_le_bytes())?; + if let Some(annex) = annex { + writer.write_all(&sha256_annex(annex)?.to_byte_array())?; + } + + Ok(()) + } + + /// Computes the BIP446 `OP_TEMPLATEHASH` hash for the provided input. + pub fn template_hash( + &mut self, + input_index: usize, + annex: Option, + ) -> Result { + let mut enc = sha256t::Hash::::engine(); + self.template_hash_encode_data_to(&mut enc, input_index, annex) + .map_err(SigningDataError::unwrap_sighash)?; + let inner = sha256t::Hash::::from_engine(enc); + Ok(TemplateHash::from_byte_array(inner.to_byte_array())) + } + /// Computes the BIP-0341 sighash for a key spend. pub fn taproot_key_spend_signature_hash>( &mut self, @@ -945,6 +1016,17 @@ impl Encodable for Annex<'_> { } } +fn sha256_annex(annex: Annex<'_>) -> Result { + let mut enc = sha256::Hash::engine(); + // Use an ad-hoc encoder since annex has no true consensus encoding format. + let mut encoder = encoding::Encoder2::new( + encoding::CompactSizeEncoder::new(annex.0.len()), + encoding::BytesEncoder::without_length_prefix(annex.0), + ); + io::drain_to_writer(&mut encoder, &mut enc)?; + Ok(sha256::Hash::from_engine(enc)) +} + fn is_invalid_use_of_sighash_single(sighash: u32, input_index: usize, outputs_len: usize) -> bool { let ty = EcdsaSighashType::from_consensus(sighash); ty.is_single() && input_index >= outputs_len @@ -1349,7 +1431,9 @@ mod tests { use super::*; use crate::consensus::deserialize; use crate::locktime::absolute; + use crate::opcodes::all::{OP_EQUAL, OP_TEMPLATEHASH}; use crate::script::{ScriptPubKey, ScriptPubKeyBuf, TapScriptBuf, WitnessScriptBuf}; + use crate::witness::WitnessExt as _; use crate::{hex, TxIn}; extern crate serde_json; @@ -1433,6 +1517,66 @@ mod tests { assert_eq!(expected, hash.to_byte_array()); } + #[test] + fn template_hash_encode_lengths() { + let tx_bytes = hex::decode_to_vec("02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000").unwrap(); + let tx: Transaction = deserialize(&tx_bytes).unwrap(); + + let mut cache = SighashCache::new(&tx); + let mut without_annex = Vec::new(); + cache.template_hash_encode_data_to(&mut without_annex, 0, None).unwrap(); + assert_eq!(without_annex.len(), 77); + + let annex_bytes = hex!("5064617461"); + let annex = Annex::new(&annex_bytes).unwrap(); + let mut with_annex = Vec::new(); + cache.template_hash_encode_data_to(&mut with_annex, 0, Some(annex)).unwrap(); + assert_eq!(with_annex.len(), 109); + } + + #[test] + fn bip446_template_hash_vectors() { + let data = include_str!("../../tests/data/bip446/basics.json"); + let testdata = serde_json::from_str::(data).unwrap(); + + for (case_index, t) in testdata.as_array().unwrap().iter().enumerate() { + let tx_hex = t.get("spending_tx").unwrap().as_str().unwrap(); + let tx_bytes = hex::decode_to_vec(tx_hex).unwrap(); + let tx: Transaction = deserialize(&tx_bytes).unwrap(); + let input_index = t.get("input_index").unwrap().as_u64().unwrap() as usize; + let valid = t.get("valid").unwrap().as_bool().unwrap(); + let comment = t.get("comment").unwrap().as_str().unwrap(); + + let expected = template_hash_from_bip446_witness(&tx, input_index); + let annex = tx.inputs[input_index] + .witness + .taproot_annex() + .map(|annex| Annex::new(annex).unwrap()); + let mut cache = SighashCache::new(&tx); + let got = cache.template_hash(input_index, annex).unwrap(); + + assert_eq!(got == expected, valid, "case {case_index}: {comment}"); + } + } + + fn template_hash_from_bip446_witness(tx: &Transaction, input_index: usize) -> TemplateHash { + let leaf_script = tx.inputs[input_index] + .witness + .taproot_leaf_script() + .expect("taproot script spend") + .script; + let bytes = leaf_script.as_bytes(); + + assert_eq!(bytes.len(), 35); + assert_eq!(bytes[0], 0x20); + assert_eq!(bytes[33], OP_TEMPLATEHASH.to_u8()); + assert_eq!(bytes[34], OP_EQUAL.to_u8()); + + let mut template_hash = [0u8; 32]; + template_hash.copy_from_slice(&bytes[1..33]); + TemplateHash::from_byte_array(template_hash) + } + #[test] fn sighashes_keyspending() { // following test case has been taken from Bitcoin Core test framework @@ -1608,6 +1752,13 @@ mod tests { length: 1 })) ); + assert_eq!( + c.template_hash(10, None), + Err(InputsIndexError(IndexOutOfBoundsError { + index: 10, + length: 1 + })) + ); } #[test] diff --git a/bitcoin/tests/data/bip446/basics.json b/bitcoin/tests/data/bip446/basics.json new file mode 100644 index 0000000000..34a8fad45e --- /dev/null +++ b/bitcoin/tests/data/bip446/basics.json @@ -0,0 +1,192 @@ +[ + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches. Input index matches." + }, + { + "spent_outputs": [ + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac", + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a" + ], + "spending_tx": "02000000000102169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffffc997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad308000022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac000000000", + "input_index": 1, + "valid": false, + "comment": "Template hash matches. Input index mismatches." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "2a000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: incorrect transaction version." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0002a000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: incorrect transaction locktime." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01337906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: incorrect output value." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3081022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: incorrect output script." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd370415000000002a000000169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: incorrect sequence in spending input." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c000000002a00000001327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: incorrect sequence in another input." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080032320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00250000000000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: spending input contains annex but none was committed." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac001065064756d6d7900000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches with malleated annex for another input." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c0000000100ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches with malleated scriptSig for another input." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "0200000000010283e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c4272a00000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches with malleated prevout for spending input." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff83e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c4272a00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches with malleated prevout for another input." + }, + { + "spent_outputs": [ + "34790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches with malleated value for corresponding spent output." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "23921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches with malleated value for other spent output." + }, + { + "spent_outputs": [ + "33790600000000002251205331c80448b5eb2daad3567c98bc99664d14e0ea12bdf3be755429055d67756a", + "2292100000000000160014266a4832c001885db26e853ef1d1dde840f7dbaf" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad3080022320d1f1955b1327167cb7ae3dc39d52c277be39d75737b9cb80514ce6e825fd8eeace8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches with malleated scriptpubkey for other spent output." + }, + { + "spent_outputs": [ + "337906000000000022512020f99a99681ccce328c592c92e0454b248e6ae65c2e97ce249da6612d0b6b980", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad30800223200000000000000000000000000000000000000000000000000000000000000000ce8721c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: spending a script with a different committed hash." + }, + { + "spent_outputs": [ + "3379060000000000225120e498b9cc13e41d4b1d141989f2626f8de87162f2e97aa5c5dc818c85638b3015", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad308003232040ee92735bd9b32fc51d9b6df6be4dafc834cf383506dcb45a61151aeee3460cce8721c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00550646174610000000000", + "input_index": 0, + "valid": true, + "comment": "Template hash matches in the presence of an annex." + }, + { + "spent_outputs": [ + "3379060000000000225120e498b9cc13e41d4b1d141989f2626f8de87162f2e97aa5c5dc818c85638b3015", + "22921000000000001976a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac" + ], + "spending_tx": "02000000000102c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37041500000000ffffffff169e1e83e930853391bc6f35f605c6754cfead57cf8387639d3b4096c54f18f40c00000000ffffffff01327906000000000016001482074bdf6ce32b071dd120a17cf99cbc01ad308003232040ee92735bd9b32fc51d9b6df6be4dafc834cf383506dcb45a61151aeee3460cce8721c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac005504a5045470000000000", + "input_index": 0, + "valid": false, + "comment": "Template hash mismatches: spending with a different annex than that committed." + } +] From 14e6baba91d4fe510e34c2c18706c9dec6585c46 Mon Sep 17 00:00:00 2001 From: woltx <94266259+w0xlt@users.noreply.github.com> Date: Sat, 30 May 2026 02:19:54 -0700 Subject: [PATCH 3/3] docs: show Inquisition BIP448 helper usage Add examples that build the common OP_TEMPLATEHASH OP_INTERNALKEY OP_CHECKSIGFROMSTACK Taproot script and sign a TemplateHash for a script-spend witness. These examples demonstrate the local construction and signing API without implying consensus validation. Document the intended Bitcoin Inquisition signet scope, the non-consensus limitations, the TemplateHash commitments, and the raw CSFS signing convention. --- bitcoin/examples/inquisition_bip448_script.rs | 49 ++++++++++++ .../inquisition_bip448_templatehash.rs | 78 +++++++++++++++++++ docs/inquisition-bip448.md | 56 +++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 bitcoin/examples/inquisition_bip448_script.rs create mode 100644 bitcoin/examples/inquisition_bip448_templatehash.rs create mode 100644 docs/inquisition-bip448.md diff --git a/bitcoin/examples/inquisition_bip448_script.rs b/bitcoin/examples/inquisition_bip448_script.rs new file mode 100644 index 0000000000..4a04ab2f44 --- /dev/null +++ b/bitcoin/examples/inquisition_bip448_script.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Build a Bitcoin Inquisition/BIP448 Taproot script output. +//! +//! This is construction-only support. Current Bitcoin consensus still treats these bytes as +//! `OP_SUCCESSx`; Bitcoin Inquisition signet gives them the BIP448 meanings. + +use bitcoin::ext::*; +use bitcoin::key::{Keypair, PrivateKey}; +use bitcoin::opcodes::all::{OP_CHECKSIGFROMSTACK, OP_INTERNALKEY, OP_TEMPLATEHASH}; +use bitcoin::script::Builder; +use bitcoin::taproot::{LeafVersion, TaprootBuilder}; +use bitcoin::{ScriptPubKeyBuf, TapScriptBuf}; +use hex::DisplayHex; + +fn main() { + let keypair = example_keypair(); + let internal_key = keypair.to_x_only_public_key(); + + let script = bip448_rebindable_script(); + assert_eq!(script.as_bytes(), &[0xce, 0xcb, 0xcc]); + + let spend_info = TaprootBuilder::new() + .add_leaf(0, script.clone()) + .expect("valid taproot tree") + .finalize(internal_key) + .expect("finalizable taproot tree"); + + let script_pubkey = ScriptPubKeyBuf::new_p2tr_tweaked(spend_info.output_key()); + let control_block = + spend_info.control_block(&(script, LeafVersion::TapScript)).expect("script is in tree"); + + println!("scriptPubKey: {script_pubkey:x}"); + println!("control block: {:x}", control_block.serialize().as_hex()); +} + +fn bip448_rebindable_script() -> TapScriptBuf { + Builder::new() + .push_opcode(OP_TEMPLATEHASH) + .push_opcode(OP_INTERNALKEY) + .push_opcode(OP_CHECKSIGFROMSTACK) + .into_script() +} + +fn example_keypair() -> Keypair { + let secret = [1u8; 32]; + let private_key = PrivateKey::from_secret_bytes(&secret).expect("valid secret key"); + Keypair::from_private_key(&private_key) +} diff --git a/bitcoin/examples/inquisition_bip448_templatehash.rs b/bitcoin/examples/inquisition_bip448_templatehash.rs new file mode 100644 index 0000000000..1732ec7261 --- /dev/null +++ b/bitcoin/examples/inquisition_bip448_templatehash.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Sign a BIP446 `TemplateHash` for a Bitcoin Inquisition/BIP448 tapscript spend. +//! +//! The common script pattern is `OP_TEMPLATEHASH OP_INTERNALKEY OP_CHECKSIGFROMSTACK`. The signature +//! is a raw 64-byte BIP340 Schnorr signature over the 32-byte template hash, with no Taproot sighash +//! byte appended. + +use bitcoin::ext::*; +use bitcoin::key::{Keypair, PrivateKey}; +use bitcoin::locktime::absolute; +use bitcoin::opcodes::all::{OP_CHECKSIGFROMSTACK, OP_INTERNALKEY, OP_TEMPLATEHASH}; +use bitcoin::script::Builder; +use bitcoin::sighash::SighashCache; +use bitcoin::taproot::{LeafVersion, TaprootBuilder}; +use bitcoin::{ + transaction, Amount, OutPoint, ScriptPubKeyBuf, ScriptSigBuf, Sequence, TapScriptBuf, + Transaction, TxIn, TxOut, Txid, Witness, +}; + +fn main() { + let keypair = example_keypair(); + let internal_key = keypair.to_x_only_public_key(); + let script = bip448_rebindable_script(); + + let spend_info = TaprootBuilder::new() + .add_leaf(0, script.clone()) + .expect("valid taproot tree") + .finalize(internal_key) + .expect("finalizable taproot tree"); + let control_block = spend_info + .control_block(&(script.clone(), LeafVersion::TapScript)) + .expect("script is in tree"); + + let input = TxIn { + previous_output: OutPoint { txid: Txid::from_byte_array([0x11; 32]), vout: 0 }, + script_sig: ScriptSigBuf::new(), + sequence: Sequence::ENABLE_LOCKTIME_AND_RBF, + witness: Witness::new(), + }; + + let destination = ScriptPubKeyBuf::new_p2tr(internal_key, None); + let output = TxOut { amount: Amount::from_sat_u32(49_000), script_pubkey: destination }; + + let mut tx = Transaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::ZERO, + inputs: vec![input], + outputs: vec![output], + }; + + let mut cache = SighashCache::new(&mut tx); + let template_hash = cache.template_hash(0, None).expect("valid input index"); + let signature = template_hash.sign_checksigfromstack(&keypair); + + let mut witness = Witness::new(); + witness.push(signature.to_byte_array()); + witness.push_p2tr_script_spend(&script, &control_block, None); + *cache.witness_mut(0).expect("valid input index") = witness; + + let signed_tx = cache.into_transaction(); + println!("template hash: {template_hash:x}"); + println!("signed transaction: {signed_tx:#?}"); +} + +fn bip448_rebindable_script() -> TapScriptBuf { + Builder::new() + .push_opcode(OP_TEMPLATEHASH) + .push_opcode(OP_INTERNALKEY) + .push_opcode(OP_CHECKSIGFROMSTACK) + .into_script() +} + +fn example_keypair() -> Keypair { + let secret = [1u8; 32]; + let private_key = PrivateKey::from_secret_bytes(&secret).expect("valid secret key"); + Keypair::from_private_key(&private_key) +} diff --git a/docs/inquisition-bip448.md b/docs/inquisition-bip448.md new file mode 100644 index 0000000000..5b91c643f4 --- /dev/null +++ b/docs/inquisition-bip448.md @@ -0,0 +1,56 @@ +# Bitcoin Inquisition BIP448 Helpers + +This branch adds local wallet/client helpers for Bitcoin Inquisition signet scripts that use the +BIP448 opcode bundle: + +- `OP_TEMPLATEHASH` (`0xce`, BIP446) +- `OP_INTERNALKEY` (`0xcb`, BIP349) +- `OP_CHECKSIGFROMSTACK` (`0xcc`, BIP348) + +The support is intentionally non-consensus. It lets applications construct Taproot scripts, +compute BIP446 `TemplateHash` values, and sign those hashes with raw BIP340 signatures for +`OP_CHECKSIGFROMSTACK`. It does not add a script interpreter, activation logic, mempool policy, or +consensus validation. + +## Scope + +Use these helpers for constructing and signing transactions intended for Bitcoin Inquisition signet. +Do not use them as evidence that a transaction is valid under Bitcoin mainnet consensus or under an +Inquisition deployment. + +The default opcode classifier remains current Bitcoin behavior: in `ClassifyContext::TapScript`, +the bytes `0xcb`, `0xcc`, and `0xce` still classify as `SuccessOp`. The named opcode aliases are for +script construction only. + +## TemplateHash + +`SighashCache::template_hash(input_index, annex)` computes the BIP446 tagged hash using the tag +`TemplateHash`. + +The preimage commits to: + +- transaction version +- transaction lock time +- all input sequences +- all outputs +- this input index +- this input's annex presence and annex hash, when present + +The preimage does not commit to prevouts, spent amounts, spent script pubkeys, scriptSigs, or other +inputs' annexes. + +## CSFS Signing + +For the common script: + +```text +OP_TEMPLATEHASH OP_INTERNALKEY OP_CHECKSIGFROMSTACK +``` + +sign the `TemplateHash` with `TemplateHash::sign_checksigfromstack`. The returned signature is a raw +64-byte BIP340 Schnorr signature. Do not append a Taproot sighash byte. + +See: + +- `bitcoin/examples/inquisition_bip448_script.rs` +- `bitcoin/examples/inquisition_bip448_templatehash.rs`