serde_2026 block support - #1439
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Python-facing support for building and hashing post-HF2 (INTERNED_GENERATOR / serde_2026) blocks, including a new anytime block builder, serde_2026-aware tree hashing, and serde_2026 framing support for Program round-trips.
Changes:
- Introduces
Block2026Builder(Rust + Python bindings/stubs) for building serde_2026 generators under the INTERNED_GENERATOR cost model. - Adds
tree_hash_auto()(Python) and updates several readers/execution entrypoints to accept classic/backrefs/serde_2026 vianode_from_bytes_auto. - Extends
Programparsing/JSON round-trip to recognize the serde_2026 magic prefix, and addsProgram.from_program_bytes()for unvalidated wrapping.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| wheel/stubtest.allowlist.3-9 | Allowlists Block2026Builder for Python 3.9 stubtest edge-case. |
| wheel/src/run_program.rs | Switches run_chia_program deserialization to node_from_bytes_auto. |
| wheel/src/api.rs | Exposes Block2026Builder, tree_hash_auto, and re-exports SERDE_2026_MAGIC_PREFIX; updates deserialization paths to auto-detect formats. |
| wheel/python/chia_rs/chia_rs.pyi | Updates Python stubs: adds Block2026Builder, tree_hash_auto, Program.from_program_bytes, and moves SERDE_2026_MAGIC_PREFIX. |
| wheel/generate_type_stubs.py | Updates stub generation rules for new APIs/constants. |
| crates/clvm-utils/src/tree_hash.rs | Adds a unit test ensuring tree hash equality across standard/backrefs/serde_2026 encodings. |
| crates/chia-protocol/src/program.rs | Adds serde_2026-aware framing in Streamable::parse/FromJsonDict; adds Python Program.from_program_bytes. |
| crates/chia-consensus/src/lib.rs | Exposes new build_block_2026 module. |
| crates/chia-consensus/src/build_block_2026.rs | Implements Block2026Builder anytime builder + tests + Python bindings. |
| crates/chia-consensus/src/additions_and_removals.rs | Switches trusted generator parsing to node_from_bytes_auto. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Coverage Report for CI Build 32538043764Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.3%) to 82.23%Details
Uncovered Changes
Coverage Regressions95 previously-covered lines in 6 files lost coverage.
Coverage Stats
💛 - Coveralls |
b661d55 to
be7877a
Compare
72bb358 to
7ace3d5
Compare
732e071 to
fbdd7c3
Compare
fbdd7c3 to
a3019b8
Compare
9a438ab to
2cd665e
Compare
be7877a to
fc51809
Compare
48dcbe4 to
efb05fd
Compare
efb05fd to
eae8264
Compare
ab0dd75 to
c22ae9b
Compare
bc87bba to
4c9de44
Compare
c22ae9b to
4972b1b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4972b1b. Configure here.
4972b1b to
e3a373d
Compare
General plumbing for serde_2026-serialized (post-HF2) block generators, plus an emission mode for the existing InternedBlockBuilder: - Program::parse / from_json_dict recognize the serde_2026 magic prefix and use a framing-only walk to find the byte boundary. Consensus caps are still enforced at run_block_generator time. - Program.from_program_bytes() wraps raw bytes without CLVM structure validation, for non-standard serializations. - tree_hash_auto() computes the tree hash of a blob in any supported serialization format. - Non-consensus readers (additions_and_removals, run_chia_program, get_puzzle_and_solution_for_coin/2) switch to node_from_bytes_auto. - InternedBlockBuilder gains a new_serde_2026() named constructor. Builders created with it serialize the generator with serialize_2026 in finalize() instead of the classic back-ref format. new() and new_with() keep their original signatures, and the classic path is unchanged and remains byte-identical. The Python binding keeps the serde_2026 kwarg (default False) and dispatches to the matching constructor. The builder does not see consensus flags; the caller decides based on INTERNED_GENERATOR activation. The aggressive anytime Block2026Builder moves out of this PR to a separate opt-in optimization PR (parked as park/block-2026-builder-optimization). Co-authored-by: Cursor <cursoragent@cursor.com>
e3a373d to
3bb84c1
Compare
serde_2026 acceptance activates at exactly the same height as INTERNED_GENERATOR (single activation), and InternedBlockBuilder's cost model (charging by interned vbytes) is only correct once INTERNED_GENERATOR is active. There is no height at which "interned builder + classic emission" is valid, so the default-off constructor flag was a footgun: a default-off caller post-fork would emit blocks that fail #1438's strict validation. finalize() now unconditionally serializes with serialize_2026. Co-authored-by: Cursor <cursoragent@cursor.com>
The cap is derived from serde_2026 vbyte math and current cost constants; applying it to classic/backrefs blobs retroactively bounds historical blocks by today's rules. Trusted readers (e.g. additions_and_removals) must accept every blob the chain ever accepted, so the classic branch is now byte-for-byte node_from_bytes_backrefs, uncapped — same as before the dispatcher existed. Consensus paths are unaffected: node_from_bytes_2026 keeps its own gate. Co-authored-by: Cursor <cursoragent@cursor.com>
The unvalidated constructor let Python wrap arbitrary bytes as a Program. Now that Program::parse handles serde_2026 framing, callers building post-HF2 generators can use from_bytes and get the framing walk; blobs stay plain bytes until they cross into a Program. Revisit if/when the node adopts the v1 raw-buffer block framing, where the generator is bytes end-to-end. Co-authored-by: Cursor <cursoragent@cursor.com>
A cap that silently applied to only one branch was confusing. The dispatcher is the trusted-reader entry point, so it now takes no size policy at all: both branches parse uncapped, like node_from_bytes_backrefs always did. clvmr's per-atom bound is satisfied with bytes.len(), which is policy-free (an atom of length L appears literally in the blob). The cost-derived cap lives only in the consensus entry point, node_from_bytes_2026; the size-bound fuzz target now exercises that gate directly. Co-authored-by: Cursor <cursoragent@cursor.com>
tree_hash_auto is called on unvalidated blobs (the post-HF2 generator_root check hashes generators from blocks straight off the wire), and backrefs/serde_2026 can encode trees whose expansion is exponential in the blob size. The uncached tree_hash walks the full expansion — a pre-charge DoS. Switch to tree_hash_cached, which memoizes shared pairs and stays linear in unique nodes (bounded by blob length). Regression test: a 64-deep doubling chain (2^64-leaf expansion from a <1 KiB blob) hashes correctly via both compact formats. node_from_bytes_auto's doc now states the full contract: policy-free (never a consensus entry point), parsing is untrusted-safe (linear), but traversal of the result must be DAG-aware. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
get_tree_hash(), run_rust(), and uncurry_rust() only knew classic/backrefs encoding, so serde_2026-encoded Programs hit the wrong path: get_tree_hash's tree_hash_from_bytes(...).unwrap() panicked (a Python-reachable panic), and run_rust/uncurry_rust failed with a misleading "unexpected end of buffer" from node_from_bytes_backrefs. All three now dispatch on the magic prefix, matching chia_consensus::serde_2026::node_from_bytes_auto's semantics (duplicated here since chia-protocol can't depend on chia-consensus). get_tree_hash's serde_2026 branch hashes with the DAG-safe tree_hash_cached, agreeing with the wheel's tree_hash_auto; its classic branch is unchanged byte-for-byte. Since the serde_2026 branch can error, get_tree_hash's signature changes to PyResult<Bytes32> (raises ValueError instead of panicking); the .pyi stub type is unaffected since a raise doesn't change it. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Replaced by #1511 |

Plumbing for serde_2026-serialized (post-HF2) block generators, plus serde_2026 emission
for the existing
InternedBlockBuilder(#1436). Stacked on #1438.What's here
Program::parse/from_json_dictrecognize the serde_2026 magic prefix and use aframing-only walk to find the byte boundary. Consensus caps are still enforced at
run_block_generatortime. Classic-format inputs are unaffected: a leading0xfdbyte was never parseable as classic CLVM serialization, so the only newly-accepted
inputs are previously-invalid serde_2026-prefixed blobs.
Accepting the prefix at parse time before the fork is deliberate: this release
ships ahead of activation and must be able to parse post-fork blocks without another
release. Consensus enforcement stays in the flag-conditioned
run_block_generator2—pre-fork, a block carrying a serde_2026 generator is still invalid; the rejection
just moves from message parsing to generator execution.
tree_hash_auto(): tree hash of a blob in any supported serialization format.Hashing is DAG-aware (
tree_hash_cached): this gets called on unvalidated blobs(the post-HF2 generator_root check on incoming blocks), and compact encodings can
have exponential expansion — the naive walk would be a pre-charge DoS. Work is
linear in unique nodes, which parsing bounds by the blob length.
(An earlier revision also added an unvalidated
Program.from_program_bytes()constructor for the emission side; it was dropped — since
parsehandlesserde_2026 framing,
from_bytesserves that role with validation. Generatorblobs stay
bytesuntil they cross into aProgram.)additions_and_removals, wheelrun_chia_program,get_puzzle_and_solution_for_coin/2) switch tonode_from_bytes_auto; identicalbehavior for classic-format input. The dispatcher imposes no size policy in either
branch — trusted readers of historical blocks must accept every blob the chain ever
accepted, so the classic branch is byte-for-byte
node_from_bytes_backrefs. Thecost-derived size cap is enforced only at the consensus entry point
(
node_from_bytes_2026).InternedBlockBuilder'sfinalize()always serializes the generator withserialize_2026— there is no classic-emission mode. serde_2026 acceptance activatesat exactly the same height as
INTERNED_GENERATOR(single activation), and thebuilder's cost accounting (interned vbytes) is only correct once
INTERNED_GENERATORis active, so there is no height at which "interned builder + classic emission" would
be valid. A caller-controlled flag would have been a footgun: a default-off caller
post-fork would emit blocks that fail strict post-fork validation.
What moved out
The aggressive anytime
Block2026Builderis no longer part of this PR. It becomes aseparate opt-in optimization PR later (seed parked at
park/block-2026-builder-optimization).Tests
run_block_generator2+INTERNED_GENERATOR, withspends/conditions matching an independently-built classic-format generator of the
same bundles run under classic rules, and
finalize()cost matching the consensuscost exactly.
tree_hash_autoagreement between serde_2026 and classic serializations of the sametree.
Note
High Risk
Touches consensus-adjacent generator parsing, tree hashing of unvalidated blobs, and InternedBlockBuilder emission. Size-cap policy is split between consensus (
node_from_bytes_2026) and policy-free historical readers.Overview
Enables serde_2026 (post-HF2 interned) block generators end-to-end: parse them on the wire, hash them DAG-aware, run them on non-consensus paths, and always emit them from
InternedBlockBuilder.Readers vs consensus.
node_from_bytes_autois now a policy-free sniff-and-dispatch (no size cap; classic branch is stillnode_from_bytes_backrefs). The cost-derived blob cap lives only onnode_from_bytes_2026. Trusted/historical readers (additions_and_removals, wheelrun_chia_program, puzzle/solution extractors) switch to auto.Protocol / Python.
Programparse and JSON accept the magic prefix with a framing-only length walk (caps still atrun_block_generator). Addsis_serde_2026_encoded, DAG-awareget_tree_hashfor 2026 blobs, and Pythontree_hash_autofor unvalidated generator_root checks.Builder.
InternedBlockBuilder::finalize()always serializes with serde_2026 (no classic-emission mode), matching interned-vbyte cost accounting and the singleINTERNED_GENERATORactivation. Tests check round-trip spends vs a classic reference generator.Reviewed by Cursor Bugbot for commit 1f16531. Bugbot is set up for automated code reviews on this repo. Configure here.