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
2 changes: 1 addition & 1 deletion ergo-chain-types/src/digest32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ impl<const N: usize> ScorexSerializable for Digest<N> {
}
fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
let mut bytes = [0; N];
r.read_exact(&mut bytes)?;
r.get_bytes_into(&mut bytes)?;
Ok(Self(bytes))
}
}
Expand Down
2 changes: 1 addition & 1 deletion ergo-chain-types/src/ec_point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ impl ScorexSerializable for EcPoint {

fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
let mut buf = [0; EcPoint::GROUP_SIZE];
r.read_exact(&mut buf[..])?;
r.get_bytes_into(&mut buf[..])?;
if buf[0] != 0 {
let pubkey = PublicKey::from_sec1_bytes(&buf[..]).map_err(|e| {
ScorexParsingError::Misc(format!("failed to parse PK from bytes: {:?}", e))
Expand Down
12 changes: 6 additions & 6 deletions ergo-chain-types/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ impl ScorexSerializable for Header {
let timestamp = r.get_u64()?;
let extension_root = Digest32::scorex_parse(r)?;
let mut n_bits_buf = [0u8, 0, 0, 0];
r.read_exact(&mut n_bits_buf)?;
r.get_bytes_into(&mut n_bits_buf)?;
let n_bits = u32::from_be_bytes(n_bits_buf);
let height = r.get_u32()?;
let mut votes_bytes = [0u8, 0, 0];
r.read_exact(&mut votes_bytes)?;
r.get_bytes_into(&mut votes_bytes)?;
let votes = Votes(votes_bytes);

// For block version >= 2, a new byte encodes length of possible new fields. If this byte >
Expand All @@ -143,7 +143,7 @@ impl ScorexSerializable for Header {
let new_field_size = r.get_u8()?;
if new_field_size > 0 {
let mut field_bytes: Vec<u8> = vec![0; new_field_size as usize];
r.read_exact(&mut field_bytes)?;
r.get_bytes_into(&mut field_bytes)?;
field_bytes.into()
} else {
Box::new([])
Expand All @@ -157,10 +157,10 @@ impl ScorexSerializable for Header {
let miner_pk = EcPoint::scorex_parse(r)?.into();
let pow_onetime_pk = Some(EcPoint::scorex_parse(r)?.into());
let mut nonce: Vec<u8> = vec![0; 8];
r.read_exact(&mut nonce)?;
r.get_bytes_into(&mut nonce)?;
let d_bytes_len = r.get_u8()?;
let mut d_bytes: Vec<u8> = vec![0; d_bytes_len as usize];
r.read_exact(&mut d_bytes)?;
r.get_bytes_into(&mut d_bytes)?;
let pow_distance = Some(BigUint::from_bytes_be(&d_bytes));
AutolykosSolution {
miner_pk,
Expand All @@ -174,7 +174,7 @@ impl ScorexSerializable for Header {
let pow_distance = None;
let miner_pk = EcPoint::scorex_parse(r)?.into();
let mut nonce: Vec<u8> = vec![0; 8];
r.read_exact(&mut nonce)?;
r.get_bytes_into(&mut nonce)?;
AutolykosSolution {
miner_pk,
pow_onetime_pk,
Expand Down
32 changes: 19 additions & 13 deletions ergo-lib/src/wallet/box_selector/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use alloc::string::String;
use alloc::vec::Vec;
use ergotree_ir::chain::ergo_box::box_value::BoxValue;
use ergotree_ir::chain::ergo_box::BoxTokens;
use ergotree_ir::chain::ergo_box::ErgoBox;
use ergotree_ir::chain::token::Token;
use ergotree_ir::chain::token::TokenAmount;
use ergotree_ir::chain::token::TokenAmountError;
Expand Down Expand Up @@ -224,7 +223,14 @@ fn check_input_preservation<T: ErgoBoxAssets>(
#[error("Not enough coins for change box(es)")]
pub struct NotEnoughCoinsForChangeBox(String);

/// Split change tokens into a multiple boxes if over ErgoBox::MAX_TOKENS_COUNT distinct tokens
/// Wallet packing policy: max tokens per change box. Deliberately NOT
/// `ErgoBox::MAX_TOKENS_COUNT` (255, the wire/type ceiling): a box that many
/// minimal 33-byte tokens would exceed `ErgoBox::MAX_BOX_SIZE` and be rejected
/// by the node's size rule. 122 is the fit-derived bound this selector has
/// always used ((4096 - ~85 bytes of other fields) / 33), kept as local policy.
const MAX_TOKENS_PER_CHANGE_BOX: usize = 122;

/// Split change tokens into a multiple boxes if over MAX_TOKENS_PER_CHANGE_BOX distinct tokens
fn make_change_boxes(
change_value: BoxValue,
change_tokens: HashMap<TokenId, TokenAmount>,
Expand All @@ -234,9 +240,9 @@ fn make_change_boxes(
value: change_value,
tokens: None,
}])
} else if change_tokens.len() <= ErgoBox::MAX_TOKENS_COUNT {
} else if change_tokens.len() <= MAX_TOKENS_PER_CHANGE_BOX {
#[allow(clippy::unwrap_used)]
// unwrap_used is ok here because we checked that change_tokens.len() <= ErgoBox::MAX_TOKENS_COUNT
// unwrap_used is ok here because we checked that change_tokens.len() <= MAX_TOKENS_PER_CHANGE_BOX
Ok(vec![ErgoBoxAssetsData {
value: change_value,
tokens: Some(
Expand All @@ -249,9 +255,9 @@ fn make_change_boxes(
change_tokens.into_iter().map(Token::from).collect();
let mut change_value_left = change_value;
while !change_tokens_left.is_empty() {
if change_tokens_left.len() <= ErgoBox::MAX_TOKENS_COUNT {
if change_tokens_left.len() <= MAX_TOKENS_PER_CHANGE_BOX {
#[allow(clippy::unwrap_used)]
// unwrap_used is ok here because we checked that change_tokens_left.len() <= ErgoBox::MAX_TOKENS_COUNT
// unwrap_used is ok here because we checked that change_tokens_left.len() <= MAX_TOKENS_PER_CHANGE_BOX
let change_box = ErgoBoxAssetsData {
value: change_value_left,
tokens: Some(BoxTokens::from_vec(change_tokens_left).unwrap()),
Expand All @@ -262,11 +268,11 @@ fn make_change_boxes(
#[allow(clippy::unwrap_used)] // safe for the box value upper bound
// doubled due to larger box size to accomodate so many tokens
let value = BoxValue::SAFE_USER_MIN.checked_mul_u32(2).unwrap();
let tokens_to_drain = ErgoBox::MAX_TOKENS_COUNT;
let tokens_to_drain = MAX_TOKENS_PER_CHANGE_BOX;
let drained_tokens: Vec<Token> =
change_tokens_left.drain(..tokens_to_drain).collect();
#[allow(clippy::unwrap_used)]
// safe since tokens_to_drain is ErgoBox::MAX_TOKENS_COUNT
// safe since tokens_to_drain is MAX_TOKENS_PER_CHANGE_BOX
let change_box = ErgoBoxAssetsData {
value,
tokens: Some(BoxTokens::from_vec(drained_tokens).unwrap()),
Expand Down Expand Up @@ -551,8 +557,8 @@ mod tests {
value_range: (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into(),
tokens_param: ArbTokensParam {
token_id_param: ArbTokenIdParam::Arbitrary,
// with min 4 boxes below gives us minimum ErgoBox::MAX_TOKENS_COUNT * 2 distinct tokens total
token_count_range: (ErgoBox::MAX_TOKENS_COUNT/2)..ErgoBox::MAX_TOKENS_COUNT,
// with min 4 boxes below gives us minimum MAX_TOKENS_PER_CHANGE_BOX * 2 distinct tokens total
token_count_range: (MAX_TOKENS_PER_CHANGE_BOX/2)..MAX_TOKENS_PER_CHANGE_BOX,
}
}),
4..10
Expand Down Expand Up @@ -611,15 +617,15 @@ mod tests {
value_range: (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into(),
tokens_param: ArbTokensParam {
token_id_param: ArbTokenIdParam::Arbitrary,
// with min 4 boxes below gives us minimum ErgoBox::MAX_TOKENS_COUNT * 2 distinct tokens total
token_count_range: (ErgoBox::MAX_TOKENS_COUNT/2)..ErgoBox::MAX_TOKENS_COUNT,
// with min 4 boxes below gives us minimum MAX_TOKENS_PER_CHANGE_BOX * 2 distinct tokens total
token_count_range: (MAX_TOKENS_PER_CHANGE_BOX/2)..MAX_TOKENS_PER_CHANGE_BOX,
}
}),
4..10
)) {
let target_tokens = inputs.iter()
.flat_map(|b| b.tokens().unwrap())
.take(ErgoBox::MAX_TOKENS_COUNT + 10)
.take(MAX_TOKENS_PER_CHANGE_BOX + 10)
.collect::<Vec<Token>>();
let target_balance = BoxValue::SAFE_USER_MIN.checked_mul_u32(2).unwrap();
let s = SimpleBoxSelector::new();
Expand Down
2 changes: 1 addition & 1 deletion ergotree-ir/src/bigint256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ impl SigmaSerializable for BigInt256 {
)));
}
let mut buf = vec![0u8; size as usize];
r.read_exact(&mut buf)?;
r.get_bytes_into(&mut buf)?;
match BigInt256::from_be_slice(&buf) {
Some(x) => Ok(x),
None => Err(SigmaParsingError::ValueOutOfBounds(String::new())),
Expand Down
158 changes: 132 additions & 26 deletions ergotree-ir/src/chain/ergo_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,13 @@ pub struct ErgoBox {
}

impl ErgoBox {
/// Safe maximum number of tokens in the box
/// Calculated from the max box size (4kb) limit and the size of the token (32 bytes)
// (4096 - 85 bytes minimal size of the rest of the fields) / 33 token id 32 bytes + minimal token amount 1 byte = 121 tokens
// let's set to 121 + 1 to be safe
pub const MAX_TOKENS_COUNT: usize = 122;
/// Maximum number of tokens a box can carry: the wire count is a single
/// byte, so 255 — the reference impl's `SigmaConstants.MaxTokens`, which
/// binds only in SDK builders. The data layer has NO token-count consensus
/// rule; the real parse gate is the [`ErgoBox::MAX_BOX_SIZE`] position-limit
/// window over the candidate span (`ErgoBoxCandidate.parseBodyWithIndexedDigests`),
/// so how many tokens actually fit depends on the rest of the box.
pub const MAX_TOKENS_COUNT: usize = 255;
/// Maximum box size in Ergo
pub const MAX_BOX_SIZE: usize = 4096;
/// Maximum script size
Expand Down Expand Up @@ -346,6 +348,16 @@ pub fn parse_box_with_indexed_digests<R: SigmaByteRead>(
) -> Result<ErgoBoxCandidate, SigmaParsingError> {
// reference implementation -https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala#L144-L144

// `ErgoBoxCandidate.parseBodyWithIndexedDigests` puts a MaxBoxSize window
// over the candidate span: each primitive read checks `position > limit`
// BEFORE reading (rule 1014), so a final field may overrun the window while
// a read STARTING past it fails. Restored on the success path only, like
// the reference impl (no `finally` there); nested candidate parses
// (Coll[Box] constants in registers) save/restore the outer window.
let previous_position_limit = r.position_limit();
let window_limit = r.position()? + ErgoBox::MAX_BOX_SIZE as u64;
r.set_position_limit(window_limit);

let value = BoxValue::sigma_parse(r)?;
let ergo_tree = ErgoTree::sigma_parse(r)?;
let creation_height = r.get_u32()?;
Expand Down Expand Up @@ -378,6 +390,7 @@ pub fn parse_box_with_indexed_digests<R: SigmaByteRead>(

let additional_registers = NonMandatoryRegisters::sigma_parse(r)?;

r.set_position_limit(previous_position_limit);
Ok(ErgoBoxCandidate {
value,
ergo_tree,
Expand Down Expand Up @@ -443,6 +456,15 @@ pub mod arbitrary {
creation_height,
},
)
// candidates whose serialization exceeds the MAX_BOX_SIZE parse
// window do not survive the consensus parser (here or on the
// JVM), so they cannot round-trip; keep generated boxes
// parseable
.prop_filter("candidate must fit the MAX_BOX_SIZE parse window", |c| {
c.sigma_serialize_bytes()
.map(|b| b.len() <= ErgoBox::MAX_BOX_SIZE)
.unwrap_or(false)
})
.boxed()
}
type Strategy = BoxedStrategy<Self>;
Expand Down Expand Up @@ -483,13 +505,12 @@ pub mod arbitrary {
mod tests {

use super::*;
use crate::chain::token::arbitrary::ArbTokenIdParam;
use crate::chain::token::TokenAmount;
use crate::mir::expr::Expr;
use crate::serialization::sigma_serialize_roundtrip;

use proptest::collection::SizeRange;
use proptest::prelude::*;
use sigma_test_util::force_any_val;
use sigma_test_util::force_any_val_with;

#[test]
fn get_register_mandatory() {
Expand Down Expand Up @@ -522,25 +543,110 @@ mod tests {
assert_eq!(b.creation_info().1, expected_bytes.to_vec().as_vec_i8());
}

// NOTE: the old `test_max_tokens` (a MAX_TOKENS_COUNT round-trip with
// arbitrary token amounts) is superseded by
// `box_window_123_minimal_tokens_fit_and_parse` below: with the
// MAX_BOX_SIZE parse window in place, "max tokens" is not a fixed count
// but whatever fits the window, exactly like the reference impl.

/// Deterministic tiny tree so the token counts below sit on known sides of
/// the MAX_BOX_SIZE window (123 minimal tokens fit for any tree <= 31
/// bytes; 124 cross for any tree).
fn small_tree() -> ErgoTree {
let tree = ErgoTree::try_from(Expr::Const(true.into())).unwrap();
assert!(tree.sigma_serialize_bytes().unwrap().len() <= 31);
tree
}

fn minimal_tokens_vec(n: usize) -> Vec<Token> {
(0..n)
.map(|i| {
let mut id = [0u8; 32];
id[0] = i as u8;
id[1] = (i >> 8) as u8;
Token {
token_id: Digest32::from(id).into(),
// amount 1 = single VLQ byte: a minimal token is 33 bytes
amount: TokenAmount::try_from(1u64).unwrap(),
}
})
.collect()
}

fn windowed_candidate(
tokens: Option<BoxTokens>,
registers: NonMandatoryRegisters,
) -> ErgoBoxCandidate {
ErgoBoxCandidate {
value: BoxValue::MIN,
ergo_tree: small_tree(),
tokens,
additional_registers: registers,
creation_height: 0,
}
}

// The token gate is the MAX_BOX_SIZE position-limit window over the
// candidate span, not a count rule (JVM
// ErgoBoxCandidate.parseBodyWithIndexedDigests; the routed
// Global.deserializeTo_Box_token_window family).

#[test]
fn test_max_tokens() {
let tokens = force_any_val_with::<Vec<Token>>((
SizeRange::new(ErgoBox::MAX_TOKENS_COUNT..=ErgoBox::MAX_TOKENS_COUNT),
ArbTokenIdParam::Arbitrary,
));
let b = ErgoBox::from_box_candidate(
&ErgoBoxCandidate {
value: BoxValue::SAFE_USER_MIN,
ergo_tree: force_any_val::<ErgoTree>(),
tokens: Some(BoxTokens::from_vec(tokens).unwrap()),
additional_registers: NonMandatoryRegisters::empty(),
creation_height: 0,
},
TxId::zero(),
0,
)
.unwrap();
assert_eq!(sigma_serialize_roundtrip(&b), b);
fn box_window_123_minimal_tokens_fit_and_parse() {
let tokens = BoxTokens::from_vec(minimal_tokens_vec(123)).unwrap();
let c = windowed_candidate(Some(tokens), NonMandatoryRegisters::empty());
let bytes = c.sigma_serialize_bytes().unwrap();
assert!(bytes.len() <= ErgoBox::MAX_BOX_SIZE);
let parsed = ErgoBoxCandidate::sigma_parse_bytes(&bytes).unwrap();
assert_eq!(parsed.tokens.as_ref().unwrap().len(), 123);
}

#[test]
fn box_window_124_minimal_tokens_cross_and_error() {
let tokens = BoxTokens::from_vec(minimal_tokens_vec(124)).unwrap();
let c = windowed_candidate(Some(tokens), NonMandatoryRegisters::empty());
let bytes = c.sigma_serialize_bytes().unwrap();
assert!(bytes.len() > ErgoBox::MAX_BOX_SIZE);
// a token read starts past the window: same verdict the count cap used
// to give, now by the JVM's mechanism
assert!(ErgoBoxCandidate::sigma_parse_bytes(&bytes).is_err());
}

#[test]
fn box_window_escape_on_final_field() {
// the window is checked BEFORE each read: a candidate whose LAST field
// overruns 4096 still parses (the JVM's lazy per-read semantics) ...
let fat_r4 = Constant::from(vec![0i8; 4200]);
let c = windowed_candidate(
Some(BoxTokens::from_vec(minimal_tokens_vec(2)).unwrap()),
NonMandatoryRegisters::try_from(vec![fat_r4.clone()]).unwrap(),
);
let bytes = c.sigma_serialize_bytes().unwrap();
assert!(bytes.len() > ErgoBox::MAX_BOX_SIZE);
let parsed = ErgoBoxCandidate::sigma_parse_bytes(&bytes).unwrap();
assert_eq!(parsed.tokens.as_ref().unwrap().len(), 2);

// ... while a read STARTING past the window fails: a small R5 placed
// after the fat R4
let c = windowed_candidate(
Some(BoxTokens::from_vec(minimal_tokens_vec(2)).unwrap()),
NonMandatoryRegisters::try_from(vec![fat_r4, Constant::from(1i32)]).unwrap(),
);
let bytes = c.sigma_serialize_bytes().unwrap();
assert!(ErgoBoxCandidate::sigma_parse_bytes(&bytes).is_err());
}

#[test]
fn box_tokens_relaxed_to_u8_ceiling() {
// 255 is the type bound (SigmaConstants.MaxTokens): such boxes
// serialize fine, and it is the window that rejects them at parse
let tokens = BoxTokens::from_vec(minimal_tokens_vec(255)).unwrap();
let c = windowed_candidate(Some(tokens), NonMandatoryRegisters::empty());
let bytes = c.sigma_serialize_bytes().unwrap();
assert!(bytes.len() > ErgoBox::MAX_BOX_SIZE);
assert!(ErgoBoxCandidate::sigma_parse_bytes(&bytes).is_err());
// 256 is unrepresentable (single count byte on the wire)
assert!(BoxTokens::from_vec(minimal_tokens_vec(256)).is_err());
}

proptest! {
Expand Down
4 changes: 2 additions & 2 deletions ergotree-ir/src/ergo_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ impl SigmaSerializable for ErgoTree {
let tree_size_bytes = r.get_u32()?;
let body_pos = r.position()?;
let mut buf = vec![0u8; tree_size_bytes as usize];
r.read_exact(buf.as_mut_slice())?;
r.get_bytes_into(buf.as_mut_slice())?;
let mut inner_r =
SigmaByteReader::new(Cursor::new(&mut buf[..]), ConstantStore::empty());
match inner_r.with_tree_version(header.version(), |inner_r| {
Expand All @@ -394,7 +394,7 @@ impl SigmaSerializable for ErgoTree {
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];
r.read_exact(&mut bytes)?;
r.get_bytes_into(&mut bytes)?;
Ok(ErgoTree::Unparsed {
tree_bytes: bytes,
error,
Expand Down
Loading
Loading