From 4ea92494bd956282c831d5890b2191f68885d9a7 Mon Sep 17 00:00:00 2001 From: Muad'Dib Date: Wed, 10 Jun 2026 21:48:00 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20deserialize=20LastBlockUtxoRootHash=20(0?= =?UTF-8?q?xa6)=20=E2=80=94=20the=20bare=20context-root=20op=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JVM serializes `LastBlockUtxoRootHash` as a dedicated case-object op code (`OpCodes.LastBlockUtxoRootHashCode` = 54, registered in `ValueSerializer.scala`); the Expr deserializer had no arm for it, so a consensus-legal tree carrying the op form failed to parse where the JVM evaluates (the sibling context globals — e.g. MinerPubkey, 0xac — already dispatch). The new `GlobalVars::LastBlockUtxoRootHash` mirrors the wiring of the other context globals; its eval matches the existing `CONTEXT.LastBlockUtxoRootHash` PropertyCall eval (`values.scala:1490`, `E.context.LastBlockUtxoRootHash`) so the two wire forms of the property agree. Co-Authored-By: Claude Fable 5 --- ergotree-interpreter/src/eval/global_vars.rs | 51 ++++++++++++++++++++ ergotree-ir/src/mir/global_vars.rs | 6 +++ ergotree-ir/src/serialization/expr.rs | 3 ++ ergotree-ir/src/serialization/global_vars.rs | 13 +++++ 4 files changed, 73 insertions(+) diff --git a/ergotree-interpreter/src/eval/global_vars.rs b/ergotree-interpreter/src/eval/global_vars.rs index ea8031ab2..85bf48c94 100644 --- a/ergotree-interpreter/src/eval/global_vars.rs +++ b/ergotree-interpreter/src/eval/global_vars.rs @@ -1,5 +1,8 @@ use crate::eval::Env; +use alloc::boxed::Box; use alloc::vec::Vec; +use ergotree_ir::mir::avl_tree_data::AvlTreeData; +use ergotree_ir::mir::avl_tree_data::AvlTreeFlags; use ergotree_ir::mir::global_vars::GlobalVars; use ergotree_ir::mir::value::Value; use ergotree_ir::reference::Ref; @@ -27,6 +30,19 @@ impl Evaluable for GlobalVars { .collect::>() .into()), GlobalVars::MinerPubKey => Ok(ctx.pre_header.miner_pk.sigma_serialize_bytes()?.into()), + GlobalVars::LastBlockUtxoRootHash => { + // Same derivation as the `Context.LastBlockUtxoRootHash` property eval + // (`LAST_BLOCK_UTXO_ROOT_HASH_EVAL_FN`) — the two wire forms of the + // property must agree. + let digest = ctx.headers[0].state_root; + let tree_flags = AvlTreeFlags::new(true, true, true); + Ok(Value::AvlTree(Box::from(AvlTreeData { + digest, + tree_flags, + key_length: 32, + value_length_opt: None, + }))) + } GlobalVars::GroupGenerator => Ok(ergo_chain_types::ec_point::generator().into()), } } @@ -43,6 +59,10 @@ mod tests { use ergoscript_compiler::script_env::ScriptEnv; use ergotree_ir::chain::context::Context; use ergotree_ir::chain::ergo_box::ErgoBox; + use ergotree_ir::ergo_tree::ErgoTree; + use ergotree_ir::mir::expr::Expr; + use ergotree_ir::mir::property_call::PropertyCall; + use ergotree_ir::types::scontext; use sigma_test_util::force_any_val; use super::*; @@ -91,4 +111,35 @@ mod tests { ergo_chain_types::ec_point::generator() ); } + + #[test] + fn eval_last_block_utxo_root_hash_op_form_blessed_bytes() { + // `10 00 a6` — a v0 constant-segregated tree whose root is the bare + // dedicated op code 0xa6 (`LastBlockUtxoRootHash`), the way the JVM + // serializes the op form (`Context.op_forms.json :: + // lastblockutxoroothash-opform`). The op form and the + // `CONTEXT.LastBlockUtxoRootHash` PropertyCall form must evaluate to + // the same AvlTree. + let ctx = force_any_val::(); + let tree = ErgoTree::sigma_parse_bytes(&[0x10, 0x00, 0xa6]).unwrap(); + let expr = tree.proposition().unwrap(); + let expected = AvlTreeData { + digest: ctx.headers[0].state_root, + tree_flags: AvlTreeFlags::new(true, true, true), + key_length: 32, + value_length_opt: None, + }; + assert_eq!(eval_out::(&expr, &ctx), expected); + + let property_form: Expr = PropertyCall::new( + Expr::Context, + scontext::LAST_BLOCK_UTXO_ROOT_HASH_PROPERTY.clone(), + ) + .unwrap() + .into(); + assert_eq!( + eval_out::(&property_form, &ctx), + eval_out::(&expr, &ctx) + ); + } } diff --git a/ergotree-ir/src/mir/global_vars.rs b/ergotree-ir/src/mir/global_vars.rs index 8335a3f20..4a6e3591c 100644 --- a/ergotree-ir/src/mir/global_vars.rs +++ b/ergotree-ir/src/mir/global_vars.rs @@ -23,6 +23,8 @@ pub enum GlobalVars { SelfBox, /// When interpreted evaluates to a ByteArrayConstant built from Context.minerPubkey MinerPubKey, + /// When interpreted evaluates to a AvlTreeConstant built from Context.lastBlockUtxoRoot + LastBlockUtxoRootHash, /// GroupElement (EcPoint) generator GroupGenerator, } @@ -36,6 +38,7 @@ impl GlobalVars { GlobalVars::Height => SType::SInt, GlobalVars::SelfBox => SType::SBox, GlobalVars::MinerPubKey => SType::SColl(Arc::new(SType::SByte)), + GlobalVars::LastBlockUtxoRootHash => SType::SAvlTree, GlobalVars::GroupGenerator => SType::SGroupElement, } } @@ -50,6 +53,7 @@ impl HasOpCode for GlobalVars { GlobalVars::Outputs => OpCode::OUTPUTS, GlobalVars::Height => OpCode::HEIGHT, GlobalVars::MinerPubKey => OpCode::MINER_PUBKEY, + GlobalVars::LastBlockUtxoRootHash => OpCode::LAST_BLOCK_UTXO_ROOT_HASH, GlobalVars::GroupGenerator => OpCode::GROUP_GENERATOR, } } @@ -63,6 +67,7 @@ impl Display for GlobalVars { GlobalVars::Outputs => write!(f, "OUTPUTS"), GlobalVars::Height => write!(f, "HEIGHT"), GlobalVars::MinerPubKey => write!(f, "MINER_PUBKEY"), + GlobalVars::LastBlockUtxoRootHash => write!(f, "LastBlockUtxoRootHash"), GlobalVars::GroupGenerator => write!(f, "GROUP_GENERATOR"), } } @@ -88,6 +93,7 @@ mod tests { Just(Height), Just(SelfBox), Just(MinerPubKey), + Just(LastBlockUtxoRootHash), Just(GroupGenerator) ] .boxed() diff --git a/ergotree-ir/src/serialization/expr.rs b/ergotree-ir/src/serialization/expr.rs index 5206721c0..45154de8d 100644 --- a/ergotree-ir/src/serialization/expr.rs +++ b/ergotree-ir/src/serialization/expr.rs @@ -110,6 +110,9 @@ impl Expr { OpCode::OUTPUTS => Ok(Expr::GlobalVars(GlobalVars::Outputs)), OpCode::MINER_PUBKEY => Ok(Expr::GlobalVars(GlobalVars::MinerPubKey)), OpCode::GROUP_GENERATOR => Ok(Expr::GlobalVars(GlobalVars::GroupGenerator)), + OpCode::LAST_BLOCK_UTXO_ROOT_HASH => { + Ok(Expr::GlobalVars(GlobalVars::LastBlockUtxoRootHash)) + } OpCode::GLOBAL => Ok(Expr::Global), OpCode::PROPERTY_CALL => { Ok(Expr::PropertyCall(PropertyCall::sigma_parse(r)?.into())) diff --git a/ergotree-ir/src/serialization/global_vars.rs b/ergotree-ir/src/serialization/global_vars.rs index d742829f6..45f8facfe 100644 --- a/ergotree-ir/src/serialization/global_vars.rs +++ b/ergotree-ir/src/serialization/global_vars.rs @@ -5,6 +5,7 @@ mod tests { use crate::mir::expr::Expr; use crate::mir::global_vars::GlobalVars; use crate::serialization::sigma_serialize_roundtrip; + use crate::serialization::SigmaSerializable; use proptest::prelude::*; @@ -16,4 +17,16 @@ mod tests { prop_assert_eq![sigma_serialize_roundtrip(&expr), expr]; } } + + #[test] + #[allow(clippy::unwrap_used)] + fn last_block_utxo_root_hash_is_a_bare_op_code() { + // `LastBlockUtxoRootHash` serializes as the bare dedicated op code 0xa6 + // (JVM `OpCodes.LastBlockUtxoRootHashCode`, registered as a case-object + // serializer in `ValueSerializer.scala`) — the op-form twin of the + // `CONTEXT.LastBlockUtxoRootHash` PropertyCall form. + let expr = Expr::GlobalVars(GlobalVars::LastBlockUtxoRootHash); + assert_eq!(expr.sigma_serialize_bytes().unwrap(), vec![0xa6]); + assert_eq!(Expr::sigma_parse_bytes(&[0xa6]).unwrap(), expr); + } }