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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ This project is a work-in-progress mostly attempting to facilitate BitVM develop
It does not yet fully implement all opcodes, but as a library already gives you pretty
good insight into the internals of the execution in a step-wise manner.

`OP_CHECKSEQUENCEVERIFY` accepts five-byte ScriptNum operands. Its relative
lock comparison uses only the type flag (bit 22) and low 16 bits, including
when the operand exceeds `u32::MAX`; the version and disable-bit rules still
apply. Regression tests cover these rules, numeric encoding and rejection
boundaries in legacy, SegWit v0 and Tapscript contexts:

```
cargo test --locked --test checksequenceverify
```


# Usage

Expand Down
13 changes: 8 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,14 @@ impl Exec {
None => return false,
};

let lock_time =
match LockTime::from_consensus(u32::try_from(sequence).expect("sequence is u32")) {
Ok(lt) => lt,
Err(_) => return false,
};
// BIP112 accepts five-byte ScriptNums, so the operand can exceed u32.
// BIP68 compares only the type flag and low 16 bits. Mask before
// narrowing; the caller already checks negativity and the disable bit.
let sequence_mask: i64 = (1 << 22) | 0xffff;
let lock_time = match LockTime::from_consensus((sequence & sequence_mask) as u32) {
Ok(lt) => lt,
Err(_) => return false,
};

match (lock_time, input_lock_time) {
(LockTime::Blocks(h1), LockTime::Blocks(h2)) if h1 > h2 => return false,
Expand Down
250 changes: 250 additions & 0 deletions tests/checksequenceverify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
//! BIP112 permits five-byte ScriptNum operands. BIP68 compares only bit 22
//! and the low 16 bits, after applying the operand/input disable-bit rules.
//! These are interpreter tests, not complete transaction validation.

use bitcoin::absolute;
use bitcoin::hashes::Hash;
use bitcoin::opcodes::all::{OP_CSV, OP_DROP};
use bitcoin::script::Builder;
use bitcoin::taproot::TapLeafHash;
use bitcoin::transaction::Version;
use bitcoin::{Sequence, Transaction, TxIn};
use bitcoin_scriptexec::{Exec, ExecCtx, ExecError, ExecutionResult, Options, TxTemplate};

const CONTEXTS: [ExecCtx; 3] = [ExecCtx::Legacy, ExecCtx::SegwitV0, ExecCtx::Tapscript];
const TIME: u32 = 1 << 22;
const DISABLE: u32 = 1 << 31;

fn number(value: i64) -> Vec<u8> {
if value == 0 {
return vec![];
}
let mut magnitude = value.unsigned_abs();
let mut bytes = Vec::new();
while magnitude != 0 {
bytes.push(magnitude as u8);
magnitude >>= 8;
}
if bytes.last().unwrap() & 0x80 != 0 {
bytes.push(if value < 0 { 0x80 } else { 0 });
} else if value < 0 {
*bytes.last_mut().unwrap() |= 0x80;
}
bytes
}

fn run(
context: ExecCtx,
operand: Vec<u8>,
version: i32,
sequence: u32,
options: Options,
) -> ExecutionResult {
let script = Builder::new()
.push_opcode(OP_CSV)
.push_opcode(OP_DROP)
.push_int(1)
.into_script();
let mut exec = Exec::new(
context,
options,
TxTemplate {
tx: Transaction {
version: Version(version),
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
sequence: Sequence::from_consensus(sequence),
..TxIn::default()
}],
output: vec![],
},
prevouts: vec![],
input_idx: 0,
taproot_annex_scriptleaf: Some((TapLeafHash::all_zeros(), None)),
},
script,
vec![operand],
)
.unwrap();
while exec.exec_next().is_ok() {}
exec.result().unwrap().clone()
}

fn check(operand: i64, version: i32, sequence: u32, error: Option<ExecError>) {
for context in CONTEXTS {
let result = run(
context,
number(operand),
version,
sequence,
Options::default(),
);
assert_eq!(
result.error, error,
"{context:?}: operand={operand:#x}, version={version}, sequence={sequence:#x}"
);
assert_eq!(result.success, error.is_none());
}
}

#[test]
fn five_byte_high_bits_do_not_change_the_relative_lock() {
// Each operand is minimally encoded in five bytes, with bit 31 unset.
// The high bits must be discarded before narrowing to a u32.
for high in [1_i64 << 32, 1 << 33, 1 << 38, 0x7f_0000_0000] {
for low in [0, 1, 0xffff] {
check(high | low, 2, low as u32, None);
if low != 0 {
check(
high | low,
2,
low as u32 - 1,
Some(ExecError::UnsatisfiedLocktime),
);
}
}
}
}

#[test]
fn reserved_bits_do_not_change_height_or_time_constraints() {
for reserved in [1_i64 << 16, 1 << 21, 1 << 23, 1 << 30] {
for high in [0, 1_i64 << 32] {
check(high | reserved | 7, 2, 7, None);
check(
high | reserved | 7,
2,
6,
Some(ExecError::UnsatisfiedLocktime),
);
check(high | reserved | i64::from(TIME) | 7, 2, TIME | 7, None);
check(
high | reserved | i64::from(TIME) | 7,
2,
TIME | 6,
Some(ExecError::UnsatisfiedLocktime),
);
}
// Reserved bits in the transaction's sequence are ignored as well.
check(7, 2, reserved as u32 | 7, None);
check(i64::from(TIME) | 7, 2, reserved as u32 | TIME | 7, None);
}
}

#[test]
fn height_and_time_are_distinct_even_for_five_byte_operands() {
for high in [0, 1_i64 << 32, 0x7f_0000_0000] {
for low in [0, 1, 0xffff] {
check(
high | low,
2,
TIME | low as u32,
Some(ExecError::UnsatisfiedLocktime),
);
check(
high | i64::from(TIME) | low,
2,
low as u32,
Some(ExecError::UnsatisfiedLocktime),
);
check(high | i64::from(TIME) | low, 2, TIME | low as u32, None);
}
}
}

#[test]
fn transaction_version_and_input_disable_bit_still_apply() {
for operand in [0, 1, 1_i64 << 32, (1 << 32) | 1] {
check(operand, 1, 1, Some(ExecError::UnsatisfiedLocktime));
check(
operand,
2,
DISABLE | 1,
Some(ExecError::UnsatisfiedLocktime),
);
check(operand, 2, u32::MAX, Some(ExecError::UnsatisfiedLocktime));
check(operand, 2, 1, None);
}
}

#[test]
fn operand_disable_bit_bypasses_relative_lock_checks() {
for operand in [
i64::from(DISABLE),
i64::from(DISABLE) | (1_i64 << 32) | 7,
(1_i64 << 39) - 1,
] {
for version in [1, 2] {
for sequence in [0, 1, TIME | 1, DISABLE, u32::MAX] {
check(operand, version, sequence, None);
}
}
}
}

#[test]
fn negative_and_overlong_operands_reject_before_masking() {
for operand in [-1, -(1_i64 << 32), -((1_i64 << 39) - 1)] {
check(operand, 2, u32::MAX, Some(ExecError::NegativeLocktime));
}
for context in CONTEXTS {
for require_minimal in [false, true] {
for operand in [number(1_i64 << 39), vec![0, 0, 0, 0x80, 0, 0]] {
let result = run(
context,
operand,
2,
0,
Options {
require_minimal,
..Options::default()
},
);
assert_eq!(result.error, Some(ExecError::ScriptIntNumericOverflow));
assert!(!result.success);
}
}
}
}

#[test]
fn numeric_minimality_remains_controlled_by_options() {
for context in CONTEXTS {
for operand in [vec![0], vec![0x80], vec![1, 0], vec![1, 0, 0, 0, 0]] {
let permissive = run(
context,
operand.clone(),
2,
1,
Options {
require_minimal: false,
..Options::default()
},
);
assert!(permissive.success, "{context:?}: {operand:?}");
let minimal = run(context, operand, 2, 1, Options::default());
assert_eq!(minimal.error, Some(ExecError::MinimalData));
assert!(!minimal.success);
}
}
}

#[test]
fn disabling_csv_skips_operand_interpretation() {
for context in CONTEXTS {
for operand in [number(-1), number(1_i64 << 39), vec![0x80]] {
let result = run(
context,
operand,
1,
u32::MAX,
Options {
verify_csv: false,
..Options::default()
},
);
assert!(result.success);
assert_eq!(result.error, None);
}
}
}