From 95196aa1a011524aa71c0d7e950716bdc6efafe8 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 8 May 2026 17:21:11 -0700 Subject: [PATCH 1/9] Wire serde_2026 serialization into chia_rs - Pin clvmr to clvm_rs PR #708 (serde_2026 git rev) via [patch.crates-io] - Add chia_consensus::serde_2026: node_from_bytes_auto with consensus caps (CONSENSUS_MAX_ATOM_LEN = 1 MiB, CONSENSUS_MAX_BLOB_SIZE = 10 MiB) - Add chia_consensus::program_bytes: re-exports node_from_bytes_auto - Add solution_generator_2026() using serialize_2026 (serde_2026 format) - Expose solution_generator_2026() and SERDE_2026_MAGIC_PREFIX to Python --- crates/chia-consensus/src/lib.rs | 1 + crates/chia-consensus/src/serde_2026.rs | 38 +++++++++++++++++++ .../chia-consensus/src/solution_generator.rs | 14 ++++++- wheel/generate_type_stubs.py | 1 + wheel/python/chia_rs/chia_rs.pyi | 2 + wheel/src/api.rs | 17 ++++++++- 6 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 crates/chia-consensus/src/serde_2026.rs diff --git a/crates/chia-consensus/src/lib.rs b/crates/chia-consensus/src/lib.rs index 044234211..9a87bd40f 100644 --- a/crates/chia-consensus/src/lib.rs +++ b/crates/chia-consensus/src/lib.rs @@ -24,6 +24,7 @@ pub mod owned_conditions; pub mod puzzle_fingerprint; pub mod run_block_generator; pub mod sanitize_int; +pub mod serde_2026; pub mod solution_generator; pub mod spend_visitor; pub mod spendbundle_conditions; diff --git a/crates/chia-consensus/src/serde_2026.rs b/crates/chia-consensus/src/serde_2026.rs new file mode 100644 index 000000000..0e5fe7f94 --- /dev/null +++ b/crates/chia-consensus/src/serde_2026.rs @@ -0,0 +1,38 @@ +//! Consensus-tuned wrappers around the `clvm_rs::serde_2026` deserializer. +//! +//! `clvm_rs` deliberately makes the caller pick `max_atom_len` and `strict`, +//! since those are policy and clvm_rs has no consensus opinion. This module +//! supplies the values chia consensus expects and exposes the +//! "sniff the magic prefix and dispatch" convenience that callers used to get +//! from `clvm_rs::serde::node_from_bytes_auto`. + +use clvmr::allocator::{Allocator, NodePtr}; +use clvmr::error::{EvalErr, Result}; +use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, deserialize_2026, node_from_bytes_backrefs}; + +/// Per-atom byte cap used by chia consensus when deserializing CLVM blobs. +/// +/// Matches the historical `clvm_rs` default. CLVM atoms above this size are +/// uneconomical to construct under cost limits anyway; this is a defensive +/// pre-allocation cap, not a hard consensus rule. +pub const CONSENSUS_MAX_ATOM_LEN: usize = 1 << 20; + +/// Maximum total blob size accepted by consensus deserialization (10 MiB). +pub const CONSENSUS_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024; + +/// Deserialize CLVM bytes, auto-detecting classic / backrefs / serde_2026. +/// +/// Sniffs `SERDE_2026_MAGIC_PREFIX` at the head of `bytes`; if present, +/// dispatches to [`deserialize_2026`] with consensus caps (atom ≤ 1 MiB, +/// total blob ≤ 10 MiB). Otherwise falls back to [`node_from_bytes_backrefs`] +/// (which also accepts plain classic). +pub fn node_from_bytes_auto(allocator: &mut Allocator, bytes: &[u8]) -> Result { + if bytes.len() > CONSENSUS_MAX_BLOB_SIZE { + return Err(EvalErr::SerializationError); + } + if bytes.starts_with(&SERDE_2026_MAGIC_PREFIX) { + deserialize_2026(allocator, bytes, CONSENSUS_MAX_ATOM_LEN, false) + } else { + node_from_bytes_backrefs(allocator, bytes) + } +} diff --git a/crates/chia-consensus/src/solution_generator.rs b/crates/chia-consensus/src/solution_generator.rs index f13c933a4..9d68da7ec 100644 --- a/crates/chia-consensus/src/solution_generator.rs +++ b/crates/chia-consensus/src/solution_generator.rs @@ -2,7 +2,9 @@ use crate::error::Result; use chia_protocol::Coin; use chia_protocol::CoinSpend; use clvmr::allocator::{Allocator, NodePtr}; -use clvmr::serde::{node_from_bytes_backrefs, node_to_bytes, node_to_bytes_backrefs}; +use clvmr::serde::{ + node_from_bytes_backrefs, node_to_bytes, node_to_bytes_backrefs, serialize_2026, +}; /// the tuple has the Coin, puzzle-reveal and solution pub(crate) fn build_generator(a: &mut Allocator, spends: I) -> Result @@ -106,6 +108,16 @@ where Ok(node_to_bytes_backrefs(&a, generator)?) } +pub fn solution_generator_2026(spends: I) -> Result> +where + BufRef: AsRef<[u8]>, + I: IntoIterator, +{ + let mut a = Allocator::new(); + let generator = build_generator(&mut a, spends)?; + Ok(serialize_2026(&a, generator, 0)?) +} + #[cfg(test)] mod tests { use super::*; diff --git a/wheel/generate_type_stubs.py b/wheel/generate_type_stubs.py index a2f27d2f9..4a9170c57 100644 --- a/wheel/generate_type_stubs.py +++ b/wheel/generate_type_stubs.py @@ -325,6 +325,7 @@ class _Unspec: def solution_generator(spends: Sequence[tuple[Coin, bytes, bytes]]) -> bytes: ... def solution_generator_backrefs(spends: Sequence[tuple[Coin, bytes, bytes]]) -> bytes: ... +def solution_generator_2026(spends: Sequence[tuple[Coin, bytes, bytes]]) -> bytes: ... def is_canonical_serialization(buf: bytes) -> bool: ... diff --git a/wheel/python/chia_rs/chia_rs.pyi b/wheel/python/chia_rs/chia_rs.pyi index 90b6c9bb6..cea472d90 100644 --- a/wheel/python/chia_rs/chia_rs.pyi +++ b/wheel/python/chia_rs/chia_rs.pyi @@ -15,6 +15,7 @@ class _Unspec: def solution_generator(spends: Sequence[tuple[Coin, bytes, bytes]]) -> bytes: ... def solution_generator_backrefs(spends: Sequence[tuple[Coin, bytes, bytes]]) -> bytes: ... +def solution_generator_2026(spends: Sequence[tuple[Coin, bytes, bytes]]) -> bytes: ... def is_canonical_serialization(buf: bytes) -> bool: ... @@ -133,6 +134,7 @@ COMPUTE_FINGERPRINT: int = ... COST_CONDITIONS: int = ... SIMPLE_GENERATOR: int = ... LIMIT_SPENDS: int = ... +SERDE_2026_MAGIC_PREFIX: bytes = ... DISABLE_OP: int = ... CANONICAL_INTS: int = ... ENABLE_SHA256_TREE: int = ... diff --git a/wheel/src/api.rs b/wheel/src/api.rs index 9393a18f6..3d8d4c6fc 100644 --- a/wheel/src/api.rs +++ b/wheel/src/api.rs @@ -17,6 +17,7 @@ use chia_consensus::run_block_generator::{ get_coinspends_for_trusted_block, get_coinspends_with_conditions_for_trusted_block, }; use chia_consensus::solution_generator::solution_generator as native_solution_generator; +use chia_consensus::solution_generator::solution_generator_2026 as native_solution_generator_2026; use chia_consensus::solution_generator::solution_generator_backrefs as native_solution_generator_backrefs; use chia_consensus::spendbundle_conditions::get_conditions_from_spendbundle; use chia_consensus::spendbundle_validation::{ @@ -82,7 +83,7 @@ use clvmr::error::EvalErr; use clvmr::reduction::Reduction; use clvmr::run_program; use clvmr::serde::is_canonical_serialization; -use clvmr::serde::{node_from_bytes, node_from_bytes_backrefs, node_to_bytes}; +use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, node_from_bytes, node_from_bytes_backrefs, node_to_bytes}; use chia_bls::{ BlsCache, DerivableKey, G1Element, GTElement, PublicKey, SecretKey, Signature, @@ -314,6 +315,15 @@ fn solution_generator_backrefs<'p>( )) } +#[pyfunction] +fn solution_generator_2026<'p>( + py: Python<'p>, + spends: &Bound<'_, PyAny>, +) -> PyResult> { + let spends = convert_list_of_tuples(spends)?; + Ok(PyBytes::new(py, &native_solution_generator_2026(spends)?)) +} + #[pyclass] struct AugSchemeMPL {} @@ -789,6 +799,7 @@ pub fn chia_rs(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(additions_and_removals, m)?)?; m.add_function(wrap_pyfunction!(solution_generator, m)?)?; m.add_function(wrap_pyfunction!(solution_generator_backrefs, m)?)?; + m.add_function(wrap_pyfunction!(solution_generator_2026, m)?)?; m.add_function(wrap_pyfunction!(supports_fast_forward, m)?)?; m.add_function(wrap_pyfunction!(fast_forward_singleton, m)?)?; m.add_class::()?; @@ -867,6 +878,10 @@ pub fn chia_rs(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("COST_CONDITIONS", ConsensusFlags::COST_CONDITIONS.bits())?; m.add("SIMPLE_GENERATOR", ConsensusFlags::SIMPLE_GENERATOR.bits())?; m.add("LIMIT_SPENDS", ConsensusFlags::LIMIT_SPENDS.bits())?; + m.add( + "SERDE_2026_MAGIC_PREFIX", + PyBytes::new(py, &SERDE_2026_MAGIC_PREFIX), + )?; // flags from clvm_rs, affecting execution m.add_function(wrap_pyfunction!(run_chia_program, m)?)?; From 0b69738ed59defb6fcd8100eee369bba3752f94d Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 22 May 2026 14:12:35 -0700 Subject: [PATCH 2/9] cargo fmt: wrap long import line in wheel/src/api.rs --- wheel/src/api.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wheel/src/api.rs b/wheel/src/api.rs index 3d8d4c6fc..f8f82fea8 100644 --- a/wheel/src/api.rs +++ b/wheel/src/api.rs @@ -83,7 +83,9 @@ use clvmr::error::EvalErr; use clvmr::reduction::Reduction; use clvmr::run_program; use clvmr::serde::is_canonical_serialization; -use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, node_from_bytes, node_from_bytes_backrefs, node_to_bytes}; +use clvmr::serde::{ + SERDE_2026_MAGIC_PREFIX, node_from_bytes, node_from_bytes_backrefs, node_to_bytes, +}; use chia_bls::{ BlsCache, DerivableKey, G1Element, GTElement, PublicKey, SecretKey, Signature, From de36de0170b43ac012edd81262109a0fde0a203b Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 22 May 2026 14:14:28 -0700 Subject: [PATCH 3/9] generate_type_stubs.py: add SERDE_2026_MAGIC_PREFIX bytes constant --- wheel/generate_type_stubs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wheel/generate_type_stubs.py b/wheel/generate_type_stubs.py index 4a9170c57..4e7ad10c7 100644 --- a/wheel/generate_type_stubs.py +++ b/wheel/generate_type_stubs.py @@ -444,6 +444,7 @@ def compute_plot_group_id_v2(strength: uint8, plot_pk: G1Element, pool_pk: G1Ele COST_CONDITIONS: int = ... SIMPLE_GENERATOR: int = ... LIMIT_SPENDS: int = ... +SERDE_2026_MAGIC_PREFIX: bytes = ... DISABLE_OP: int = ... CANONICAL_INTS: int = ... ENABLE_SHA256_TREE: int = ... From f58878338f8ce9b5bbecb0cf2941a6e33db85b93 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 13 Jul 2026 15:37:13 -0700 Subject: [PATCH 4/9] serde_2026: document why strict=false in node_from_bytes_auto --- crates/chia-consensus/src/serde_2026.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/chia-consensus/src/serde_2026.rs b/crates/chia-consensus/src/serde_2026.rs index 0e5fe7f94..3d8f28512 100644 --- a/crates/chia-consensus/src/serde_2026.rs +++ b/crates/chia-consensus/src/serde_2026.rs @@ -31,6 +31,13 @@ pub fn node_from_bytes_auto(allocator: &mut Allocator, bytes: &[u8]) -> Result Date: Mon, 13 Jul 2026 23:21:26 -0700 Subject: [PATCH 5/9] serde_2026: derive the blob-size bound instead of asserting it Add max_canonical_blob_size(max_cost, cost_per_byte), which computes the largest canonical serde_2026 wire size any within-cost generator can have (max_cost / cost_per_byte + 5 + prefix). The proof lives in its doc comment: the canonical encoding never exceeds interned_vbytes + 5 (tight at atom length 2^20; requires atom lengths < 2^27, guaranteed by CONSENSUS_MAX_ATOM_LEN), and the cost ceiling bounds interned_vbytes. At mainnet constants the bound is 916,677 bytes. CONSENSUS_MAX_BLOB_SIZE stays 10 MiB but its comment now states the principled floor (~917 KB), that the value was inherited from clvm_rs's DeserializeLimits default, and that tightening it (and consensus-vs-peer layering) is deliberately left open for review. Tests: - adversarial shapes where the bound's slack is smallest (max-length atom, >63 distinct atom lengths, >4096 unique pairs, doubling DAG), including exact tightness at the 2^20 atom - generality across arbitrary cost constants, (0, 7) to (u64::MAX, u64::MAX) - the bound is a theorem parameterized by the constants, not a property of 11e9/12000 - new fuzz target serde-2026-size-bound: arbitrary trees AND arbitrary cost constants per run; checks the encoding bound, the derived cap for affordable trees, and blob round-trip identity. Seeded with inputs at 0.98-0.999 of the bound (fuzz/seeds/, corpus/ is gitignored). Fuzzing already caught an overflow in the bound function at extreme constants (fixed with saturating_add). Also: solution_generator_2026 round-trip test, auto-dispatch and size-cap tests for node_from_bytes_auto, and a named SERDE_2026_COMPRESSION_LEVEL constant replacing the bare 0 at the serialize_2026 callsite. Derivation provenance: SERDE2026_UPPER_BOUND.md and SERIALIZATION_DOS_ANALYSIS.md in the generator-identity-hf-analysis repo. --- crates/chia-consensus/fuzz/Cargo.toml | 7 + .../fuzz_targets/serde-2026-size-bound.rs | 65 +++++ .../seeds/serde-2026-size-bound/README.md | 15 + .../seeds/serde-2026-size-bound/near-bound-0 | 2 + .../seeds/serde-2026-size-bound/near-bound-1 | 1 + .../seeds/serde-2026-size-bound/near-bound-2 | 1 + .../seeds/serde-2026-size-bound/near-bound-3 | 1 + .../seeds/serde-2026-size-bound/near-bound-4 | 1 + .../seeds/serde-2026-size-bound/near-bound-5 | 1 + .../seeds/serde-2026-size-bound/near-bound-6 | 1 + .../seeds/serde-2026-size-bound/near-bound-7 | 1 + .../seeds/serde-2026-size-bound/near-bound-8 | 1 + crates/chia-consensus/src/serde_2026.rs | 274 +++++++++++++++++- .../chia-consensus/src/solution_generator.rs | 34 ++- 14 files changed, 400 insertions(+), 5 deletions(-) create mode 100644 crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/README.md create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-0 create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-1 create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-2 create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-3 create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-4 create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-5 create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-6 create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-7 create mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-8 diff --git a/crates/chia-consensus/fuzz/Cargo.toml b/crates/chia-consensus/fuzz/Cargo.toml index d07fb5787..ed68eb60c 100644 --- a/crates/chia-consensus/fuzz/Cargo.toml +++ b/crates/chia-consensus/fuzz/Cargo.toml @@ -149,3 +149,10 @@ path = "fuzz_targets/puzzle-fingerprint.rs" test = false doc = false bench = false + +[[bin]] +name = "serde-2026-size-bound" +path = "fuzz_targets/serde-2026-size-bound.rs" +test = false +doc = false +bench = false diff --git a/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs b/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs new file mode 100644 index 000000000..76f3c82e6 --- /dev/null +++ b/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs @@ -0,0 +1,65 @@ +#![no_main] +use libfuzzer_sys::{arbitrary, fuzz_target}; + +use chia_consensus::generator_cost::interned_vbytes; +use chia_consensus::serde_2026::{ + SERDE_2026_COMPRESSION_LEVEL, max_canonical_blob_size, node_from_bytes_auto, +}; +use clvm_fuzzing::make_tree; +use clvmr::Allocator; +use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, intern_tree, serialize_2026}; + +// Empirically checks the theorem behind `max_canonical_blob_size` (see the +// proof on that function; this target hunts for counterexamples): +// +// 1. Encoding bound: for ANY CLVM tree, the canonical serde_2026 wire +// encoding is at most interned_vbytes(tree) + 5 bytes plus the magic +// prefix. +// 2. Corollary, at fuzzed cost constants: if the tree is affordable under +// (max_cost, cost_per_byte), its blob fits in +// max_canonical_blob_size(max_cost, cost_per_byte). +// +// Also verifies the blob round-trips to the same tree. +fuzz_target!(|data: &[u8]| { + let mut unstructured = arbitrary::Unstructured::new(data); + let max_cost: u64 = unstructured.arbitrary().unwrap_or(11_000_000_000); + let cost_per_byte: u64 = unstructured.arbitrary().unwrap_or(12_000); + + let mut a = Allocator::new(); + let (node, _) = make_tree(&mut a, &mut unstructured); + + let blob = serialize_2026(&a, node, SERDE_2026_COMPRESSION_LEVEL).expect("serialize_2026"); + + let tree = intern_tree(&a, node).expect("intern_tree"); + let vbytes = interned_vbytes(&tree); + let bound = vbytes as usize + 5 + SERDE_2026_MAGIC_PREFIX.len(); + assert!( + blob.len() <= bound, + "size bound violated: blob {} > interned_vbytes-derived bound {}", + blob.len(), + bound + ); + + // Corollary at arbitrary cost constants: any tree affordable under + // (max_cost, cost_per_byte) must encode within the derived cap. + if vbytes + .checked_mul(cost_per_byte) + .is_some_and(|c| c <= max_cost) + { + let cap = max_canonical_blob_size(max_cost, cost_per_byte); + assert!( + blob.len() <= cap, + "cap violated: blob {} > max_canonical_blob_size({max_cost}, {cost_per_byte}) = {cap}", + blob.len(), + ); + } + + // Round-trip check via canonical re-serialization: serialize_2026 is + // deterministic and DAG-aware, so equal trees produce equal blobs. + // (Comparing classic encodings instead would blow up on trees whose + // classic expansion is huge — compressing those is the format's point.) + let mut b = Allocator::new(); + let parsed = node_from_bytes_auto(&mut b, &blob).expect("node_from_bytes_auto"); + let blob2 = serialize_2026(&b, parsed, SERDE_2026_COMPRESSION_LEVEL).expect("serialize_2026"); + assert_eq!(blob, blob2, "round-trip mismatch"); +}); diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/README.md b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/README.md new file mode 100644 index 000000000..5eabbb067 --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/README.md @@ -0,0 +1,15 @@ +# Seeds for `serde-2026-size-bound` + +Starting inputs whose trees encode very close to the proven size bound +(fullness 0.98-0.999 of `interned_vbytes + 5 + prefix`). Random exploration +rarely reaches this boundary on its own; seeding here makes the fuzzer mutate +*around* the tight edge of the theorem instead of hoping to wander there. + +Found by a fullness-maximizing random search over the target's input format +(16 bytes of cost constants + `clvm_fuzzing::make_tree` entropy). + +Use as an extra corpus directory: + +```sh +cargo fuzz run serde-2026-size-bound corpus/serde-2026-size-bound seeds/serde-2026-size-bound +``` diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-0 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-0 new file mode 100644 index 000000000..75c4f8ba0 --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-0 @@ -0,0 +1,2 @@ +vt…ꪪŽ< ¦—vÇ +Æ1“ЛMe¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥ \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-1 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-1 new file mode 100644 index 000000000..298f3dc95 --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-1 @@ -0,0 +1 @@ +õÐq$J_™:§¦Ò›Ÿá%ÒéßE \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-2 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-2 new file mode 100644 index 000000000..e5437df6a --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-2 @@ -0,0 +1 @@ +eÇ¢Ý Û•'D D>¢•b_33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333 \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-3 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-3 new file mode 100644 index 000000000..d292459f6 --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-3 @@ -0,0 +1 @@ +Í~"í­i¹ð°˜Ö 2 EòOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-4 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-4 new file mode 100644 index 000000000..7b7bc327b --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-4 @@ -0,0 +1 @@ +¨ýÈ÷86ë¢:ÃiQë; 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-5 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-5 new file mode 100644 index 000000000..fe90693d5 --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-5 @@ -0,0 +1 @@ +ÀãCšÞa‘+È16‰m GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-6 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-6 new file mode 100644 index 000000000..8c06e160e --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-6 @@ -0,0 +1 @@ +î[ˆ}¬ËŒÚQd¤Áþæˆ!MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-7 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-7 new file mode 100644 index 000000000..2c7951a39 --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-7 @@ -0,0 +1 @@ +ªBN^wEwùxyÖ‹UtÆ SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-8 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-8 new file mode 100644 index 000000000..66fe15530 --- /dev/null +++ b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-8 @@ -0,0 +1 @@ +O\uP=p2)1žM0ó¡ÀRX11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 \ No newline at end of file diff --git a/crates/chia-consensus/src/serde_2026.rs b/crates/chia-consensus/src/serde_2026.rs index 3d8f28512..b23dfef9f 100644 --- a/crates/chia-consensus/src/serde_2026.rs +++ b/crates/chia-consensus/src/serde_2026.rs @@ -17,15 +17,89 @@ use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, deserialize_2026, node_from_bytes_ba /// pre-allocation cap, not a hard consensus rule. pub const CONSENSUS_MAX_ATOM_LEN: usize = 1 << 20; -/// Maximum total blob size accepted by consensus deserialization (10 MiB). +/// Compression level passed to [`clvmr::serde::serialize_2026`] when chia +/// produces serde_2026 blobs. +/// +/// The level only affects the serializer's effort/output size; every level +/// produces blobs that the one deserializer accepts (like zlib levels). +/// clvmr keeps its `Compression` enum private and takes a bare `u32`, +/// saturating values above the highest implemented level. Level 0 is the +/// fast/left-first encoding (currently the only one implemented). +pub const SERDE_2026_COMPRESSION_LEVEL: u32 = 0; + +/// Maximum serde_2026 wire size, in bytes, of any generator whose cost fits +/// within `max_cost` at `cost_per_byte` — i.e. every generator that could +/// possibly be valid under those constants has a canonical encoding no +/// larger than this. A blob above this size is either over-cost or +/// non-minimally encoded (and its sender could re-encode it smaller). +/// +/// # Why such a bound matters +/// +/// Under the interned cost model, cost is charged on the *deduplicated* +/// tree, but the decoder must process every *wire* byte before any cost is +/// charged. Cost alone therefore does not bound pre-charge decoding work; a +/// size cap derived from this function does. +/// +/// # Derivation +/// +/// Let a tree have `atom_bytes` total atom payload, `U_a` unique atoms and +/// `U_p` unique pairs, so its interned weight (see +/// [`generator_cost::interned_vbytes`](crate::generator_cost::interned_vbytes)) +/// is `vbytes = atom_bytes + 2*U_a + 3*U_p`. +/// +/// **Step 1 — encoding bound:** the canonical serde_2026 *body* is at most +/// `vbytes + 5` bytes. Sketch: the atom table costs at most 2 bytes of +/// overhead per atom (length varint, amortized group headers) plus a group +/// count header; the instruction stream is exactly `2*U_p + 1` instructions +/// (each push adds one stack entry, each cons nets -1, one root remains), +/// costing 1 byte per cons and at most 2 bytes per push, plus a count +/// header; the headers and per-item slack together never exceed the `+5` +/// because `U_a <= U_p + 1` forces cheap 1-byte pushes to exist whenever the +/// headers grow. The bound is tight (slack reaches 0 at atom length 2^20) +/// and requires atom lengths < 2^27, guaranteed by +/// [`CONSENSUS_MAX_ATOM_LEN`]. The wire blob adds the +/// [`SERDE_2026_MAGIC_PREFIX`] on top of the body. +/// +/// **Step 2 — cost bound:** consensus charges `vbytes * cost_per_byte`, and +/// rejects anything over `max_cost`, so any potentially-valid generator has +/// `vbytes <= max_cost / cost_per_byte`. +/// +/// Combining: `wire_size <= max_cost / cost_per_byte + 5 + prefix_len`. +/// +/// If `cost_per_byte` is 0, bytes are free and no size is over-cost, so the +/// bound degenerates to `usize::MAX` (unbounded). +/// +/// At mainnet constants (`max_cost` = 11e9, `cost_per_byte` = 12_000) this +/// is 916_677 bytes, a little under 1 MiB. +pub fn max_canonical_blob_size(max_cost: u64, cost_per_byte: u64) -> usize { + if cost_per_byte == 0 { + // Free bytes: no blob size exhausts the budget, so the least upper + // bound is "unbounded". + return usize::MAX; + } + // Saturating: a clamped result is still a correct upper bound, and this + // keeps the function total for extreme (non-mainnet) constants. + ((max_cost / cost_per_byte) as usize).saturating_add(5 + SERDE_2026_MAGIC_PREFIX.len()) +} + +/// Maximum total blob size accepted by consensus deserialization. +/// +/// The principled floor for this value is [`max_canonical_blob_size`] at the +/// network's cost constants (~917 KB on mainnet) — see its doc for the +/// proof. The current value is ~11x that floor: it was inherited from +/// clvm_rs's `DeserializeLimits` default, and the extra headroom admits +/// non-minimal encodings (accepted because `strict = false` below). Whether +/// to tighten it toward the floor, and whether a size cap belongs here or at +/// the peer-protocol layer, are deliberately left open for review. pub const CONSENSUS_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024; /// Deserialize CLVM bytes, auto-detecting classic / backrefs / serde_2026. /// /// Sniffs `SERDE_2026_MAGIC_PREFIX` at the head of `bytes`; if present, -/// dispatches to [`deserialize_2026`] with consensus caps (atom ≤ 1 MiB, -/// total blob ≤ 10 MiB). Otherwise falls back to [`node_from_bytes_backrefs`] -/// (which also accepts plain classic). +/// dispatches to [`deserialize_2026`] with consensus caps +/// ([`CONSENSUS_MAX_ATOM_LEN`], [`CONSENSUS_MAX_BLOB_SIZE`]). Otherwise +/// falls back to [`node_from_bytes_backrefs`] (which also accepts plain +/// classic). pub fn node_from_bytes_auto(allocator: &mut Allocator, bytes: &[u8]) -> Result { if bytes.len() > CONSENSUS_MAX_BLOB_SIZE { return Err(EvalErr::SerializationError); @@ -43,3 +117,195 @@ pub fn node_from_bytes_auto(allocator: &mut Allocator, bytes: &[u8]) -> Result NodePtr { + let atom = a.new_atom(b"hello world, this is a test atom").unwrap(); + let pair = a.new_pair(atom, atom).unwrap(); + a.new_pair(pair, pair).unwrap() + } + + /// The provable per-tree wire-size bound: interned_vbytes + 5 + prefix. + fn encoding_bound(a: &Allocator, node: NodePtr) -> usize { + let tree = intern_tree(a, node).unwrap(); + interned_vbytes(&tree) as usize + 5 + SERDE_2026_MAGIC_PREFIX.len() + } + + /// Adversarial tree shapes: the configurations where the encoding + /// bound's slack is smallest (each stresses a different header/varint + /// growth case in the proof on [`max_canonical_blob_size`]). + fn tight_trees() -> Vec<(Allocator, NodePtr)> { + let mut trees = Vec::new(); + + // Single atom at the max length: the bound is exactly tight here. + let mut a = Allocator::new(); + let node = a.new_atom(&vec![0xa5; CONSENSUS_MAX_ATOM_LEN]).unwrap(); + trees.push((a, node)); + + // >63 distinct atom lengths: forces a 2-byte atom-group-count header. + let mut a = Allocator::new(); + let mut node = a.nil(); + for len in 0..=63usize { + let atom = a.new_atom(&vec![0x5a; len]).unwrap(); + node = a.new_pair(atom, node).unwrap(); + } + trees.push((a, node)); + + // >4096 unique pairs: forces a 3-byte instruction-count header, and + // pushes/back-references beyond the 1-byte varint range. + let mut a = Allocator::new(); + let mut node = a.nil(); + for i in 1..=5000u32 { + let atom = a.new_number(i.into()).unwrap(); + node = a.new_pair(atom, node).unwrap(); + } + trees.push((a, node)); + + // Doubling DAG: maximal sharing, so wire bytes come almost entirely + // from back-references rather than atom payload. + let mut a = Allocator::new(); + let mut node = a.one(); + for _ in 0..20 { + node = a.new_pair(node, node).unwrap(); + } + trees.push((a, node)); + + // Small mixed tree. + let mut a = Allocator::new(); + let node = sample_tree(&mut a); + trees.push((a, node)); + + trees + } + + #[test] + fn test_encoding_bound_holds_on_tight_shapes() { + for (a, node) in tight_trees() { + let blob = serialize_2026(&a, node, SERDE_2026_COMPRESSION_LEVEL).unwrap(); + assert!( + blob.len() <= encoding_bound(&a, node), + "encoding bound violated: blob {} > bound {}", + blob.len(), + encoding_bound(&a, node) + ); + } + } + + #[test] + fn test_encoding_bound_tight_at_max_atom_len() { + // A single atom of exactly CONSENSUS_MAX_ATOM_LEN (2^20) bytes is the + // known worst case: the encoding uses every byte the bound allows. + // If this stops being exact, the "+5" analysis has changed — revisit + // the proof on max_canonical_blob_size. + let mut a = Allocator::new(); + let node = a.new_atom(&vec![0xa5; CONSENSUS_MAX_ATOM_LEN]).unwrap(); + let blob = serialize_2026(&a, node, SERDE_2026_COMPRESSION_LEVEL).unwrap(); + assert_eq!(blob.len(), encoding_bound(&a, node)); + } + + #[rstest] + // mainnet constants + #[case(11_000_000_000, 12_000)] + // tiny budget: only trivial trees are affordable + #[case(100, 1)] + // zero budget: nothing is affordable, cap is just the fixed overhead + #[case(0, 7)] + // free bytes / huge budget extremes + #[case(u64::MAX, 1)] + #[case(u64::MAX, u64::MAX)] + // zero cost per byte: everything is affordable, cap must be unbounded + #[case(11_000_000_000, 0)] + #[case(0, 0)] + // awkward non-divisible pair + #[case(1_000_003, 17)] + fn test_max_canonical_blob_size_general(#[case] max_cost: u64, #[case] cost_per_byte: u64) { + // The theorem is generic over the cost constants: for ANY + // (max_cost, cost_per_byte), every tree affordable under them + // encodes within the derived cap. + let cap = max_canonical_blob_size(max_cost, cost_per_byte); + for (a, node) in tight_trees() { + let tree = intern_tree(&a, node).unwrap(); + let affordable = interned_vbytes(&tree) + .checked_mul(cost_per_byte) + .is_some_and(|cost| cost <= max_cost); + if affordable { + let blob = serialize_2026(&a, node, SERDE_2026_COMPRESSION_LEVEL).unwrap(); + assert!( + blob.len() <= cap, + "cap violated at ({max_cost}, {cost_per_byte}): blob {} > cap {cap}", + blob.len(), + ); + } + } + } + + #[test] + fn test_auto_dispatch_all_formats() { + let mut a = Allocator::new(); + let node = sample_tree(&mut a); + let expected = node_to_bytes(&a, node).unwrap(); + + let classic = expected.clone(); + let backrefs = node_to_bytes_backrefs(&a, node).unwrap(); + let serde2026 = serialize_2026(&a, node, 0).unwrap(); + assert!(serde2026.starts_with(&SERDE_2026_MAGIC_PREFIX)); + + for blob in [classic, backrefs, serde2026] { + let mut b = Allocator::new(); + let parsed = node_from_bytes_auto(&mut b, &blob).expect("node_from_bytes_auto"); + assert_eq!(node_to_bytes(&b, parsed).unwrap(), expected); + } + } + + #[test] + fn test_max_canonical_blob_size_at_real_constants() { + use crate::consensus_constants::TEST_CONSTANTS; + // Ties the doc-comment numbers to the real consensus constants so + // drift gets caught here instead of silently invalidating the bound. + let bound = max_canonical_blob_size( + TEST_CONSTANTS.max_block_cost_clvm, + TEST_CONSTANTS.cost_per_byte, + ); + assert_eq!(bound, 916_677); + assert!(CONSENSUS_MAX_BLOB_SIZE >= bound); + } + + #[test] + fn test_blob_size_cap() { + // One byte over the cap must be rejected before any parsing, + // regardless of format. + let mut blob = vec![0u8; CONSENSUS_MAX_BLOB_SIZE + 1]; + blob[..SERDE_2026_MAGIC_PREFIX.len()].copy_from_slice(&SERDE_2026_MAGIC_PREFIX); + let mut a = Allocator::new(); + assert!(matches!( + node_from_bytes_auto(&mut a, &blob), + Err(EvalErr::SerializationError) + )); + + // At exactly the cap, the size gate lets the blob through to the + // parser (an all-zeros blob happens to parse on the backrefs path: + // leading 0x00 is a valid atom). + let blob = vec![0u8; CONSENSUS_MAX_BLOB_SIZE]; + let mut a = Allocator::new(); + assert!(node_from_bytes_auto(&mut a, &blob).is_ok()); + + // A valid small blob parses. + let mut a = Allocator::new(); + let node = sample_tree(&mut a); + let blob = serialize_2026(&a, node, 0).unwrap(); + let mut b = Allocator::new(); + let parsed = node_from_bytes_auto(&mut b, &blob).unwrap(); + assert_eq!( + node_to_bytes(&b, parsed).unwrap(), + node_to_bytes(&a, node).unwrap() + ); + } +} diff --git a/crates/chia-consensus/src/solution_generator.rs b/crates/chia-consensus/src/solution_generator.rs index 9d68da7ec..d9adc08eb 100644 --- a/crates/chia-consensus/src/solution_generator.rs +++ b/crates/chia-consensus/src/solution_generator.rs @@ -1,4 +1,5 @@ use crate::error::Result; +use crate::serde_2026::SERDE_2026_COMPRESSION_LEVEL; use chia_protocol::Coin; use chia_protocol::CoinSpend; use clvmr::allocator::{Allocator, NodePtr}; @@ -115,7 +116,7 @@ where { let mut a = Allocator::new(); let generator = build_generator(&mut a, spends)?; - Ok(serialize_2026(&a, generator, 0)?) + Ok(serialize_2026(&a, generator, SERDE_2026_COMPRESSION_LEVEL)?) } #[cfg(test)] @@ -456,6 +457,37 @@ mod tests { assert_eq!(generator_output, EXPECTED_GENERATOR_OUTPUT); } + #[test] + fn test_solution_generator_2026() { + use crate::serde_2026::node_from_bytes_auto; + use clvmr::serde::SERDE_2026_MAGIC_PREFIX; + + let coin1: Coin = Coin::new( + hex!("ccd5bb71183532bff220ba46c268991a00000000000000000000000000036840").into(), + hex!("fcc78a9e396df6ceebc217d2446bc016e0b3d5922fb32e5783ec5a85d490cfb6").into(), + 1_750_000_000_000, + ); + let coin2: Coin = Coin::new( + hex!("ccd5bb71183532bff220ba46c268991a00000000000000000000000000000000").into(), + hex!("d23da14695a188ae5708dd152263c4db883eb27edeb936178d4d988b8f3ce5fc").into(), + 18_375_000_000_000_000_000, + ); + let spends = [ + (coin1, PUZZLE1.as_ref(), SOLUTION1.as_ref()), + (coin2, PUZZLE2.as_ref(), SOLUTION2.as_ref()), + ]; + + let result = solution_generator_2026(spends).expect("solution_generator_2026"); + assert!(result.starts_with(&SERDE_2026_MAGIC_PREFIX)); + + // Round-trip through the consensus auto-deserializer and confirm the + // tree is identical to the one behind the classic encoding. + let mut a = Allocator::new(); + let node = node_from_bytes_auto(&mut a, &result).expect("node_from_bytes_auto"); + let classic = solution_generator(spends).expect("solution_generator"); + assert_eq!(node_to_bytes(&a, node).expect("node_to_bytes"), classic); + } + #[rstest] #[case(0)] #[case(1)] From beb1e8775d36e54e02ddb393f1d03b2abed15086 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 13 Jul 2026 23:26:08 -0700 Subject: [PATCH 6/9] serde_2026: make the blob-size cap caller-supplied, not hardcoded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop CONSENSUS_MAX_BLOB_SIZE (10 MiB, inherited from clvm_rs's DeserializeLimits default, unconnected to any consensus property). node_from_bytes_auto now takes max_blob_size, which callers derive from the network's cost constants via max_canonical_blob_size — the limit is a property of max_block_cost_clvm / cost_per_byte and the proof, not a standalone number. Any headroom multiplier (e.g. to tolerate the non-minimal encodings that strict=false admits) is caller policy. --- .../fuzz_targets/serde-2026-size-bound.rs | 4 +- crates/chia-consensus/src/serde_2026.rs | 87 ++++++++++--------- .../chia-consensus/src/solution_generator.rs | 9 +- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs b/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs index 76f3c82e6..dc2a441ff 100644 --- a/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs +++ b/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs @@ -58,8 +58,10 @@ fuzz_target!(|data: &[u8]| { // deterministic and DAG-aware, so equal trees produce equal blobs. // (Comparing classic encodings instead would blow up on trees whose // classic expansion is huge — compressing those is the format's point.) + // Passing `bound` as the size cap doubles as a check that the gate + // admits every canonical blob. let mut b = Allocator::new(); - let parsed = node_from_bytes_auto(&mut b, &blob).expect("node_from_bytes_auto"); + let parsed = node_from_bytes_auto(&mut b, &blob, bound).expect("node_from_bytes_auto"); let blob2 = serialize_2026(&b, parsed, SERDE_2026_COMPRESSION_LEVEL).expect("serialize_2026"); assert_eq!(blob, blob2, "round-trip mismatch"); }); diff --git a/crates/chia-consensus/src/serde_2026.rs b/crates/chia-consensus/src/serde_2026.rs index b23dfef9f..8b45ecc89 100644 --- a/crates/chia-consensus/src/serde_2026.rs +++ b/crates/chia-consensus/src/serde_2026.rs @@ -82,26 +82,24 @@ pub fn max_canonical_blob_size(max_cost: u64, cost_per_byte: u64) -> usize { ((max_cost / cost_per_byte) as usize).saturating_add(5 + SERDE_2026_MAGIC_PREFIX.len()) } -/// Maximum total blob size accepted by consensus deserialization. -/// -/// The principled floor for this value is [`max_canonical_blob_size`] at the -/// network's cost constants (~917 KB on mainnet) — see its doc for the -/// proof. The current value is ~11x that floor: it was inherited from -/// clvm_rs's `DeserializeLimits` default, and the extra headroom admits -/// non-minimal encodings (accepted because `strict = false` below). Whether -/// to tighten it toward the floor, and whether a size cap belongs here or at -/// the peer-protocol layer, are deliberately left open for review. -pub const CONSENSUS_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024; - /// Deserialize CLVM bytes, auto-detecting classic / backrefs / serde_2026. /// /// Sniffs `SERDE_2026_MAGIC_PREFIX` at the head of `bytes`; if present, -/// dispatches to [`deserialize_2026`] with consensus caps -/// ([`CONSENSUS_MAX_ATOM_LEN`], [`CONSENSUS_MAX_BLOB_SIZE`]). Otherwise -/// falls back to [`node_from_bytes_backrefs`] (which also accepts plain -/// classic). -pub fn node_from_bytes_auto(allocator: &mut Allocator, bytes: &[u8]) -> Result { - if bytes.len() > CONSENSUS_MAX_BLOB_SIZE { +/// dispatches to [`deserialize_2026`] with the per-atom consensus cap +/// ([`CONSENSUS_MAX_ATOM_LEN`]). Otherwise falls back to +/// [`node_from_bytes_backrefs`] (which also accepts plain classic). +/// +/// `max_blob_size` bounds the total wire size accepted; blobs above it are +/// rejected before any parsing. Callers should derive it from the network's +/// cost constants via [`max_canonical_blob_size`] (any headroom multiplier +/// on top — e.g. to tolerate non-minimal encodings, which `strict = false` +/// otherwise admits — is caller policy). +pub fn node_from_bytes_auto( + allocator: &mut Allocator, + bytes: &[u8], + max_blob_size: usize, +) -> Result { + if bytes.len() > max_blob_size { return Err(EvalErr::SerializationError); } if bytes.starts_with(&SERDE_2026_MAGIC_PREFIX) { @@ -260,52 +258,55 @@ mod tests { for blob in [classic, backrefs, serde2026] { let mut b = Allocator::new(); - let parsed = node_from_bytes_auto(&mut b, &blob).expect("node_from_bytes_auto"); + let parsed = + node_from_bytes_auto(&mut b, &blob, mainnet_cap()).expect("node_from_bytes_auto"); assert_eq!(node_to_bytes(&b, parsed).unwrap(), expected); } } - #[test] - fn test_max_canonical_blob_size_at_real_constants() { + /// The derived cap at the real consensus constants. + fn mainnet_cap() -> usize { use crate::consensus_constants::TEST_CONSTANTS; - // Ties the doc-comment numbers to the real consensus constants so - // drift gets caught here instead of silently invalidating the bound. - let bound = max_canonical_blob_size( + max_canonical_blob_size( TEST_CONSTANTS.max_block_cost_clvm, TEST_CONSTANTS.cost_per_byte, - ); - assert_eq!(bound, 916_677); - assert!(CONSENSUS_MAX_BLOB_SIZE >= bound); + ) + } + + #[test] + fn test_max_canonical_blob_size_at_real_constants() { + // Ties the doc-comment number to the real consensus constants so + // drift gets caught here instead of silently invalidating the bound. + assert_eq!(mainnet_cap(), 916_677); } #[test] fn test_blob_size_cap() { - // One byte over the cap must be rejected before any parsing, - // regardless of format. - let mut blob = vec![0u8; CONSENSUS_MAX_BLOB_SIZE + 1]; - blob[..SERDE_2026_MAGIC_PREFIX.len()].copy_from_slice(&SERDE_2026_MAGIC_PREFIX); let mut a = Allocator::new(); + let node = sample_tree(&mut a); + let blob = serialize_2026(&a, node, SERDE_2026_COMPRESSION_LEVEL).unwrap(); + + // One byte over the cap: rejected before any parsing. + let mut b = Allocator::new(); assert!(matches!( - node_from_bytes_auto(&mut a, &blob), + node_from_bytes_auto(&mut b, &blob, blob.len() - 1), Err(EvalErr::SerializationError) )); - // At exactly the cap, the size gate lets the blob through to the - // parser (an all-zeros blob happens to parse on the backrefs path: - // leading 0x00 is a valid atom). - let blob = vec![0u8; CONSENSUS_MAX_BLOB_SIZE]; - let mut a = Allocator::new(); - assert!(node_from_bytes_auto(&mut a, &blob).is_ok()); - - // A valid small blob parses. - let mut a = Allocator::new(); - let node = sample_tree(&mut a); - let blob = serialize_2026(&a, node, 0).unwrap(); + // At exactly the cap: parses. let mut b = Allocator::new(); - let parsed = node_from_bytes_auto(&mut b, &blob).unwrap(); + let parsed = node_from_bytes_auto(&mut b, &blob, blob.len()).unwrap(); assert_eq!( node_to_bytes(&b, parsed).unwrap(), node_to_bytes(&a, node).unwrap() ); + + // The size gate applies to non-serde_2026 formats too. + let classic = node_to_bytes(&a, node).unwrap(); + let mut b = Allocator::new(); + assert!(matches!( + node_from_bytes_auto(&mut b, &classic, classic.len() - 1), + Err(EvalErr::SerializationError) + )); } } diff --git a/crates/chia-consensus/src/solution_generator.rs b/crates/chia-consensus/src/solution_generator.rs index d9adc08eb..4f8dc664c 100644 --- a/crates/chia-consensus/src/solution_generator.rs +++ b/crates/chia-consensus/src/solution_generator.rs @@ -459,7 +459,8 @@ mod tests { #[test] fn test_solution_generator_2026() { - use crate::serde_2026::node_from_bytes_auto; + use crate::consensus_constants::TEST_CONSTANTS; + use crate::serde_2026::{max_canonical_blob_size, node_from_bytes_auto}; use clvmr::serde::SERDE_2026_MAGIC_PREFIX; let coin1: Coin = Coin::new( @@ -482,8 +483,12 @@ mod tests { // Round-trip through the consensus auto-deserializer and confirm the // tree is identical to the one behind the classic encoding. + let cap = max_canonical_blob_size( + TEST_CONSTANTS.max_block_cost_clvm, + TEST_CONSTANTS.cost_per_byte, + ); let mut a = Allocator::new(); - let node = node_from_bytes_auto(&mut a, &result).expect("node_from_bytes_auto"); + let node = node_from_bytes_auto(&mut a, &result, cap).expect("node_from_bytes_auto"); let classic = solution_generator(spends).expect("solution_generator"); assert_eq!(node_to_bytes(&a, node).expect("node_to_bytes"), classic); } From 80131ead93ab7a23ab5a99d669d5fdee8276b6fb Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Mon, 13 Jul 2026 23:30:26 -0700 Subject: [PATCH 7/9] fuzz seeds: replace checked-in binaries with a deterministic generator Drop the 9 binary near-bound seed files in favor of a gen_serde_2026_fuzz_seeds example that reproduces them (fixed RNG seed): a fullness-maximizing search over the fuzz target's input format, writing seeds at 0.98-0.999 of the proven bound directly into the gitignored corpus dir. Reviewable and regenerable instead of opaque blobs in git. --- Cargo.lock | 2 + crates/chia-consensus/Cargo.toml | 2 + .../examples/gen_serde_2026_fuzz_seeds.rs | 95 +++++++++++++++++++ .../fuzz_targets/serde-2026-size-bound.rs | 4 + .../seeds/serde-2026-size-bound/README.md | 15 --- .../seeds/serde-2026-size-bound/near-bound-0 | 2 - .../seeds/serde-2026-size-bound/near-bound-1 | 1 - .../seeds/serde-2026-size-bound/near-bound-2 | 1 - .../seeds/serde-2026-size-bound/near-bound-3 | 1 - .../seeds/serde-2026-size-bound/near-bound-4 | 1 - .../seeds/serde-2026-size-bound/near-bound-5 | 1 - .../seeds/serde-2026-size-bound/near-bound-6 | 1 - .../seeds/serde-2026-size-bound/near-bound-7 | 1 - .../seeds/serde-2026-size-bound/near-bound-8 | 1 - 14 files changed, 103 insertions(+), 25 deletions(-) create mode 100644 crates/chia-consensus/examples/gen_serde_2026_fuzz_seeds.rs delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/README.md delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-0 delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-1 delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-2 delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-3 delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-4 delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-5 delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-6 delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-7 delete mode 100644 crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-8 diff --git a/Cargo.lock b/Cargo.lock index c7e110135..745195eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,6 +384,7 @@ dependencies = [ name = "chia-consensus" version = "0.46.0" dependencies = [ + "arbitrary", "bitflags", "blocking-threadpool", "chia-bls 0.46.0", @@ -394,6 +395,7 @@ dependencies = [ "chia-traits 0.46.0", "chia_py_streamable_macro", "chia_streamable_macro 0.46.0", + "clvm-fuzzing", "clvm-traits", "clvm-utils", "clvmr", diff --git a/crates/chia-consensus/Cargo.toml b/crates/chia-consensus/Cargo.toml index c278047ea..e8e82b4c1 100644 --- a/crates/chia-consensus/Cargo.toml +++ b/crates/chia-consensus/Cargo.toml @@ -44,6 +44,8 @@ text-diff = { workspace = true } criterion = { workspace = true } rand = { workspace = true, features = [ "small_rng" ] } blocking-threadpool = { workspace = true } +clvm-fuzzing = { workspace = true } +arbitrary = { workspace = true } [lib] bench = false diff --git a/crates/chia-consensus/examples/gen_serde_2026_fuzz_seeds.rs b/crates/chia-consensus/examples/gen_serde_2026_fuzz_seeds.rs new file mode 100644 index 000000000..462caea7d --- /dev/null +++ b/crates/chia-consensus/examples/gen_serde_2026_fuzz_seeds.rs @@ -0,0 +1,95 @@ +//! Generate seed inputs for the `serde-2026-size-bound` fuzz target. +//! +//! The target checks that serde_2026 encodings stay within the proven size +//! bound (see `chia_consensus::serde_2026::max_canonical_blob_size`). The +//! bound is only *tight* for specific tree shapes, which coverage-guided +//! fuzzing rarely reaches on its own. This example searches random inputs +//! (through the exact decode path the fuzz target uses) for trees whose +//! encoding comes closest to the bound, and writes the best as seeds, so +//! fuzz runs mutate around the boundary instead of wandering toward it. +//! +//! Deterministic: fixed RNG seed, so regenerated seeds are reproducible. +//! +//! ```sh +//! cargo run --release --example gen_serde_2026_fuzz_seeds -- \ +//! fuzz/corpus/serde-2026-size-bound +//! cargo fuzz run serde-2026-size-bound +//! ``` + +use chia_consensus::generator_cost::interned_vbytes; +use chia_consensus::serde_2026::SERDE_2026_COMPRESSION_LEVEL; +use clvm_fuzzing::make_tree; +use clvmr::Allocator; +use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, intern_tree, serialize_2026}; +use std::fs; + +struct XorShift(u64); + +impl XorShift { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +/// Interpret `data` exactly as the fuzz target does and return +/// (blob_size / bound, blob_size). +fn fullness(data: &[u8]) -> Option<(f64, usize)> { + let mut u = arbitrary::Unstructured::new(data); + let _max_cost: u64 = u.arbitrary().ok()?; + let _cost_per_byte: u64 = u.arbitrary().ok()?; + let mut a = Allocator::new(); + let (node, _) = make_tree(&mut a, &mut u); + let blob = serialize_2026(&a, node, SERDE_2026_COMPRESSION_LEVEL).ok()?; + let tree = intern_tree(&a, node).ok()?; + let bound = interned_vbytes(&tree) as usize + 5 + SERDE_2026_MAGIC_PREFIX.len(); + Some((blob.len() as f64 / bound as f64, blob.len())) +} + +fn main() { + let out = std::env::args() + .nth(1) + .unwrap_or_else(|| "fuzz/corpus/serde-2026-size-bound".to_string()); + fs::create_dir_all(&out).unwrap(); + + let mut rng = XorShift(0x9e37_79b9_7f4a_7c15); + let mut best: Vec<(f64, usize, Vec)> = Vec::new(); + + for round in 0..400_000u64 { + let len = 16 + (rng.next() % 3000) as usize; + let mut data = vec![0u8; len]; + for b in &mut data { + *b = (rng.next() >> 32) as u8; + } + // Bias some inputs toward long constant runs, which favors large + // atoms and deep spines over noise. + if round % 3 == 0 { + let run_byte = (rng.next() >> 24) as u8; + let start = 16 + (rng.next() as usize % (len - 16).max(1)).min(len - 16); + for b in &mut data[start..] { + *b = run_byte; + } + } + if let Some((score, size)) = fullness(&data) { + best.push((score, size, data)); + best.sort_by(|x, y| y.0.total_cmp(&x.0)); + best.truncate(200); + } + } + + // Keep the fullest inputs across several blob-size buckets, so seeds + // aren't all tiny. + let mut count = 0; + for (lo, hi) in [(0, 100), (100, 1000), (1000, 10_000), (10_000, usize::MAX)] { + for (score, size, data) in best.iter().filter(|e| e.1 >= lo && e.1 < hi).take(3) { + let path = format!("{out}/near-bound-{count:02}"); + fs::write(&path, data).unwrap(); + println!("{path}: fullness {score:.4}, blob {size} bytes"); + count += 1; + } + } +} diff --git a/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs b/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs index dc2a441ff..e2d7c4230 100644 --- a/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs +++ b/crates/chia-consensus/fuzz/fuzz_targets/serde-2026-size-bound.rs @@ -20,6 +20,10 @@ use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, intern_tree, serialize_2026}; // max_canonical_blob_size(max_cost, cost_per_byte). // // Also verifies the blob round-trips to the same tree. +// +// The bound is only tight for specific tree shapes; seed the corpus with +// near-bound inputs first (see the gen_serde_2026_fuzz_seeds example in +// chia-consensus) so mutation starts at the boundary. fuzz_target!(|data: &[u8]| { let mut unstructured = arbitrary::Unstructured::new(data); let max_cost: u64 = unstructured.arbitrary().unwrap_or(11_000_000_000); diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/README.md b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/README.md deleted file mode 100644 index 5eabbb067..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Seeds for `serde-2026-size-bound` - -Starting inputs whose trees encode very close to the proven size bound -(fullness 0.98-0.999 of `interned_vbytes + 5 + prefix`). Random exploration -rarely reaches this boundary on its own; seeding here makes the fuzzer mutate -*around* the tight edge of the theorem instead of hoping to wander there. - -Found by a fullness-maximizing random search over the target's input format -(16 bytes of cost constants + `clvm_fuzzing::make_tree` entropy). - -Use as an extra corpus directory: - -```sh -cargo fuzz run serde-2026-size-bound corpus/serde-2026-size-bound seeds/serde-2026-size-bound -``` diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-0 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-0 deleted file mode 100644 index 75c4f8ba0..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-0 +++ /dev/null @@ -1,2 +0,0 @@ -vt…ꪪŽ< ¦—vÇ -Æ1“ЛMe¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥ \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-1 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-1 deleted file mode 100644 index 298f3dc95..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-1 +++ /dev/null @@ -1 +0,0 @@ -õÐq$J_™:§¦Ò›Ÿá%ÒéßE \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-2 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-2 deleted file mode 100644 index e5437df6a..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-2 +++ /dev/null @@ -1 +0,0 @@ -eÇ¢Ý Û•'D D>¢•b_33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333 \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-3 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-3 deleted file mode 100644 index d292459f6..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-3 +++ /dev/null @@ -1 +0,0 @@ -Í~"í­i¹ð°˜Ö 2 EòOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-4 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-4 deleted file mode 100644 index 7b7bc327b..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-4 +++ /dev/null @@ -1 +0,0 @@ -¨ýÈ÷86ë¢:ÃiQë; 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-5 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-5 deleted file mode 100644 index fe90693d5..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-5 +++ /dev/null @@ -1 +0,0 @@ -ÀãCšÞa‘+È16‰m GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-6 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-6 deleted file mode 100644 index 8c06e160e..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-6 +++ /dev/null @@ -1 +0,0 @@ -î[ˆ}¬ËŒÚQd¤Áþæˆ!MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-7 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-7 deleted file mode 100644 index 2c7951a39..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-7 +++ /dev/null @@ -1 +0,0 @@ -ªBN^wEwùxyÖ‹UtÆ SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS \ No newline at end of file diff --git a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-8 b/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-8 deleted file mode 100644 index 66fe15530..000000000 --- a/crates/chia-consensus/fuzz/seeds/serde-2026-size-bound/near-bound-8 +++ /dev/null @@ -1 +0,0 @@ -O\uP=p2)1žM0ó¡ÀRX11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 \ No newline at end of file From 42ad2a66283a9cf80701ec49193db9f6097d7e31 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Wed, 15 Jul 2026 14:17:31 -0700 Subject: [PATCH 8/9] fuzz seeds: use seeded SmallRng instead of hand-rolled xorshift Per review: rand (with small_rng) is already a dev-dependency. The corpus is regenerable scratch, so cross-version drift of seeded SmallRng output is acceptable; a fixed seed keeps runs reproducible per rand version. Co-authored-by: Cursor --- .../examples/gen_serde_2026_fuzz_seeds.rs | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/crates/chia-consensus/examples/gen_serde_2026_fuzz_seeds.rs b/crates/chia-consensus/examples/gen_serde_2026_fuzz_seeds.rs index 462caea7d..9b6a8e5c2 100644 --- a/crates/chia-consensus/examples/gen_serde_2026_fuzz_seeds.rs +++ b/crates/chia-consensus/examples/gen_serde_2026_fuzz_seeds.rs @@ -8,7 +8,9 @@ //! encoding comes closest to the bound, and writes the best as seeds, so //! fuzz runs mutate around the boundary instead of wandering toward it. //! -//! Deterministic: fixed RNG seed, so regenerated seeds are reproducible. +//! Deterministic per rand version: fixed RNG seed, so regenerated seeds are +//! reproducible (seed corpora are regenerable scratch, so drift across rand +//! releases is fine). //! //! ```sh //! cargo run --release --example gen_serde_2026_fuzz_seeds -- \ @@ -21,21 +23,10 @@ use chia_consensus::serde_2026::SERDE_2026_COMPRESSION_LEVEL; use clvm_fuzzing::make_tree; use clvmr::Allocator; use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, intern_tree, serialize_2026}; +use rand::rngs::SmallRng; +use rand::{Rng, RngCore, SeedableRng}; use std::fs; -struct XorShift(u64); - -impl XorShift { - fn next(&mut self) -> u64 { - let mut x = self.0; - x ^= x << 13; - x ^= x >> 7; - x ^= x << 17; - self.0 = x; - x - } -} - /// Interpret `data` exactly as the fuzz target does and return /// (blob_size / bound, blob_size). fn fullness(data: &[u8]) -> Option<(f64, usize)> { @@ -56,20 +47,18 @@ fn main() { .unwrap_or_else(|| "fuzz/corpus/serde-2026-size-bound".to_string()); fs::create_dir_all(&out).unwrap(); - let mut rng = XorShift(0x9e37_79b9_7f4a_7c15); + let mut rng = SmallRng::seed_from_u64(0x9e37_79b9_7f4a_7c15); let mut best: Vec<(f64, usize, Vec)> = Vec::new(); for round in 0..400_000u64 { - let len = 16 + (rng.next() % 3000) as usize; + let len = 16 + (rng.next_u64() % 3000) as usize; let mut data = vec![0u8; len]; - for b in &mut data { - *b = (rng.next() >> 32) as u8; - } + rng.fill_bytes(&mut data); // Bias some inputs toward long constant runs, which favors large // atoms and deep spines over noise. if round % 3 == 0 { - let run_byte = (rng.next() >> 24) as u8; - let start = 16 + (rng.next() as usize % (len - 16).max(1)).min(len - 16); + let run_byte = rng.random::(); + let start = 16 + (rng.next_u64() as usize % (len - 16).max(1)).min(len - 16); for b in &mut data[start..] { *b = run_byte; } From 2cd665e561a13ff3dfd3373066399e142f380924 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Wed, 15 Jul 2026 22:03:33 -0700 Subject: [PATCH 9/9] serde_2026: derive the per-atom cap from the blob cap There is no independent max-atom-length constant: atoms appear as literals in the canonical serialization, so an atom of length L forces a canonical blob of at least L bytes. Any atom too big for the caller-derived blob cap therefore cannot be part of a cost-valid generator, and max_blob_size doubles as the per-atom cap passed to deserialize_2026. Per review feedback. Co-authored-by: Cursor --- crates/chia-consensus/src/serde_2026.rs | 39 +++++++++++++------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/crates/chia-consensus/src/serde_2026.rs b/crates/chia-consensus/src/serde_2026.rs index 8b45ecc89..fc547cb79 100644 --- a/crates/chia-consensus/src/serde_2026.rs +++ b/crates/chia-consensus/src/serde_2026.rs @@ -10,13 +10,6 @@ use clvmr::allocator::{Allocator, NodePtr}; use clvmr::error::{EvalErr, Result}; use clvmr::serde::{SERDE_2026_MAGIC_PREFIX, deserialize_2026, node_from_bytes_backrefs}; -/// Per-atom byte cap used by chia consensus when deserializing CLVM blobs. -/// -/// Matches the historical `clvm_rs` default. CLVM atoms above this size are -/// uneconomical to construct under cost limits anyway; this is a defensive -/// pre-allocation cap, not a hard consensus rule. -pub const CONSENSUS_MAX_ATOM_LEN: usize = 1 << 20; - /// Compression level passed to [`clvmr::serde::serialize_2026`] when chia /// produces serde_2026 blobs. /// @@ -56,8 +49,9 @@ pub const SERDE_2026_COMPRESSION_LEVEL: u32 = 0; /// header; the headers and per-item slack together never exceed the `+5` /// because `U_a <= U_p + 1` forces cheap 1-byte pushes to exist whenever the /// headers grow. The bound is tight (slack reaches 0 at atom length 2^20) -/// and requires atom lengths < 2^27, guaranteed by -/// [`CONSENSUS_MAX_ATOM_LEN`]. The wire blob adds the +/// and requires atom lengths < 2^27 — enforced by the per-atom cap in +/// [`node_from_bytes_auto`] whenever the derived blob cap is below 2^27 +/// (at mainnet constants it is ~0.9 MB). The wire blob adds the /// [`SERDE_2026_MAGIC_PREFIX`] on top of the body. /// /// **Step 2 — cost bound:** consensus charges `vbytes * cost_per_byte`, and @@ -85,8 +79,7 @@ pub fn max_canonical_blob_size(max_cost: u64, cost_per_byte: u64) -> usize { /// Deserialize CLVM bytes, auto-detecting classic / backrefs / serde_2026. /// /// Sniffs `SERDE_2026_MAGIC_PREFIX` at the head of `bytes`; if present, -/// dispatches to [`deserialize_2026`] with the per-atom consensus cap -/// ([`CONSENSUS_MAX_ATOM_LEN`]). Otherwise falls back to +/// dispatches to [`deserialize_2026`]. Otherwise falls back to /// [`node_from_bytes_backrefs`] (which also accepts plain classic). /// /// `max_blob_size` bounds the total wire size accepted; blobs above it are @@ -94,6 +87,12 @@ pub fn max_canonical_blob_size(max_cost: u64, cost_per_byte: u64) -> usize { /// cost constants via [`max_canonical_blob_size`] (any headroom multiplier /// on top — e.g. to tolerate non-minimal encodings, which `strict = false` /// otherwise admits — is caller policy). +/// +/// The same value doubles as the per-atom cap: atoms appear as literals in +/// the canonical serialization, so an atom of length `L` forces a canonical +/// blob of at least `L` bytes — no atom of a cost-valid generator can ever +/// exceed the blob bound. There is deliberately no separate atom-length +/// constant. pub fn node_from_bytes_auto( allocator: &mut Allocator, bytes: &[u8], @@ -110,7 +109,7 @@ pub fn node_from_bytes_auto( // rejecting valid transactions over a self-inflicted encoding choice; // a node is free to re-encode strictly before relaying, and to // disconnect a peer that habitually sends non-minimal encodings. - deserialize_2026(allocator, bytes, CONSENSUS_MAX_ATOM_LEN, false) + deserialize_2026(allocator, bytes, max_blob_size, false) } else { node_from_bytes_backrefs(allocator, bytes) } @@ -143,9 +142,10 @@ mod tests { fn tight_trees() -> Vec<(Allocator, NodePtr)> { let mut trees = Vec::new(); - // Single atom at the max length: the bound is exactly tight here. + // Single atom at 2^20 bytes, where the encoding bound's slack + // reaches exactly zero (see the proof's tightness note). let mut a = Allocator::new(); - let node = a.new_atom(&vec![0xa5; CONSENSUS_MAX_ATOM_LEN]).unwrap(); + let node = a.new_atom(&vec![0xa5; 1 << 20]).unwrap(); trees.push((a, node)); // >63 distinct atom lengths: forces a 2-byte atom-group-count header. @@ -199,12 +199,15 @@ mod tests { #[test] fn test_encoding_bound_tight_at_max_atom_len() { - // A single atom of exactly CONSENSUS_MAX_ATOM_LEN (2^20) bytes is the - // known worst case: the encoding uses every byte the bound allows. - // If this stops being exact, the "+5" analysis has changed — revisit + // A single atom of exactly 2^20 bytes is the known worst case: the + // encoding uses every byte the bound allows. 2^20 is a property of + // the encoding's varint/header boundaries, NOT of any consensus cap + // (mainnet's derived cap is ~917 KB, below this), which is why it is + // hardcoded rather than computed via max_canonical_blob_size. + // If equality stops holding, the "+5" analysis has changed — revisit // the proof on max_canonical_blob_size. let mut a = Allocator::new(); - let node = a.new_atom(&vec![0xa5; CONSENSUS_MAX_ATOM_LEN]).unwrap(); + let node = a.new_atom(&vec![0xa5; 1 << 20]).unwrap(); let blob = serialize_2026(&a, node, SERDE_2026_COMPRESSION_LEVEL).unwrap(); assert_eq!(blob.len(), encoding_bound(&a, node)); }