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/bitcoin/src/blockdata/opcodes.rs b/bitcoin/src/blockdata/opcodes.rs index 6ca490d980..c6a2c1dc7d 100644 --- a/bitcoin/src/blockdata/opcodes.rs +++ b/bitcoin/src/blockdata/opcodes.rs @@ -264,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."; @@ -282,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."; @@ -367,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 @@ -376,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 @@ -408,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)), } } @@ -507,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, @@ -530,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 { @@ -564,6 +570,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::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] fn decode_pushnum() { // Test all possible opcodes @@ -836,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/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." + } +] 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, 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`