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
37 changes: 32 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ pub struct Options {
pub verify_csv: bool,
/// Verify conditionals are minimally encoded.
pub verify_minimal_if: bool,
/// Enfore a strict limit of 1000 total stack items.
/// Enforce a strict limit of 1000 total stack items after each instruction,
/// and at entry in the Tapscript context.
pub enforce_stack_limit: bool,

pub experimental: Experimental,
Expand Down Expand Up @@ -299,6 +300,8 @@ impl Exec {
if let Ok(exec) = &mut ret {
exec.stack = stack;
exec.altstack = altstack;
// The supplied stacks replace the witness as the initial state.
exec.stats.max_nb_stack_items = exec.stack.len() + exec.altstack.len();
}
ret
}
Expand Down Expand Up @@ -480,6 +483,26 @@ impl Exec {
return Err(res);
}

if self.script_position() == 0 {
// ExecuteWitnessScript checks the initial item count for Tapscript
// only, and the initial element sizes for both witness versions.
if self.ctx == ExecCtx::Tapscript
&& self.opt.enforce_stack_limit
&& self.stack.len() + self.altstack.len() > MAX_STACK_SIZE
{
return self.fail(ExecError::StackSize);
}
if self.ctx != ExecCtx::Legacy
&& self
.stack
.iter_str()
.chain(self.altstack.iter_str())
.any(|item| item.len() > MAX_SCRIPT_ELEMENT_SIZE)
{
return self.fail(ExecError::PushSize);
}
}

self.current_position = self.script.len() - self.instructions.as_script().len();
let instruction = match self.instructions.next() {
Some(Ok(i)) => i,
Expand Down Expand Up @@ -541,6 +564,14 @@ impl Exec {
}

self.update_stats();
// This must also cover PushBytes (including OP_0), which never calls
// exec_opcode. Record the peak before rejecting the overflowing step.
if self.opt.enforce_stack_limit && self.stack.len() + self.altstack.len() > MAX_STACK_SIZE {
return match instruction {
Instruction::Op(op) => self.failop(ExecError::StackSize, op),
Instruction::PushBytes(_) => self.fail(ExecError::StackSize),
};
}
Ok(())
}

Expand Down Expand Up @@ -1020,10 +1051,6 @@ impl Exec {
_ => return Err(ExecError::BadOpcode),
}

if self.opt.enforce_stack_limit && self.stack.len() + self.altstack.len() > MAX_STACK_SIZE {
return Err(ExecError::StackSize);
}

Ok(())
}

Expand Down
225 changes: 225 additions & 0 deletions tests/resource_limits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
//! Raw bytecode preserves temporary overflows that a script optimizer could erase.
//! Entry rules follow ExecuteWitnessScript, and per-instruction rules EvalScript:
//! https://github.com/bitcoin/bitcoin/blob/d0f6d9953a15d7c7111d46dcb76ab2bb18e5dee3/src/script/interpreter.cpp
//! These tests do not claim complete consensus compatibility (including OP_SUCCESSx).

use bitcoin::{hashes::Hash, opcodes::all::OP_PUSHNUM_1, ScriptBuf, TapLeafHash, Transaction};
use bitcoin_scriptexec::{Exec, ExecCtx, ExecError, Options, Stack, TxTemplate};

fn transaction() -> TxTemplate {
TxTemplate {
tx: Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![],
output: vec![],
},
prevouts: vec![],
input_idx: 0,
taproot_annex_scriptleaf: Some((TapLeafHash::all_zeros(), None)),
}
}

fn executor(ctx: ExecCtx, script: Vec<u8>, witness: Vec<Vec<u8>>, limit: bool) -> Exec {
Exec::new(
ctx,
Options {
enforce_stack_limit: limit,
..Default::default()
},
transaction(),
ScriptBuf::from_bytes(script),
witness,
)
.unwrap()
}

fn run(exec: &mut Exec) {
while exec.exec_next().is_ok() {}
}

fn drop_items(count: usize) -> Vec<u8> {
let mut bytes = vec![0x6d; count / 2]; // OP_2DROP
if count % 2 != 0 {
bytes.push(0x75); // OP_DROP
}
bytes.push(0x51); // OP_TRUE
bytes
}

#[test]
fn tapscript_rejects_oversized_initial_stack_before_cleanup() {
for count in [1000, 1001] {
for limit in [false, true] {
let mut exec = executor(
ExecCtx::Tapscript,
drop_items(count),
vec![vec![]; count],
limit,
);
run(&mut exec);
let rejected = limit && count > 1000;
assert_eq!(
exec.result().unwrap().error,
rejected.then_some(ExecError::StackSize)
);
assert_eq!(exec.result().unwrap().success, !rejected);
assert_eq!(exec.stats().max_nb_stack_items, count);
if rejected {
assert_eq!(exec.script_position(), 0);
assert_eq!(exec.stack().len(), count);
}
}
}
}

#[test]
fn tapscript_checks_entry_even_for_an_empty_script() {
let mut exec = executor(ExecCtx::Tapscript, vec![], vec![vec![]; 1001], true);
assert_eq!(
exec.exec_next().unwrap_err().error,
Some(ExecError::StackSize)
);
assert_eq!(
exec.exec_next().unwrap_err().error,
Some(ExecError::StackSize)
);
assert_eq!(exec.script_position(), 0);
}

#[test]
fn pre_tapscript_count_is_checked_after_the_first_instruction() {
for ctx in [ExecCtx::Legacy, ExecCtx::SegwitV0] {
// ExecuteWitnessScript has no entry item-count check for SegwitV0.
// Inspect a single step: a complete SegwitV0 leaf would also face its
// 201-opcode limit when attempting to clean up the remaining items.
let mut exec = executor(ctx, vec![0x75], vec![vec![]; 1001], true);
assert!(exec.exec_next().is_ok());
assert_eq!(exec.stack().len(), 1000);
assert_eq!(exec.stats().max_nb_stack_items, 1001);
let mut exec = executor(ctx, vec![0x61], vec![vec![]; 1001], true); // OP_NOP
assert_eq!(
exec.exec_next().unwrap_err().error,
Some(ExecError::StackSize)
);
}
}

#[test]
fn witness_element_sizes_are_checked_independently_of_the_item_limit() {
for ctx in [ExecCtx::Legacy, ExecCtx::SegwitV0, ExecCtx::Tapscript] {
for size in [520, 521] {
for limit in [false, true] {
let mut exec = executor(ctx, drop_items(1), vec![vec![0x42; size]], limit);
run(&mut exec);
// A Legacy initial stack is supplied by the caller, not a
// witness. Its actual script pushes still enforce 520 bytes.
let rejected = ctx != ExecCtx::Legacy && size > 520;
assert_eq!(
exec.result().unwrap().error,
rejected.then_some(ExecError::PushSize)
);
assert_eq!(exec.result().unwrap().success, !rejected);
if rejected {
assert_eq!(exec.script_position(), 0);
assert_eq!(exec.stack().len(), 1);
}
}
}
}
}

#[test]
fn data_and_empty_pushes_enforce_the_limit_in_every_context() {
for ctx in [ExecCtx::Legacy, ExecCtx::SegwitV0, ExecCtx::Tapscript] {
for push in [vec![0x00], vec![0x01, 0x11]] {
for count in [999, 1000] {
for limit in [false, true] {
let mut bytes = push.clone();
bytes.push(0x75); // OP_DROP cannot hide the preceding overflow.
let mut exec = executor(ctx, bytes, vec![vec![]; count], limit);
let rejected = limit && count == 1000;
assert_eq!(exec.exec_next().is_err(), rejected);
assert_eq!(exec.stats().max_nb_stack_items, count + 1);
assert_eq!(exec.stack().len(), count + 1);
if rejected {
assert_eq!(exec.result().unwrap().error, Some(ExecError::StackSize));
assert_eq!(exec.remaining_script().as_bytes(), &[0x75]);
} else {
assert!(exec.exec_next().is_ok());
assert_eq!(exec.stack().len(), count);
}
}
}
}
}
}

#[test]
fn data_push_limit_includes_the_altstack() {
for ctx in [ExecCtx::Legacy, ExecCtx::SegwitV0, ExecCtx::Tapscript] {
// Main stack reaches 1000; the altstack still contains one live item.
let mut exec = executor(
ctx,
vec![0x6b, 0x01, 0x11, 0x01, 0x12],
vec![vec![]; 999],
true,
);
assert!(exec.exec_next().is_ok());
assert!(exec.exec_next().is_ok());
assert_eq!(
exec.exec_next().unwrap_err().error,
Some(ExecError::StackSize)
);
assert_eq!(exec.stack().len(), 1000);
assert_eq!(exec.altstack().len(), 1);
assert_eq!(exec.stats().max_nb_stack_items, 1001);
}
}

#[test]
fn numeric_push_failure_records_peak_and_opcode() {
let mut exec = executor(ExecCtx::Tapscript, vec![0x51], vec![vec![]; 1000], true);
let result = exec.exec_next().unwrap_err();
assert_eq!(result.error, Some(ExecError::StackSize));
assert_eq!(result.opcode, Some(OP_PUSHNUM_1));
assert_eq!(exec.stats().max_nb_stack_items, 1001);
}

#[test]
fn skipped_pushes_do_not_increase_the_peak() {
let mut bytes = vec![0x63, 0x01, 0x11, 0x00, 0x68]; // OP_IF ... OP_ENDIF
bytes.extend(drop_items(999));
let mut exec = executor(ExecCtx::Tapscript, bytes, vec![vec![]; 1000], true);
run(&mut exec);
assert!(exec.result().unwrap().success);
assert_eq!(exec.stats().max_nb_stack_items, 1000);
}

#[test]
fn replacement_stacks_define_entry_limits_and_statistics() {
for (main_count, alt_count, limit) in [(999, 1, true), (1000, 1, true), (1000, 1, false)] {
let mut exec = Exec::with_stack(
ExecCtx::Tapscript,
Options {
enforce_stack_limit: limit,
..Default::default()
},
transaction(),
ScriptBuf::from_bytes(vec![0x75]),
// Used for validation weight, but replaced as the execution state.
vec![vec![]; 1010],
Stack::from_u8_vec(vec![vec![]; main_count]),
Stack::from_u8_vec(vec![vec![]; alt_count]),
)
.unwrap();
assert_eq!(exec.stats().max_nb_stack_items, main_count + alt_count);
let rejected = limit && main_count + alt_count > 1000;
assert_eq!(exec.exec_next().is_err(), rejected);
if rejected {
assert_eq!(exec.result().unwrap().error, Some(ExecError::StackSize));
assert_eq!(exec.script_position(), 0);
}
assert_eq!(exec.stats().max_nb_stack_items, main_count + alt_count);
}
}