use blvm_consensus::constants::MAX_STACK_SIZE;
use blvm_consensus::error::{ConsensusError, ScriptErrorCode};
use blvm_consensus::script::{self, SigVersion};
use proptest::prelude::*;
use ripemd::Ripemd160;
use sha2::{Digest, Sha256};
use blvm_consensus::opcodes::{
OP_0, OP_1, OP_1_RANGE_END, OP_1_RANGE_START, OP_2DROP, OP_ADD, OP_CHECKSIG, OP_DUP, OP_ELSE,
OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_IF, OP_MUL, OP_N_BASE, OP_RETURN,
OP_RIPEMD160, OP_SHA256, OP_SUB, OP_SWAP, OP_VERIFY,
};
fn eval(script: &[u8], stack: &mut Vec<script::StackElement>) -> Result<bool, ConsensusError> {
script::eval_script(script, stack, 0, SigVersion::Base)
}
fn script_code(result: Result<bool, ConsensusError>) -> Option<ScriptErrorCode> {
match result {
Err(ConsensusError::ScriptErrorWithCode { code, .. }) => Some(code),
_ => None,
}
}
fn numeric_opcode(n: u8) -> u8 {
if n == 0 { OP_0 } else { OP_N_BASE + n }
}
fn script_num(n: i64) -> Vec<u8> {
if n == 0 {
return Vec::new();
}
let magnitude = n.unsigned_abs() as u8;
if n < 0 {
vec![magnitude | 0x80]
} else {
vec![magnitude]
}
}
fn push_data(data: &[u8]) -> Vec<u8> {
let mut script = Vec::with_capacity(1 + data.len());
script.push(data.len() as u8);
script.extend_from_slice(data);
script
}
proptest! {
#[test]
fn prop_op_dup_duplicates(
item in prop::collection::vec(any::<u8>(), 0..50)
) {
let mut script = push_data(&item);
script.push(OP_DUP);
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
prop_assert_eq!(stack.len(), 2);
prop_assert_eq!(stack[0].as_slice(), item.as_slice());
prop_assert_eq!(stack[1].as_slice(), item.as_slice());
}
}
proptest! {
#[test]
fn prop_script_with_op_equalverify(
item1 in prop::collection::vec(any::<u8>(), 0..10),
item2 in prop::collection::vec(any::<u8>(), 0..10)
) {
let mut script = push_data(&item1);
script.extend(push_data(&item2));
script.push(OP_EQUALVERIFY);
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
if item1 == item2 {
prop_assert_eq!(result.unwrap(), true);
prop_assert!(stack.is_empty());
} else {
prop_assert_eq!(script_code(result), Some(ScriptErrorCode::EqualVerify));
}
}
}
proptest! {
#[test]
fn prop_op_hash160_fixed_length(
input in prop::collection::vec(any::<u8>(), 1..=75)
) {
let mut script = push_data(&input);
script.push(OP_HASH160);
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
let expected = Ripemd160::digest(Sha256::digest(&input));
prop_assert_eq!(stack.len(), 1);
prop_assert_eq!(stack[0].as_slice(), expected.as_slice());
}
}
proptest! {
#[test]
fn prop_op_checksig_stack_requirements(
pubkey in prop::collection::vec(any::<u8>(), 0..75),
bare in any::<bool>()
) {
let mut stack = Vec::new();
if bare {
let result = eval(&[OP_CHECKSIG], &mut stack);
prop_assert_eq!(script_code(result), Some(ScriptErrorCode::InvalidStackOperation));
} else {
let mut script = vec![OP_0];
script.extend(push_data(&pubkey));
script.push(OP_CHECKSIG);
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
prop_assert_eq!(stack.len(), 1);
prop_assert!(stack[0].is_empty());
}
}
}
proptest! {
#[test]
fn prop_op_1_to_16_values(
opcode in OP_1_RANGE_START..=OP_1_RANGE_END
) {
let mut stack = Vec::new();
let result = eval(&[opcode], &mut stack);
prop_assert_eq!(result.unwrap(), true);
prop_assert_eq!(stack.len(), 1);
prop_assert_eq!(stack[0].as_slice(), &[opcode - 0x50]);
}
}
#[test]
fn prop_op_0_pushes_empty() {
let mut stack = Vec::new();
let result = eval(&[OP_0], &mut stack);
assert_eq!(result.unwrap(), true);
assert_eq!(stack.len(), 1);
assert!(stack[0].is_empty());
}
proptest! {
#[test]
fn prop_script_with_op_if(
condition in prop::collection::vec(any::<u8>(), 0..10)
) {
let mut script = push_data(&condition);
script.extend_from_slice(&[OP_IF, OP_1, OP_ELSE, OP_1 + 1, OP_ENDIF]);
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
prop_assert_eq!(stack.len(), 1);
let taken = if script::cast_to_bool(&condition) { 1 } else { 2 };
prop_assert_eq!(stack[0].as_slice(), &[taken]);
}
}
proptest! {
#[test]
fn prop_pushdata_preserves_data(
data in prop::collection::vec(any::<u8>(), 1..=75)
) {
let script = push_data(&data);
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
prop_assert_eq!(stack.len(), 1);
prop_assert_eq!(stack[0].as_slice(), data.as_slice());
}
}
proptest! {
#[test]
fn prop_script_arithmetic_opcodes(
opcode in OP_ADD..=OP_MUL,
a in 0u8..=16u8,
b in 0u8..=16u8
) {
let script = vec![numeric_opcode(a), numeric_opcode(b), opcode];
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
if opcode == OP_MUL {
prop_assert_eq!(script_code(result), Some(ScriptErrorCode::DisabledOpcode));
} else {
let value = if opcode == OP_ADD {
i64::from(a) + i64::from(b)
} else {
i64::from(a) - i64::from(b)
};
let expected = script_num(value);
prop_assert_eq!(result.unwrap(), true);
prop_assert_eq!(stack.len(), 1);
prop_assert_eq!(stack[0].as_slice(), expected.as_slice());
}
}
}
proptest! {
#[test]
fn prop_script_op_equal(
a in 0u8..=16u8,
b in 0u8..=16u8
) {
let script = vec![numeric_opcode(a), numeric_opcode(b), OP_EQUAL];
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
let expect: &[u8] = if a == b { &[1] } else { &[] };
prop_assert_eq!(stack[0].as_slice(), expect);
}
}
proptest! {
#[test]
fn prop_script_op_verify(
value in 0u8..=16u8
) {
let script = vec![numeric_opcode(value), OP_VERIFY];
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), value != 0);
prop_assert!(stack.is_empty());
}
}
#[test]
fn prop_script_op_return_always_fails() {
let mut stack = Vec::new();
let result = eval(&[OP_1, OP_RETURN], &mut stack);
assert_eq!(result.unwrap(), false);
}
proptest! {
#[test]
fn prop_op_sha256_fixed_length(
input in prop::collection::vec(any::<u8>(), 1..=75)
) {
let mut script = push_data(&input);
script.push(OP_SHA256);
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
let expected = Sha256::digest(&input);
prop_assert_eq!(stack[0].as_slice(), expected.as_slice());
}
}
proptest! {
#[test]
fn prop_op_ripemd160_fixed_length(
input in prop::collection::vec(any::<u8>(), 1..=75)
) {
let mut script = push_data(&input);
script.push(OP_RIPEMD160);
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
let expected = Ripemd160::digest(&input);
prop_assert_eq!(stack[0].as_slice(), expected.as_slice());
}
}
proptest! {
#[test]
fn prop_stack_size_limit_enforced(
push_count in (MAX_STACK_SIZE - 2)..=(MAX_STACK_SIZE + 2)
) {
let script = vec![OP_1; push_count];
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
if push_count > MAX_STACK_SIZE {
prop_assert_eq!(script_code(result), Some(ScriptErrorCode::StackSize));
} else {
prop_assert_eq!(result.unwrap(), true);
prop_assert_eq!(stack.len(), push_count);
}
}
}
proptest! {
#[test]
fn prop_script_op_2drop(
a in 1u8..=16u8,
b in 1u8..=16u8,
both in any::<bool>()
) {
let script = if both {
vec![numeric_opcode(a), numeric_opcode(b), OP_2DROP]
} else {
vec![numeric_opcode(a), OP_2DROP]
};
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), both);
if both {
prop_assert!(stack.is_empty());
} else {
prop_assert_eq!(stack.len(), 1);
prop_assert_eq!(stack[0].as_slice(), &[a]);
}
}
}
proptest! {
#[test]
fn prop_script_op_swap(
a in 1u8..=16u8,
b in 1u8..=16u8
) {
let script = vec![numeric_opcode(a), numeric_opcode(b), OP_SWAP];
let mut stack = Vec::new();
let result = eval(&script, &mut stack);
prop_assert_eq!(result.unwrap(), true);
prop_assert_eq!(stack[0].as_slice(), &[b]);
prop_assert_eq!(stack[1].as_slice(), &[a]);
}
}