From 8c7d0190c2d68f162e336ca76c1c1bfbdc207f40 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/4] 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 | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bitcoin/src/blockdata/opcodes.rs b/bitcoin/src/blockdata/opcodes.rs index 6ca490d980..9e3d301506 100644 --- a/bitcoin/src/blockdata/opcodes.rs +++ b/bitcoin/src/blockdata/opcodes.rs @@ -50,6 +50,13 @@ macro_rules! all_opcodes { #[doc = $doc] pub const $op: Opcode = Opcode { code: $val}; )* + + /// 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 an empty array onto the stack. @@ -564,6 +571,26 @@ mod tests { assert_eq!(s, " OP_NOP"); } + #[test] + fn inquisition_bip448_aliases() { + use crate::script::Builder; + + 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 46a3f39db1bd13e650a3016b1a0d978a27619ab5 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/4] 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 | 130 ++++++++++++++++- bitcoin/tests/data/bip446/basics.json | 192 ++++++++++++++++++++++++++ 2 files changed, 318 insertions(+), 4 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 aea38f32e3..08fb03263b 100644 --- a/bitcoin/src/crypto/sighash.rs +++ b/bitcoin/src/crypto/sighash.rs @@ -66,9 +66,18 @@ sha256t_hash_newtype! { /// This hash type is used for computing taproot signature hash." #[hash_newtype(forward)] pub struct TapSighash(_); + + pub struct TemplateHashTag = hash_str("TemplateHash"); + + /// Taproot-tagged hash with tag \"TemplateHash\". + /// + /// This hash type is used for computing OP_TEMPLATEHASH hash." + #[hash_newtype(forward)] + pub struct TemplateHash(_); } impl_message_from_hash!(TapSighash); +impl_message_from_hash!(TemplateHash); /// Efficiently calculates signature hash message for legacy, segwit and taproot inputs. #[derive(Debug)] @@ -669,10 +678,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(); - annex.consensus_encode(&mut enc)?; - let hash = sha256::Hash::from_engine(enc); - hash.consensus_encode(writer)?; + sha256_annex(annex)?.consensus_encode(writer)?; } // * Data about this output: @@ -729,6 +735,49 @@ impl> SighashCache { Ok(TapSighash::from_engine(enc)) } + /// 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, + writer: &mut W, + input_index: usize, + annex: Option, + ) -> Result<(), SigningDataError> { + self.tx.borrow().tx_in(input_index).map_err(SigningDataError::sighash)?; + + // Transaction data. + self.tx.borrow().version.consensus_encode(writer)?; + self.tx.borrow().lock_time.consensus_encode(writer)?; + self.common_cache().sequences.consensus_encode(writer)?; + self.common_cache().outputs.consensus_encode(writer)?; + + // Data about this input. + u8::from(annex.is_some()).consensus_encode(writer)?; + (input_index as u32).consensus_encode(writer)?; + if let Some(annex) = annex { + sha256_annex(annex)?.consensus_encode(writer)?; + } + + 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 = TemplateHash::engine(); + self.template_hash_encode_data_to(&mut enc, input_index, annex) + .map_err(SigningDataError::unwrap_sighash)?; + Ok(TemplateHash::from_engine(enc)) + } + /// Computes the BIP341 sighash for a key spend. pub fn taproot_key_spend_signature_hash>( &mut self, @@ -1160,6 +1209,12 @@ impl<'a> Encodable for Annex<'a> { } } +fn sha256_annex(annex: Annex<'_>) -> Result { + let mut enc = sha256::Hash::engine(); + annex.consensus_encode(&mut enc)?; + Ok(sha256::Hash::from_engine(enc)) +} + /// Error computing a taproot sighash. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] @@ -1500,6 +1555,7 @@ mod tests { use super::*; use crate::blockdata::locktime::absolute; use crate::consensus::deserialize; + use crate::opcodes::all::{OP_EQUAL, OP_TEMPLATEHASH}; extern crate serde_json; @@ -1579,6 +1635,65 @@ mod tests { assert_eq!(expected, hash.to_byte_array()); } + #[test] + fn template_hash_encode_lengths() { + let tx_bytes = Vec::from_hex("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 = Vec::from_hex(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.input[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.input[input_index] + .witness + .tapscript() + .expect("taproot script spend"); + 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 test_sighashes_keyspending() { // following test case has been taken from Bitcoin Core test framework @@ -1754,6 +1869,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 9ff9f0e175e675cbea479990386524c17a46564b 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/4] 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 | 79 +++++++++++++++++++ docs/inquisition-bip448.md | 45 +++++++++++ 3 files changed, 173 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..7c45d2d936 --- /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::opcodes::all::{OP_CHECKSIGFROMSTACK, OP_INTERNALKEY, OP_TEMPLATEHASH}; +use bitcoin::script::Builder; +use bitcoin::secp256k1::{Keypair, Secp256k1, SecretKey}; +use bitcoin::taproot::{LeafVersion, TaprootBuilder}; +use bitcoin::ScriptBuf; +use hex::DisplayHex; + +fn main() { + let secp = Secp256k1::new(); + let keypair = example_keypair(); + let internal_key = keypair.x_only_public_key().0; + + 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(&secp, internal_key) + .expect("finalizable taproot tree"); + + let script_pubkey = ScriptBuf::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() -> ScriptBuf { + Builder::new() + .push_opcode(OP_TEMPLATEHASH) + .push_opcode(OP_INTERNALKEY) + .push_opcode(OP_CHECKSIGFROMSTACK) + .into_script() +} + +fn example_keypair() -> Keypair { + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key"); + Keypair::from_secret_key(&secp, &secret_key) +} diff --git a/bitcoin/examples/inquisition_bip448_templatehash.rs b/bitcoin/examples/inquisition_bip448_templatehash.rs new file mode 100644 index 0000000000..13c1b95949 --- /dev/null +++ b/bitcoin/examples/inquisition_bip448_templatehash.rs @@ -0,0 +1,79 @@ +// 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::hashes::Hash; +use bitcoin::locktime::absolute; +use bitcoin::opcodes::all::{OP_CHECKSIGFROMSTACK, OP_INTERNALKEY, OP_TEMPLATEHASH}; +use bitcoin::script::Builder; +use bitcoin::secp256k1::{Keypair, Message, Secp256k1, SecretKey}; +use bitcoin::sighash::SighashCache; +use bitcoin::taproot::{LeafVersion, TaprootBuilder}; +use bitcoin::{ + transaction, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, +}; + +fn main() { + let secp = Secp256k1::new(); + let keypair = example_keypair(); + let internal_key = keypair.x_only_public_key().0; + let script = bip448_rebindable_script(); + + let spend_info = TaprootBuilder::new() + .add_leaf(0, script.clone()) + .expect("valid taproot tree") + .finalize(&secp, 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: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }; + + let destination = ScriptBuf::new_p2tr(&secp, internal_key, None); + let output = TxOut { value: Amount::from_sat(49_000), script_pubkey: destination }; + + let mut tx = Transaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::ZERO, + input: vec![input], + output: vec![output], + }; + + let mut cache = SighashCache::new(&mut tx); + let template_hash = cache.template_hash(0, None).expect("valid input index"); + let signature = secp.sign_schnorr_no_aux_rand(&Message::from(template_hash), &keypair); + + let mut witness = Witness::new(); + witness.push(signature.serialize()); + witness.push(script.as_bytes()); + witness.push(control_block.serialize()); + *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() -> ScriptBuf { + Builder::new() + .push_opcode(OP_TEMPLATEHASH) + .push_opcode(OP_INTERNALKEY) + .push_opcode(OP_CHECKSIGFROMSTACK) + .into_script() +} + +fn example_keypair() -> Keypair { + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key"); + Keypair::from_secret_key(&secp, &secret_key) +} diff --git a/docs/inquisition-bip448.md b/docs/inquisition-bip448.md new file mode 100644 index 0000000000..653f742a6b --- /dev/null +++ b/docs/inquisition-bip448.md @@ -0,0 +1,45 @@ +# 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. + +See: + +- `bitcoin/examples/inquisition_bip448_script.rs` +- `bitcoin/examples/inquisition_bip448_templatehash.rs` From 06d7bdeb8cc5c7b2a1c817dc116c381385ee79f3 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Mon, 29 Jun 2026 14:51:59 -0400 Subject: [PATCH 4/4] opcodes: make BIP 448 opcodes first class citizens --- bitcoin/src/blockdata/opcodes.rs | 41 ++++++++++++++++---------------- bitcoin/tests/serde_opcodes.rs | 6 ++--- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/bitcoin/src/blockdata/opcodes.rs b/bitcoin/src/blockdata/opcodes.rs index 9e3d301506..c6a2c1dc7d 100644 --- a/bitcoin/src/blockdata/opcodes.rs +++ b/bitcoin/src/blockdata/opcodes.rs @@ -50,13 +50,6 @@ macro_rules! all_opcodes { #[doc = $doc] pub const $op: Opcode = Opcode { code: $val}; )* - - /// 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 an empty array onto the stack. @@ -271,6 +264,12 @@ all_opcodes! { OP_NOP8 => 0xb7, "Does nothing."; OP_NOP9 => 0xb8, "Does nothing."; OP_NOP10 => 0xb9, "Does nothing."; + + // BIP 448 opcodes. + OP_INTERNALKEY => 0xcb, "Push the Taproot internal key onto the stack. (Tapscript-only.)"; + OP_CHECKSIGFROMSTACK => 0xcc, "Pop a signature (bottom), message and public key (top) from the stack. Push 1 if the signature is valid, 0 otherwise. (Tapscript-only.)"; + OP_TEMPLATEHASH => 0xce, "Push the hash of the spending transaction onto the stack. (Tapscript-only.)"; + // Every other opcode acts as OP_RETURN OP_CHECKSIGADD => 0xba, "OP_CHECKSIGADD post tapscript."; OP_RETURN_187 => 0xbb, "Synonym for OP_RETURN."; @@ -289,10 +288,7 @@ all_opcodes! { OP_RETURN_200 => 0xc8, "Synonym for OP_RETURN."; OP_RETURN_201 => 0xc9, "Synonym for OP_RETURN."; OP_RETURN_202 => 0xca, "Synonym for OP_RETURN."; - OP_RETURN_203 => 0xcb, "Synonym for OP_RETURN."; - OP_RETURN_204 => 0xcc, "Synonym for OP_RETURN."; OP_RETURN_205 => 0xcd, "Synonym for OP_RETURN."; - OP_RETURN_206 => 0xce, "Synonym for OP_RETURN."; OP_RETURN_207 => 0xcf, "Synonym for OP_RETURN."; OP_RETURN_208 => 0xd0, "Synonym for OP_RETURN."; OP_RETURN_209 => 0xd1, "Synonym for OP_RETURN."; @@ -374,7 +370,7 @@ impl Opcode { | (OP_MUL, ctx) | (OP_DIV, ctx) | (OP_MOD, ctx) | (OP_LSHIFT, ctx) | (OP_RSHIFT, ctx) if ctx == ClassifyContext::Legacy => Class::IllegalOp, - // 87 opcodes of SuccessOp class only in TapScript context + // 84 opcodes of SuccessOp class only in TapScript context (op, ClassifyContext::TapScript) if op.code == 80 || op.code == 98 @@ -383,7 +379,9 @@ impl Opcode { || (op.code >= 137 && op.code <= 138) || (op.code >= 141 && op.code <= 142) || (op.code >= 149 && op.code <= 153) - || (op.code >= 187 && op.code <= 254) => + || (op.code >= 187 && op.code <= 202) + || (op.code == 205) + || (op.code >= 207 && op.code <= 254) => Class::SuccessOp, // 11 opcodes of NoOp class @@ -415,7 +413,7 @@ impl Opcode { // 76 opcodes of PushBytes class (op, _) if op.code <= OP_PUSHBYTES_75.code => Class::PushBytes(self.code as u32), - // opcodes of Ordinary class: 61 for Legacy and 60 for TapScript context + // opcodes of Ordinary class: 61 for Legacy and 63 for TapScript context (_, _) => Class::Ordinary(Ordinary::with(self)), } } @@ -514,7 +512,7 @@ macro_rules! ordinary_opcode { ); } -// "Ordinary" opcodes -- should be 61 of these +// "Ordinary" opcodes -- should be 64 of these ordinary_opcode! { // pushdata OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, @@ -537,7 +535,8 @@ ordinary_opcode! { OP_RIPEMD160, OP_SHA1, OP_SHA256, OP_HASH160, OP_HASH256, OP_CODESEPARATOR, OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, - OP_CHECKSIGADD + OP_CHECKSIGADD, + OP_INTERNALKEY, OP_CHECKSIGFROMSTACK, OP_TEMPLATEHASH } impl Ordinary { @@ -586,9 +585,9 @@ mod tests { .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); + assert_eq!(OP_INTERNALKEY.classify(ClassifyContext::TapScript), Class::Ordinary(Ordinary::OP_INTERNALKEY)); + assert_eq!(OP_CHECKSIGFROMSTACK.classify(ClassifyContext::TapScript), Class::Ordinary(Ordinary::OP_CHECKSIGFROMSTACK)); + assert_eq!(OP_TEMPLATEHASH.classify(ClassifyContext::TapScript), Class::Ordinary(Ordinary::OP_TEMPLATEHASH)); } #[test] @@ -863,10 +862,10 @@ mod tests { roundtrip!(unique, OP_RETURN_200); roundtrip!(unique, OP_RETURN_201); roundtrip!(unique, OP_RETURN_202); - roundtrip!(unique, OP_RETURN_203); - roundtrip!(unique, OP_RETURN_204); + roundtrip!(unique, OP_INTERNALKEY); + roundtrip!(unique, OP_CHECKSIGFROMSTACK); roundtrip!(unique, OP_RETURN_205); - roundtrip!(unique, OP_RETURN_206); + roundtrip!(unique, OP_TEMPLATEHASH); roundtrip!(unique, OP_RETURN_207); roundtrip!(unique, OP_RETURN_208); roundtrip!(unique, OP_RETURN_209); diff --git a/bitcoin/tests/serde_opcodes.rs b/bitcoin/tests/serde_opcodes.rs index 3d73a84c0e..21898d59b6 100644 --- a/bitcoin/tests/serde_opcodes.rs +++ b/bitcoin/tests/serde_opcodes.rs @@ -223,10 +223,10 @@ fn serde_regression_opcodes() { OP_RETURN_200, OP_RETURN_201, OP_RETURN_202, - OP_RETURN_203, - OP_RETURN_204, + OP_INTERNALKEY, + OP_CHECKSIGFROMSTACK, OP_RETURN_205, - OP_RETURN_206, + OP_TEMPLATEHASH, OP_RETURN_207, OP_RETURN_208, OP_RETURN_209,