Skip to content

VM: adopt sha256tree at 0x3f (divergence D9) - #16

Merged
EvanWinget merged 9 commits into
mainfrom
sha256tree
Jul 30, 2026
Merged

EvanWinget merged 9 commits into
mainfrom
sha256tree

Conversation

@EvanWinget

@EvanWinget EvanWinget commented Jul 29, 2026 •

Copy link
Copy Markdown
Owner

What changed

sha256tree joins the v0 operator table at opcode 0x3f (decision by Evan, 2026-07-29). It returns the 32-byte tree hash of its single argument: atoms hash as SHA-256 of a 0x01 tag then the atom's bytes, pairs as SHA-256 of a 0x02 tag then the two child hashes. Semantics, opcode, and cost constants are the pinned consensus wheel's own flag-gated sha256tree, which upstream has scheduled for consensus activation in Chia's 3.0 hard fork (CHIP-0049, in review, cited by name deliberately without a link).

Spec sections that authorize it

  • VM.md section 4 (operator table and crypto family prose), section 6 row D9, section 7 (oracle strategy), section 8 entry 7 (the ratified decision record, including the tagged-hash alternative declined and the work-amplification analysis).
  • COSTS.md section 1 (charge order) and section 8 (formula, constants, and the soundness rule).

Read the commits in this order

  1. Spec: adopt sha256tree at 0x3f (divergence D9). The behavior, the D9 row, the decision record, and the cost model. The load-bearing paragraph is the per-node charge rule: evaluation can double a shared environment per (a (q . inner) (c 1 1)) level, reaching 2^k visited nodes in linear program bytes, so an implementation must charge as it walks or a small budget buys unbounded hashing.
  2. VM: implement sha256tree. Thirty-two lines: explicit stack (no Python recursion limit), per-node charges with the base cost riding on the first node's charge, sharing never deduplicated.
  3. Vectors: pin sha256tree. Twenty-one cases, each success case cross-checked against the flag-enabled wheel (result and cost) and the tree_hash utility at generation time.
  4. Tools: sha256tree differential harness, CI leg, smoke test. Four released-binary legs (the fourth, added post-review, pins the D9 oracle column at flags 0 on every run) plus the exponential-DAG fail-fast probe.

How to verify independently

  • .venv/bin/pytest python/tests (77 tests, includes the new fixed-seed smoke slice)
  • .venv/bin/python tools/run_vectors.py (481 cases)
  • .venv/bin/python tools/run_upstream.py (832 vendored cases, unaffected: the upstream unknown-op corpus uses multi-byte opcodes, not 0x3f)
  • .venv/bin/python tools/diff_clvm.py --count 10000 --seed <fresh> (intersection unaffected)
  • .venv/bin/python tools/diff_sha256tree.py --count 400 --seed <fresh> (the new operator's three oracle legs, boundary budgets, arity, exponential DAG)
  • Independent probe of the released wheel: chia_rs.run_chia_program(prog, env, cost, chia_rs.ENABLE_SHA256_TREE) reproduces every success vector byte for byte, and chia_rs.tree_hash reproduces every digest.

All of the above ran clean on this branch before pushing (fresh seeds 8151623, plus 424242 and 20260729 during development).

Notes for review

  • Prior-spec statements this touches: the section 4 preamble and crypto-family prose, the boolean-family pair-argument sentence (the exclusivity claim is now dropped entirely, review finding), the section 5 arg_not_atom taxonomy row (now excepts sha256tree, review finding), and the D2 record's family enumeration (now explicitly amended by entry 7, review finding).
  • The upstream constants could still change before Chia's activation (their CHIP is in review). The decision record treats that as a routine pin-bump triage, and Phase 3 re-measures every inherited constant regardless.
  • docs/opcode-comparison.md (PR Docs: CLVM/bllsh/BitLisp opcode comparison #13, open) is updated separately on that PR's branch.

Post-review fixes

A five-reviewer pass (adversarial, including mutation testing of the implementation and hand-verification of every vector digest and cost) found no implementation, vector, or constant defects. It found spec and doc consistency issues, fixed in the five commits after the original four: the arg_not_atom taxonomy row, the D2 family-enumeration amendment, an over-broad pair-argument claim, the overclaimed leg independence, an unpinned oracle-column claim (now pinned by a flags-0 harness leg, 400 of 400 per run), the operators module docstring, the corpus README's divergence enumeration, and the plan's Phase 1 operator-set line.

Decision by Evan, 2026-07-29, ratified in VM.md section 8 entry 7.
The operator returns the 32-byte tree hash of its single argument:
an atom hashes as SHA-256 of a 0x01 tag then the atom's bytes, a
pair as SHA-256 of a 0x02 tag then the two child hashes. Any node is
a legal argument, arity is the only check, and it precedes every
charge.

Semantics, opcode, and cost constants (270 base, 460 per pair, 2 per
byte counting the leaf tag byte, flat 320 malloc) are the pinned
consensus wheel's own sha256tree, carried behind its release flag
and scheduled for consensus activation upstream (CHIP-0049, in
review). Verified against the released binary by probe 2026-07-29:
tree hashes, totals at the exact budget boundary, arity errors, and
flags-0 unknown acceptance all match the constants and ordering
specified here. Upstream source (clvm_rs treehash.rs) informed the
charge interleaving, and the binary confirmed it.

The per-node charge rule is normative for soundness, not only for
pricing: (c 1 1) doubles the environment reachable from its result
for one cons, and k nested applies of that shape reach 2^k visited
nodes for build cost linear in k, sharing the witness never spells.
Charging each visited node as the walk reaches it bounds the work a
budget can buy, recorded in COSTS.md section 8 alongside the substr
copy-on-slice note.

New divergence row D9: deployed consensus at flags 0 treats 0x3f as
an unknown operator, BitLisp dispatches sha256tree. Oracle strategy
recorded in VM.md section 7: the flag-enabled wheel, the wheel's
tree_hash puzzle-hash utility, and an in-language tree-hash program
over the intersection, three released-binary legs.
The walk mirrors the machine's explicit-stack rule so argument depth
is never limited by the Python recursion limit, charges each visited
node as it reaches it with the base cost riding on the first node's
charge, and never deduplicates sharing: a node reachable twice is
charged and hashed twice. Constants come from the consensus oracle's
flag-gated operator, verified by probe against the released wheel
(results, boundary totals, arity errors) before this commit.

Upstream source (clvm_rs treehash.rs) informed the traversal and
charge interleaving, and the released binary confirmed every
constant and ordering, per the reference-material guardrails.

Spec: VM.md section 4 (operator table, crypto family), section 6 row
D9, section 8 entry 7, COSTS.md sections 1 and 8.
Twenty-one cases in vectors/vm/sha256tree.json: the two COSTS.md
worked examples, leaf-tag and actual-bytes-as-given rules (redundant
and minimal integer spellings hash differently), pair-tag child
order, an evaluated pair argument from the environment, hash-of-hash
composition, the shared-subtree double charge under (c 1 1), the
inclusive budget boundary at the exact total and one below, all
three arity shapes including arity beating the budget at max_cost 1,
the zero-budget dispatch burst (D7 interaction), a raising argument
beating the operator, and the exponential shared-environment DAG
(forty nested applies doubling the environment per level) reporting
cost_exceeded under a 500000 budget, the fail-fast rule of COSTS.md
section 8.

Every success case and every oracle-expressible error case was
cross-checked against the flag-enabled consensus wheel (result and
cost) and the tree_hash utility at generation time.

Spec: VM.md sections 4 and 6 (D9), COSTS.md sections 1 and 8.
tools/diff_sha256tree.py pins the operator on three released-binary
legs: (cost, result) against the pinned wheel's flag-enabled
dispatch including budgets within a few units of the measured cost,
results against the wheel's tree_hash puzzle-hash utility, and
results against an in-language tree-hash program built from
intersection operators and run through bitlisp and both pinned
oracles at flags 0. The generator draws atom sizes across the
serialization forms, redundant integer spellings, and (c X X)
shared-structure chains where the walk charges per visit. Every run
also probes arity defects and an exponential shared-environment DAG
(25 to 50 nested doublings under a small budget), where both sides
must report cost_exceeded in bounded time: an implementation that
hashed before charging would hang the harness instead.

CI runs 400 cases per push with the run id as seed, printed for
local reproduction, alongside the existing diff legs. The unit suite
gains a fixed-seed 150-case smoke slice so pytest alone stays a
complete local gate.

Spec: VM.md section 7 (oracle strategy for D9).
Findings from the five-reviewer pass on this PR, each verified
before fixing. The section 5 error-taxonomy row scoped arg_not_atom
to the whole crypto family, contradicting the section 4 statement
that sha256tree has no arg_not_atom path: the row now excepts it,
the reverse of the edit that widened the row when secp_verify
landed. The D2 record's family enumeration (sha256 plus secp_verify,
nothing else) is now explicitly amended by entry 7 rather than left
silently overturned, per the section 8 preamble's amendment rule.
The rewritten boolean-family sentence claimed only booleans and
sha256tree accept pair arguments outside the tree ops, false given
apply's environment argument and raise's arguments: the exclusivity
claim is dropped. Section 7 no longer calls the three legs
independent, since two are views of the one pinned wheel and only
one checks cost, and it now describes the fourth leg added in this
PR: every generated program runs through both oracles at flags 0,
pinning the D9 oracle column continuously instead of by a one-time
probe. The D9 row's vector cell now says which column the vector
file pins.
The docstring's two-way split (consensus-oracle order for everyone,
secp_verify its own normative choice) missed the third category this
PR introduced: op_sha256tree's order matches the same released wheel
behind its release flag, while flags-0 consensus treats the opcode
as unknown, a recorded divergence.

Spec: VM.md sections 6 (D9) and 7.
The envelope's spec field now spells divergence D9 the way sibling
files spell their rows, and the corpus README's divergence-case
enumeration now includes vm/sha256tree.json with the flag-enabled
cross-check that produced it.

Spec: VM.md section 6 (D9).
The divergence row's oracle column (deployed consensus accepts 0x3f
as an unknown operator returning nil) was verified by a one-time
probe but re-verified by nothing. The harness now runs every
generated program through both oracles at flags 0 and requires
unknown-op acceptance with a nil result, guarded on success so an
erroring argument is never misread as rejection. Costs are not
compared on that leg, the unknown-op charge has nothing to do with
the operator's constants.

Also from review: the docstring no longer claims the budget-boundary
leg observes charge interleaving (only the order-independent total
decides this operator's outcome), it now states which legs share the
one wheel and that only the first checks cost, and the exponential
stat counts like its siblings.

Spec: VM.md sections 6 (D9) and 7.
The Phase 1 checklist line defined the operator set as CLVM core
minus BLS plus secp_verify, stale once sha256tree joined. The line
now records the post-close amendment with its date and divergence
row, following the plan's precedent of correcting frozen facts that
drifted.
@EvanWinget
EvanWinget merged commit 44a25d7 into main Jul 30, 2026
2 checks passed
@EvanWinget
EvanWinget deleted the sha256tree branch July 30, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant