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
25 changes: 12 additions & 13 deletions bindings/ergo-lib-c-core/src/ergo_state_ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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::<Vec<_>>()
.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(),
)),
}
}
Expand Down
21 changes: 16 additions & 5 deletions bindings/ergo-lib-python/src/chain/ergo_state_context.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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<Header>, parameters: Parameters) -> PyResult<Self> {
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(),
))
)))
}
}
18 changes: 8 additions & 10 deletions bindings/ergo-lib-wasm/src/block_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -126,15 +126,13 @@ impl TryFrom<BlockHeaders> for Headers {
type Error = JsValue;
fn try_from(bs: BlockHeaders) -> Result<Self, Self::Error> {
let headers: Vec<Header> = 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()
})
}
}
22 changes: 16 additions & 6 deletions ergo-lib/src/chain/ergo_state_context.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
//! 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<Header, 1, 10>;

/// Blockchain state (last headers, etc.)
#[derive(PartialEq, Eq, Debug, Clone)]
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,
Expand All @@ -36,19 +41,24 @@ 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 = ();
type Strategy = BoxedStrategy<Self>;

fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
// TODO: parameters should implement arbitrary as well, based on minimum/maximum constraints of each parameter
(any::<PreHeader>(), any::<Headers>())
(any::<PreHeader>(), vec(any::<Header>(), 10))
.prop_map(|(pre_header, headers)| {
Self::new(pre_header, headers, Parameters::default())
Self::new(
pre_header,
headers.try_into().unwrap(),
Parameters::default(),
)
})
.boxed()
}
Expand Down
18 changes: 16 additions & 2 deletions ergo-lib/src/wallet/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ 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;

pub use super::tx_context::TransactionContext;
Expand Down Expand Up @@ -101,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,
Expand All @@ -109,7 +113,17 @@ pub fn make_context<'ctx, T: ErgoTransaction>(
inputs: inputs_ir,
pre_header: state_ctx.pre_header.clone(),
extension,
headers: state_ctx.headers.clone(),
// 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 at
// least one).
last_block_utxo_root: AvlTreeData {
digest: state_ctx.headers.first().state_root,
tree_flags: AvlTreeFlags::new(true, true, true),
key_length: 32,
value_length_opt: None,
},
headers,
tree_version: Default::default(),
extension_provider: &tx_ctx.spending_tx,
})
Expand Down
97 changes: 76 additions & 21 deletions ergotree-interpreter/src/eval/scontext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,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,
}))
};
Expand All @@ -74,14 +76,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| {
Expand Down Expand Up @@ -124,10 +121,11 @@ 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::ergo_tree::ErgoTree;
use ergotree_ir::mir::avl_tree_data::{AvlTreeData, AvlTreeFlags};
use ergotree_ir::mir::constant::TryExtractFrom;
use ergotree_ir::mir::expr::Expr;
Expand Down Expand Up @@ -170,7 +168,7 @@ mod tests {
.expect("internal error: `headers` method has parameters length != 1")
.into();
let ctx = force_any_val::<Context>();
assert_eq!(eval_out::<[Header; 10]>(&expr, &ctx), ctx.headers);
assert_eq!(eval_out::<Vec<Header>>(&expr, &ctx), *ctx.headers.as_vec());
}

#[test]
Expand Down Expand Up @@ -202,16 +200,73 @@ mod tests {
)
.unwrap()
.into();
let ctx = force_any_val::<Context>();
let digest = ctx.headers[0].state_root;
let tree_flags = AvlTreeFlags::new(true, true, true);
let avl_tree_data = AvlTreeData {
digest,
tree_flags,
let mut ctx = force_any_val::<Context>();
// 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.first().unwrap().state_root
);
assert_eq!(
eval_out::<AvlTreeData>(&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::<Context>();
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,
};
assert_eq!(eval_out::<AvlTreeData>(&expr, &ctx), avl_tree_data);
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<T: TryExtractFrom<Value<'static>> + '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::<T>(&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::<Vec<Header>>("1200db6502fe", &ctx),
Vec::<Header>::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::<AvlTreeData>("1200db6509fe", &ctx),
ctx.last_block_utxo_root
);
}

#[test]
Expand Down
Loading
Loading