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
34 changes: 19 additions & 15 deletions ergo-chain-types/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::autolykos_pow_scheme::{
use crate::{ADDigest, BlockId, Digest32, EcPoint};
use alloc::boxed::Box;
use alloc::vec::Vec;
use core2::io::SeekFrom;
use core2::io::Write;
use num_bigint::{BigUint, ToBigInt};
use sigma_ser::vlq_encode::{ReadSigmaVlqExt, WriteSigmaVlqExt};
Expand Down Expand Up @@ -121,7 +122,11 @@ impl ScorexSerializable for Header {
Ok(())
}

// `seek(SeekFrom::Current(0))` reads the stream position; `Seek::stream_position` is
// std-only in core2 (this crate is `no_std`), so the `seek_from_current` lint can't apply.
#[allow(clippy::seek_from_current)]
fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
let start = r.seek(SeekFrom::Current(0))?;
let version = r.get_u8()?;
let parent_id = BlockId(Digest32::scorex_parse(r)?);
let ad_proofs_root = Digest32::scorex_parse(r)?;
Expand Down Expand Up @@ -183,12 +188,19 @@ impl ScorexSerializable for Header {
}
};

// The `Header.id` field isn't serialized/deserialized but rather computed as a hash of
// every other field in `Header`. First we initialize header with dummy id field then
// compute the hash.
let mut header = Header {
// The `Header.id` is computed as a hash of the serialized header. Mirror
// `ErgoHeader.sigmaSerializer.parse` (ErgoHeader.scala:167-180): hash the EXACT consumed
// input slice rather than re-serializing the decoded fields, so a header carrying a
// non-canonically-encoded (but accepted) value — e.g. a `0x00`-lead "garbage identity"
// minerPk — keeps its on-the-wire id and compares per the reference impl.
let end = r.seek(SeekFrom::Current(0))?;
r.seek(SeekFrom::Start(start))?;
let mut header_bytes = vec![0u8; (end - start) as usize];
r.read_exact(&mut header_bytes)?;
let id = BlockId(blake2b256_hash(&header_bytes).into());
Ok(Header {
version,
id: BlockId(Digest32::zero()),
id,
parent_id,
ad_proofs_root,
state_root,
Expand All @@ -197,18 +209,10 @@ impl ScorexSerializable for Header {
n_bits,
height,
extension_root,
autolykos_solution: autolykos_solution.clone(),
autolykos_solution,
votes,
unparsed_bytes,
};

let mut id_bytes = header.serialize_without_pow()?;
let mut data = Vec::new();
autolykos_solution.serialize_bytes(version, &mut data)?;
id_bytes.extend(data);
let id = BlockId(blake2b256_hash(&id_bytes).into());
header.id = id;
Ok(header)
})
}
}

Expand Down
8 changes: 5 additions & 3 deletions ergotree-interpreter/src/eval/extract_bytes.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use ergotree_ir::mir::extract_bytes::ExtractBytes;
use ergotree_ir::mir::value::Value;
use ergotree_ir::serialization::SigmaSerializable;

use crate::eval::env::Env;
use crate::eval::Context;
Expand All @@ -15,7 +14,10 @@ impl Evaluable for ExtractBytes {
) -> Result<Value<'ctx>, EvalError> {
let input_v = self.input.eval(env, ctx)?;
match input_v {
Value::CBox(b) => Ok(b.sigma_serialize_bytes()?.into()),
// `Box.bytes` returns the retained wire slice for a parsed box (the reference impl's
// `ErgoBox.bytes`), so a non-canonically-encoded box keeps its on-the-wire byte image
// — unlike `bytesWithoutRef`, which re-serializes the candidate canonically.
Value::CBox(b) => Ok(b.bytes()?.into()),
_ => Err(EvalError::UnexpectedValue(format!(
"Expected ExtractBytes input to be Value::CBox, got {0:?}",
input_v
Expand Down Expand Up @@ -45,7 +47,7 @@ mod tests {
let ctx = force_any_val::<Context>();
assert_eq!(
eval_out::<Vec<i8>>(&e, &ctx),
ctx.self_box.sigma_serialize_bytes().unwrap().as_vec_i8()
ctx.self_box.bytes().unwrap().as_vec_i8()
);
}
}
117 changes: 115 additions & 2 deletions ergotree-ir/src/chain/ergo_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::serialization::SigmaSerializeResult;
use alloc::string::ToString;
use alloc::vec::Vec;
pub use box_id::*;
use core2::io::SeekFrom;
use ergo_chain_types::Digest32;
pub use register::*;

Expand Down Expand Up @@ -58,7 +59,7 @@ pub type BoxTokens = BoundedVec<Token, 1, { ErgoBox::MAX_TOKENS_COUNT }>;
serde(try_from = "crate::chain::json::ergo_box::ErgoBoxJson"),
serde(into = "crate::chain::json::ergo_box::ErgoBoxJson")
)]
#[derive(PartialEq, Eq, Debug, Clone)]
#[derive(Eq, Debug, Clone)]
pub struct ErgoBox {
pub(crate) box_id: BoxId,
/// amount of money associated with the box
Expand All @@ -77,6 +78,23 @@ pub struct ErgoBox {
pub transaction_id: TxId,
/// number of box (from 0 to total number of boxes the transaction with transactionId created - 1)
pub index: u16,
/// Exact serialized bytes retained when this box was parsed off the wire (`None` for boxes
/// built from fields, which serialize canonically). Mirrors the reference impl's
/// `ErgoBox._bytes`: `ErgoBox.bytes` returns this slice verbatim, so a box carrying a
/// non-canonically-encoded value keeps its on-the-wire byte image (and thus `id`).
pub(crate) serialized_bytes: Option<Vec<u8>>,
}

// Mirror the reference impl's `ErgoBox.equals` (ErgoBox.scala:188-191), which compares the
// box `id` (a `Blake2b256` hash of the box bytes) rather than the decoded fields. For a box
// parsed off the wire `id` is computed over the exact retained input slice, so two boxes whose
// bytes differ only in a non-canonical-but-accepted encoding (e.g. a `0x00`-lead "garbage
// identity" GroupElement, where bytes 1..32 are discarded at parse) have different ids and
// therefore compare unequal — even though every decoded field is equal.
impl PartialEq for ErgoBox {
fn eq(&self, other: &Self) -> bool {
self.box_id == other.box_id
}
}

impl ErgoBox {
Expand Down Expand Up @@ -109,6 +127,7 @@ impl ErgoBox {
creation_height,
transaction_id,
index,
serialized_bytes: None,
};
let box_id = box_with_zero_id.calc_box_id()?;
Ok(ErgoBox {
Expand All @@ -122,6 +141,18 @@ impl ErgoBox {
self.box_id
}

/// Serialized box bytes. For a box parsed off the wire this is the exact retained input
/// slice (`ErgoBox._bytes`); for a box built from fields it is the canonical serialization.
/// `ExtractBytes` (`Box.bytes`) surfaces this, so non-canonically-encoded inputs keep their
/// on-the-wire byte image. (Note `bytesWithoutRef`/`ErgoBoxCandidate` has no retained slice
/// and always re-serializes canonically — see `ErgoBoxCandidate`.)
pub fn bytes(&self) -> Result<Vec<u8>, SigmaSerializationError> {
match &self.serialized_bytes {
Some(bytes) => Ok(bytes.clone()),
None => self.sigma_serialize_bytes(),
}
}

/// Create ErgoBox from ErgoBoxCandidate by adding transaction id
/// and index of the box in the transaction
pub fn from_box_candidate(
Expand All @@ -138,6 +169,7 @@ impl ErgoBox {
creation_height: box_candidate.creation_height,
transaction_id,
index,
serialized_bytes: None,
};
let box_id = box_with_zero_id.calc_box_id()?;
Ok(ErgoBox {
Expand Down Expand Up @@ -215,10 +247,31 @@ impl SigmaSerializable for ErgoBox {
Ok(())
}
fn sigma_parse<R: SigmaByteRead>(r: &mut R) -> Result<Self, SigmaParsingError> {
// Mirror `ErgoBox.sigmaSerializer.parse` (ErgoBox.scala:214-225): retain the exact
// consumed input slice, compute `id` over it, and keep it (`serialized_bytes`) so
// `ErgoBox.bytes` returns it verbatim — instead of re-serializing the decoded box. This
// preserves the on-the-wire identity and byte image of boxes carrying
// non-canonically-encoded (but accepted) values, matching the reference impl.
let start = r.position()?;
let box_candidate = ErgoBoxCandidate::parse_body_with_indexed_digests(None, r)?;
let tx_id = TxId::sigma_parse(r)?;
let index = r.get_u16()?;
Ok(ErgoBox::from_box_candidate(&box_candidate, tx_id, index)?)
let end = r.position()?;
r.seek(SeekFrom::Start(start))?;
let mut box_bytes = alloc::vec![0u8; (end - start) as usize];
r.read_exact(&mut box_bytes)?;
let box_id: BoxId = Digest32::from(*blake2b256_hash(&box_bytes)).into();
Ok(ErgoBox {
box_id,
value: box_candidate.value,
ergo_tree: box_candidate.ergo_tree,
tokens: box_candidate.tokens,
additional_registers: box_candidate.additional_registers,
creation_height: box_candidate.creation_height,
transaction_id: tx_id,
index,
serialized_bytes: Some(box_bytes),
})
}
}

Expand Down Expand Up @@ -491,6 +544,66 @@ mod tests {
use sigma_test_util::force_any_val;
use sigma_test_util::force_any_val_with;

// Regression: a box parsed off the wire keeps its on-the-wire identity. Two boxes whose
// bytes differ only in a non-canonical-but-accepted GroupElement encoding (`0x00`-lead
// "garbage identity", bytes 1..32 discarded at parse) get different ids and compare
// unequal, even though their decoded R4 GroupElements are equal — mirroring
// `ErgoBox.equals` (id over the retained slice). SANTA `Box.eq_id_basis`.
#[test]
fn box_id_uses_retained_wire_bytes() {
use ergo_chain_types::ec_point::identity;
let ge_const: Constant = identity().into();
let regs = NonMandatoryRegisters::new([(NonMandatoryRegisterId::R4, ge_const)]).unwrap();
let constructed = ErgoBox::from_box_candidate(
&ErgoBoxCandidate {
value: BoxValue::SAFE_USER_MIN,
ergo_tree: force_any_val::<ErgoTree>(),
tokens: None,
additional_registers: regs,
creation_height: 0,
},
TxId::zero(),
0,
)
.unwrap();
let canon_bytes = constructed.sigma_serialize_bytes().unwrap();

// Locate the R4 GroupElement constant (type byte `0x07` then the `0x00` identity lead).
// It is the last such marker: only zero bytes (txid + index) follow it. Flip the 32
// bytes after the lead to 0xaa; the GroupElement still parses to the identity point
// (bytes 1..32 are discarded), so only the box bytes — and thus the id — change.
let ge_pos = canon_bytes
.windows(2)
.rposition(|w| w == [0x07, 0x00])
.unwrap();
let mut garbage_bytes = canon_bytes.clone();
for b in &mut garbage_bytes[ge_pos + 2..ge_pos + 2 + 32] {
*b = 0xaa;
}
assert_ne!(garbage_bytes, canon_bytes);

let box_canon = ErgoBox::sigma_parse_bytes(&canon_bytes).unwrap();
let box_garbage = ErgoBox::sigma_parse_bytes(&garbage_bytes).unwrap();

// id is computed over the exact retained input slice, not the re-serialized box.
let canon_id: BoxId = Digest32::from(*blake2b256_hash(&canon_bytes)).into();
let garbage_id: BoxId = Digest32::from(*blake2b256_hash(&garbage_bytes)).into();
assert_eq!(box_canon.box_id(), canon_id);
assert_eq!(box_garbage.box_id(), garbage_id);
// `bytes()` likewise returns the retained slice verbatim (the `Box.bytes` basis).
assert_eq!(box_garbage.bytes().unwrap(), garbage_bytes);
assert_eq!(box_canon.bytes().unwrap(), canon_bytes);
// Different ids => unequal boxes...
assert_ne!(box_canon.box_id(), box_garbage.box_id());
assert_ne!(box_canon, box_garbage);
// ...even though the decoded R4 GroupElements compare equal (value basis preserved).
let r4 = RegisterId::NonMandatoryRegisterId(NonMandatoryRegisterId::R4);
assert_eq!(
box_canon.get_register(r4).unwrap(),
box_garbage.get_register(r4).unwrap()
);
}

#[test]
fn get_register_mandatory() {
let b = force_any_val::<ErgoBox>();
Expand Down
1 change: 1 addition & 0 deletions ergotree-ir/src/chain/json/ergo_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ impl TryFrom<ErgoBoxJson> for ErgoBox {
creation_height: box_json.creation_height,
transaction_id: box_json.transaction_id,
index: box_json.index,
serialized_bytes: None,
};
let box_id = box_with_zero_id.calc_box_id()?;
let ergo_box = ErgoBox {
Expand Down
Loading