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
57 changes: 56 additions & 1 deletion ergotree-interpreter/src/eval/bool_to_sigma.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use ergotree_ir::ergo_tree::ErgoTreeVersion;
use ergotree_ir::mir::bool_to_sigma::BoolToSigmaProp;
use ergotree_ir::mir::constant::TryExtractInto;
use ergotree_ir::mir::value::Value;
Expand All @@ -16,6 +17,20 @@ impl Evaluable for BoolToSigmaProp {
ctx: &Context<'ctx>,
) -> Result<Value<'ctx>, EvalError> {
let input_v = self.input.eval(env, ctx)?;
// JVM v4.x compatibility: pre-v2 ErgoTree scripts (versions V0/V1)
// accepted a SigmaProp input to BoolToSigmaProp and passed it
// through unchanged. Mainnet contains historical transactions
// with `sigmaProp(sigmaProp(...))` (e.g. tx 5fe235558... at
// block 680,692, address Fo6oijFP2JM87ac7w) that rely on this.
// The gate is on the SCRIPT's ErgoTree header version, not the
// block's activated version: a v0 tree spent in a v3+ block
// still gets the lenient path.
// See: data/shared/src/main/scala/sigma/ast/trees.scala BoolToSigmaProp.eval
if ctx.tree_version() < ErgoTreeVersion::V2 {
if let Value::SigmaProp(sp) = input_v {
return Ok(Value::SigmaProp(sp));
}
}
let input_v_bool = input_v.try_extract_into::<bool>()?;
Ok((SigmaProp::new(SigmaBoolean::TrivialProp(input_v_bool))).into())
}
Expand All @@ -25,9 +40,12 @@ impl Evaluable for BoolToSigmaProp {
#[allow(clippy::panic)]
mod tests {
use super::*;
use crate::eval::test_util::eval_out_wo_ctx;
use crate::eval::test_util::{eval_out, eval_out_wo_ctx, try_eval_out};
use core::cell::Cell;
use ergotree_ir::chain::context::Context;
use ergotree_ir::mir::expr::Expr;
use proptest::prelude::*;
use sigma_test_util::force_any_val;

proptest! {

Expand All @@ -38,4 +56,41 @@ mod tests {
prop_assert_eq!(res, SigmaProp::new(SigmaBoolean::TrivialProp(b)));
}
}

/// Pre-v2 ErgoTree scripts: BoolToSigmaProp(SigmaProp) → passthrough.
/// Matches JVM v4.x behavior for historical mainnet scripts.
#[test]
fn eval_v0_tree_passes_sigmaprop_through() {
let inner: Expr = BoolToSigmaProp {
input: Expr::Const(true.into()).into(),
}
.into();
let outer: Expr = BoolToSigmaProp { input: inner.into() }.into();

let ctx = force_any_val::<Context>();
let ctx = Context {
tree_version: Cell::new(ErgoTreeVersion::V0),
..ctx
};
let res = eval_out::<SigmaProp>(&outer, &ctx);
assert_eq!(res, SigmaProp::new(SigmaBoolean::TrivialProp(true)));
}

/// V2+ ErgoTree scripts: BoolToSigmaProp(SigmaProp) → strict bool
/// extraction error. Matches JVM v5+ JIT behavior.
#[test]
fn eval_v2_tree_rejects_sigmaprop() {
let inner: Expr = BoolToSigmaProp {
input: Expr::Const(true.into()).into(),
}
.into();
let outer: Expr = BoolToSigmaProp { input: inner.into() }.into();

let ctx = force_any_val::<Context>();
let ctx = Context {
tree_version: Cell::new(ErgoTreeVersion::V2),
..ctx
};
assert!(try_eval_out::<SigmaProp>(&outer, &ctx).is_err());
}
}
61 changes: 58 additions & 3 deletions ergotree-interpreter/src/eval/scontext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ pub(crate) static SELF_BOX_INDEX_EVAL_FN: EvalFn = |_mc, _env, ctx, obj, _args|
obj
)));
}
// JVM bug compatibility: selfBoxIndex always returned -1 before JIT
// activation (v5.0). The bug was `eq` (reference equality) instead of
// `==` (value equality) in CostingDataContext.scala — a global impl
// bug, not per-script semantics. Fixed in v5.x for ALL scripts.
// Gate: activated_script_version (block level), NOT tree_version.
// See: https://github.com/ScorexFoundation/sigmastate-interpreter/issues/603
if ctx.activated_script_version() < ergotree_ir::ergo_tree::ErgoTreeVersion::V2 {
return Ok(Value::Int(-1));
}
let box_index = ctx
.inputs
.iter()
Expand Down Expand Up @@ -134,33 +143,79 @@ mod tests {
use ergotree_ir::mir::method_call::MethodCall;
use ergotree_ir::mir::property_call::PropertyCall;
use ergotree_ir::mir::value::Value;
use ergotree_ir::ergo_tree::ErgoTreeVersion;
use ergotree_ir::serialization::SigmaSerializable;
use ergotree_ir::types::scontext::{self, GET_VAR_FROM_INPUT_METHOD};
use ergotree_ir::types::stype::LiftIntoSType;
use ergotree_ir::types::stype_param::STypeVar;
use sigma_test_util::force_any_val;
use core::cell::Cell;

fn make_ctx_inputs_includes_self_box() -> Context<'static> {
fn make_ctx_inputs_includes_self_box(
tree_version: ErgoTreeVersion,
pre_header_version: u8,
) -> Context<'static> {
let ctx = force_any_val::<Context>();
let self_box = &*Box::leak(Box::new(force_any_val::<ErgoBox>()));
let inputs = vec![&*Box::leak(Box::new(force_any_val::<ErgoBox>())), self_box]
.try_into()
.unwrap();
let pre_header = PreHeader {
version: pre_header_version,
..ctx.pre_header.clone()
};
Context {
height: 0u32,
self_box,
inputs,
pre_header,
tree_version: Cell::new(tree_version),
..ctx
}
}

#[test]
fn eval_self_box_index() {
fn eval_self_box_index_v2_tree() {
let expr: Expr =
PropertyCall::new(Expr::Context, scontext::SELF_BOX_INDEX_PROPERTY.clone())
.unwrap()
.into();
// V2 tree in v5+ block (pre_header.version=3 → activated=V2): real index.
let context = make_ctx_inputs_includes_self_box(ErgoTreeVersion::V2, 3);
assert_eq!(eval_out::<i32>(&expr, &context), 1);
}

#[test]
fn eval_self_box_index_v0_tree_pre_v5() {
let expr: Expr =
PropertyCall::new(Expr::Context, scontext::SELF_BOX_INDEX_PROPERTY.clone())
.unwrap()
.into();
// V0 tree in pre-v5 block (pre_header.version=1 → activated=V0): -1.
let context = make_ctx_inputs_includes_self_box(ErgoTreeVersion::V0, 1);
assert_eq!(eval_out::<i32>(&expr, &context), -1);
}

#[test]
fn eval_self_box_index_v1_tree_pre_v5() {
let expr: Expr =
PropertyCall::new(Expr::Context, scontext::SELF_BOX_INDEX_PROPERTY.clone())
.unwrap()
.into();
// V1 tree in pre-v5 block (pre_header.version=1 → activated=V0): -1.
let context = make_ctx_inputs_includes_self_box(ErgoTreeVersion::V1, 1);
assert_eq!(eval_out::<i32>(&expr, &context), -1);
}

#[test]
fn eval_self_box_index_v0_tree_v5_context() {
let expr: Expr =
PropertyCall::new(Expr::Context, scontext::SELF_BOX_INDEX_PROPERTY.clone())
.unwrap()
.into();
let context = make_ctx_inputs_includes_self_box();
// V0 tree in v5+ block (pre_header.version=3 → activated=V2): real index.
// JVM bug #603 was a global impl bug fixed in v5.x for ALL scripts.
let context = make_ctx_inputs_includes_self_box(ErgoTreeVersion::V0, 3);
assert_eq!(eval_out::<i32>(&expr, &context), 1);
}

Expand Down
67 changes: 64 additions & 3 deletions ergotree-interpreter/src/eval/xor_of.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use alloc::vec::Vec;
use ergotree_ir::ergo_tree::ErgoTreeVersion;
use ergotree_ir::mir::constant::TryExtractInto;
use ergotree_ir::mir::value::Value;

Expand All @@ -16,6 +17,21 @@ impl Evaluable for XorOf {
) -> Result<Value<'ctx>, EvalError> {
let input_v = self.input.eval(env, ctx)?;
let input_v_bools = input_v.try_extract_into::<Vec<bool>>()?;
// JVM v4.x compatibility: pre-v2 ErgoTree scripts (V0/V1) computed
// xorOf as `distinct.length == 2` — true iff the collection
// contains BOTH true and false (count and order independent).
// V2+ uses the correct left-fold XOR (true iff odd number of trues).
// See: data/shared/src/main/scala/sigma/data/CSigmaDslBuilder.scala
// (xorOf method, comment "This is buggy version used in v4.x interpreter").
if ctx.tree_version() < ErgoTreeVersion::V2 {
let mut has_true = false;
let mut has_false = false;
for b in input_v_bools {
if b { has_true = true; } else { has_false = true; }
if has_true && has_false { break; }
}
return Ok((has_true && has_false).into());
}
Ok(input_v_bools.into_iter().fold(false, |a, b| a ^ b).into())
}
}
Expand All @@ -25,22 +41,67 @@ impl Evaluable for XorOf {
mod tests {
use super::*;
use crate::eval::test_util::eval_out;
use core::cell::Cell;
use ergotree_ir::chain::context::Context;
use ergotree_ir::mir::expr::Expr;
use proptest::collection;
use proptest::prelude::*;
use sigma_test_util::force_any_val;

fn ctx_with_tree_version(v: ErgoTreeVersion) -> Context<'static> {
let ctx = force_any_val::<Context>();
Context {
tree_version: Cell::new(v),
..ctx
}
}

proptest! {

#[test]
fn eval(bools in collection::vec(any::<bool>(), 0..=10)) {
fn eval_v2_tree_left_fold(bools in collection::vec(any::<bool>(), 0..=10)) {
// V2+ ErgoTree: proper XOR (true iff odd number of trues).
let expr: Expr = XorOf {input: Expr::Const(bools.clone().into()).into()}.into();
let ctx = force_any_val::<Context>();
let ctx = ctx_with_tree_version(ErgoTreeVersion::V2);
let res = eval_out::<bool>(&expr, &ctx);
// eval is true when collection has odd number of "true" values
let expected = bools.into_iter().filter(|x| *x).count() & 1 == 1;
prop_assert_eq!(res, expected);
}

#[test]
fn eval_v0_tree_distinct_length(bools in collection::vec(any::<bool>(), 0..=10)) {
// Pre-v2 ErgoTree: JVM v4.x bug — true iff coll contains
// BOTH true and false (count/order independent).
let expr: Expr = XorOf {input: Expr::Const(bools.clone().into()).into()}.into();
let ctx = ctx_with_tree_version(ErgoTreeVersion::V0);
let res = eval_out::<bool>(&expr, &ctx);
let has_true = bools.iter().any(|x| *x);
let has_false = bools.iter().any(|x| !*x);
prop_assert_eq!(res, has_true && has_false);
}
}

/// Concrete v4.x bug case: [true, true, false] is true under
/// v4.x bug (both present) but false under v5+ XOR (even count of trues).
#[test]
fn eval_v0_tree_two_trues_one_false_is_true() {
let bools = vec![true, true, false];
let expr: Expr = XorOf {
input: Expr::Const(bools.into()).into(),
}
.into();
let ctx = ctx_with_tree_version(ErgoTreeVersion::V0);
assert_eq!(eval_out::<bool>(&expr, &ctx), true);
}

#[test]
fn eval_v2_tree_two_trues_one_false_is_false() {
let bools = vec![true, true, false];
let expr: Expr = XorOf {
input: Expr::Const(bools.into()).into(),
}
.into();
let ctx = ctx_with_tree_version(ErgoTreeVersion::V2);
assert_eq!(eval_out::<bool>(&expr, &ctx), false);
}
}
Loading