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
31 changes: 13 additions & 18 deletions ergotree-interpreter/src/eval/subst_const.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use ergotree_ir::mir::subst_const::SubstConstants;
use ergotree_ir::mir::value::CollKind;
use ergotree_ir::mir::value::NativeColl;
use ergotree_ir::mir::value::Value;
use ergotree_ir::serialization::SigmaSerializable;
use sigma_util::AsVecI8;
use sigma_util::AsVecU8;

Expand Down Expand Up @@ -55,24 +54,20 @@ impl Evaluable for SubstConstants {
}

if let Value::Coll(CollKind::NativeColl(NativeColl::CollByte(b))) = script_bytes_v {
// Substitue constants with repeated calls to `ErgoTree::with_constant`.
let mut ergo_tree = ErgoTree::sigma_parse_bytes(&b.as_vec_u8())?;
let num_constants = ergo_tree.constants_len().map_err(to_misc_err)?;
for (ix, i) in positions.iter().enumerate() {
if *i < num_constants {
ergo_tree = ergo_tree
.with_constant(*i, new_constants[ix].clone())
.map_err(to_misc_err)?;
} else {
return Err(EvalError::Misc(format!(
"SubstConstants: positions[{}] == {} is an out of bound index with \
respect to the serialized ErgoTree's constant list",
ix, *i
)));
}
}
// Byte-level substitution mirroring sigma-state's
// `ErgoTreeSerializer.substituteConstants`: the tree body is never
// parsed and out-of-range positions are a no-op, so a malformed
// body or an OOB position returns the original bytes (JVM parity)
// instead of erroring.
let (new_bytes, _num_constants) = ErgoTree::substitute_constants(
b.as_vec_u8(),
&positions,
&new_constants,
ctx.tree_version(),
)
.map_err(to_misc_err)?;
Ok(Value::Coll(CollKind::NativeColl(NativeColl::CollByte(
ergo_tree.sigma_serialize_bytes()?.as_vec_i8().into(),
new_bytes.as_vec_i8().into(),
))))
} else {
Err(EvalError::Misc(format!(
Expand Down
170 changes: 170 additions & 0 deletions ergotree-ir/src/ergo_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,105 @@ impl ErgoTree {
pub fn template_bytes(&self) -> Result<Vec<u8>, ErgoTreeError> {
self.clone().parsed_tree()?.template_bytes()
}

/// Replaces constants at the given `positions` with `new_values` in a
/// serialized ErgoTree, mirroring sigma-state's
/// `ErgoTreeSerializer.substituteConstants`. Only the header and the
/// constants segment are parsed; the body bytes are kept verbatim and
/// never deserialized, so an unparseable body is tolerated. Positions
/// outside the tree's constants list are silently ignored (no-op), and
/// the first position referencing a given constant index wins. Returns
/// the resulting bytes and the number of constants in the tree;
/// `positions.len()` must equal `new_values.len()`.
///
/// `tree_version` is the *evaluation's* ErgoTree version (not the
/// template header's). The tree-size slot is re-emitted only when it is
/// `>= V3` — the V6 soft-fork `isV3OrLaterErgoTreeVersion` gate in
/// `ErgoTreeSerializer.scala`; for `<= V2` the slot is dropped even
/// though the header's `has_size` bit stays set, a JVM quirk we mirror
/// byte-for-byte.
pub fn substitute_constants(
script_bytes: Vec<u8>,
positions: &[usize],
new_values: &[Constant],
tree_version: ErgoTreeVersion,
) -> Result<(Vec<u8>, usize), ErgoTreeError> {
use core2::io::Write;
use sigma_ser::vlq_encode::ReadSigmaVlqExt;
// Parse only the header + constants segment; keep the body raw.
let (header, mut constants, body_start) = {
let mut r =
SigmaByteReader::new(Cursor::new(script_bytes.as_slice()), ConstantStore::empty());
let header = ErgoTreeHeader::sigma_parse(&mut r)?;
let (constants, body_start) = r.with_tree_version(
header.version(),
|r| -> Result<(Vec<Constant>, usize), SigmaParsingError> {
if header.has_size() {
let _ = r.get_u32()?;
}
let constants = if header.is_constant_segregation() {
ErgoTree::sigma_parse_constants(r)?
} else {
Vec::new()
};
let body_start = r.position()? as usize;
Ok((constants, body_start))
},
)?;
(header, constants, body_start)
};
let num_constants = constants.len();
let tree_bytes = script_bytes.get(body_start..).unwrap_or_default().to_vec();

// First position referencing a given index wins (matches Scala's
// `getPositionsBackref`); out-of-range positions are dropped.
let mut already_set = vec![false; num_constants];
for (i_pos, &pos) in positions.iter().enumerate() {
if pos < num_constants && !already_set[pos] {
let new_c = &new_values[i_pos];
if new_c.tpe != constants[pos].tpe {
return Err(ErgoTreeConstantError::SetConstantError(
SetConstantError::TypeMismatch(format!(
"substitute_constants: position {} expected type {:?}, got {:?}",
pos, constants[pos].tpe, new_c.tpe
)),
)
.into());
}
constants[pos] = new_c.clone();
already_set[pos] = true;
}
}

// Re-emit header + [size] + [count + constants (if segregated)] +
// verbatim body, mirroring `<ErgoTree as SigmaSerializable>`.
let body_section = {
let mut data = Vec::new();
let mut inner_w = SigmaByteWriter::new(&mut data, None);
inner_w.with_tree_version(header.version(), |inner_w| -> SigmaSerializeResult {
if header.is_constant_segregation() {
inner_w.put_usize_as_u32_unwrapped(constants.len())?;
constants
.iter()
.try_for_each(|c| c.sigma_serialize(inner_w))?;
}
inner_w.write_all(&tree_bytes)?;
Ok(())
})?;
data
};
let mut out = Vec::new();
let mut w = SigmaByteWriter::new(&mut out, None);
header.sigma_serialize(&mut w)?;
// V6 soft-fork: re-emit the size slot only when the evaluation's tree
// version is >= V3 (`isV3OrLaterErgoTreeVersion`); for <= V2 it is
// dropped even with the has_size bit set (JVM parity).
if tree_version >= ErgoTreeVersion::V3 && header.has_size() {
w.put_usize_as_u32_unwrapped(body_section.len())?;
}
w.write_all(&body_section)?;
Ok((out, num_constants))
}
}

/// Constants related errors
Expand Down Expand Up @@ -653,6 +752,77 @@ mod tests {
assert_eq!(ergo_tree.get_constant(0).unwrap().unwrap(), false.into());
}

// JVM parity (jvm:sigma-state-6.0.3 LanguageSpecificationV5 substConstants):
// a position outside the tree's constant list is a no-op that returns the
// original bytes, not an error. substitute_constants never parses the body,
// so even #1 (`[0,0,8,-45]`), whose body sigma-rust's full parser rejects
// with InvalidTypeCode, no-ops cleanly. (`-45` == `0xd3`.)
#[test]
fn substitute_constants_oob_is_noop() {
let dummy: Constant = 0i32.into();
let run = |bytes: Vec<u8>, pos: usize| -> (Vec<u8>, usize) {
ErgoTree::substitute_constants(
bytes,
&[pos],
core::slice::from_ref(&dummy),
ErgoTreeVersion::V3,
)
.unwrap()
};
// #0: non-segregated header, 0 constants
assert_eq!(run(vec![0x00, 0x08, 0xd3], 0), (vec![0x00, 0x08, 0xd3], 0));
// #1: non-segregated, body unparseable by the full deserializer
assert_eq!(
run(vec![0x00, 0x00, 0x08, 0xd3], 0),
(vec![0x00, 0x00, 0x08, 0xd3], 0)
);
// #2/#3: segregated header, 0 constants
assert_eq!(
run(vec![0x10, 0x00, 0x08, 0xd3], 0),
(vec![0x10, 0x00, 0x08, 0xd3], 0)
);
// #6: segregated, 1 constant, position 1 is out of range
assert_eq!(
run(vec![0x10, 0x01, 0x08, 0xd3, 0x73, 0x00], 1),
(vec![0x10, 0x01, 0x08, 0xd3, 0x73, 0x00], 1)
);
}

// JVM parity (jvm:sigma-state-6.0.3 substituteConstants): the tree-size
// slot is re-emitted only when the evaluation's ErgoTree version is >= V3
// (the V6 soft-fork `isV3OrLaterErgoTreeVersion` gate,
// ErgoTreeSerializer.scala:369). For v<=2 the slot is dropped even though
// the header's has_size bit stays set. No SANTA substConstants vector is a
// has_size template, so this path is certified against the Scala source.
#[test]
fn substitute_constants_v3_gates_size_slot() {
// A v1 (has_size) segregated template with a single constant.
let expr = Expr::Const(Constant {
tpe: SType::SBoolean,
v: Literal::Boolean(false),
});
let bytes = ErgoTree::new(ErgoTreeHeader::v1(true), &expr)
.unwrap()
.sigma_serialize_bytes()
.unwrap();
assert!(ErgoTreeHeader::new(bytes[0]).unwrap().has_size());
// Tiny tree => single-byte size VLQ, so it can be stripped positionally.
assert!(bytes[1] < 0x80, "test assumes a single-byte size VLQ");

// No substitution: the only inter-version difference is the size slot.
let (out_v3, _) =
ErgoTree::substitute_constants(bytes.clone(), &[], &[], ErgoTreeVersion::V3).unwrap();
let (out_v2, _) =
ErgoTree::substitute_constants(bytes.clone(), &[], &[], ErgoTreeVersion::V2).unwrap();

// v>=3: size slot kept => byte-identical round-trip.
assert_eq!(out_v3, bytes, "v3 must re-emit the size slot");
// v<=2: size slot dropped => header byte then the bytes after the slot.
let mut expected_v2 = vec![bytes[0]];
expected_v2.extend_from_slice(&bytes[2..]);
assert_eq!(out_v2, expected_v2, "v<=2 must drop the size slot");
}

#[test]
fn test_set_constant() {
let expr = Expr::Const(Constant {
Expand Down
Loading