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
96 changes: 96 additions & 0 deletions ergotree-ir/src/ergo_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,22 @@ impl SigmaSerializable for ErgoTree {
}) {
Ok(parsed_tree) => Ok(parsed_tree.into()),
Err(error) => {
// Mirror sigma-state `ErgoTreeSerializer.deserializeErgoTree`:
// the size-flagged `UnparsedErgoTree` fallback wraps ONLY a
// soft-forkable `ValidationException`. A HARD wire-structure
// failure escapes the fallback and REJECTS instead of
// degrading to `Unparsed`: a non-soft-forkable data type code
// (rule 1009 `CheckSerializableTypeCode` does not fire), an
// invalid EC point, EOF/truncation, or a VLQ overflow — the
// JVM's `SerializerException` / `IllegalArgumentException` /
// `IOException`. Position-limit (rule 1014) is the one
// soft-forkable wire error and still degrades. See
// `SigmaParsingError::escapes_sized_tree_degrade`.
if let ErgoTreeError::SigmaParsingError(e) = &error {
if e.escapes_sized_tree_degrade() {
return Err(e.clone());
}
}
let num_bytes = (body_pos - start_pos) + tree_size_bytes as u64;
r.seek(io::SeekFrom::Start(start_pos))?;
let mut bytes = vec![0; num_bytes as usize];
Expand Down Expand Up @@ -713,6 +729,86 @@ mod tests {
assert_eq!(tree.sigma_serialize_bytes().unwrap(), valid_ergo_tree_bytes);
}

#[test]
fn sized_tree_rejects_non_soft_forkable_data_type_code() {
// SANTA wire reject vector `ErgoTree.unparsed_soft_fork_header_constant`: a
// v2 + size + const-seg tree with one segregated `SHeader` constant (typeCode
// 0x68 = 104). sigma-state rule 1009 (`CheckSerializableTypeCode`) does NOT
// special-case `SHeader` (neither `OptionTypeCode` 36 nor `> LastDataType`
// 111), so the JVM throws a hard `SerializerException` that escapes
// `deserializeErgoTree`'s `UnparsedErgoTree` fallback and REJECTS — even with
// the size flag set. We must reject, not degrade to `Unparsed`.
let bytes = base16::decode("1adb01016802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0843d0000000000000000000000000000000000000000000000000000000000000000070239b8010000000000000000000000000000000000000000000000000000000000000000000000000000000001000000017300").unwrap();
assert!(ErgoTree::sigma_parse_bytes(&bytes).is_err());
}

#[test]
fn sized_tree_degrades_soft_forkable_option_constant() {
// Twin accept vector `ErgoTree.unparsed_soft_fork_option_constant`: a v2 +
// size + const-seg tree with one segregated `SOption[SInt]` constant
// (`Some(5)`). The Option typecode (36) IS rule-1009 soft-forkable, so the
// size flag degrades the whole tree to `Unparsed` and it re-serializes
// byte-identical (identity round-trip) — must stay accepted, not regress.
let bytes = base16::decode("1a060128010a7300").unwrap();
let tree = ErgoTree::sigma_parse_bytes(&bytes).unwrap();
assert!(matches!(tree, ErgoTree::Unparsed { .. }));
assert_eq!(tree.sigma_serialize_bytes().unwrap(), bytes);
}

#[test]
fn sized_tree_rejects_malformed_ec_point_pk() {
// SANTA wire reject vector `ErgoTree.sheader_constant_v3_malformed_pk_reject`:
// a v3 + size + const-seg tree with one segregated `SHeader` constant whose
// AutolykosSolution pk is an INVALID compressed EC point (prefix 0x05). The JVM
// rejects — `GroupElementSerializer.parse` throws `IllegalArgumentException`, a
// HARD error that escapes `deserializeErgoTree`'s `UnparsedErgoTree` soft-fork
// fallback. We must reject (the strict production parse), not degrade to
// `Unparsed`. (The integration branch additionally exercises the same gate via
// the lenient/conformance entry against this exact SANTA vector.)
let bytes = base16::decode("1bdb01016802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0843d0000000000000000000000000000000000000000000000000000000000000000070239b8010000000005000000000000000000000000000000000000000000000000000000000000000000000001000000017300").unwrap();
assert!(ErgoTree::sigma_parse_bytes(&bytes).is_err());
}

#[test]
fn sized_tree_degrade_gate_escapes_hard_errors_but_degrades_position_limit() {
// The gate policy (`SigmaParsingError::escapes_sized_tree_degrade`): hard
// wire-structure failures REJECT (escape == true), matching the JVM's
// non-`ValidationException` exceptions; position-limit (rule 1014) is the one
// soft-forkable wire error and DEGRADES (escape == false), in every channel it
// can arrive through (top-level, nested in `VlqEncode` / `ScorexParsingError`).
use sigma_ser::vlq_encode::VlqEncodingError;
use sigma_ser::ScorexParsingError;
let degrades = [
SigmaParsingError::PositionLimitExceeded,
SigmaParsingError::VlqEncode(VlqEncodingError::PositionLimitExceeded),
SigmaParsingError::ScorexParsingError(ScorexParsingError::PositionLimitExceeded),
SigmaParsingError::ScorexParsingError(ScorexParsingError::VlqEncode(
VlqEncodingError::PositionLimitExceeded,
)),
// soft-forkable type/opcode errors keep degrading (behavior unchanged)
SigmaParsingError::NotSupported("SOption data"),
SigmaParsingError::InvalidOpCode(0xff),
];
for e in &degrades {
assert!(!e.escapes_sized_tree_degrade(), "should degrade: {e:?}");
}
let rejects = [
// invalid EC point (this finding)
SigmaParsingError::ScorexParsingError(ScorexParsingError::Misc(
"failed to parse PK from bytes".to_string(),
)),
// EOF / truncation (the sibling over-accept this fix also closes)
SigmaParsingError::Io("unexpected end of file".to_string()),
// VLQ overflow
SigmaParsingError::VlqEncode(VlqEncodingError::VlqDecodingFailed),
// non-soft-forkable data type code (rule 1009, prior round)
SigmaParsingError::NonSerializableTypeCode(104),
];
for e in &rejects {
assert!(e.escapes_sized_tree_degrade(), "should reject: {e:?}");
}
}

#[test]
fn parse_p2pk_672() {
// see https://github.com/ergoplatform/sigma-rust/issues/672
Expand Down
46 changes: 40 additions & 6 deletions ergotree-ir/src/serialization/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::unsignedbigint256::UnsignedBigInt;
use ergo_chain_types::EcPoint;

use super::sigma_byte_writer::SigmaByteWrite;
use super::types::TypeCode;
use alloc::sync::Arc;
use core::convert::TryInto;

Expand Down Expand Up @@ -196,14 +197,47 @@ impl DataSerializer {
SHeader if r.tree_version() >= ErgoTreeVersion::V3 => {
Literal::Header(Box::new(Header::scorex_parse(r)?))
}
STypeVar(_) => return Err(SigmaParsingError::NotSupported("TypeVar data")),
SAny => return Err(SigmaParsingError::NotSupported("SAny data")),
// Non-serializable constant data types: mirror sigma-state's
// `CoreDataSerializer` fallback + rule 1009 (`CheckSerializableTypeCode`).
// A type code that is neither `OptionTypeCode` (36) nor `> LastDataType`
// (111) is NOT soft-forkable — the JVM throws a hard `SerializerException`
// that escapes `deserializeErgoTree`'s `UnparsedErgoTree` fallback, so a
// size-flagged tree carrying one is rejected (`NonSerializableTypeCode`).
// `SOption` (36) and `SFunc` (112 > 111) ARE soft-forkable and keep the
// degradable `NotSupported` (a size-flagged tree carrying one degrades to
// `Unparsed`, matching the JVM soft-fork).
STypeVar(_) => {
return Err(SigmaParsingError::NonSerializableTypeCode(
TypeCode::STYPE_VAR.value(),
))
}
SAny => {
return Err(SigmaParsingError::NonSerializableTypeCode(
TypeCode::SANY.value(),
))
}
SOption(_) => return Err(SigmaParsingError::NotSupported("SOption data")),
SFunc(_) => return Err(SigmaParsingError::NotSupported("SFunc data")),
SContext => return Err(SigmaParsingError::NotSupported("SContext data")),
SHeader => return Err(SigmaParsingError::NotSupported("SHeader data")),
SPreHeader => return Err(SigmaParsingError::NotSupported("SPreHeader data")),
SGlobal => return Err(SigmaParsingError::NotSupported("SGlobal data")),
SContext => {
return Err(SigmaParsingError::NonSerializableTypeCode(
TypeCode::SCONTEXT.value(),
))
}
SHeader => {
return Err(SigmaParsingError::NonSerializableTypeCode(
TypeCode::SHEADER.value(),
))
}
SPreHeader => {
return Err(SigmaParsingError::NonSerializableTypeCode(
TypeCode::SPRE_HEADER.value(),
))
}
SGlobal => {
return Err(SigmaParsingError::NonSerializableTypeCode(
TypeCode::SGLOBAL.value(),
))
}
})
}
}
69 changes: 67 additions & 2 deletions ergotree-ir/src/serialization/serializable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,82 @@ pub enum SigmaParsingError {
/// Invalid register value
#[error("Invalid register value: {0}")]
InvalidRegisterValue(#[from] RegisterValueError),
/// Data value of a type whose type code has no `DataSerializer` and is NOT
/// soft-forkable per sigma-state rule 1009 (`CheckSerializableTypeCode`): the
/// code is neither `OptionTypeCode` (36) nor `> LastDataType` (111). Mirrors the
/// JVM's hard `SerializerException` ("Not defined DataSerializer for type ..."),
/// which escapes `ErgoTreeSerializer.deserializeErgoTree`'s `UnparsedErgoTree`
/// soft-fork fallback — so a size-flagged tree carrying such a constant is
/// rejected, not degraded to `Unparsed`.
#[error("data value of type code {0} cannot be deserialized (rule 1009: not soft-forkable)")]
NonSerializableTypeCode(u8),
/// A read started past the position limit (the reference impl's rule 1014
/// `CheckPositionLimit`, set as the `MaxBoxSize` window during box parse).
/// SOFT-FORKABLE: a size-flagged tree whose body overruns its position
/// window degrades to `Unparsed` (matching the JVM's `ValidationException`),
/// unlike a hard EOF/structural failure which rejects.
#[error("read position exceeds the position limit")]
PositionLimitExceeded,
}

impl From<io::Error> for SigmaParsingError {
fn from(error: io::Error) -> Self {
SigmaParsingError::Io(error.to_string())
// `InvalidData` is the position-limit signal (rule 1014); see
// `VlqEncodingError`'s `From<io::Error>`. Keep it apart from `Io` (EOF).
if error.kind() == io::ErrorKind::InvalidData {
SigmaParsingError::PositionLimitExceeded
} else {
SigmaParsingError::Io(error.to_string())
}
}
}

impl From<&io::Error> for SigmaParsingError {
fn from(error: &io::Error) -> Self {
SigmaParsingError::Io(error.to_string())
if error.kind() == io::ErrorKind::InvalidData {
SigmaParsingError::PositionLimitExceeded
} else {
SigmaParsingError::Io(error.to_string())
}
}
}

impl SigmaParsingError {
/// True if this is the soft-forkable position-limit error (rule 1014),
/// whether at the top level or nested in `VlqEncode` / `ScorexParsingError`
/// (a windowed read can trip the limit through either channel).
pub fn is_position_limit_exceeded(&self) -> bool {
match self {
SigmaParsingError::PositionLimitExceeded => true,
SigmaParsingError::VlqEncode(vlq_encode::VlqEncodingError::PositionLimitExceeded) => {
true
}
SigmaParsingError::ScorexParsingError(e) => e.is_position_limit_exceeded(),
_ => false,
}
}

/// True if a size-flagged `ErgoTree` carrying a constant whose body fails
/// with this error must REJECT (escape the soft-fork degrade) instead of
/// degrading to `Unparsed`. Mirrors sigma-state
/// `ErgoTreeSerializer.deserializeErgoTree`, which wraps as `UnparsedErgoTree`
/// ONLY a soft-forkable `ValidationException`: a hard wire-structure failure
/// (invalid EC point, EOF/truncation, VLQ overflow → the JVM's
/// `IllegalArgumentException` / `IOException`) escapes and rejects. The one
/// soft-forkable case in these wire channels is position-limit (rule 1014),
/// which is excluded so it still degrades. Soft-forkable type/opcode/method
/// errors live in other variants and keep degrading (not listed here).
pub fn escapes_sized_tree_degrade(&self) -> bool {
if self.is_position_limit_exceeded() {
return false;
}
matches!(
self,
SigmaParsingError::NonSerializableTypeCode(_)
| SigmaParsingError::ScorexParsingError(_)
| SigmaParsingError::Io(_)
| SigmaParsingError::VlqEncode(_)
)
}
}

Expand Down
35 changes: 33 additions & 2 deletions sigma-ser/src/scorex_serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,48 @@ pub enum ScorexParsingError {
/// Failed to convert integer type
#[error("Bounds check error: {0}")]
TryFrom(#[from] core::num::TryFromIntError),
/// A read started past the position limit (rule 1014). DISTINCT from
/// [`ScorexParsingError::Io`] (EOF): soft-forkable (degrade a size-flagged
/// tree) vs hard reject. See [`ReadSigmaVlqExt::check_position_limit`].
#[error("read position exceeds the position limit")]
PositionLimitExceeded,
}

impl From<io::Error> for ScorexParsingError {
fn from(error: io::Error) -> Self {
ScorexParsingError::Io(error.to_string())
// See `VlqEncodingError`'s `From<io::Error>`: `InvalidData` is the
// position-limit signal (rule 1014), kept apart from `Io` (EOF).
if error.kind() == io::ErrorKind::InvalidData {
ScorexParsingError::PositionLimitExceeded
} else {
ScorexParsingError::Io(error.to_string())
}
}
}

impl From<&io::Error> for ScorexParsingError {
fn from(error: &io::Error) -> Self {
ScorexParsingError::Io(error.to_string())
if error.kind() == io::ErrorKind::InvalidData {
ScorexParsingError::PositionLimitExceeded
} else {
ScorexParsingError::Io(error.to_string())
}
}
}

impl ScorexParsingError {
/// True if this is the soft-forkable position-limit error (rule 1014),
/// at the top level or nested in [`ScorexParsingError::VlqEncode`] (a VLQ
/// read can trip the limit and arrive wrapped). Used by the sized-`ErgoTree`
/// degrade gate to DEGRADE position-limit while REJECTING hard wire errors.
pub fn is_position_limit_exceeded(&self) -> bool {
matches!(
self,
ScorexParsingError::PositionLimitExceeded
| ScorexParsingError::VlqEncode(
vlq_encode::VlqEncodingError::PositionLimitExceeded
)
)
}
}

Expand Down
18 changes: 17 additions & 1 deletion sigma-ser/src/vlq_encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,27 @@ pub enum VlqEncodingError {
/// Fail to decode a value from bytes
#[error("VLQ decoding failed")]
VlqDecodingFailed,
/// A read started past the position limit (the reference impl's rule 1014
/// `CheckPositionLimit`). Kept DISTINCT from [`VlqEncodingError::Io`] (EOF):
/// the JVM treats this as a soft-forkable `ValidationException` (a
/// size-flagged tree degrades to `Unparsed`), whereas EOF/truncation is a
/// hard reject. See [`ReadSigmaVlqExt::check_position_limit`].
#[error("read position exceeds the position limit")]
PositionLimitExceeded,
}

impl From<io::Error> for VlqEncodingError {
fn from(error: io::Error) -> Self {
VlqEncodingError::Io(error.to_string())
// `check_position_limit` is the sole producer of `InvalidData` on this
// (core2) reader path, so route it to the distinct soft-forkable variant
// rather than collapsing it into `Io` (EOF). Keeping them apart is
// consensus-critical: the sized-`ErgoTree` degrade gate must DEGRADE
// position-limit (rule 1014) but REJECT a hard EOF.
if error.kind() == io::ErrorKind::InvalidData {
VlqEncodingError::PositionLimitExceeded
} else {
VlqEncodingError::Io(error.to_string())
}
}
}

Expand Down
Loading