diff --git a/ergotree-interpreter/src/eval.rs b/ergotree-interpreter/src/eval.rs index 356a48261..38a38945f 100644 --- a/ergotree-interpreter/src/eval.rs +++ b/ergotree-interpreter/src/eval.rs @@ -259,6 +259,11 @@ fn smethod_eval_fn(method: &SMethod) -> Result { }, sbox::TYPE_CODE => match method.method_id() { sbox::VALUE_METHOD_ID => self::sbox::VALUE_EVAL_FN, + sbox::PROPOSITION_BYTES_METHOD_ID => self::sbox::PROPOSITION_BYTES_EVAL_FN, + sbox::BYTES_METHOD_ID => self::sbox::BYTES_EVAL_FN, + sbox::BYTES_WITHOUT_REF_METHOD_ID => self::sbox::BYTES_WITHOUT_REF_EVAL_FN, + sbox::ID_METHOD_ID => self::sbox::ID_EVAL_FN, + sbox::CREATION_INFO_METHOD_ID => self::sbox::CREATION_INFO_EVAL_FN, sbox::GET_REG_METHOD_ID => self::sbox::GET_REG_EVAL_FN, sbox::TOKENS_METHOD_ID => self::sbox::TOKENS_EVAL_FN, method_id => { diff --git a/ergotree-interpreter/src/eval/sbox.rs b/ergotree-interpreter/src/eval/sbox.rs index 1e595029a..b713d6e12 100644 --- a/ergotree-interpreter/src/eval/sbox.rs +++ b/ergotree-interpreter/src/eval/sbox.rs @@ -2,11 +2,13 @@ use crate::eval::EvalError; use alloc::boxed::Box; use alloc::string::ToString; +use alloc::vec::Vec; use ergotree_ir::chain::ergo_box::ErgoBox; use ergotree_ir::ergo_tree::ErgoTreeVersion; use ergotree_ir::mir::constant::TryExtractInto; use ergotree_ir::mir::value::Value; use ergotree_ir::reference::Ref; +use ergotree_ir::serialization::SigmaSerializable; use ergotree_ir::types::stype::SType; use super::EvalFn; @@ -75,6 +77,45 @@ pub(crate) static TOKENS_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| { Ok(res) }; +// The accessor method-forms (PropertyCall 99:2..6) dispatch through the same box +// accessors as their op-form twins (ExtractScriptBytes/ExtractBytes/ExtractBytesWithNoRef/ +// ExtractId/ExtractCreationInfo), so a hand-crafted `PropertyCall(99,N)` tree evaluates +// (JVM `MethodCall.eval` -> `invokeFixed`, methods.scala SBoxMethods, present from +// v5Methods with no version gate) instead of erroring. + +pub(crate) static PROPOSITION_BYTES_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| { + Ok(obj + .try_extract_into::>()? + .script_bytes()? + .into()) +}; + +pub(crate) static BYTES_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| { + Ok(obj + .try_extract_into::>()? + .sigma_serialize_bytes()? + .into()) +}; + +pub(crate) static BYTES_WITHOUT_REF_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| { + Ok(obj + .try_extract_into::>()? + .bytes_without_ref()? + .into()) +}; + +pub(crate) static ID_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| { + let bytes: Vec = obj.try_extract_into::>()?.box_id().into(); + Ok(bytes.into()) +}; + +pub(crate) static CREATION_INFO_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| { + Ok(obj + .try_extract_into::>()? + .creation_info() + .into()) +}; + #[allow(clippy::unwrap_used)] #[allow(clippy::panic)] #[cfg(test)] @@ -116,6 +157,48 @@ mod tests { ); } + // The accessor method-forms (PropertyCall 99:2..6) must dispatch (instead of erroring) + // and return the same value as their op-form twins + // (ExtractScriptBytes/ExtractBytes/ExtractBytesWithNoRef/ExtractId/ExtractCreationInfo) — + // the JVM evaluates them via `MethodCall.eval`'s `invokeFixed` reflection over + // `commonBoxMethods` (methods.scala SBoxMethods), present from v5Methods with no gate. + #[test] + fn eval_box_accessor_method_forms() { + use ergotree_ir::serialization::SigmaSerializable; + use sigma_util::AsVecI8; + + let ctx = force_any_val::(); + let pc = |m: &ergotree_ir::types::smethod::SMethod| -> Expr { + PropertyCall::new(GlobalVars::SelfBox.into(), m.clone()) + .unwrap() + .into() + }; + + // propositionBytes (99:2) == ExtractScriptBytes + assert_eq!( + eval_out::>(&pc(&sbox::PROPOSITION_BYTES_METHOD), &ctx), + ctx.self_box.script_bytes().unwrap() + ); + // bytes (99:3) == ExtractBytes (the box's serialized content on this branch) + assert_eq!( + eval_out::>(&pc(&sbox::BYTES_METHOD), &ctx), + ctx.self_box.sigma_serialize_bytes().unwrap().as_vec_i8() + ); + // bytesWithoutRef (99:4) == ExtractBytesWithNoRef + assert_eq!( + eval_out::>(&pc(&sbox::BYTES_WITHOUT_REF_METHOD), &ctx), + ctx.self_box.bytes_without_ref().unwrap() + ); + // id (99:5) == ExtractId + let id: Vec = ctx.self_box.box_id().into(); + assert_eq!(eval_out::>(&pc(&sbox::ID_METHOD), &ctx), id); + // creationInfo (99:6) == ExtractCreationInfo + assert_eq!( + eval_out::<(i32, Vec)>(&pc(&sbox::CREATION_INFO_METHOD), &ctx), + ctx.self_box.creation_info() + ); + } + #[test] fn eval_reg_out() { let type_args = std::iter::once((STypeVar::t(), SType::SLong)).collect(); diff --git a/ergotree-ir/src/types/sbox.rs b/ergotree-ir/src/types/sbox.rs index fc9117aa1..dfecd8bcd 100644 --- a/ergotree-ir/src/types/sbox.rs +++ b/ergotree-ir/src/types/sbox.rs @@ -21,6 +21,16 @@ 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.propositionBytes property (JVM `PropositionBytesMethod`, op-form `ExtractScriptBytes`) +pub const PROPOSITION_BYTES_METHOD_ID: MethodId = MethodId(2); +/// Box.bytes property (JVM `BytesMethod`, op-form `ExtractBytes`) +pub const BYTES_METHOD_ID: MethodId = MethodId(3); +/// Box.bytesWithoutRef property (JVM `BytesWithoutRefMethod`, op-form `ExtractBytesWithNoRef`) +pub const BYTES_WITHOUT_REF_METHOD_ID: MethodId = MethodId(4); +/// Box.id property (JVM `IdMethod`, op-form `ExtractId`) +pub const ID_METHOD_ID: MethodId = MethodId(5); +/// Box.creationInfo property (JVM `creationInfoMethod`, op-form `ExtractCreationInfo`) +pub const CREATION_INFO_METHOD_ID: MethodId = MethodId(6); /// Box.Rx property pub const GET_REG_METHOD_ID: MethodId = MethodId(7); /// Box.tokens property @@ -32,6 +42,11 @@ lazy_static! { vec![ GET_REG_METHOD_DESC.clone(), VALUE_METHOD_DESC.clone(), + PROPOSITION_BYTES_METHOD_DESC.clone(), + BYTES_METHOD_DESC.clone(), + BYTES_WITHOUT_REF_METHOD_DESC.clone(), + ID_METHOD_DESC.clone(), + CREATION_INFO_METHOD_DESC.clone(), TOKENS_METHOD_DESC.clone() ] ; @@ -53,6 +68,100 @@ lazy_static! { pub static ref VALUE_METHOD: SMethod = SMethod::new(STypeCompanion::Box, VALUE_METHOD_DESC.clone(),); } +// The byte-array accessor method-forms (propositionBytes / bytes / bytesWithoutRef / id) +// mirror the JVM `commonBoxMethods` entries (methods.scala, SBoxMethods): each is a zero-arg +// PropertyCall `SFunc(SBox, Coll[Byte])`, present from v5Methods with NO version gate, evaluated +// JVM-side via `MethodCall.eval`'s `invokeFixed` reflection over the op-form's `costKind`. +lazy_static! { + static ref PROPOSITION_BYTES_METHOD_DESC: SMethodDesc = SMethodDesc { + method_id: PROPOSITION_BYTES_METHOD_ID, + name: "propositionBytes", + tpe: SFunc { + t_dom: vec![SType::SBox], + t_range: Box::new(SType::SColl(SType::SByte.into())), + tpe_params: vec![], + }, + explicit_type_args: vec![], + min_version: ErgoTreeVersion::V0 + }; + /// Box.propositionBytes + pub static ref PROPOSITION_BYTES_METHOD: SMethod = + SMethod::new(STypeCompanion::Box, PROPOSITION_BYTES_METHOD_DESC.clone(),); +} + +lazy_static! { + static ref BYTES_METHOD_DESC: SMethodDesc = SMethodDesc { + method_id: BYTES_METHOD_ID, + name: "bytes", + tpe: SFunc { + t_dom: vec![SType::SBox], + t_range: Box::new(SType::SColl(SType::SByte.into())), + tpe_params: vec![], + }, + explicit_type_args: vec![], + min_version: ErgoTreeVersion::V0 + }; + /// Box.bytes + pub static ref BYTES_METHOD: SMethod = + SMethod::new(STypeCompanion::Box, BYTES_METHOD_DESC.clone(),); +} + +lazy_static! { + static ref BYTES_WITHOUT_REF_METHOD_DESC: SMethodDesc = SMethodDesc { + method_id: BYTES_WITHOUT_REF_METHOD_ID, + name: "bytesWithoutRef", + tpe: SFunc { + t_dom: vec![SType::SBox], + t_range: Box::new(SType::SColl(SType::SByte.into())), + tpe_params: vec![], + }, + explicit_type_args: vec![], + min_version: ErgoTreeVersion::V0 + }; + /// Box.bytesWithoutRef + pub static ref BYTES_WITHOUT_REF_METHOD: SMethod = + SMethod::new(STypeCompanion::Box, BYTES_WITHOUT_REF_METHOD_DESC.clone(),); +} + +lazy_static! { + static ref ID_METHOD_DESC: SMethodDesc = SMethodDesc { + method_id: ID_METHOD_ID, + name: "id", + tpe: SFunc { + t_dom: vec![SType::SBox], + t_range: Box::new(SType::SColl(SType::SByte.into())), + tpe_params: vec![], + }, + explicit_type_args: vec![], + min_version: ErgoTreeVersion::V0 + }; + /// Box.id + pub static ref ID_METHOD: SMethod = + SMethod::new(STypeCompanion::Box, ID_METHOD_DESC.clone(),); +} + +// creationInfo returns `(Int, Coll[Byte])` — the tx block height paired with the serialized +// transaction id followed by the box's output index (JVM `ExtractCreationInfo.OpType`). +lazy_static! { + static ref CREATION_INFO_METHOD_DESC: SMethodDesc = SMethodDesc { + method_id: CREATION_INFO_METHOD_ID, + name: "creationInfo", + tpe: SFunc { + t_dom: vec![SType::SBox], + t_range: Box::new(SType::STuple(STuple::pair( + SType::SInt, + SType::SColl(SType::SByte.into()) + ))), + tpe_params: vec![], + }, + explicit_type_args: vec![], + min_version: ErgoTreeVersion::V0 + }; + /// Box.creationInfo + pub static ref CREATION_INFO_METHOD: SMethod = + SMethod::new(STypeCompanion::Box, CREATION_INFO_METHOD_DESC.clone(),); +} + lazy_static! { static ref GET_REG_METHOD_DESC: SMethodDesc = SMethodDesc { method_id: GET_REG_METHOD_ID, @@ -104,6 +213,20 @@ mod tests { #[test] 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, PROPOSITION_BYTES_METHOD_ID).map(|e| e.name()) + == Ok("propositionBytes") + ); + assert!(SMethod::from_ids(TYPE_CODE, BYTES_METHOD_ID).map(|e| e.name()) == Ok("bytes")); + assert!( + SMethod::from_ids(TYPE_CODE, BYTES_WITHOUT_REF_METHOD_ID).map(|e| e.name()) + == Ok("bytesWithoutRef") + ); + assert!(SMethod::from_ids(TYPE_CODE, ID_METHOD_ID).map(|e| e.name()) == Ok("id")); + assert!( + SMethod::from_ids(TYPE_CODE, CREATION_INFO_METHOD_ID).map(|e| e.name()) + == Ok("creationInfo") + ); assert!(SMethod::from_ids(TYPE_CODE, GET_REG_METHOD_ID).map(|e| e.name()) == Ok("getReg")); assert!(SMethod::from_ids(TYPE_CODE, TOKENS_METHOD_ID).map(|e| e.name()) == Ok("tokens")); }