use anyhow::{anyhow, Result};
use bitcoin::blockdata::{
opcodes,
script::{
Instruction::{self, Op, PushBytes},
Instructions,
},
};
use bitcoin::key::Secp256k1;
use bitcoin::script::Builder;
use bitcoin::secp256k1::{All, Keypair};
use bitcoin::sighash::{Prevouts, SighashCache};
use bitcoin::taproot::{LeafVersion, Signature, TaprootBuilder, TAPROOT_ANNEX_PREFIX};
use bitcoin::{
script, Script, ScriptBuf, TapLeafHash, TapSighashType, Transaction, TxIn, TxOut, Witness,
XOnlyPublicKey,
};
use std::iter::Peekable;
pub use bitcoin;
/// Protocol ID for the envelope
pub const PROTOCOL_ID: [u8; 3] = *b"cat";
/// Hash of G per BIP-341: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
pub const NUMS: [u8; 32] = [
0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e,
0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0,
];
type RawEnvelope = Envelope<Vec<Vec<u8>>>;
pub(crate) type ParsedEnvelope = Envelope<ScriptBuf>;
#[derive(Default, PartialEq, Clone, Debug, Eq)]
pub struct Envelope<T> {
pub input: u32,
pub offset: u32,
pub payload: T,
pub pushnum: bool,
pub stutter: bool,
}
impl From<RawEnvelope> for ParsedEnvelope {
fn from(envelope: RawEnvelope) -> Self {
let bytes = envelope.payload.into_iter().flatten().collect::<Vec<_>>();
let script = ScriptBuf::from(bytes);
Self {
payload: script,
input: envelope.input,
offset: envelope.offset,
pushnum: envelope.pushnum,
stutter: envelope.stutter,
}
}
}
impl ParsedEnvelope {
pub(crate) fn from_transaction(transaction: &Transaction) -> Vec<Self> {
RawEnvelope::from_transaction(transaction)
.into_iter()
.map(|envelope| envelope.into())
.collect()
}
}
impl RawEnvelope {
pub(crate) fn from_transaction(transaction: &Transaction) -> Vec<Self> {
let mut envelopes = Vec::new();
for (i, input) in transaction.input.iter().enumerate() {
if let Some(tapscript) = input.witness.tapscript() {
if let Ok(input_envelopes) = Self::from_tapscript(tapscript, i) {
envelopes.extend(input_envelopes);
}
}
}
envelopes
}
pub(crate) fn from_tapscript(tapscript: &Script, input: usize) -> Result<Vec<Self>> {
let mut envelopes = Vec::new();
let mut instructions = tapscript.instructions().peekable();
let mut stuttered = false;
while let Some(instruction) = instructions.next().transpose()? {
if instruction == PushBytes((&[]).into()) {
let (stutter, envelope) =
Self::from_instructions(&mut instructions, input, envelopes.len(), stuttered)?;
if let Some(envelope) = envelope {
envelopes.push(envelope);
} else {
stuttered = stutter;
}
}
}
Ok(envelopes)
}
fn accept(instructions: &mut Peekable<Instructions>, instruction: Instruction) -> Result<bool> {
if instructions.peek() == Some(&Ok(instruction)) {
instructions.next().transpose()?;
Ok(true)
} else {
Ok(false)
}
}
fn from_instructions(
instructions: &mut Peekable<Instructions>,
input: usize,
offset: usize,
stutter: bool,
) -> Result<(bool, Option<Self>)> {
if !Self::accept(instructions, Op(opcodes::all::OP_IF))? {
let stutter = instructions.peek() == Some(&Ok(PushBytes((&[]).into())));
return Ok((stutter, None));
}
if !Self::accept(instructions, PushBytes((&PROTOCOL_ID).into()))? {
let stutter = instructions.peek() == Some(&Ok(PushBytes((&[]).into())));
return Ok((stutter, None));
}
let mut pushnum = false;
let mut payload = Vec::new();
loop {
match instructions.next().transpose()? {
None => return Ok((false, None)),
Some(Op(opcodes::all::OP_ENDIF)) => {
return Ok((
false,
Some(Envelope {
input: input.try_into().expect("invalid input"),
offset: offset.try_into().expect("invalid offset"),
payload,
pushnum,
stutter,
}),
));
}
Some(Op(opcodes::all::OP_PUSHNUM_NEG1)) => {
pushnum = true;
payload.push(vec![0x81]);
}
Some(Op(opcodes::all::OP_PUSHNUM_1)) => {
pushnum = true;
payload.push(vec![1]);
}
Some(Op(opcodes::all::OP_PUSHNUM_2)) => {
pushnum = true;
payload.push(vec![2]);
}
Some(Op(opcodes::all::OP_PUSHNUM_3)) => {
pushnum = true;
payload.push(vec![3]);
}
Some(Op(opcodes::all::OP_PUSHNUM_4)) => {
pushnum = true;
payload.push(vec![4]);
}
Some(Op(opcodes::all::OP_PUSHNUM_5)) => {
pushnum = true;
payload.push(vec![5]);
}
Some(Op(opcodes::all::OP_PUSHNUM_6)) => {
pushnum = true;
payload.push(vec![6]);
}
Some(Op(opcodes::all::OP_PUSHNUM_7)) => {
pushnum = true;
payload.push(vec![7]);
}
Some(Op(opcodes::all::OP_PUSHNUM_8)) => {
pushnum = true;
payload.push(vec![8]);
}
Some(Op(opcodes::all::OP_PUSHNUM_9)) => {
pushnum = true;
payload.push(vec![9]);
}
Some(Op(opcodes::all::OP_PUSHNUM_10)) => {
pushnum = true;
payload.push(vec![10]);
}
Some(Op(opcodes::all::OP_PUSHNUM_11)) => {
pushnum = true;
payload.push(vec![11]);
}
Some(Op(opcodes::all::OP_PUSHNUM_12)) => {
pushnum = true;
payload.push(vec![12]);
}
Some(Op(opcodes::all::OP_PUSHNUM_13)) => {
pushnum = true;
payload.push(vec![13]);
}
Some(Op(opcodes::all::OP_PUSHNUM_14)) => {
pushnum = true;
payload.push(vec![14]);
}
Some(Op(opcodes::all::OP_PUSHNUM_15)) => {
pushnum = true;
payload.push(vec![15]);
}
Some(Op(opcodes::all::OP_PUSHNUM_16)) => {
pushnum = true;
payload.push(vec![16]);
}
Some(PushBytes(push)) => {
payload.push(push.as_bytes().to_vec());
}
Some(_) => return Ok((false, None)),
}
}
}
}
/// Checks if the input has a taproot annex in its witness.
pub fn has_annex(input: &TxIn) -> bool {
input.witness.last().is_some_and(|last_elem|
// From BIP341:
// If there are at least two witness elements, and the first byte of
// the last element is 0x50, this last element is called annex a
// and is removed from the witness stack.
input.witness.len() >= 2 && last_elem.first() == Some(&TAPROOT_ANNEX_PREFIX))
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TaptreeElement {
/// Hex encoded script
pub script: ScriptBuf,
/// Number of witness elements expected
pub num_witness_elements: usize,
}
/// Takes every script and wraps it, then builds a taptree out of it
pub fn wrap_taptree(
secp: &Secp256k1<All>,
scripts: Vec<TaptreeElement>,
internal_key: XOnlyPublicKey,
signer_pk: &XOnlyPublicKey,
) -> Result<ScriptBuf> {
if scripts.is_empty() {
return Err(anyhow::anyhow!("empty scripts"));
}
let len = scripts.len() as u32;
let wrapped_scripts = scripts
.into_iter()
.enumerate()
.map(|(i, e)| {
(
len - i as u32,
wrap_script(e.script, e.num_witness_elements, signer_pk),
)
})
.collect::<Vec<_>>();
let spend_info = TaprootBuilder::with_huffman_tree(wrapped_scripts)?
.finalize(secp, internal_key)
.map_err(|_| anyhow::anyhow!("failed to build spend info"))?;
Ok(ScriptBuf::new_p2tr_tweaked(spend_info.output_key()))
}
/// Wraps the script in an envelope with the protocol id "cat".
/// Adds [`num_witness_elements`] OP_DROPs so the witness can be provided
/// with the transaction still being valid.
/// The script is then locked to a OP_CHECKSIG with the [`signer_pk`].
pub fn wrap_script(
script: ScriptBuf,
num_witness_elements: usize,
signer_pk: &XOnlyPublicKey,
) -> ScriptBuf {
// start with envelope
let builder = Builder::new()
.push_slice([])
.push_opcode(opcodes::all::OP_IF)
.push_slice(PROTOCOL_ID);
// split script into 520 byte chunks, the max stack element size
let with_script = script
.as_bytes()
.chunks(520)
.fold(builder, |builder, chunk| {
let bytes: &script::PushBytes = chunk.try_into().expect("520 bytes in valid");
builder.push_slice(bytes)
});
// envelope ends with an OP_ENDIF
let enveloped = with_script.push_opcode(opcodes::all::OP_ENDIF);
// add OP_DROPs to handle the witness elements to satisfy the
// enveloped script
let with_witness_drops = if num_witness_elements == 0 {
// no witness elements, no OP_DROPs needed
enveloped
} else if num_witness_elements == 1 {
// only a single witness element, can just use a OP_DROP
enveloped.push_opcode(opcodes::all::OP_DROP)
} else if num_witness_elements % 2 == 0 {
// odd even of witness elements, divide by 2 and just use OP_2DROPs
(0..(num_witness_elements / 2)).fold(enveloped, |builder, _| {
builder.push_opcode(opcodes::all::OP_2DROP)
})
} else {
// odd number of witness elements, divide by 2 and use OP_2DROPs
// then add a final OP_DROP
(0..(num_witness_elements / 2))
.fold(enveloped, |builder, _| {
builder.push_opcode(opcodes::all::OP_2DROP)
})
.push_opcode(opcodes::all::OP_DROP)
};
// Add final checksig and finalize
with_witness_drops
.push_x_only_key(signer_pk)
.push_opcode(opcodes::all::OP_CHECKSIG)
.into_script()
}
/// Changes the script to the wrapped script and modifies the control block accordingly
/// Returns the newly created wrapped script
pub fn wrap_tx_input(input: &mut TxIn, signer_pk: &XOnlyPublicKey) -> Result<Option<ScriptBuf>> {
match input.witness.tapscript() {
None => Ok(None), // if it isn't a taproot input, ignore this input
Some(script) => {
let buf = ScriptBuf::from(script);
// annex(optional) + control block + script
let num_taptree_elements: usize = if has_annex(input) { 3 } else { 2 };
let script_index = input.witness.len() - num_taptree_elements;
let num_witness_elements = input.witness.len() - num_taptree_elements;
let wrapped = wrap_script(buf, num_witness_elements, signer_pk);
let mut wit = input.witness.to_vec();
// change out the script, to wrapped version
if let Some(element) = wit.get_mut(script_index) {
*element = wrapped.as_bytes().to_vec();
}
input.witness = Witness::from(wit);
Ok(Some(wrapped))
}
}
}
/// Signs the transaction input and adds the signature as the first element of the witness.
/// This function assumes the input is a taproot script spend and uses sig hash default.
pub fn sign_tx_input(
secp: &Secp256k1<All>,
tx: &mut Transaction,
signer: &Keypair,
input_idx: usize,
prevouts: &Prevouts<TxOut>,
script: &Script,
) -> Result<Signature> {
let mut cache = SighashCache::new(tx.clone());
let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript);
let sighash_type = TapSighashType::Default;
let hash = cache
.taproot_script_spend_signature_hash(input_idx, prevouts, leaf_hash, sighash_type)
.map_err(|e| anyhow!("Taproot spend signature hash failed {e}"))?;
let signature = secp.sign_schnorr_no_aux_rand(&hash.into(), signer);
// add signature to witness
// no easy way to add to front of witness, need to rebuild :/
let mut wit = tx.input[input_idx].witness.to_vec();
let sig = Signature {
signature,
sighash_type,
};
wit.insert(0, sig.to_vec());
tx.input[input_idx].witness = Witness::from(wit);
Ok(sig)
}
/// Changes the script to the unwrapped script
/// Returns the previous wrapped script of the input
pub fn unwrap_tx_input(tx: &mut Transaction, idx: usize) -> Option<ScriptBuf> {
let envelopes = ParsedEnvelope::from_transaction(tx);
if let Some(envelope) = envelopes.into_iter().find(|e| e.input == idx as u32) {
let input = &mut tx.input[envelope.input as usize];
// annex(optional) + control block + script
let num_taptree_elements: usize = if has_annex(input) { 3 } else { 2 };
let script_index = input.witness.len() - num_taptree_elements;
// must be a taproot tx to have an envelope
let script = input.witness.tapscript()?;
let script_buf = ScriptBuf::from(script);
let mut wit = input.witness.to_vec();
// change out the script,
// script is first taptree witness element
if let Some(element) = wit.get_mut(script_index) {
if element == script.as_bytes() {
*element = envelope.payload.as_bytes().to_vec();
}
}
input.witness = Witness::from(wit);
Some(script_buf)
} else {
None
}
}
pub fn get_unwrapped_script(tx: &Transaction, idx: usize) -> Option<ScriptBuf> {
let envelopes = ParsedEnvelope::from_transaction(tx);
envelopes
.into_iter()
.find(|e| e.input == idx as u32)
.map(|e| e.payload)
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::consensus::Decodable;
use bitcoin::hashes::Hash;
use bitcoin::hex::FromHex;
use bitcoin::io::Cursor;
use bitcoin::key::UntweakedPublicKey;
use bitcoin::transaction::Version;
use bitcoin::{absolute, Amount, OutPoint, Sequence, Txid, WPubkeyHash};
use bitcoin_tx_verify::{verify_tx, verify_tx_input_tapscript, VerifyFlags};
use rand::rngs::OsRng;
use std::collections::HashMap;
use std::str::FromStr;
#[test]
fn test_wrap_script() {
let script = ScriptBuf::new_p2wpkh(
&WPubkeyHash::from_str("0eb77d4815ed327d4d724de8b89a9592a5f74e69").unwrap(),
);
let signer_key = XOnlyPublicKey::from_str(
"e1ff3bfdd4e40315959b08b4fcc8245eaa514637e1d4ec2ae166b743341be1af",
)
.unwrap();
let wrapped = wrap_script(script.clone(), 1, &signer_key);
let envelopes = RawEnvelope::from_tapscript(wrapped.as_script(), 0).unwrap();
assert_eq!(envelopes.len(), 1);
let parsed = ParsedEnvelope::from(envelopes[0].clone());
assert_eq!(parsed.payload, script);
}
#[test]
fn test_wrap_tx_input() {
let tx_bytes: Vec<u8> = FromHex::from_hex("02000000000101b125029710f2ba7fe064292418d2e833bae2897f7bf6e2f1a2242c87635dfc2e0100000000ffffffff01905f010000000000160014cea9d080198881e00baead0521d5be4e660693771001000100040200000004bb00000020f92434758cd2d2eaa58881b31cdfd3c3515448f80e1b51ac32d77c6ec65f1dce20184c0ede118ec8cd31f699524bae48a81ed01ded5f8d08f2f4ff4286b33b027020d0c8f23f944956475f4ef6823c171a46f2f39123fb8e62c3255087e4d68e366c20ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e01020400000000209d9f03916f15de9baac8de5a785e8f3c401d85a49f476a14de38ad0dcea4d3db010004ffffffff3f79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ef3d745bcf6458f86768ae8adba97ac0a1702cd9cebb786f8665c5a84d87d8ae6b7e7e7e7e201f8f848c1c8015fba58504000a171722182cb30ac3716afa9ecb8d398ab039847c7e7e7e7e7e7e7e7e7e0a54617053696768617368a8767b7e7ea811424950303334302f6368616c6c656e6765a8767b2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179876766b7b7e7e7e7ea86c7c7e6c7601007e7b88517e2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0bb000000").unwrap();
let mut tx_cursor = Cursor::new(tx_bytes);
let mut tx: Transaction = Transaction::consensus_decode(&mut tx_cursor).unwrap();
let original = tx.clone();
let output_key = XOnlyPublicKey::from_str(
"c6ee2efbb6a663bd2d9996e2e7cf5d2a27cb4375879fe3b6beb669dcce6505cd",
)
.unwrap();
wrap_tx_input(&mut tx.input[0], &output_key).unwrap();
assert_ne!(original, tx)
}
#[test]
fn test_unwrap_tx_inputs() {
let tx_bytes: Vec<u8> = FromHex::from_hex("02000000000101b125029710f2ba7fe064292418d2e833bae2897f7bf6e2f1a2242c87635dfc2e0100000000ffffffff01905f010000000000160014cea9d080198881e00baead0521d5be4e660693771001000100040200000004bb00000020f92434758cd2d2eaa58881b31cdfd3c3515448f80e1b51ac32d77c6ec65f1dce20184c0ede118ec8cd31f699524bae48a81ed01ded5f8d08f2f4ff4286b33b027020d0c8f23f944956475f4ef6823c171a46f2f39123fb8e62c3255087e4d68e366c20ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e01020400000000209d9f03916f15de9baac8de5a785e8f3c401d85a49f476a14de38ad0dcea4d3db010004ffffffff3f79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ef3d745bcf6458f86768ae8adba97ac0a1702cd9cebb786f8665c5a84d87d8ae6b7e7e7e7e201f8f848c1c8015fba58504000a171722182cb30ac3716afa9ecb8d398ab039847c7e7e7e7e7e7e7e7e7e0a54617053696768617368a8767b7e7ea811424950303334302f6368616c6c656e6765a8767b2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179876766b7b7e7e7e7ea86c7c7e6c7601007e7b88517e2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0bb000000").unwrap();
let mut tx_cursor = Cursor::new(tx_bytes);
let mut tx: Transaction = Transaction::consensus_decode(&mut tx_cursor).unwrap();
let original = tx.clone();
let output_key = XOnlyPublicKey::from_str(
"c6ee2efbb6a663bd2d9996e2e7cf5d2a27cb4375879fe3b6beb669dcce6505cd",
)
.unwrap();
wrap_tx_input(&mut tx.input[0], &output_key).unwrap();
assert_ne!(original, tx);
let script = unwrap_tx_input(&mut tx, 0);
assert!(script.is_some());
}
#[test]
fn validate_wrapping_tx_inputs() {
let tx_bytes: Vec<u8> = FromHex::from_hex("02000000000101b125029710f2ba7fe064292418d2e833bae2897f7bf6e2f1a2242c87635dfc2e0100000000ffffffff01905f010000000000160014cea9d080198881e00baead0521d5be4e660693771001000100040200000004bb00000020f92434758cd2d2eaa58881b31cdfd3c3515448f80e1b51ac32d77c6ec65f1dce20184c0ede118ec8cd31f699524bae48a81ed01ded5f8d08f2f4ff4286b33b027020d0c8f23f944956475f4ef6823c171a46f2f39123fb8e62c3255087e4d68e366c20ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e01020400000000209d9f03916f15de9baac8de5a785e8f3c401d85a49f476a14de38ad0dcea4d3db010004ffffffff3f79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ef3d745bcf6458f86768ae8adba97ac0a1702cd9cebb786f8665c5a84d87d8ae6b7e7e7e7e201f8f848c1c8015fba58504000a171722182cb30ac3716afa9ecb8d398ab039847c7e7e7e7e7e7e7e7e7e0a54617053696768617368a8767b7e7ea811424950303334302f6368616c6c656e6765a8767b2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179876766b7b7e7e7e7ea86c7c7e6c7601007e7b88517e2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0bb000000").unwrap();
let mut tx_cursor = Cursor::new(tx_bytes);
let mut tx: Transaction = Transaction::consensus_decode(&mut tx_cursor).unwrap();
let original = tx.clone();
let script = ScriptBuf::from_hex("6b7e7e7e7e201f8f848c1c8015fba58504000a171722182cb30ac3716afa9ecb8d398ab039847c7e7e7e7e7e7e7e7e7e0a54617053696768617368a8767b7e7ea811424950303334302f6368616c6c656e6765a8767b2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179876766b7b7e7e7e7ea86c7c7e6c7601007e7b88517e2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac").unwrap();
assert_eq!(tx.input[0].witness.tapscript().unwrap(), script.as_script());
// validate original tx
let secp = Secp256k1::new();
let value = Amount::from_sat(100000);
let ikey = UntweakedPublicKey::from_slice(&NUMS).expect("NUMS point from BIP 341");
let spend_info = TaprootBuilder::new()
.add_leaf(0, script.clone())
.unwrap()
.finalize(&secp, ikey)
.unwrap();
let script_pubkey = ScriptBuf::new_p2tr_tweaked(spend_info.output_key());
let prevout: TxOut = TxOut {
value,
script_pubkey,
};
let prevouts = tx
.input
.iter()
.map(|i| i.previous_output)
.zip(vec![prevout])
.collect();
// validate
let result = verify_tx(&tx, &prevouts, VerifyFlags::OpCatEnabled);
assert!(result.is_ok());
let signer = Keypair::new(&secp, &mut OsRng);
let (signer_pk, _) = signer.x_only_public_key();
// modify tx to wrapped version
let wrapped_script = wrap_tx_input(&mut tx.input[0], &signer_pk)
.unwrap()
.unwrap();
assert_ne!(original, tx); // make sure it changed
// create new prevout
let wrapped = wrap_script(script, tx.input[0].witness.len() - 2, &signer_pk);
assert_eq!(wrapped_script, wrapped);
let spend_info = TaprootBuilder::new()
.add_leaf(0, wrapped.clone())
.unwrap()
.finalize(&secp, ikey)
.unwrap();
let script_pubkey = ScriptBuf::new_p2tr_tweaked(spend_info.output_key());
let prevout: TxOut = TxOut {
value,
script_pubkey,
};
// sign tx input
sign_tx_input(
&secp,
&mut tx,
&signer,
0,
&Prevouts::All(&[prevout.clone()]),
wrapped.as_script(),
)
.unwrap();
let prevouts = tx
.input
.iter()
.map(|i| i.previous_output)
.zip(vec![prevout.clone()])
.collect();
// validate
let result = verify_tx_input_tapscript(&tx, &prevouts, 0);
assert_eq!(result, Ok(()))
}
#[test]
fn end_to_end_test() {
let secp = Secp256k1::new();
let signer = Keypair::new(&secp, &mut OsRng);
let (signer_pk, _) = signer.x_only_public_key();
// basic addition
let script = Builder::new()
.push_opcode(opcodes::all::OP_1ADD)
.push_opcode(opcodes::all::OP_PUSHNUM_2)
.push_opcode(opcodes::all::OP_EQUAL)
.into_script();
let ikey = UntweakedPublicKey::from_slice(&NUMS).expect("NUMS point from BIP 341");
let spend_info = TaprootBuilder::new()
.add_leaf(0, script.clone())
.unwrap()
.finalize(&secp, ikey)
.unwrap();
let addr_script = ScriptBuf::new_p2tr_tweaked(spend_info.output_key());
// create spending tx
let witness = {
let control = spend_info
.control_block(&(script.clone(), LeafVersion::TapScript))
.unwrap();
Witness::from(vec![
vec![0x01],
script.as_bytes().to_vec(),
control.serialize(),
])
};
let mut tx = Transaction {
version: Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::new(Txid::all_zeros(), 0),
script_sig: Default::default(),
sequence: Sequence::MAX,
witness,
}],
output: vec![TxOut {
value: Amount::from_sat(100_000),
script_pubkey: Default::default(),
}],
};
assert_eq!(tx.input[0].witness.tapscript().unwrap(), script.as_script());
let value = Amount::from_sat(100_100);
let dummy_prevout = TxOut {
value,
script_pubkey: addr_script,
};
let prevouts = tx
.input
.iter()
.map(|i| i.previous_output)
.zip(vec![dummy_prevout])
.collect();
// validate original tx
let result = verify_tx(&tx, &prevouts, VerifyFlags::OpCatEnabled);
assert_eq!(result, Ok(()));
let original = tx.clone();
// modify tx to wrapped version
let wrapped_script = wrap_tx_input(&mut tx.input[0], &signer_pk)
.unwrap()
.unwrap();
assert_ne!(original, tx); // make sure it changed
// create new prevout
let wrapped = wrap_script(script, tx.input[0].witness.len() - 2, &signer_pk);
assert_eq!(wrapped_script, wrapped);
let spend_info = TaprootBuilder::new()
.add_leaf(0, wrapped.clone())
.unwrap()
.finalize(&secp, ikey)
.unwrap();
let script_pubkey = ScriptBuf::new_p2tr_tweaked(spend_info.output_key());
let prevout: TxOut = TxOut {
value,
script_pubkey,
};
// sign tx input
sign_tx_input(
&secp,
&mut tx,
&signer,
0,
&Prevouts::All(&[prevout.clone()]),
wrapped.as_script(),
)
.unwrap();
let prevouts = tx
.input
.iter()
.map(|i| i.previous_output)
.zip(vec![prevout])
.collect::<HashMap<_, _>>();
// validate
let result = verify_tx_input_tapscript(&tx, &prevouts, 0);
assert_eq!(result, Ok(()))
}
}