use neo_devpack_solidity::runtime::{NeoRuntime, RuntimeConfig};
use proptest::prelude::*;
const TAKEN_MARKER_OP: u8 = 0x17; const NOT_TAKEN_MARKER_OP: u8 = 0x13; const RET: u8 = 0x40;
fn taken_return() -> Vec<u8> {
7i64.to_le_bytes().to_vec()
}
fn not_taken_return() -> Vec<u8> {
3i64.to_le_bytes().to_vec()
}
#[derive(Debug, Clone)]
enum Operand {
I8(i8),
I64(i64),
Bool(bool),
Bytes(Vec<u8>),
}
impl Operand {
fn emit(&self, out: &mut Vec<u8>) {
match self {
Operand::I8(v) => {
out.push(0x00); out.push(*v as u8);
}
Operand::I64(v) => {
out.push(0x03); out.extend_from_slice(&v.to_le_bytes());
}
Operand::Bool(true) => out.push(0x08), Operand::Bool(false) => out.push(0x09), Operand::Bytes(b) => {
assert!(b.len() <= u8::MAX as usize, "test PUSHDATA1 bound");
out.push(0x0C); out.push(b.len() as u8);
out.extend_from_slice(b);
}
}
}
fn eq_runtime(a: &Operand, b: &Operand) -> bool {
match (a, b) {
(Operand::I8(x), Operand::I8(y)) => (*x as i64) == (*y as i64),
(Operand::I8(x), Operand::I64(y)) => (*x as i64) == *y,
(Operand::I64(x), Operand::I8(y)) => *x == (*y as i64),
(Operand::I64(x), Operand::I64(y)) => x == y,
(Operand::Bool(x), Operand::Bool(y)) => x == y,
(Operand::Bytes(x), Operand::Bytes(y)) => x == y,
_ => false,
}
}
fn lt_runtime(a: &Operand, b: &Operand) -> Option<bool> {
match (a, b) {
(Operand::I8(x), Operand::I8(y)) => Some((*x as i64) < (*y as i64)),
(Operand::I64(x), Operand::I64(y)) => Some(x < y),
(Operand::I8(x), Operand::I64(y)) => Some((*x as i64) < *y),
(Operand::I64(x), Operand::I8(y)) => Some(*x < (*y as i64)),
(Operand::Bool(x), Operand::Bool(y)) => Some(!x & y),
(Operand::Bytes(x), Operand::Bytes(y)) => {
let xv = bytes_as_i64(x);
let yv = bytes_as_i64(y);
Some(xv < yv)
}
_ => None,
}
}
fn gt_runtime(a: &Operand, b: &Operand) -> Option<bool> {
Operand::lt_runtime(b, a)
}
}
fn bytes_as_i64(b: &[u8]) -> i64 {
let mut buf = [0u8; 8];
for (i, byte) in b.iter().take(8).enumerate() {
buf[i] = *byte;
}
i64::from_le_bytes(buf)
}
fn op_emitted_size(o: &Operand) -> usize {
match o {
Operand::I8(_) => 2,
Operand::I64(_) => 9,
Operand::Bool(_) => 1,
Operand::Bytes(b) => 2 + b.len(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Pred {
Eq,
Ne,
Gt,
Lt,
Ge,
Le,
}
const ALL_COMPARE_JUMPS: &[(u8, bool, Pred)] = &[
(0x28, false, Pred::Eq), (0x29, true, Pred::Eq), (0x2A, false, Pred::Ne), (0x2B, true, Pred::Ne), (0x2C, false, Pred::Gt), (0x2D, true, Pred::Gt), (0x2E, false, Pred::Ge), (0x2F, true, Pred::Ge), (0x30, false, Pred::Lt), (0x31, true, Pred::Lt), (0x32, false, Pred::Le), (0x33, true, Pred::Le), ];
fn predicate_holds(p: Pred, a: &Operand, b: &Operand) -> Option<bool> {
match p {
Pred::Eq => Some(Operand::eq_runtime(a, b)),
Pred::Ne => Some(!Operand::eq_runtime(a, b)),
Pred::Gt => Operand::gt_runtime(a, b),
Pred::Lt => Operand::lt_runtime(a, b),
Pred::Ge => match (Operand::gt_runtime(a, b), Operand::eq_runtime(a, b)) {
(Some(g), e) => Some(g || e),
_ => None,
},
Pred::Le => match (Operand::lt_runtime(a, b), Operand::eq_runtime(a, b)) {
(Some(l), e) => Some(l || e),
_ => None,
},
}
}
struct CompareJumpScript {
bytes: Vec<u8>,
opcode_pos: usize,
taken_pad_pos: usize,
not_taken_pad_pos: usize,
}
fn build_default_script(opcode: u8, is_long: bool, a: &Operand, b: &Operand) -> CompareJumpScript {
let mut out = Vec::new();
a.emit(&mut out);
b.emit(&mut out);
let opcode_pos = out.len();
out.push(opcode);
let offset_size: usize = if is_long { 4 } else { 1 };
let taken_distance = (1 + offset_size + 2) as i64;
if is_long {
out.extend_from_slice(&(taken_distance as i32).to_le_bytes());
} else {
out.push(taken_distance as i8 as u8);
}
let not_taken_pad_pos = out.len();
out.push(NOT_TAKEN_MARKER_OP);
out.push(RET);
let taken_pad_pos = out.len();
out.push(TAKEN_MARKER_OP);
out.push(RET);
CompareJumpScript {
bytes: out,
opcode_pos,
taken_pad_pos,
not_taken_pad_pos,
}
}
fn op_strategy_any() -> impl Strategy<Value = Operand> {
prop_oneof![
(-128i8..=127i8).prop_map(Operand::I8),
(-1_000_000i64..=1_000_000i64).prop_map(Operand::I64),
any::<bool>().prop_map(Operand::Bool),
prop::collection::vec(0u8..=255u8, 0..=8).prop_map(Operand::Bytes),
]
}
fn op_strategy_eq_biased() -> impl Strategy<Value = (Operand, Operand)> {
(op_strategy_any(), any::<bool>(), op_strategy_any()).prop_map(|(a, force_eq, b)| {
if force_eq {
(a.clone(), a)
} else {
(a, b)
}
})
}
fn op_strategy_ordered_same_kind() -> impl Strategy<Value = (Operand, Operand)> {
prop_oneof![
(-100i8..=100i8, -100i8..=100i8).prop_map(|(x, y)| (Operand::I8(x), Operand::I8(y))),
(-10_000i64..=10_000i64, -10_000i64..=10_000i64)
.prop_map(|(x, y)| (Operand::I64(x), Operand::I64(y))),
(any::<bool>(), any::<bool>()).prop_map(|(x, y)| (Operand::Bool(x), Operand::Bool(y))),
(
prop::collection::vec(0u8..=255u8, 1..=4),
prop::collection::vec(0u8..=255u8, 1..=4),
)
.prop_map(|(x, y)| (Operand::Bytes(x), Operand::Bytes(y))),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(96))]
#[test]
fn cond_jump_taken_advances_by_offset(
(a_eq, b_eq) in op_strategy_eq_biased(),
(a_ord, b_ord) in op_strategy_ordered_same_kind(),
) {
for &(opcode, is_long, pred) in ALL_COMPARE_JUMPS {
let (a, b) = match pred {
Pred::Eq => {
if !Operand::eq_runtime(&a_eq, &b_eq) {
continue;
}
(a_eq.clone(), b_eq.clone())
}
Pred::Ne => {
if Operand::eq_runtime(&a_ord, &b_ord) {
continue;
}
(a_ord.clone(), b_ord.clone())
}
Pred::Gt => {
let g = Operand::gt_runtime(&a_ord, &b_ord);
if g != Some(true) {
continue;
}
(a_ord.clone(), b_ord.clone())
}
Pred::Lt => {
let l = Operand::lt_runtime(&a_ord, &b_ord);
if l != Some(true) {
continue;
}
(a_ord.clone(), b_ord.clone())
}
Pred::Ge => {
let g = Operand::gt_runtime(&a_ord, &b_ord);
let e = Operand::eq_runtime(&a_ord, &b_ord);
if g != Some(true) && !e {
continue;
}
(a_ord.clone(), b_ord.clone())
}
Pred::Le => {
let l = Operand::lt_runtime(&a_ord, &b_ord);
let e = Operand::eq_runtime(&a_ord, &b_ord);
if l != Some(true) && !e {
continue;
}
(a_ord.clone(), b_ord.clone())
}
};
prop_assert_eq!(
predicate_holds(pred, &a, &b),
Some(true),
"oracle bug: predicate {:?} on {:?},{:?} should hold",
pred, a, b
);
let script = build_default_script(opcode, is_long, &a, &b);
let mut rt = NeoRuntime::new(RuntimeConfig::default())
.expect("runtime construction must not fail");
let res = rt.execute(&script.bytes, &[])
.expect("execute must not fail at host level (a fault would surface as Ok+success=false)");
prop_assert!(
res.success,
"opcode 0x{:02X} predicate-true: execute returned !success: {:?}",
opcode, res.exception
);
prop_assert_eq!(
&res.return_data,
&taken_return(),
"opcode 0x{:02X} (is_long={}) predicate-true on a={:?} b={:?}: \
return_data should be the TAKEN marker (PC jumped to opcode_pos+offset = {}), \
not the not-taken marker (PC fell through to {}). \
If this fires: the dispatcher's PC arithmetic for the \
taken branch is wrong — see compare.rs. \
script bytes: {:02X?}",
opcode, is_long, a, b,
script.taken_pad_pos, script.not_taken_pad_pos,
&script.bytes
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(96))]
#[test]
fn cond_jump_not_taken_advances_naturally(
(a_eq, b_eq) in op_strategy_eq_biased(),
(a_ord, b_ord) in op_strategy_ordered_same_kind(),
) {
for &(opcode, is_long, pred) in ALL_COMPARE_JUMPS {
let (a, b) = match pred {
Pred::Eq => {
if Operand::eq_runtime(&a_eq, &b_eq) {
continue;
}
(a_eq.clone(), b_eq.clone())
}
Pred::Ne => {
if !Operand::eq_runtime(&a_eq, &b_eq) {
continue;
}
(a_eq.clone(), b_eq.clone())
}
Pred::Gt => {
let g = Operand::gt_runtime(&a_ord, &b_ord);
if g != Some(false) {
continue;
}
(a_ord.clone(), b_ord.clone())
}
Pred::Lt => {
let l = Operand::lt_runtime(&a_ord, &b_ord);
if l != Some(false) {
continue;
}
(a_ord.clone(), b_ord.clone())
}
Pred::Ge => {
let g = Operand::gt_runtime(&a_ord, &b_ord);
let e = Operand::eq_runtime(&a_ord, &b_ord);
if g != Some(false) || e {
continue;
}
(a_ord.clone(), b_ord.clone())
}
Pred::Le => {
let l = Operand::lt_runtime(&a_ord, &b_ord);
let e = Operand::eq_runtime(&a_ord, &b_ord);
if l != Some(false) || e {
continue;
}
(a_ord.clone(), b_ord.clone())
}
};
prop_assert_eq!(
predicate_holds(pred, &a, &b),
Some(false),
"oracle bug: predicate {:?} on {:?},{:?} should fail",
pred, a, b
);
let script = build_default_script(opcode, is_long, &a, &b);
let mut rt = NeoRuntime::new(RuntimeConfig::default())
.expect("runtime construction must not fail");
let res = rt.execute(&script.bytes, &[])
.expect("execute must not fail at host level");
prop_assert!(
res.success,
"opcode 0x{:02X} predicate-false: execute returned !success: {:?}",
opcode, res.exception
);
prop_assert_eq!(
&res.return_data,
¬_taken_return(),
"opcode 0x{:02X} (is_long={}) predicate-false on a={:?} b={:?}: \
return_data should be the NOT-TAKEN marker (PC advanced by \
instruction length to {}), not the taken marker (PC = {}). \
If this fires: the dispatcher's natural-fallthrough PC \
advance is wrong (off-by-one or wrong immediate length). \
See compare.rs. script bytes: {:02X?}",
opcode, is_long, a, b,
script.not_taken_pad_pos, script.taken_pad_pos,
&script.bytes
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn cond_jump_long_offset_handles_full_i32_range(
(a, b) in op_strategy_eq_biased(),
) {
for &(opcode, is_long, pred) in ALL_COMPARE_JUMPS {
if !is_long {
continue;
}
let _seed = (a.clone(), b.clone());
let (oa, ob) = match pred {
Pred::Eq => (Operand::I64(42), Operand::I64(42)),
Pred::Ne => (Operand::I64(42), Operand::I64(43)),
Pred::Gt => (Operand::I64(10), Operand::I64(5)),
Pred::Lt => (Operand::I64(5), Operand::I64(10)),
Pred::Ge => (Operand::I64(7), Operand::I64(7)),
Pred::Le => (Operand::I64(7), Operand::I64(7)),
};
prop_assert_eq!(
predicate_holds(pred, &oa, &ob),
Some(true),
"oracle bug: forced-true predicate {:?} on {:?},{:?} did not hold",
pred, oa, ob
);
{
let script = build_default_script(opcode, true, &oa, &ob);
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("rt");
let res = rt.execute(&script.bytes, &[])
.expect("execute must not fail at host level");
prop_assert!(res.success);
prop_assert_eq!(&res.return_data, &taken_return(),
"opcode 0x{:02X} small +offset: expected taken marker", opcode);
}
for &extreme in &[i32::MIN, i32::MAX, -1_000_000_000i32, 1_000_000_000i32] {
let mut script = build_default_script(opcode, true, &oa, &ob);
let off_pos = script.opcode_pos + 1;
script.bytes[off_pos..off_pos + 4]
.copy_from_slice(&extreme.to_le_bytes());
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("rt");
let res = rt.execute(&script.bytes, &[]);
match res {
Err(_) => { }
Ok(r) => {
if r.success {
prop_assert_ne!(
&r.return_data,
&taken_return(),
"opcode 0x{:02X} extreme offset {}: runtime claims success \
with the TAKEN marker — that means PC arithmetic wrapped \
i32 silently (CONTROL-FLOW HIJACK). compare.rs / \
compute_offset_target must reject targets < 0 or ≥ len.",
opcode, extreme
);
prop_assert_ne!(
&r.return_data,
¬_taken_return(),
"opcode 0x{:02X} extreme offset {}: runtime claims success \
with the NOT-TAKEN marker. (Predicate held; any success \
path other than the taken pad is unexpected.)",
opcode, extreme
);
}
}
}
}
{
let mut script = build_default_script(opcode, true, &oa, &ob);
let off_pos = script.opcode_pos + 1;
script.bytes[off_pos..off_pos + 4]
.copy_from_slice(&0i32.to_le_bytes());
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("rt");
let res = rt.execute(&script.bytes, &[]);
match res {
Err(_) => {}
Ok(r) => {
prop_assert!(
!r.success,
"opcode 0x{:02X} offset=0 self-loop: expected \
non-success (gas exhaustion or fault), got success",
opcode
);
}
}
}
}
}
}
#[test]
fn smoke_jmpeq_short_taken() {
let script = vec![
0x15,
0x15,
0x28,
0x04,
NOT_TAKEN_MARKER_OP,
RET,
TAKEN_MARKER_OP,
RET,
];
let mut rt = NeoRuntime::new(RuntimeConfig::default()).unwrap();
let res = rt.execute(&script, &[]).expect("execute");
assert!(res.success, "execute failed: {:?}", res.exception);
assert_eq!(
res.return_data,
taken_return(),
"JMPEQ with equal operands must take"
);
}
#[test]
fn smoke_jmpeq_short_not_taken() {
let script = vec![
0x15,
0x17,
0x28,
0x04,
NOT_TAKEN_MARKER_OP,
RET,
TAKEN_MARKER_OP,
RET,
];
let mut rt = NeoRuntime::new(RuntimeConfig::default()).unwrap();
let res = rt.execute(&script, &[]).expect("execute");
assert!(res.success, "execute failed: {:?}", res.exception);
assert_eq!(
res.return_data,
not_taken_return(),
"JMPEQ with unequal operands must fall through"
);
}
#[test]
fn smoke_jmpeq_long_taken() {
let mut script = vec![0x15, 0x15, 0x29];
script.extend_from_slice(&7i32.to_le_bytes());
script.extend_from_slice(&[NOT_TAKEN_MARKER_OP, RET, TAKEN_MARKER_OP, RET]);
let mut rt = NeoRuntime::new(RuntimeConfig::default()).unwrap();
let res = rt.execute(&script, &[]).expect("execute");
assert!(res.success);
assert_eq!(res.return_data, taken_return());
}
#[test]
fn smoke_op_emitted_size_matches_emit_len() {
let cases: &[Operand] = &[
Operand::I8(0),
Operand::I8(-128),
Operand::I64(0),
Operand::I64(i64::MIN),
Operand::Bool(true),
Operand::Bool(false),
Operand::Bytes(Vec::new()),
Operand::Bytes(vec![1, 2, 3, 4]),
];
for c in cases {
let mut buf = Vec::new();
c.emit(&mut buf);
assert_eq!(
op_emitted_size(c),
buf.len(),
"op_emitted_size out of sync with emit() for {:?}",
c
);
}
}