diff --git a/ergo-chain-types/src/header.rs b/ergo-chain-types/src/header.rs index 44d40b3f4..73d7f52b3 100644 --- a/ergo-chain-types/src/header.rs +++ b/ergo-chain-types/src/header.rs @@ -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}; @@ -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: &mut R) -> Result { + 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)?; @@ -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, @@ -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) + }) } } diff --git a/ergotree-interpreter/src/eval/extract_bytes.rs b/ergotree-interpreter/src/eval/extract_bytes.rs index 4e51f19a9..73430e9f0 100644 --- a/ergotree-interpreter/src/eval/extract_bytes.rs +++ b/ergotree-interpreter/src/eval/extract_bytes.rs @@ -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; @@ -15,7 +14,10 @@ impl Evaluable for ExtractBytes { ) -> Result, 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 @@ -45,7 +47,7 @@ mod tests { let ctx = force_any_val::(); assert_eq!( eval_out::>(&e, &ctx), - ctx.self_box.sigma_serialize_bytes().unwrap().as_vec_i8() + ctx.self_box.bytes().unwrap().as_vec_i8() ); } } diff --git a/ergotree-ir/src/chain/ergo_box.rs b/ergotree-ir/src/chain/ergo_box.rs index ddbe44748..fda81fc62 100644 --- a/ergotree-ir/src/chain/ergo_box.rs +++ b/ergotree-ir/src/chain/ergo_box.rs @@ -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::*; @@ -58,7 +59,7 @@ pub type BoxTokens = BoundedVec; 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 @@ -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>, +} + +// 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 { @@ -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 { @@ -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, 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( @@ -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 { @@ -215,10 +247,31 @@ impl SigmaSerializable for ErgoBox { Ok(()) } fn sigma_parse(r: &mut R) -> Result { + // 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), + }) } } @@ -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::(), + 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::(); diff --git a/ergotree-ir/src/chain/json/ergo_box.rs b/ergotree-ir/src/chain/json/ergo_box.rs index 83b4c5d4d..50e438e01 100644 --- a/ergotree-ir/src/chain/json/ergo_box.rs +++ b/ergotree-ir/src/chain/json/ergo_box.rs @@ -76,6 +76,7 @@ impl TryFrom 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 {