use bsv_rs::primitives::bsv::sighash::{parse_transaction, TxOutput};
use bsv_rs::primitives::from_hex;
use bsv_rs::script::flags::{DEFAULT_STACK_MEMORY_USAGE_POLICY, STACK_ELEMENT_OVERHEAD};
use bsv_rs::script::{
LockingScript, ProtocolEra, ScriptFlags, ScriptResource, Spend, SpendParams, UnlockingScript,
};
use std::sync::{Mutex, MutexGuard};
const TX_READ_32000001: &str = "02000000010282f68044beaa91d8f3424e5499046c9a2fc62222344f786bdf10882a0dcb050000000000ffffffff010100000000000000015100000000";
const LOCK_READ_32000001: &str = "51040148e801808b7551";
const TX_SHIFT_AT_WIDTH: &str = "020000000188ff7652209492801e7845ba25b446b8bfe34cd1840af382dd6d55dc323f86510000000000ffffffff010100000000000000015100000000";
const LOCK_SHIFT_AT_WIDTH: &str = "5104f83f420fb67551";
const TX_SHIFT_OVER_WIDTH: &str = "02000000019b4268a2acf6c06f35a22cd21ddd350d89b57a588e9aab80a90369f1406a47a00000000000ffffffff010100000000000000015100000000";
const LOCK_SHIFT_OVER_WIDTH: &str = "51040040420fb67551";
const TX_SHIFT_SIGN_BYTE: &str = "020000000130764b0bd4e481b72d3e25340480db3adfe11d0af95c2a7880e9116be3f7b5110000000000ffffffff010100000000000000015100000000";
const LOCK_SHIFT_SIGN_BYTE: &str = "5104ff3f420fb67551";
const TX_NUM2BIN_40M: &str = "02000000016f34f2d0251b3daa8f4fceda5679f220b289f671eaafef33213ff7143d935c75000000000151ffffffff010100000000000000015100000000";
const LOCK_NUM2BIN_40M: &str = "04005a6202808204005a62028777";
const TX_NUM2BIN_2POW31: &str = "020000000187a34732a06ac0f3edeca5c9a239399da4c91d80f507fbd34c06ce985d415f27000000000151ffffffff010100000000000000015100000000";
const LOCK_NUM2BIN_2POW31: &str = "0500000080008051";
const LOCK_PAD_AT_POLICY: &str = "5104e0e0f505807551";
const LOCK_PAD_OVER_POLICY: &str = "5104e1e0f505807551";
const LOCK_SPLIT_AT_POLICY: &str = "510460f0fa02806b510460f0fa0280756c7551";
const LOCK_SPLIT_OVER_SECOND: &str = "510460f0fa02806b510461f0fa0280756c7551";
const LOCK_SPLIT_OVER_FIRST: &str = "510461f0fa02806b510460f0fa0280756c7551";
const UNLOCK_ALT_RESIDUE: &str = "510460f0fa02806b";
const LOCK_PAD_HALF: &str = "510460f0fa02807551";
const LOCK_PAD_HALF_PLUS_1: &str = "510461f0fa02807551";
const LOCK_PAD_INT32_MAX: &str = "5104ffffff7f807551";
const LOCK_PAD_NEGATIVE: &str = "514f807551";
#[derive(Clone, Copy, Debug)]
enum Mode {
Block,
Standard,
Default,
}
#[derive(Debug, PartialEq)]
enum Outcome {
Valid,
Verdict(String),
Resource {
resource: ScriptResource,
limit: usize,
attempted: usize,
},
}
use Outcome::{Resource, Valid, Verdict};
static HEAVY: Mutex<()> = Mutex::new(());
fn heavy() -> MutexGuard<'static, ()> {
HEAVY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn outcome(
tx_hex: &str,
unlock_hex: Option<&str>,
lock_hex: &str,
mode: Mode,
memory_limit: Option<usize>,
tune: impl FnOnce(&mut Spend),
) -> Outcome {
let tx = parse_transaction(&from_hex(tx_hex).unwrap()).unwrap();
let input = &tx.inputs[0];
let outputs: Vec<TxOutput> = tx
.outputs
.iter()
.map(|o| TxOutput {
satoshis: o.satoshis,
script: o.script.clone(),
})
.collect();
let unlocking_script = match unlock_hex {
Some(hex) => UnlockingScript::from_hex(hex).unwrap(),
None => UnlockingScript::from_binary(&input.script).unwrap(),
};
let mut s = Spend::new(SpendParams {
source_txid: input.txid,
source_output_index: input.output_index,
source_satoshis: 1000,
locking_script: LockingScript::from_hex(lock_hex).unwrap(),
transaction_version: tx.version,
other_inputs: vec![],
outputs,
input_index: 0,
unlocking_script,
input_sequence: input.sequence,
lock_time: tx.locktime,
memory_limit,
});
match mode {
Mode::Block => s.set_flags(ScriptFlags::block(ProtocolEra::PostChronicle)),
Mode::Standard => s.set_flags(ScriptFlags::standard(ProtocolEra::PostChronicle)),
Mode::Default => {}
}
s.set_utxo_after_chronicle(true);
tune(&mut s);
match s.validate() {
Ok(valid) => {
assert!(valid, "validate returns Ok(true) or an error");
Valid
}
Err(e) => match e.resource_limit {
Some(limit) => Resource {
resource: limit.resource,
limit: limit.limit,
attempted: limit.attempted,
},
None => Verdict(e.message),
},
}
}
fn run(tx_hex: &str, lock_hex: &str, mode: Mode) -> Outcome {
outcome(tx_hex, None, lock_hex, mode, None, |_| {})
}
fn verdict(message: &str) -> Outcome {
Verdict(message.to_string())
}
fn over_word_budget(attempted: usize) -> Outcome {
Resource {
resource: ScriptResource::Stack,
limit: 100_000_000,
attempted,
}
}
fn over_policy(attempted: usize) -> Outcome {
verdict(&format!(
"Stack size limit exceeded: {attempted} bytes, the stack memory policy is 100000000 bytes."
))
}
const OVERFLOW_READ_BLOCK: &str =
"Script number overflow: 32000001 bytes, the limit is 32000000 bytes.";
const OVERFLOW_READ_POLICY: &str =
"Script number overflow: 32000001 bytes, the limit is 10000 bytes.";
const OVERFLOW_32000000_POLICY: &str =
"Script number overflow: 32000000 bytes, the limit is 10000 bytes.";
const PUSH_SIZE_2POW31: &str = "OP_NUM2BIN requires a size from 0 to 2147483647, found 2147483648.";
const PUSH_SIZE_NEGATIVE: &str = "OP_NUM2BIN requires a size from 0 to 2147483647, found -1.";
const DEFAULT_PUSH_SIZE: &str =
"It's not currently possible to push data larger than 1073741824 bytes or negative size.";
#[test]
fn the_32000001_byte_read_overflows_under_the_block_word() {
let _heavy = heavy();
assert_eq!(
run(TX_READ_32000001, LOCK_READ_32000001, Mode::Block),
verdict(OVERFLOW_READ_BLOCK)
);
}
#[test]
fn the_32000001_byte_read_overflows_under_the_standard_word() {
let _heavy = heavy();
assert_eq!(
run(TX_READ_32000001, LOCK_READ_32000001, Mode::Standard),
verdict(OVERFLOW_READ_POLICY)
);
}
#[test]
fn the_32000001_byte_read_is_the_default_modes_resource_limit() {
assert_eq!(
run(TX_READ_32000001, LOCK_READ_32000001, Mode::Default),
Resource {
resource: ScriptResource::ElementSize,
limit: 32_000_000,
attempted: 32_000_001,
}
);
}
#[test]
fn a_left_shift_to_exactly_32000000_bytes_is_valid_under_the_block_word() {
let _heavy = heavy();
assert_eq!(
run(TX_SHIFT_AT_WIDTH, LOCK_SHIFT_AT_WIDTH, Mode::Block),
Valid
);
}
#[test]
fn a_left_shift_to_exactly_32000000_bytes_overflows_the_policy() {
assert_eq!(
run(TX_SHIFT_AT_WIDTH, LOCK_SHIFT_AT_WIDTH, Mode::Standard),
verdict(OVERFLOW_32000000_POLICY)
);
assert_eq!(
run(TX_SHIFT_AT_WIDTH, LOCK_SHIFT_AT_WIDTH, Mode::Default),
Resource {
resource: ScriptResource::ElementSize,
limit: 32_000_000,
attempted: 32_000_001,
}
);
}
#[test]
fn a_left_shift_over_the_width_overflows_before_the_shift() {
assert_eq!(
run(TX_SHIFT_OVER_WIDTH, LOCK_SHIFT_OVER_WIDTH, Mode::Block),
verdict(OVERFLOW_READ_BLOCK)
);
assert_eq!(
run(TX_SHIFT_OVER_WIDTH, LOCK_SHIFT_OVER_WIDTH, Mode::Standard),
verdict(OVERFLOW_READ_POLICY)
);
}
#[test]
fn a_left_shift_whose_result_takes_a_sign_byte_overflows_after_the_shift() {
assert_eq!(
run(TX_SHIFT_SIGN_BYTE, LOCK_SHIFT_SIGN_BYTE, Mode::Block),
verdict(OVERFLOW_READ_BLOCK)
);
assert_eq!(
run(TX_SHIFT_SIGN_BYTE, LOCK_SHIFT_SIGN_BYTE, Mode::Standard),
verdict(OVERFLOW_32000000_POLICY)
);
assert_eq!(
run(TX_SHIFT_SIGN_BYTE, LOCK_SHIFT_SIGN_BYTE, Mode::Default),
Resource {
resource: ScriptResource::ElementSize,
limit: 32_000_000,
attempted: 32_000_002,
}
);
}
#[test]
fn a_40000000_byte_element_is_valid_under_both_words() {
let _heavy = heavy();
assert_eq!(run(TX_NUM2BIN_40M, LOCK_NUM2BIN_40M, Mode::Block), Valid);
assert_eq!(run(TX_NUM2BIN_40M, LOCK_NUM2BIN_40M, Mode::Standard), Valid);
assert_eq!(
run(TX_NUM2BIN_40M, LOCK_NUM2BIN_40M, Mode::Default),
Resource {
resource: ScriptResource::ElementSize,
limit: 32_000_000,
attempted: 40_000_000,
}
);
}
#[test]
fn a_num2bin_size_above_int32_max_is_a_push_size_refusal() {
assert_eq!(
run(TX_NUM2BIN_2POW31, LOCK_NUM2BIN_2POW31, Mode::Block),
verdict(PUSH_SIZE_2POW31)
);
assert_eq!(
run(TX_NUM2BIN_2POW31, LOCK_NUM2BIN_2POW31, Mode::Standard),
verdict(PUSH_SIZE_2POW31)
);
assert_eq!(
run(TX_NUM2BIN_2POW31, LOCK_NUM2BIN_2POW31, Mode::Default),
verdict(DEFAULT_PUSH_SIZE)
);
}
#[test]
fn a_negative_num2bin_size_is_a_push_size_refusal() {
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_NEGATIVE, Mode::Block),
verdict(PUSH_SIZE_NEGATIVE)
);
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_NEGATIVE, Mode::Default),
verdict(DEFAULT_PUSH_SIZE)
);
}
#[test]
fn a_num2bin_to_int32_max_is_refused_before_it_is_allocated() {
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_INT32_MAX, Mode::Standard),
over_policy(2_147_483_679)
);
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_INT32_MAX, Mode::Block),
over_word_budget(2_147_483_679)
);
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_INT32_MAX, Mode::Default),
verdict(DEFAULT_PUSH_SIZE)
);
}
#[test]
fn the_stack_memory_policy_admits_exactly_its_figure() {
let _heavy = heavy();
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_AT_POLICY, Mode::Standard),
Valid
);
}
#[test]
fn one_byte_over_the_stack_memory_policy_is_a_verdict() {
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_OVER_POLICY, Mode::Standard),
over_policy(100_000_001)
);
}
#[test]
fn the_block_path_declines_above_its_local_budget() {
let _heavy = heavy();
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_AT_POLICY, Mode::Block),
Valid
);
assert_eq!(
run(TX_READ_32000001, LOCK_PAD_OVER_POLICY, Mode::Block),
over_word_budget(100_000_001)
);
}
#[test]
fn the_two_stacks_share_one_budget() {
let _heavy = heavy();
assert_eq!(
run(TX_READ_32000001, LOCK_SPLIT_AT_POLICY, Mode::Standard),
Valid
);
assert_eq!(
run(TX_READ_32000001, LOCK_SPLIT_AT_POLICY, Mode::Block),
Valid
);
}
#[test]
fn one_byte_over_the_shared_budget_on_either_stack() {
let _heavy = heavy();
for lock in [LOCK_SPLIT_OVER_SECOND, LOCK_SPLIT_OVER_FIRST] {
assert_eq!(
run(TX_READ_32000001, lock, Mode::Standard),
over_policy(100_000_001)
);
assert_eq!(
run(TX_READ_32000001, lock, Mode::Block),
over_word_budget(100_000_001)
);
}
}
#[test]
fn the_unlocking_scripts_alt_stack_stays_charged() {
let _heavy = heavy();
assert_eq!(
outcome(
TX_READ_32000001,
Some(UNLOCK_ALT_RESIDUE),
LOCK_PAD_HALF,
Mode::Standard,
None,
|_| {}
),
Valid
);
assert_eq!(
outcome(
TX_READ_32000001,
Some(UNLOCK_ALT_RESIDUE),
LOCK_PAD_HALF_PLUS_1,
Mode::Standard,
None,
|_| {}
),
over_policy(100_000_001)
);
assert_eq!(
outcome(
TX_READ_32000001,
Some(UNLOCK_ALT_RESIDUE),
LOCK_PAD_HALF_PLUS_1,
Mode::Block,
None,
|_| {}
),
over_word_budget(100_000_001)
);
}
#[test]
fn a_policy_of_zero_leaves_the_local_budget() {
assert_eq!(
outcome(
TX_READ_32000001,
None,
LOCK_PAD_OVER_POLICY,
Mode::Standard,
None,
|s| s.set_stack_memory_policy(0)
),
over_word_budget(100_000_001)
);
}
#[test]
fn an_explicit_budget_wins_under_a_word() {
let at = "5102c803807551";
let over = "5102c903807551";
for mode in [Mode::Block, Mode::Standard] {
assert_eq!(
outcome(TX_READ_32000001, None, at, mode, Some(1000), |_| {}),
Valid
);
assert_eq!(
outcome(TX_READ_32000001, None, over, mode, Some(1000), |_| {}),
Resource {
resource: ScriptResource::Stack,
limit: 1000,
attempted: 1001,
}
);
}
}
#[test]
fn a_local_budget_above_the_policy_leaves_the_verdict() {
let _heavy = heavy();
assert_eq!(
outcome(
TX_READ_32000001,
None,
LOCK_PAD_OVER_POLICY,
Mode::Standard,
Some(200_000_000),
|_| {}
),
over_policy(100_000_001)
);
assert_eq!(
outcome(
TX_READ_32000001,
None,
LOCK_PAD_OVER_POLICY,
Mode::Block,
Some(200_000_000),
|_| {}
),
Valid
);
}
#[test]
fn each_element_costs_32_bytes_beyond_its_own() {
assert_eq!(STACK_ELEMENT_OVERHEAD, 32);
assert_eq!(DEFAULT_STACK_MEMORY_USAGE_POLICY, 100_000_000);
assert_eq!(
outcome(
TX_READ_32000001,
None,
"515151",
Mode::Block,
Some(100),
|_| {}
),
Valid
);
assert_eq!(
outcome(
TX_READ_32000001,
None,
"51515151",
Mode::Block,
Some(100),
|_| {}
),
Resource {
resource: ScriptResource::Stack,
limit: 100,
attempted: 132,
}
);
assert_eq!(
outcome(
TX_READ_32000001,
None,
"51515151",
Mode::Default,
Some(100),
|_| {}
),
Valid
);
}
#[test]
fn the_alt_stack_draws_on_the_same_budget() {
let lock = "516b516b516b51";
assert_eq!(
outcome(TX_READ_32000001, None, lock, Mode::Block, Some(100), |_| {}),
Resource {
resource: ScriptResource::Stack,
limit: 100,
attempted: 132,
}
);
assert_eq!(
outcome(
TX_READ_32000001,
None,
lock,
Mode::Default,
Some(100),
|_| {}
),
Valid
);
}
#[test]
fn the_policy_is_a_verdict_on_the_mempool_path_only() {
let policy_100 = |s: &mut Spend| s.set_stack_memory_policy(100);
assert_eq!(
outcome(
TX_READ_32000001,
None,
"51515151",
Mode::Standard,
None,
policy_100
),
verdict("Stack size limit exceeded: 132 bytes, the stack memory policy is 100 bytes.")
);
assert_eq!(
outcome(
TX_READ_32000001,
None,
"51515151",
Mode::Block,
None,
policy_100
),
Valid
);
}
#[test]
fn the_alt_stack_residue_at_a_small_policy() {
let policy_100 = |s: &mut Spend| s.set_stack_memory_policy(100);
assert_eq!(
outcome(
TX_READ_32000001,
Some("516b516b"),
"51",
Mode::Standard,
None,
policy_100
),
Valid
);
assert_eq!(
outcome(
TX_READ_32000001,
Some("516b516b"),
"5151",
Mode::Standard,
None,
policy_100
),
verdict("Stack size limit exceeded: 132 bytes, the stack memory policy is 100 bytes.")
);
}
#[test]
fn the_pad_is_charged_before_the_resize() {
let policy_100 = |s: &mut Spend| s.set_stack_memory_policy(100);
for (w, expected) in [
("43", Valid),
("44", Valid),
(
"45",
verdict("Stack size limit exceeded: 101 bytes, the stack memory policy is 100 bytes."),
),
] {
let lock = format!("5101{w}807551");
assert_eq!(
outcome(
TX_READ_32000001,
None,
&lock,
Mode::Standard,
None,
policy_100
),
expected,
"OP_1 <0x{w}> OP_NUM2BIN"
);
}
}
#[test]
fn the_left_shifts_result_is_sized_exactly() {
let length_16 = |s: &mut Spend| s.set_script_num_length_policy(16);
for (count, expected) in [
("0178", Valid),
("017e", Valid),
(
"017f",
verdict("Script number overflow: 17 bytes, the limit is 16 bytes."),
),
] {
let lock = format!("51{count}b67551");
assert_eq!(
outcome(
TX_READ_32000001,
None,
&lock,
Mode::Standard,
None,
length_16
),
expected,
"OP_1 <{count}> OP_LSHIFTNUM"
);
}
}
#[test]
fn the_left_shifts_result_is_charged_before_it_is_computed() {
let tune = |s: &mut Spend| {
s.set_script_num_length_policy(1000);
s.set_stack_memory_policy(100);
};
assert_eq!(
outcome(
TX_READ_32000001,
None,
"5102e001b67551",
Mode::Standard,
None,
tune
),
Valid
);
assert_eq!(
outcome(
TX_READ_32000001,
None,
"51022002b67551",
Mode::Standard,
None,
tune
),
verdict("Stack size limit exceeded: 101 bytes, the stack memory policy is 100 bytes.")
);
}
#[test]
fn the_budgets_in_force() {
let spend = |mode: Mode, memory_limit: Option<usize>| {
let mut s = Spend::new(SpendParams {
source_txid: [0; 32],
source_output_index: 0,
source_satoshis: 1000,
locking_script: LockingScript::from_hex("51").unwrap(),
transaction_version: 2,
other_inputs: vec![],
outputs: vec![],
input_index: 0,
unlocking_script: UnlockingScript::from_hex("").unwrap(),
input_sequence: 0xffff_ffff,
lock_time: 0,
memory_limit,
});
match mode {
Mode::Block => s.set_flags(ScriptFlags::block(ProtocolEra::PostChronicle)),
Mode::Standard => s.set_flags(ScriptFlags::standard(ProtocolEra::PostChronicle)),
Mode::Default => {}
}
s
};
assert_eq!(spend(Mode::Default, None).memory_limit(), 32_000_000);
assert_eq!(spend(Mode::Block, None).memory_limit(), 100_000_000);
assert_eq!(spend(Mode::Standard, None).memory_limit(), 100_000_000);
assert_eq!(spend(Mode::Block, Some(7)).memory_limit(), 7);
assert_eq!(spend(Mode::Default, Some(7)).memory_limit(), 7);
assert_eq!(spend(Mode::Default, None).stack_memory_policy(), None);
assert_eq!(spend(Mode::Block, None).stack_memory_policy(), None);
assert_eq!(
spend(Mode::Standard, None).stack_memory_policy(),
Some(100_000_000)
);
let mut s = spend(Mode::Standard, None);
s.set_stack_memory_policy(0);
assert_eq!(s.stack_memory_policy(), None);
assert_eq!(
ScriptFlags::standard(ProtocolEra::PostChronicle).max_stack_memory_usage(5),
Some(5)
);
assert_eq!(
ScriptFlags::block(ProtocolEra::PostChronicle).max_stack_memory_usage(5),
None
);
}