From 7f6e7cb4c113e33acf6d439be1d5cf122d5e34d4 Mon Sep 17 00:00:00 2001 From: Muad'Dib Date: Sun, 7 Jun 2026 14:37:20 +0200 Subject: [PATCH 1/2] fix: carry lastBlockUtxoRoot as a standalone context input The JVM models lastBlockUtxoRoot as its own ErgoLikeContext input and CContext.LastBlockUtxoRootHash returns it directly; deriving it from headers(0).stateRoot only coincides when headers are non-empty (the JVM requires digest agreement then, ErgoLikeContext.scala:85) and has no value for an empty header chain. Add Context.last_block_utxo_root and return it from the eval; make_context derives it from the newest header, which is exactly the JVM-required relationship. Co-Authored-By: Claude Opus 4.8 --- ergo-lib/src/wallet/signing.rs | 10 +++++++ ergotree-interpreter/src/eval/scontext.rs | 35 +++++++++-------------- ergotree-ir/src/chain/context.rs | 13 +++++++++ 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/ergo-lib/src/wallet/signing.rs b/ergo-lib/src/wallet/signing.rs index e56b06814..03ceb5722 100644 --- a/ergo-lib/src/wallet/signing.rs +++ b/ergo-lib/src/wallet/signing.rs @@ -20,6 +20,7 @@ use ergotree_interpreter::sigma_protocol::prover::Prover; use ergotree_interpreter::sigma_protocol::prover::ProverError; use ergotree_interpreter::sigma_protocol::prover::ProverResult; use ergotree_ir::chain::context::Context; +use ergotree_ir::mir::avl_tree_data::{AvlTreeData, AvlTreeFlags}; use thiserror::Error; pub use super::tx_context::TransactionContext; @@ -109,6 +110,15 @@ pub fn make_context<'ctx, T: ErgoTransaction>( inputs: inputs_ir, pre_header: state_ctx.pre_header.clone(), extension, + // The JVM requires `headers(0).stateRoot.digest == lastBlockUtxoRoot.digest` + // (`ErgoLikeContext.scala:85`), so deriving the root from the newest header + // is faithful whenever headers exist (`ErgoStateContext` always has them). + last_block_utxo_root: AvlTreeData { + digest: state_ctx.headers[0].state_root, + tree_flags: AvlTreeFlags::new(true, true, true), + key_length: 32, + value_length_opt: None, + }, headers: state_ctx.headers.clone(), tree_version: Default::default(), extension_provider: &tx_ctx.spending_tx, diff --git a/ergotree-interpreter/src/eval/scontext.rs b/ergotree-interpreter/src/eval/scontext.rs index 190e2ceed..0c935e4ab 100644 --- a/ergotree-interpreter/src/eval/scontext.rs +++ b/ergotree-interpreter/src/eval/scontext.rs @@ -2,8 +2,6 @@ use alloc::boxed::Box; use alloc::string::ToString; use alloc::sync::Arc; -use ergotree_ir::mir::avl_tree_data::AvlTreeData; -use ergotree_ir::mir::avl_tree_data::AvlTreeFlags; use ergotree_ir::mir::constant::TryExtractInto; use ergotree_ir::mir::value::CollKind; use ergotree_ir::mir::value::Value; @@ -74,14 +72,9 @@ pub(crate) static LAST_BLOCK_UTXO_ROOT_HASH_EVAL_FN: EvalFn = |_mc, _env, ctx, o obj ))); } - 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, - }))) + // The root is a standalone context input, as in the JVM (`CContext` returns + // `lastBlockUtxoRootHash` directly, never deriving it from `headers(0)`). + Ok(Value::AvlTree(Box::from(ctx.last_block_utxo_root.clone()))) }; pub(crate) static MINER_PUBKEY_EVAL_FN: EvalFn = |_mc, _env, ctx, obj, _args| { @@ -128,7 +121,7 @@ mod tests { use ergo_chain_types::{Header, PreHeader}; use ergotree_ir::chain::context::Context; use ergotree_ir::chain::ergo_box::ErgoBox; - use ergotree_ir::mir::avl_tree_data::{AvlTreeData, AvlTreeFlags}; + use ergotree_ir::mir::avl_tree_data::AvlTreeData; use ergotree_ir::mir::constant::TryExtractFrom; use ergotree_ir::mir::expr::Expr; use ergotree_ir::mir::method_call::MethodCall; @@ -202,16 +195,16 @@ mod tests { ) .unwrap() .into(); - let ctx = force_any_val::(); - let digest = ctx.headers[0].state_root; - let tree_flags = AvlTreeFlags::new(true, true, true); - let avl_tree_data = AvlTreeData { - digest, - tree_flags, - key_length: 32, - value_length_opt: None, - }; - assert_eq!(eval_out::(&expr, &ctx), avl_tree_data); + let mut ctx = force_any_val::(); + // Pin the field to a digest distinct from headers[0].state_root: the eval + // must return the standalone context input (JVM `CContext` semantics), + // not a value derived from the headers. + ctx.last_block_utxo_root.digest = [7u8; 33].into(); + assert_ne!(ctx.last_block_utxo_root.digest, ctx.headers[0].state_root); + assert_eq!( + eval_out::(&expr, &ctx), + ctx.last_block_utxo_root + ); } #[test] diff --git a/ergotree-ir/src/chain/context.rs b/ergotree-ir/src/chain/context.rs index 3c234936c..4fb638aa4 100644 --- a/ergotree-ir/src/chain/context.rs +++ b/ergotree-ir/src/chain/context.rs @@ -2,6 +2,7 @@ use core::cell::Cell; use crate::chain::ergo_box::ErgoBox; +use crate::mir::avl_tree_data::AvlTreeData; use crate::{chain::context_extension::ContextExtension, ergo_tree::ErgoTreeVersion}; use bounded_vec::BoundedVec; use ergo_chain_types::{Header, PreHeader}; @@ -24,6 +25,11 @@ pub struct Context<'ctx> { pub inputs: TxIoVec<&'ctx ErgoBox>, /// Pre header of current block pub pre_header: PreHeader, + /// State root of the UTXO state before current block application. A standalone + /// context input as in the JVM (`ErgoLikeContext.lastBlockUtxoRoot`), not derived + /// from `headers`: with non-empty headers the two agree by construction, and with + /// empty headers this field is the only source of the root. + pub last_block_utxo_root: AvlTreeData, /// Fixed number of last block headers in descending order (first header is the newest one) pub headers: [Header; 10], /// prover-defined key-value pairs, that may be used inside a script @@ -66,6 +72,7 @@ pub trait ContextExtensionProvider { pub mod arbitrary { use super::*; + use crate::mir::avl_tree_data::AvlTreeFlags; use proptest::{collection::vec, option::of, prelude::*}; pub struct DummyContextExtensionProvider(pub Vec); @@ -121,6 +128,12 @@ pub mod arbitrary { .unwrap(), pre_header, extension: Box::leak(extensions[0].clone().into()), + last_block_utxo_root: AvlTreeData { + digest: headers[0].state_root, + tree_flags: AvlTreeFlags::new(true, true, true), + key_length: 32, + value_length_opt: None, + }, headers, tree_version: Default::default(), extension_provider: Box::leak( From dd33103be08e6f0ea3406313e6662ab28be5333f Mon Sep 17 00:00:00 2001 From: Muad'Dib Date: Sun, 7 Jun 2026 14:52:51 +0200 Subject: [PATCH 2/2] fix: make Context.headers variable-length (genesis window) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JVM models ErgoLikeContext.headers as a variable-length Coll[Header]: empty is explicitly legal, and at block height h <= 10 the window that headerChainBack gathers stops at genesis, so scripts see h - 1 headers. sigma-rust's fixed [Header; 10] cannot represent that; a node bridging it must pad, and a script reading CONTEXT.headers.size or a high index in the first blocks of a chain gets a different answer than the JVM — a silent value fork (practical exposure: testnet from-genesis validation). Context.headers becomes ContextHeaders (BoundedVec 0..=10), and the SDK ErgoStateContext.headers becomes BoundedVec 1..=10 — signing always has a chain tip, and make_context still derives the root from the newest header. The wasm/python/C constructors relax accordingly: every existing caller passing 10 headers keeps working; passing fewer becomes possible. Regression tests drive the two JVM-blessed Context.properties vectors (empty headers -> empty Coll[Header]; LastBlockUtxoRootHash on an empty-headers context -> the standalone root field). Co-Authored-By: Claude Opus 4.8 --- .../ergo-lib-c-core/src/ergo_state_ctx.rs | 25 +++---- .../src/chain/ergo_state_context.rs | 21 ++++-- bindings/ergo-lib-wasm/src/block_header.rs | 18 ++--- ergo-lib/src/chain/ergo_state_context.rs | 22 ++++-- ergo-lib/src/wallet/signing.rs | 12 ++- ergotree-interpreter/src/eval/scontext.rs | 74 +++++++++++++++++-- ergotree-interpreter/src/eval/sheader.rs | 30 +++++--- ergotree-ir/src/chain/context.rs | 17 +++-- 8 files changed, 158 insertions(+), 61 deletions(-) diff --git a/bindings/ergo-lib-c-core/src/ergo_state_ctx.rs b/bindings/ergo-lib-c-core/src/ergo_state_ctx.rs index 6160653f6..1bc6566e0 100644 --- a/bindings/ergo-lib-c-core/src/ergo_state_ctx.rs +++ b/bindings/ergo-lib-c-core/src/ergo_state_ctx.rs @@ -7,7 +7,6 @@ use crate::header::PreHeader; use crate::parameters::ConstParametersPtr; use crate::util::const_ptr_as_ref; use crate::Error; -use std::convert::TryInto; /// Blockchain state (last headers, etc.) #[derive(PartialEq, Eq, Debug, Clone)] @@ -25,26 +24,26 @@ pub unsafe fn ergo_state_context_new( let pre_header = const_ptr_as_ref(pre_header_ptr, "pre_header_ptr")?; let headers = const_ptr_as_ref(headers, "headers")?; let parameters = const_ptr_as_ref(parameters_ptr, "parameters_ptr")?; - match headers.0.len() { - 10 => { + let count = headers.0.len(); + match chain::ergo_state_context::Headers::from_vec( + headers.0.clone().into_iter().map(|x| x.0).collect(), + ) { + Ok(headers) => { *ergo_state_context_out = Box::into_raw(Box::new(ErgoStateContext( chain::ergo_state_context::ErgoStateContext::new( pre_header.clone().0, - headers - .0 - .clone() - .into_iter() - .map(|x| x.0) - .collect::>() - .try_into() - .unwrap(), + headers, parameters.0.clone(), ), ))); Ok(()) } - h => Err(Error::Misc( - format!("Not enough block headers, expected 10 but got {}", h).into(), + Err(_) => Err(Error::Misc( + format!( + "Incorrect number of block headers, expected 1..=10 but got {}", + count + ) + .into(), )), } } diff --git a/bindings/ergo-lib-python/src/chain/ergo_state_context.rs b/bindings/ergo-lib-python/src/chain/ergo_state_context.rs index 4338f8ae6..a280a0258 100644 --- a/bindings/ergo-lib-python/src/chain/ergo_state_context.rs +++ b/bindings/ergo-lib-python/src/chain/ergo_state_context.rs @@ -1,5 +1,8 @@ use derive_more::{AsRef, From, Into}; -use ergo_lib::chain::ergo_state_context::ErgoStateContext as ErgoStateContextInner; +use ergo_lib::chain::ergo_state_context::{ + ErgoStateContext as ErgoStateContextInner, Headers as HeadersInner, +}; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use super::{ @@ -14,11 +17,19 @@ pub(crate) struct ErgoStateContext(pub(crate) ErgoStateContextInner); #[pymethods] impl ErgoStateContext { #[new] - fn new(pre_header: PreHeader, headers: [Header; 10], parameters: Parameters) -> Self { - Self(ErgoStateContextInner::new( + fn new(pre_header: PreHeader, headers: Vec
, parameters: Parameters) -> PyResult { + let count = headers.len(); + let headers = HeadersInner::from_vec(headers.into_iter().map(Into::into).collect()) + .map_err(|_| { + PyValueError::new_err(format!( + "Incorrect number of block headers, expected 1..=10 but got {}", + count + )) + })?; + Ok(Self(ErgoStateContextInner::new( pre_header.into(), - headers.map(Into::into), + headers, parameters.into(), - )) + ))) } } diff --git a/bindings/ergo-lib-wasm/src/block_header.rs b/bindings/ergo-lib-wasm/src/block_header.rs index b03059793..3e47b1b39 100644 --- a/bindings/ergo-lib-wasm/src/block_header.rs +++ b/bindings/ergo-lib-wasm/src/block_header.rs @@ -9,7 +9,7 @@ use derive_more::{From, Into}; use crate::error_conversion::to_js; use ergo_lib::chain::ergo_state_context::Headers; -use std::convert::{TryFrom, TryInto}; +use std::convert::TryFrom; /// Block header #[wasm_bindgen] @@ -126,15 +126,13 @@ impl TryFrom for Headers { type Error = JsValue; fn try_from(bs: BlockHeaders) -> Result { let headers: Vec
= bs.0.into_iter().map(Header::from).collect(); - if headers.len() == 10 { - #[allow(clippy::unwrap_used)] - Ok(headers.try_into().unwrap()) - } else { - Err(js_sys::Error::new(&format!( - "Incorrect number of block headers, expected 10 but got {}", - headers.len() + let count = headers.len(); + Headers::from_vec(headers).map_err(|_| { + js_sys::Error::new(&format!( + "Incorrect number of block headers, expected 1..=10 but got {}", + count )) - .into()) - } + .into() + }) } } diff --git a/ergo-lib/src/chain/ergo_state_context.rs b/ergo-lib/src/chain/ergo_state_context.rs index 2beaef7e0..85b02f9f6 100644 --- a/ergo-lib/src/chain/ergo_state_context.rs +++ b/ergo-lib/src/chain/ergo_state_context.rs @@ -1,10 +1,15 @@ //! Blockchain state +use bounded_vec::BoundedVec; use ergo_chain_types::{Header, PreHeader}; use super::parameters::Parameters; -/// Fixed number of last block headers in descending order (first header is the newest one) -pub type Headers = [Header; 10]; +/// Last block headers in descending order (first header is the newest one). +/// Between 1 and 10: the SDK signs and validates against an existing chain tip, +/// so at least the newest header is always available (the script context itself +/// allows fewer — see `ergotree_ir::chain::context::ContextHeaders`). A node +/// near genesis supplies as many real headers as exist instead of padding. +pub type Headers = BoundedVec; /// Blockchain state (last headers, etc.) #[derive(PartialEq, Eq, Debug, Clone)] @@ -12,7 +17,7 @@ pub struct ErgoStateContext { /// Block header with the current `spendingTransaction`, that can be predicted /// by a miner before it's formation pub pre_header: PreHeader, - /// Fixed number of last block headers in descending order (first header is the newest one) + /// Last block headers in descending order (first header is the newest one) pub headers: Headers, /// Parameters that can be adjusted by voting pub parameters: Parameters, @@ -36,9 +41,10 @@ impl ErgoStateContext { } #[cfg(feature = "arbitrary")] +#[allow(clippy::unwrap_used)] mod arbitrary { use super::*; - use proptest::prelude::*; + use proptest::{collection::vec, prelude::*}; impl Arbitrary for ErgoStateContext { type Parameters = (); @@ -46,9 +52,13 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { // TODO: parameters should implement arbitrary as well, based on minimum/maximum constraints of each parameter - (any::(), any::()) + (any::(), vec(any::
(), 10)) .prop_map(|(pre_header, headers)| { - Self::new(pre_header, headers, Parameters::default()) + Self::new( + pre_header, + headers.try_into().unwrap(), + Parameters::default(), + ) }) .boxed() } diff --git a/ergo-lib/src/wallet/signing.rs b/ergo-lib/src/wallet/signing.rs index 03ceb5722..725ccbf99 100644 --- a/ergo-lib/src/wallet/signing.rs +++ b/ergo-lib/src/wallet/signing.rs @@ -19,7 +19,7 @@ use ergotree_interpreter::sigma_protocol::prover::ProofBytes; use ergotree_interpreter::sigma_protocol::prover::Prover; use ergotree_interpreter::sigma_protocol::prover::ProverError; use ergotree_interpreter::sigma_protocol::prover::ProverResult; -use ergotree_ir::chain::context::Context; +use ergotree_ir::chain::context::{Context, ContextHeaders}; use ergotree_ir::mir::avl_tree_data::{AvlTreeData, AvlTreeFlags}; use thiserror::Error; @@ -102,6 +102,9 @@ pub fn make_context<'ctx, T: ErgoTransaction>( .spending_tx .context_extension(self_index) .ok_or(TransactionError::InputNofFound(self_index))?; + // `ErgoStateContext` headers (1..=10) always fit the context's 0..=10. + #[allow(clippy::unwrap_used)] + let headers = ContextHeaders::from_vec(state_ctx.headers.as_vec().clone()).unwrap(); Ok(Context { height, self_box, @@ -112,14 +115,15 @@ pub fn make_context<'ctx, T: ErgoTransaction>( extension, // The JVM requires `headers(0).stateRoot.digest == lastBlockUtxoRoot.digest` // (`ErgoLikeContext.scala:85`), so deriving the root from the newest header - // is faithful whenever headers exist (`ErgoStateContext` always has them). + // is faithful whenever headers exist (`ErgoStateContext` always has at + // least one). last_block_utxo_root: AvlTreeData { - digest: state_ctx.headers[0].state_root, + digest: state_ctx.headers.first().state_root, tree_flags: AvlTreeFlags::new(true, true, true), key_length: 32, value_length_opt: None, }, - headers: state_ctx.headers.clone(), + headers, tree_version: Default::default(), extension_provider: &tx_ctx.spending_tx, }) diff --git a/ergotree-interpreter/src/eval/scontext.rs b/ergotree-interpreter/src/eval/scontext.rs index 0c935e4ab..9f4442b74 100644 --- a/ergotree-interpreter/src/eval/scontext.rs +++ b/ergotree-interpreter/src/eval/scontext.rs @@ -50,7 +50,11 @@ pub(crate) static HEADERS_EVAL_FN: EvalFn = |_mc, _env, ctx, obj, _args| { ))); } Ok(Value::Coll(CollKind::WrappedColl { - items: Arc::new(ctx.headers.clone().map(Box::new).map(Value::Header)), + items: ctx + .headers + .iter() + .map(|h| Value::Header(Box::new(h.clone()))) + .collect(), elem_tpe: SType::SHeader, })) }; @@ -117,11 +121,12 @@ pub(crate) static GET_VAR_FROM_INPUT_EVAL_FN: EvalFn = |mc, _env, ctx, _obj, arg #[cfg(feature = "arbitrary")] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { - use crate::eval::test_util::eval_out; + use crate::eval::test_util::{eval_out, try_eval_out_with_version}; use ergo_chain_types::{Header, PreHeader}; - use ergotree_ir::chain::context::Context; + use ergotree_ir::chain::context::{Context, ContextHeaders}; use ergotree_ir::chain::ergo_box::ErgoBox; - use ergotree_ir::mir::avl_tree_data::AvlTreeData; + use ergotree_ir::ergo_tree::ErgoTree; + use ergotree_ir::mir::avl_tree_data::{AvlTreeData, AvlTreeFlags}; use ergotree_ir::mir::constant::TryExtractFrom; use ergotree_ir::mir::expr::Expr; use ergotree_ir::mir::method_call::MethodCall; @@ -163,7 +168,7 @@ mod tests { .expect("internal error: `headers` method has parameters length != 1") .into(); let ctx = force_any_val::(); - assert_eq!(eval_out::<[Header; 10]>(&expr, &ctx), ctx.headers); + assert_eq!(eval_out::>(&expr, &ctx), *ctx.headers.as_vec()); } #[test] @@ -200,13 +205,70 @@ mod tests { // must return the standalone context input (JVM `CContext` semantics), // not a value derived from the headers. ctx.last_block_utxo_root.digest = [7u8; 33].into(); - assert_ne!(ctx.last_block_utxo_root.digest, ctx.headers[0].state_root); + assert_ne!( + ctx.last_block_utxo_root.digest, + ctx.headers.first().unwrap().state_root + ); assert_eq!( eval_out::(&expr, &ctx), ctx.last_block_utxo_root ); } + /// Canonical synthetic eval context essentials for the blessed vectors below: + /// EMPTY headers — the honest value for a contextless eval, which the JVM + /// expresses (`headers.isEmpty` is legal, `ErgoLikeContext.scala:85`) — and + /// the dummy root (all-zero 33-byte digest, all operations allowed). + fn empty_headers_ctx() -> Context<'static> { + let mut ctx = force_any_val::(); + ctx.headers = ContextHeaders::from_vec(vec![]).unwrap(); + ctx.last_block_utxo_root = AvlTreeData { + digest: [0u8; 33].into(), + tree_flags: AvlTreeFlags::new(true, true, true), + key_length: 32, + value_length_opt: None, + }; + ctx + } + + /// JVM-blessed byte vectors (santa-eval `Context.properties`, eval/v5/authored): + /// closed v2 trees. The blessed sized header (`1a` + size VLQ) is rewritten to + /// the non-sized `12` (size bit cleared, size byte dropped) because the sized + /// parse path rejects non-SigmaProp roots — the same lenient deserialize the + /// conformance runner applies to expression-rooted corpus trees; body verbatim. + fn eval_blessed_context_tree> + 'static>( + tree_hex: &str, + ctx: &Context<'static>, + ) -> T { + let tree_bytes = base16::decode(tree_hex).unwrap(); + let tree = ErgoTree::sigma_parse_bytes(&tree_bytes).unwrap(); + let expr = tree.proposition().unwrap(); + try_eval_out_with_version::(&expr, ctx, 2, 2).unwrap() + } + + #[test] + fn eval_headers_empty_context_blessed_bytes() { + // `{ CONTEXT.headers }` (`CONTEXT.headers#dummy`): the JVM yields the + // context's actual — here empty — header collection. + let ctx = empty_headers_ctx(); + assert_eq!( + eval_blessed_context_tree::>("1200db6502fe", &ctx), + Vec::
::new() + ); + } + + #[test] + fn eval_last_block_utxo_root_hash_empty_context_blessed_bytes() { + // `{ CONTEXT.LastBlockUtxoRootHash }` (`CONTEXT.LastBlockUtxoRootHash#dummy`): + // with no headers the standalone field is the only source of the root — + // the JVM returns it; a `headers(0)`-derived value cannot express this. + let ctx = empty_headers_ctx(); + assert_eq!( + eval_blessed_context_tree::("1200db6509fe", &ctx), + ctx.last_block_utxo_root + ); + } + #[test] fn eval_get_var_from_input() { fn get_var_from_input> + 'static>( diff --git a/ergotree-interpreter/src/eval/sheader.rs b/ergotree-interpreter/src/eval/sheader.rs index 11d7f1c05..f525f8051 100644 --- a/ergotree-interpreter/src/eval/sheader.rs +++ b/ergotree-interpreter/src/eval/sheader.rs @@ -218,7 +218,7 @@ mod tests { fn test_eval_version() { let expr = create_get_header_property_expr(sheader::VERSION_PROPERTY.clone()); let ctx = force_any_val::(); - let version = ctx.headers[HEADER_INDEX].version as i8; + let version = ctx.headers.as_slice()[HEADER_INDEX].version as i8; assert_eq!(version, eval_out::(&expr, &ctx)); } @@ -250,7 +250,7 @@ mod tests { fn test_eval_state_root() { let expr = create_get_header_property_expr(sheader::STATE_ROOT_PROPERTY.clone()); let ctx = force_any_val::(); - let expected = ctx.headers[HEADER_INDEX].state_root; + let expected = ctx.headers.as_slice()[HEADER_INDEX].state_root; let actual = digest_from_bytes_signed::<33>(eval_out::>(&expr, &ctx)); assert_eq!(expected, actual); } @@ -259,7 +259,7 @@ mod tests { fn test_eval_timestamp() { let expr = create_get_header_property_expr(sheader::TIMESTAMP_PROPERTY.clone()); let ctx = force_any_val::(); - let expected = ctx.headers[HEADER_INDEX].timestamp as i64; + let expected = ctx.headers.as_slice()[HEADER_INDEX].timestamp as i64; let actual = eval_out::(&expr, &ctx); assert_eq!(expected, actual); } @@ -268,7 +268,7 @@ mod tests { fn test_eval_n_bits() { let expr = create_get_header_property_expr(sheader::N_BITS_PROPERTY.clone()); let ctx = force_any_val::(); - let expected = ctx.headers[HEADER_INDEX].n_bits as i64; + let expected = ctx.headers.as_slice()[HEADER_INDEX].n_bits as i64; let actual = eval_out::(&expr, &ctx); assert_eq!(expected, actual); } @@ -277,7 +277,7 @@ mod tests { fn test_eval_height() { let expr = create_get_header_property_expr(sheader::HEIGHT_PROPERTY.clone()); let ctx = force_any_val::(); - let expected = ctx.headers[HEADER_INDEX].height as i32; + let expected = ctx.headers.as_slice()[HEADER_INDEX].height as i32; let actual = eval_out::(&expr, &ctx); assert_eq!(expected, actual); } @@ -306,7 +306,7 @@ mod tests { fn test_eval_pow_distance() { let expr = create_get_header_property_expr(sheader::POW_DISTANCE_PROPERTY.clone()); let ctx = force_any_val::(); - let expected = ctx.headers[HEADER_INDEX] + let expected = ctx.headers.as_slice()[HEADER_INDEX] .autolykos_solution .pow_distance .clone() @@ -324,7 +324,10 @@ mod tests { fn test_eval_pow_nonce() { let expr = create_get_header_property_expr(sheader::POW_NONCE_PROPERTY.clone()); let ctx = force_any_val::(); - let expected = ctx.headers[HEADER_INDEX].autolykos_solution.nonce.clone(); + let expected = ctx.headers.as_slice()[HEADER_INDEX] + .autolykos_solution + .nonce + .clone(); let actual = eval_out::>(&expr, &ctx).as_vec_u8(); assert_eq!(expected, actual); } @@ -333,7 +336,7 @@ mod tests { fn test_eval_votes() { let expr = create_get_header_property_expr(sheader::VOTES_PROPERTY.clone()); let ctx = force_any_val::(); - let expected = ctx.headers[HEADER_INDEX].votes.clone(); + let expected = ctx.headers.as_slice()[HEADER_INDEX].votes.clone(); let actual = { let votes_bytes = eval_out::>(&expr, &ctx).as_vec_u8(); Votes::try_from(votes_bytes) @@ -371,7 +374,8 @@ mod tests { #[test] fn test_eval_check_pow() { let mut ctx = force_any_val::(); - ctx.headers[0] = serde_json::from_str( + let mut headers = ctx.headers.as_vec().clone(); + headers[0] = serde_json::from_str( r#"{ "extensionId": "d51a477cc12b187d9bc7f464b22d00e3aa7c92463874e863bf3acf2f427bb48b", "difficulty": "1595361307131904", @@ -399,16 +403,18 @@ mod tests { }"#, ) .unwrap(); + ctx.headers = headers.clone().try_into().unwrap(); // Add a mainnet block header with valid PoW to context. TODO: this can be simplified once Header serialization is added to sigma-rust (v6.0), right now we need to access CONTEXT.headers(0) - let headers = PropertyCall::new(Expr::Context, HEADERS_PROPERTY.clone()).unwrap(); - let header = ByIndex::new(headers.into(), 0i32.into(), None).unwrap(); + let headers_pc = PropertyCall::new(Expr::Context, HEADERS_PROPERTY.clone()).unwrap(); + let header = ByIndex::new(headers_pc.into(), 0i32.into(), None).unwrap(); let check_pow: Expr = MethodCall::new(header.into(), sheader::CHECK_POW_METHOD.clone(), vec![]) .unwrap() .into(); assert!(eval_out::(&check_pow, &ctx)); // Mutate header to invalidate proof-of-work - ctx.headers[0].timestamp -= 1; + headers[0].timestamp -= 1; + ctx.headers = headers.try_into().unwrap(); assert!(!eval_out::(&check_pow, &ctx)); } } diff --git a/ergotree-ir/src/chain/context.rs b/ergotree-ir/src/chain/context.rs index 4fb638aa4..960b4ca2e 100644 --- a/ergotree-ir/src/chain/context.rs +++ b/ergotree-ir/src/chain/context.rs @@ -4,12 +4,18 @@ use core::cell::Cell; use crate::chain::ergo_box::ErgoBox; use crate::mir::avl_tree_data::AvlTreeData; use crate::{chain::context_extension::ContextExtension, ergo_tree::ErgoTreeVersion}; -use bounded_vec::BoundedVec; +use bounded_vec::{witnesses, BoundedVec}; use ergo_chain_types::{Header, PreHeader}; /// BoundedVec type for Tx inputs, output_candidates and outputs pub type TxIoVec = BoundedVec; +/// Last block headers as carried by the script context: up to 10, variable-length +/// as in the JVM (`ErgoLikeContext.headers` is a `Coll[Header]`). At block height +/// `h <= 10` only `h - 1` real headers exist (the window stops at genesis), so +/// fewer than 10 — empty at height 1 — is a legal chain state. +pub type ContextHeaders = BoundedVec>; + /// Interpreter's context (blockchain state) #[derive(derive_more::Debug, Clone)] pub struct Context<'ctx> { @@ -30,8 +36,9 @@ pub struct Context<'ctx> { /// from `headers`: with non-empty headers the two agree by construction, and with /// empty headers this field is the only source of the root. pub last_block_utxo_root: AvlTreeData, - /// Fixed number of last block headers in descending order (first header is the newest one) - pub headers: [Header; 10], + /// Last block headers in descending order (first header is the newest one). + /// Up to 10; fewer near genesis (see [`ContextHeaders`]). + pub headers: ContextHeaders, /// prover-defined key-value pairs, that may be used inside a script pub extension: &'ctx ContextExtension, /// ergo tree version @@ -97,7 +104,7 @@ pub mod arbitrary { input_strategy, of(vec(any::(), 1..3)), any::(), - any::<[Header; 10]>(), + vec(any::
(), 10), ) .prop_map( |( @@ -134,7 +141,7 @@ pub mod arbitrary { key_length: 32, value_length_opt: None, }, - headers, + headers: headers.try_into().unwrap(), tree_version: Default::default(), extension_provider: Box::leak( DummyContextExtensionProvider(extensions).into(),