diff --git a/ergotree-interpreter/src/eval/bool_to_sigma.rs b/ergotree-interpreter/src/eval/bool_to_sigma.rs index 2470d5951..8938dde0d 100644 --- a/ergotree-interpreter/src/eval/bool_to_sigma.rs +++ b/ergotree-interpreter/src/eval/bool_to_sigma.rs @@ -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; @@ -16,6 +17,20 @@ impl Evaluable for BoolToSigmaProp { ctx: &Context<'ctx>, ) -> Result, 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::()?; Ok((SigmaProp::new(SigmaBoolean::TrivialProp(input_v_bool))).into()) } @@ -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! { @@ -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::(); + let ctx = Context { + tree_version: Cell::new(ErgoTreeVersion::V0), + ..ctx + }; + let res = eval_out::(&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::(); + let ctx = Context { + tree_version: Cell::new(ErgoTreeVersion::V2), + ..ctx + }; + assert!(try_eval_out::(&outer, &ctx).is_err()); + } } diff --git a/ergotree-interpreter/src/eval/scontext.rs b/ergotree-interpreter/src/eval/scontext.rs index 190e2ceed..72f49f2ab 100644 --- a/ergotree-interpreter/src/eval/scontext.rs +++ b/ergotree-interpreter/src/eval/scontext.rs @@ -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() @@ -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::(); let self_box = &*Box::leak(Box::new(force_any_val::())); let inputs = vec![&*Box::leak(Box::new(force_any_val::())), 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::(&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::(&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::(&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::(&expr, &context), 1); } diff --git a/ergotree-interpreter/src/eval/xor_of.rs b/ergotree-interpreter/src/eval/xor_of.rs index 279d6483b..4016778bc 100644 --- a/ergotree-interpreter/src/eval/xor_of.rs +++ b/ergotree-interpreter/src/eval/xor_of.rs @@ -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; @@ -16,6 +17,21 @@ impl Evaluable for XorOf { ) -> Result, EvalError> { let input_v = self.input.eval(env, ctx)?; let input_v_bools = input_v.try_extract_into::>()?; + // 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()) } } @@ -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 { + tree_version: Cell::new(v), + ..ctx + } + } + proptest! { #[test] - fn eval(bools in collection::vec(any::(), 0..=10)) { + fn eval_v2_tree_left_fold(bools in collection::vec(any::(), 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::(); + let ctx = ctx_with_tree_version(ErgoTreeVersion::V2); let res = eval_out::(&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::(), 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::(&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::(&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::(&expr, &ctx), false); } }