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
141 changes: 108 additions & 33 deletions ergotree-interpreter/src/eval/sbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::eval::EvalError;
use alloc::boxed::Box;
use alloc::string::ToString;
use ergotree_ir::chain::ergo_box::ErgoBox;
use ergotree_ir::chain::ergo_box::RegisterId;
use ergotree_ir::ergo_tree::ErgoTreeVersion;
use ergotree_ir::mir::constant::TryExtractInto;
use ergotree_ir::mir::value::Value;
Expand All @@ -24,31 +25,28 @@ pub(crate) static GET_REG_EVAL_FN: EvalFn = |mc, _env, ctx, obj, args| {
activated_version: ctx.tree_version(),
});
}
#[allow(clippy::unwrap_used)]
let reg_id: i8 = args
let reg_idx = args
.first()
.cloned()
.ok_or_else(|| EvalError::NotFound("register index is missing".to_string()))?
.try_extract_into::<i32>()?
.try_into()
.map_err(|e| {
EvalError::RegisterIdOutOfBounds(format!("register index is out of bounds: {:?} ", e))
})?;
let reg_id = reg_id.try_into().map_err(|e| {
EvalError::RegisterIdOutOfBounds(format!(
"register index {reg_id} is out of bounds: {:?} ",
e
))
})?;

let reg_val_opt = obj
.try_extract_into::<Ref<'_, ErgoBox>>()?
.get_register(reg_id)
.map_err(|e| {
EvalError::NotFound(format!(
"Error getting the register id {reg_id} with error {e:?}"
))
})?;
.try_extract_into::<i32>()?;
// Mirror JVM CBox.getReg: an out-of-range index (negative or >= maxRegisters)
// yields None rather than an error; only a present register of the wrong type
// errors below.
let reg_id = i8::try_from(reg_idx)
.ok()
.and_then(|id| RegisterId::try_from(id).ok());
let reg_val_opt = match reg_id {
Some(reg_id) => obj
.try_extract_into::<Ref<'_, ErgoBox>>()?
.get_register(reg_id)
.map_err(|e| {
EvalError::NotFound(format!(
"Error getting the register id {reg_id} with error {e:?}"
))
})?,
None => None,
};
// Return type of getReg[T] is always Option[T]
#[allow(clippy::unreachable)]
let SType::SOption(expected_type) = &*mc.tpe().t_range
Expand All @@ -60,7 +58,7 @@ pub(crate) static GET_REG_EVAL_FN: EvalFn = |mc, _env, ctx, obj, args| {
Ok(Value::Opt(Some(Box::new(constant.v.into()))))
}
Some(constant) => Err(EvalError::UnexpectedValue(format!(
"Expected register {reg_id} to be of type {}, got {}",
"Expected register {reg_idx} to be of type {}, got {}",
expected_type, constant.tpe
))),
None => Ok(Value::Opt(None)),
Expand All @@ -80,12 +78,16 @@ pub(crate) static TOKENS_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| {
#[cfg(test)]
#[cfg(feature = "arbitrary")]
mod tests {
use ergotree_ir::ergo_tree::ErgoTreeVersion;
use ergotree_ir::chain::context_extension::ContextExtension;
use ergotree_ir::chain::ergo_box::ErgoBox;
use ergotree_ir::ergo_tree::{ErgoTree, ErgoTreeVersion};
use ergotree_ir::mir::constant::Constant;
use ergotree_ir::mir::expr::Expr;
use ergotree_ir::mir::global_vars::GlobalVars;
use ergotree_ir::mir::method_call::MethodCall;
use ergotree_ir::mir::property_call::PropertyCall;
use ergotree_ir::mir::value::Value;
use ergotree_ir::serialization::SigmaSerializable;
use ergotree_ir::types::sbox;
use ergotree_ir::types::stype::SType;
use ergotree_ir::types::stype_param::STypeVar;
Expand All @@ -95,6 +97,36 @@ mod tests {
use crate::eval::EvalError;
use ergotree_ir::chain::context::Context;

// The vector trees carry arbitrary-typed roots (eval-tier corpus); the sized
// parse path rejects non-SigmaProp roots, so clear the size bit and drop the
// size byte to route through the non-sized path — the same leniency the
// conformance runner applies.
fn parse_tree_lenient(hex: &str) -> Expr {
let bytes: Vec<u8> = (0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
.collect();
let mut lenient = Vec::with_capacity(bytes.len() - 1);
lenient.push(bytes[0] & !0x08);
lenient.extend_from_slice(&bytes[2..]); // size VLQ is 1 byte here
let tree = ErgoTree::sigma_parse_bytes(&lenient).unwrap();
tree.proposition().unwrap()
}

// SELF with exactly R4 = Long(7) and ContextExtension var 1 = `idx` — the
// setup of the blessed `Box.getReg_dynamic_index` vectors.
fn ctx_with_r4_long7_and_var1(idx: i32) -> Context<'static> {
let b = force_any_val::<ErgoBox>()
.with_additional_registers(vec![Constant::from(7i64)].try_into().unwrap());
let mut ext = ContextExtension::empty();
ext.values.insert(1u8, Constant::from(idx));
Context {
self_box: Box::leak(Box::new(b)),
extension: Box::leak(Box::new(ext)),
..force_any_val::<Context>()
}
}

#[test]
fn eval_box_value() {
let expr: Expr = PropertyCall::new(GlobalVars::SelfBox.into(), sbox::VALUE_METHOD.clone())
Expand Down Expand Up @@ -157,17 +189,13 @@ mod tests {
.into();
let ctx = force_any_val::<Context>();
(0..ErgoTreeVersion::V3.into()).for_each(|version| {
// Pre-V3 the method id is not in the method table at all (min_version),
// so the versioned roundtrip fails at deserialization — mirroring JVM
// v5Methods, which has no getReg (id 19) entry.
let res = try_eval_out_with_version::<Option<i64>>(&expr, &ctx, version, version);
match res {
Err(EvalError::Spanned(err))
if matches!(
*err.error,
EvalError::ScriptVersionError {
required_version: ErgoTreeVersion::V3,
activated_version: _
}
) => {}
_ => panic!("Expected script version error"),
Err(EvalError::SigmaParsingError(_)) => {}
_ => panic!("Expected method-id parsing rejection, got {:?}", res),
}
});
(ErgoTreeVersion::V3.into()..=ErgoTreeVersion::MAX_SCRIPT_VERSION.into()).for_each(
Expand All @@ -179,4 +207,51 @@ mod tests {
},
);
}

// End-to-end over the blessed `Box.getReg_dynamic_index` vector trees
// (sigma-state 6.0.3): `{ SELF.getReg[Long](getVar[Int](1).get) }` with
// SELF carrying only R4 = Long(7). JVM CBox.getReg yields None for an
// absent or out-of-range index; only a present register of the wrong
// type errors.
#[test]
fn eval_reg_dynamic_index_absent_or_out_of_range_is_none() {
const GET_REG_LONG: &str = "1b0b00dc6313a701e4e3010405";
let run = |idx: i32| -> Option<i64> {
let expr = parse_tree_lenient(GET_REG_LONG);
let ctx = ctx_with_r4_long7_and_var1(idx);
try_eval_out_with_version::<Option<i64>>(&expr, &ctx, 3, 3).unwrap()
};
assert_eq!(run(4), Some(7), "present register of matching type");
assert_eq!(run(5), None, "absent register R5");
assert_eq!(run(10), None, "index beyond R9");
assert_eq!(run(-1), None, "negative index");
assert_eq!(run(1_000_000), None, "index beyond i8 range");

// The wrong-type boundary stays an error: getReg[Int] over the Long R4.
const GET_REG_INT: &str = "1b0b00dc6313a701e4e3010404";
let expr = parse_tree_lenient(GET_REG_INT);
let ctx = ctx_with_r4_long7_and_var1(4);
assert!(
try_eval_out_with_version::<Option<i32>>(&expr, &ctx, 3, 3).is_err(),
"present register of the wrong type must error"
);
}

// getRegV5 (method id 7) deserializes but has no eval — mirroring the JVM,
// where getRegMethodV5's reflective lookup fails. A live occurrence errors;
// a dead-branch occurrence leaves the script evaluable. Trees are the
// blessed `Box.getReg_adversarial` vectors.
#[test]
fn eval_getregv5_parses_but_does_not_eval() {
// { SELF.getRegV5(getVar[Int](1).get) } — live, must error at eval.
let expr = parse_tree_lenient("1b0a00dc6307a701e4e30104");
let ctx = ctx_with_r4_long7_and_var1(4);
assert!(try_eval_out_with_version::<Value>(&expr, &ctx, 3, 3).is_err());

// { if (true) true else SELF.getRegV5(getVar[Int](1).get).isDefined }
// — dead branch, the tree must parse and evaluate to true.
let expr = parse_tree_lenient("1b1402010101019573007301e6dc6307a701e4e30104");
let ctx = ctx_with_r4_long7_and_var1(4);
assert!(try_eval_out_with_version::<bool>(&expr, &ctx, 3, 3).unwrap());
}
}
62 changes: 53 additions & 9 deletions ergotree-ir/src/types/sbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ pub const TYPE_CODE: TypeCode = TypeCode::SBOX;
pub static TYPE_NAME: &str = "Box";
/// Box.value property
pub const VALUE_METHOD_ID: MethodId = MethodId(1);
/// Box.Rx property
pub const GET_REG_METHOD_ID: MethodId = MethodId(7);
/// Box.getReg[T](index) — the v6 dynamic register access method (JVM `getRegMethodV6`)
pub const GET_REG_METHOD_ID: MethodId = MethodId(19);
/// Box.getRegV5 — the legacy v5-era method id. Mirrors JVM `getRegMethodV5`: trees
/// carrying it deserialize at any version, but there is no eval for it (on the JVM
/// its reflective lookup fails), so a live occurrence errors at evaluation.
pub const GET_REG_V5_METHOD_ID: MethodId = MethodId(7);
/// Box.tokens property
pub const TOKENS_METHOD_ID: MethodId = MethodId(8);

Expand All @@ -31,6 +35,7 @@ lazy_static! {
pub(crate) static ref METHOD_DESC: Vec<SMethodDesc> =
vec![
GET_REG_METHOD_DESC.clone(),
GET_REG_V5_METHOD_DESC.clone(),
VALUE_METHOD_DESC.clone(),
TOKENS_METHOD_DESC.clone()
]
Expand Down Expand Up @@ -63,13 +68,31 @@ lazy_static! {
tpe_params: vec![],
},
explicit_type_args: vec![STypeVar::t()],
min_version: ErgoTreeVersion::V0
min_version: ErgoTreeVersion::V3
};
/// Box.getReg
pub static ref GET_REG_METHOD: SMethod =
SMethod::new(STypeCompanion::Box, GET_REG_METHOD_DESC.clone(),);
}

lazy_static! {
// Unlike getReg, getRegV5 carries no explicit type args on the wire
// (JVM getRegMethodV5 has no `Seq(tT)`), so its serialized form ends
// after the args. The unresolved T in t_range is fine: the node only
// ever deserializes (dead-branch occurrences) and is rejected at eval.
static ref GET_REG_V5_METHOD_DESC: SMethodDesc = SMethodDesc {
method_id: GET_REG_V5_METHOD_ID,
name: "getRegV5",
tpe: SFunc {
t_dom: vec![SType::SBox, SType::SInt],
t_range: SType::SOption(Arc::new(STypeVar::t().into())).into(),
tpe_params: vec![],
},
explicit_type_args: vec![],
min_version: ErgoTreeVersion::V0
};
}

lazy_static! {
static ref TOKENS_METHOD_DESC: SMethodDesc = SMethodDesc {
method_id: TOKENS_METHOD_ID,
Expand All @@ -92,11 +115,12 @@ lazy_static! {
}

#[allow(clippy::unwrap_used)]
#[allow(clippy::panic)]
#[cfg(test)]
mod tests {
use crate::{
mir::{constant::Constant, global_vars::GlobalVars, method_call::MethodCall},
serialization::SigmaSerializable,
mir::{constant::Constant, expr::Expr, global_vars::GlobalVars, method_call::MethodCall},
serialization::{roundtrip_new_feature, SigmaSerializable},
};

use super::*;
Expand All @@ -105,9 +129,14 @@ mod tests {
fn test_from_ids() {
assert!(SMethod::from_ids(TYPE_CODE, VALUE_METHOD_ID).map(|e| e.name()) == Ok("value"));
assert!(SMethod::from_ids(TYPE_CODE, GET_REG_METHOD_ID).map(|e| e.name()) == Ok("getReg"));
assert!(
SMethod::from_ids(TYPE_CODE, GET_REG_V5_METHOD_ID).map(|e| e.name()) == Ok("getRegV5")
);
assert!(SMethod::from_ids(TYPE_CODE, TOKENS_METHOD_ID).map(|e| e.name()) == Ok("tokens"));
}

// getReg is v6-only (method id 19): rejected when parsing v0-v2 trees,
// round-trips from v3 on — mirroring JVM SBoxMethods.v6Methods gating.
#[test]
fn test_getreg_serialization_roundtrip() {
let type_args = core::iter::once((STypeVar::t(), SType::SInt)).collect();
Expand All @@ -118,9 +147,24 @@ mod tests {
type_args,
)
.unwrap();
assert_eq!(
MethodCall::sigma_parse_bytes(&mc.sigma_serialize_bytes().unwrap()).unwrap(),
mc
);
roundtrip_new_feature(&mc, ErgoTreeVersion::V3);
}

// getRegV5 (method id 7) deserializes at any version and carries NO trailing
// explicit-type-arg byte — the wire shape JVM getRegMethodV5 produces. Byte-exact
// roundtrip guards both directions. Bytes are the root expr of the blessed
// adversarial vector `{ SELF.getRegV5(getVar[Int](1).get) }`:
// dc=MethodCall 63=SBox 07=methodId a7=SELF 01=argc e4=OptionGet e3 01 04=getVar[Int](1)
#[test]
fn test_getregv5_parses_without_type_args() {
let bytes: Vec<u8> = vec![0xdc, 0x63, 0x07, 0xa7, 0x01, 0xe4, 0xe3, 0x01, 0x04];
let expr = Expr::sigma_parse_bytes(&bytes).unwrap();
let Expr::MethodCall(mc) = &expr else {
panic!("expected MethodCall, got {:?}", expr)
};
assert_eq!(mc.expr.method.method_id(), GET_REG_V5_METHOD_ID);
assert_eq!(mc.expr.method.name(), "getRegV5");
assert!(mc.expr.explicit_type_args.is_empty());
assert_eq!(expr.sigma_serialize_bytes().unwrap(), bytes);
}
}
Loading