Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ impl Exec {
Ok(false)
}
} else {
Ok(true)
Ok(!sig.is_empty())
}
}

Expand Down
212 changes: 212 additions & 0 deletions tests/tapscript_unknown_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
//! Unknown-key signature results follow BIP342, including the empty-signature rules:
//! https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#rules-for-signature-opcodes
//! Bitcoin Core v30.3 initializes success from signature nonemptiness before key dispatch:
//! https://github.com/bitcoin/bitcoin/blob/49faec4f87f5cd19c88db01a82e5c68b087c8227/src/script/interpreter.cpp#L346-L384
//! These direct Exec tests do not validate a complete Taproot spend or relay policy.

use bitcoin::{
opcodes::all::*, script::Builder, taproot::LeafVersion, Amount, Opcode, ScriptBuf, TapLeafHash,
Transaction, TxIn, TxOut,
};
use bitcoin_scriptexec::{Exec, ExecCtx, ExecError, Experimental, Options, TxTemplate};

const UNKNOWN_KEY_LENGTHS: [usize; 6] = [1, 2, 31, 33, 65, 520];

fn signatures() -> [Vec<u8>; 4] {
// Nonempty unknown-key signatures need not be valid Schnorr encodings.
[vec![], vec![0], vec![0x42; 64], vec![0x42; 65]]
}

fn executor(script: ScriptBuf, witness: Vec<Vec<u8>>) -> Exec {
let leaf_hash = TapLeafHash::from_script(&script, LeafVersion::TapScript);
let prevout = TxOut {
value: Amount::from_sat(10_000),
script_pubkey: ScriptBuf::new(),
};
Exec::new(
ExecCtx::Tapscript,
Options {
require_minimal: false,
experimental: Experimental { op_cat: false },
..Options::default()
},
TxTemplate {
tx: Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn::default()],
output: vec![prevout.clone()],
},
prevouts: vec![prevout],
input_idx: 0,
taproot_annex_scriptleaf: Some((leaf_hash, None)),
},
script,
witness,
)
.unwrap()
}

fn run(opcode: Opcode, signature: Vec<u8>, key: Vec<u8>) -> Exec {
let mut witness = vec![signature];
if opcode == OP_CHECKSIGADD {
witness.push(vec![7]);
}
witness.push(key);
let mut script = Builder::new().push_opcode(opcode);
if opcode == OP_CHECKSIGVERIFY {
script = script.push_int(1);
}
let mut exec = executor(script.into_script(), witness);
while exec.exec_next().is_ok() {}
exec
}

fn assert_charge(exec: &Exec, signature_nonempty: bool) {
// Assert the charge, without claiming the current API includes the full
// transaction witness (script/control block/annex) in its initial budget.
assert_eq!(
exec.stats().start_validation_weight - exec.stats().validation_weight,
if signature_nonempty { 50 } else { 0 }
);
}

#[test]
fn checksig_unknown_keys_return_false_for_empty_signatures() {
for key_length in UNKNOWN_KEY_LENGTHS {
for signature in signatures() {
let nonempty = !signature.is_empty();
let exec = run(OP_CHECKSIG, signature, vec![0x42; key_length]);
let result = exec.result().unwrap();
assert_eq!(result.error, None, "key length {key_length}");
assert_eq!(result.success, nonempty, "key length {key_length}");
assert_eq!(result.final_stack.len(), 1);
assert_eq!(
result.final_stack.get(0),
if nonempty { vec![1] } else { vec![] }
);
assert_charge(&exec, nonempty);
}
}
}

#[test]
fn checksigverify_unknown_keys_reject_empty_signatures() {
for key_length in UNKNOWN_KEY_LENGTHS {
for signature in signatures() {
let nonempty = !signature.is_empty();
let exec = run(OP_CHECKSIGVERIFY, signature, vec![0x42; key_length]);
let result = exec.result().unwrap();
assert_eq!(result.success, nonempty, "key length {key_length}");
assert_eq!(
result.error,
(!nonempty).then_some(ExecError::CheckSigVerify)
);
if nonempty {
assert_eq!(result.final_stack.len(), 1);
assert_eq!(result.final_stack.get(0), vec![1]);
} else {
assert!(result.final_stack.is_empty());
assert_eq!(result.opcode, Some(OP_CHECKSIGVERIFY));
}
assert_charge(&exec, nonempty);
}
}
}

#[test]
fn checksigadd_unknown_keys_leave_the_accumulator_unchanged_for_empty_signatures() {
for key_length in UNKNOWN_KEY_LENGTHS {
for signature in signatures() {
let nonempty = !signature.is_empty();
let exec = run(OP_CHECKSIGADD, signature, vec![0x42; key_length]);
let result = exec.result().unwrap();
assert_eq!(result.error, None, "key length {key_length}");
assert!(result.success);
assert_eq!(result.final_stack.len(), 1);
assert_eq!(
result.final_stack.get(0),
vec![if nonempty { 8 } else { 7 }]
);
assert_charge(&exec, nonempty);
}
}
}

#[test]
fn empty_public_keys_reject_regardless_of_signature_nonemptiness() {
for opcode in [OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKSIGADD] {
for signature in signatures() {
let exec = run(opcode, signature, vec![]);
let result = exec.result().unwrap();
assert!(!result.success);
assert_eq!(result.error, Some(ExecError::PubkeyType));
assert_eq!(result.opcode, Some(opcode));
}
}
}

#[test]
fn known_key_length_keeps_empty_signature_semantics() {
// The 32-byte key path must still treat an empty signature as false,
// without attempting Schnorr validation of these arbitrary key bytes.
for opcode in [OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKSIGADD] {
let exec = run(opcode, vec![], vec![0x42; 32]);
let result = exec.result().unwrap();
match opcode {
OP_CHECKSIG => {
assert!(!result.success);
assert_eq!(result.error, None);
assert!(result.final_stack.get(0).is_empty());
}
OP_CHECKSIGVERIFY => {
assert!(!result.success);
assert_eq!(result.error, Some(ExecError::CheckSigVerify));
}
OP_CHECKSIGADD => {
assert!(result.success);
assert_eq!(result.error, None);
assert_eq!(result.final_stack.get(0), vec![7]);
}
_ => unreachable!(),
}
assert_charge(&exec, false);
}
}

#[test]
fn only_nonempty_unknown_key_signatures_exhaust_validation_weight() {
let mut script = Builder::new();
for _ in 0..32 {
script = script
.push_opcode(OP_2DUP)
.push_opcode(OP_CHECKSIG)
.push_opcode(OP_DROP);
}
let script = script.push_opcode(OP_2DROP).push_int(1).into_script();

let mut empty = executor(script.clone(), vec![vec![], vec![0x42]]);
while empty.exec_next().is_ok() {}
assert!(empty.result().unwrap().success);
assert_eq!(
empty.stats().validation_weight,
empty.stats().start_validation_weight
);

let mut nonempty = executor(script, vec![vec![0], vec![0x42]]);
for _ in 0..32 {
assert!(nonempty.exec_next().is_ok()); // OP_2DUP
let before = nonempty.stats().validation_weight;
if before < 50 {
assert_eq!(
nonempty.exec_next().unwrap_err().error,
Some(ExecError::TapscriptValidationWeight)
);
return;
}
assert!(nonempty.exec_next().is_ok()); // OP_CHECKSIG
assert_eq!(nonempty.stats().validation_weight, before - 50);
assert!(nonempty.exec_next().is_ok()); // OP_DROP
}
panic!("repeated nonempty signatures must exhaust this small fixture's budget");
}