Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions tests/tapscript_codeseparators.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
//! Deterministic signature regressions for the BIP342 CODESEPARATOR position.
//!
//! The expected instruction positions are explicit fixture constants, not
//! inferred from the interpreter's counters. Multi-byte pushes count once,
//! skipped instructions still count, and only executed separators update the
//! committed position. See BIP342 at revision 24e96e870fffaa257b465ce1f0370c14aac588e8:
//! https://github.com/bitcoin/bips/blob/24e96e870fffaa257b465ce1f0370c14aac588e8/bip-0342.mediawiki#common-signature-message-extension
//! and Bitcoin Core v30.3 at 49faec4f87f5cd19c88db01a82e5c68b087c8227,
//! src/script/interpreter.cpp (opcode_pos and OP_CODESEPARATOR).
//!
//! These tests verify signature hashing and fragment execution; they do not
//! connect transactions to Bitcoin Core or establish relay-policy acceptance.

use bitcoin::{
absolute,
hashes::Hash,
secp256k1::{Keypair, Message, Secp256k1, SecretKey},
sighash::{Prevouts, SighashCache, TapSighashType},
taproot::{LeafVersion, TaprootBuilder},
transaction, Amount, OutPoint, ScriptBuf, Sequence, TapLeafHash, Transaction, TxIn, TxOut,
Txid, Witness,
};
use bitcoin_scriptexec::{Exec, ExecCtx, ExecError, ExecutionResult, Options, TxTemplate};

const NO_SEPARATOR: u32 = u32::MAX;

struct SignedFixture {
script: ScriptBuf,
tx: Transaction,
prevouts: Vec<TxOut>,
leaf: TapLeafHash,
keypair: Keypair,
}

impl SignedFixture {
fn new(mut prefix: Vec<u8>) -> Self {
let secp = Secp256k1::new();
// Public deterministic test key; never use with real funds.
let secret = SecretKey::from_slice(&[1; 32]).unwrap();
let keypair = Keypair::from_secret_key(&secp, &secret);
let (pubkey, _) = keypair.x_only_public_key();
prefix.push(32);
prefix.extend(pubkey.serialize());
prefix.push(0xac); // CHECKSIG
let script = ScriptBuf::from_bytes(prefix);
let leaf = TapLeafHash::from_script(&script, LeafVersion::TapScript);
let spend_info = TaprootBuilder::new()
.add_leaf(0, script.clone())
.unwrap()
.finalize(&secp, pubkey)
.unwrap();
let prevouts = vec![TxOut {
value: Amount::from_sat(10_000),
script_pubkey: ScriptBuf::new_p2tr_tweaked(spend_info.output_key()),
}];
let tx = Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: Txid::from_byte_array([0x11; 32]),
vout: 0,
},
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
}],
output: vec![TxOut {
value: Amount::from_sat(9_000),
script_pubkey: ScriptBuf::new_p2tr_tweaked(spend_info.output_key()),
}],
};
Self {
script,
tx,
prevouts,
leaf,
keypair,
}
}

fn execute_signature_for(&self, separator_position: u32) -> ExecutionResult {
let sighash = SighashCache::new(&self.tx)
.taproot_signature_hash(
0,
&Prevouts::All(&self.prevouts),
None,
Some((self.leaf, separator_position)),
TapSighashType::Default,
)
.unwrap();
let signature = Secp256k1::new().sign_schnorr_no_aux_rand(
&Message::from_digest(sighash.to_byte_array()),
&self.keypair,
);
execute(
ExecCtx::Tapscript,
self.script.clone(),
vec![signature.as_ref().to_vec()],
self.tx.clone(),
self.prevouts.clone(),
)
}

fn assert_signature_valid(&self, position: u32) {
let result = self.execute_signature_for(position);
assert_eq!(result.error, None, "position {position}, {}", self.script);
assert!(result.success, "position {position}, {}", self.script);
assert_eq!(
result.final_stack.iter_str().collect::<Vec<_>>(),
vec![vec![1]]
);
}

fn assert_signature_invalid(&self, position: u32) {
let result = self.execute_signature_for(position);
assert_eq!(
result.error,
Some(ExecError::SchnorrSig),
"wrong position {position}, {}",
self.script,
);
assert!(!result.success);
}
}

fn execute(
context: ExecCtx,
script: ScriptBuf,
witness: Vec<Vec<u8>>,
tx: Transaction,
prevouts: Vec<TxOut>,
) -> ExecutionResult {
let leaf = TapLeafHash::from_script(&script, LeafVersion::TapScript);
let mut exec = Exec::new(
context,
Options {
// PUSHDATA4 with <=520 bytes is necessarily nonminimal. It is
// permitted by consensus and must still count as one instruction.
require_minimal: false,
..Options::default()
},
TxTemplate {
tx,
prevouts,
input_idx: 0,
taproot_annex_scriptleaf: Some((leaf, None)),
},
script,
witness,
)
.unwrap();
loop {
if let Err(result) = exec.exec_next() {
return result.clone();
}
}
}

fn push_encodings() -> Vec<Vec<u8>> {
let mut pushdata1 = vec![0x4c, 76];
pushdata1.extend(vec![0x11; 76]);
let mut pushdata2 = vec![0x4d, 0, 1];
pushdata2.extend(vec![0x11; 256]);
vec![
vec![0x01, 0x11],
pushdata1,
pushdata2,
vec![0x4e, 1, 0, 0, 0, 0x11],
]
}

#[test]
fn no_separator_uses_the_u32_max_sentinel() {
let fixture = SignedFixture::new(vec![0x01, 0x11, 0x75]); // push 17, DROP
fixture.assert_signature_valid(NO_SEPARATOR);
fixture.assert_signature_invalid(0);
}

#[test]
fn multibyte_pushes_count_as_one_opcode() {
for mut prefix in push_encodings() {
// Push is opcode 0, DROP is 1, CODESEPARATOR is 2.
prefix.extend([0x75, 0xab]);
SignedFixture::new(prefix).assert_signature_valid(2);
}
for push in [0x00, 0x4f, 0x51, 0x60] {
SignedFixture::new(vec![push, 0x75, 0xab]).assert_signature_valid(2);
}
}

#[test]
fn signatures_for_separator_byte_offsets_are_rejected() {
for mut prefix in push_encodings() {
prefix.extend([0x75, 0xab]);
let byte_offset = (prefix.len() - 1) as u32;
assert_ne!(byte_offset, 2);
SignedFixture::new(prefix).assert_signature_invalid(byte_offset);
}
}

#[test]
fn skipped_pushes_and_opcodes_still_count() {
// FALSE=0, IF=1, skipped push=2, DROP=3, NOP=4, ENDIF=5, separator=6.
let mut prefix = vec![0x00, 0x63, 0x4c, 76];
prefix.extend(vec![0x11; 76]);
prefix.extend([0x75, 0x61, 0x68, 0xab]);
let byte_offset = (prefix.len() - 1) as u32;
let fixture = SignedFixture::new(prefix);
fixture.assert_signature_valid(6);
fixture.assert_signature_invalid(3); // Counting only executed instructions.
fixture.assert_signature_invalid(byte_offset);
}

#[test]
fn an_unexecuted_separator_preserves_the_sentinel() {
// The separator at position 2 is inside a false branch.
let fixture = SignedFixture::new(vec![0x00, 0x63, 0xab, 0x68]);
fixture.assert_signature_valid(NO_SEPARATOR);
fixture.assert_signature_invalid(2);
}

#[test]
fn skipped_separators_do_not_replace_the_last_executed_separator() {
// Push=0, DROP=1, executed separator=2, FALSE=3, IF=4,
// skipped separator=5, NOP=6, ENDIF=7.
let fixture = SignedFixture::new(vec![0x01, 0x11, 0x75, 0xab, 0x00, 0x63, 0xab, 0x61, 0x68]);
fixture.assert_signature_valid(2);
fixture.assert_signature_invalid(5);
fixture.assert_signature_invalid(3); // Byte offset of the executed separator.
}

#[test]
fn the_most_recent_executed_separator_wins() {
// Executed separators at positions 0 and 3; skipped separator at 6.
let fixture = SignedFixture::new(vec![0xab, 0x01, 0x11, 0x75, 0xab, 0x00, 0x63, 0xab, 0x68]);
fixture.assert_signature_valid(3);
fixture.assert_signature_invalid(0);
fixture.assert_signature_invalid(6);
fixture.assert_signature_invalid(4); // Byte offset of the last executed separator.
}

#[test]
fn legacy_and_segwit_keep_their_nonpush_opcode_limit() {
// CODESEPARATOR and DROP contribute two counted opcodes; data and small
// integer pushes do not count toward the legacy/Segwit-v0 limit of 201.
for nops in [199, 200] {
let mut bytes = vec![0xab, 0x01, 0x11, 0x75];
bytes.extend(vec![0x61; nops]);
bytes.push(0x51);
for context in [ExecCtx::Legacy, ExecCtx::SegwitV0, ExecCtx::Tapscript] {
let result = execute(
context,
ScriptBuf::from_bytes(bytes.clone()),
vec![],
Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![],
},
vec![],
);
let over_limit = context != ExecCtx::Tapscript && nops == 200;
assert_eq!(result.error, over_limit.then_some(ExecError::OpCount));
assert_eq!(result.success, !over_limit);
}
}
}