Puzzle rework: both puzzles adopt the cheap identity assert - #62
Merged
Conversation
Swap ASSERT_MY_TAPROOT (0x37) for ASSERT_MY_TAPTREE (0x38) in the vault, triggered, shared-recovery, and singleton sources, operand for operand, the internal key staying the BIP341 NUMS point in both puzzles. The taptree assert reads the internal key and merkle root base consensus authenticated from the control block instead of re-deriving the 34-byte scriptPubKey, so every puzzle spend drops the point multiplication it paid on every path, 1,300,000 of cost per spend at the provisional constants. The test suites re-pin the three changed mod hashes (the owner inner program is untouched), and the curried-identity closure now reads the internal key and merkle root off the emitted assert, which carries no derived script_pubkey field. Authorized by spec/CONDITIONS.md, the ASSERT_MY_TAPTREE entry (0x38), and spec/COSTS.md section 10. Ratified as condition-record decision 28, divergence C24. The rework itself is the acceptance step the execution plan's cheap-identity-assert amendment states.
Regenerated from the swapped sources: every compiled program, condition payload, instance root, instance scriptPubKey, and singleton lifecycle txid moves, while every case name, shape, amount, and literal txid stays. Evaluation costs are unchanged, the opcode atom having the same width, so the vm expects differ only inside program and result hex. One expectation changes by spec: program_on_wrong_coin now fails as unsatisfied_taptree_assert where the derivation assert failed as unsatisfied_scriptpubkey_assert. CONDITIONS.md states the two asserts are satisfied together and fail together on an honest input, each with its own error, and this vector pins the error swap. The two validation files' spec citations name ASSERT_MY_TAPTREE in place of ASSERT_MY_TAPROOT. Regeneration used a construction-for-construction generator that first reproduced all five files byte for byte from the unswapped sources, so the re-pin carries no silent case drift. The closure tests recompute every program and condition payload from source and the full corpus runner passes: 38 files, 1132 cases.
Both puzzle docs move their identity sections onto ASSERT_MY_TAPTREE, re-pin the worked-instance mod hashes, and record the measured saving: every spend of either puzzle costs exactly 1,300,000 less than under ASSERT_MY_TAPROOT, the point multiplication the taptree assert does not run. Vault spends now range from 49,468 (matured withdrawal) to 4,054,131 (trigger), and the singleton lifecycle runs 4,167,737 to 4,262,797. The two remaining ASSERT_MY_TAPROOT mentions are the comparison sentences stating that saving.
PR 60 review deferral: the filler triple (0a, 0b, 0c) was retyped in ten test modules. conftest.py now owns the three constants and two builders, TxInput keyword arguments and the hex fields of a runner tx context, and every module spreads the builder instead. The self-assert suite's FILLER_IDENTITY pair is derived from the same constants. No behavior change: 328 tests in the touched modules pass unchanged.
PR 60 review deferrals: the corpus-identity test was duplicated across the vault and singleton suites with the derived-scriptPubKey expression spelled four times, and each run re-derived the EC tweak per input. conftest.py now owns assert_corpus_identities, which derives each distinct root's tweak once, and both suites call it with their files and expected filler count. The two suites drop from about 9 seconds to under 4.
PR 60 review deferral: load_context spelled the same present-or-None hex conversion three times for the identity triple. _optional_hex reads them all. No behavior change.
PR 60 review deferral: the table restated what TAPROOT_ASSERTS already holds and had one reader, which now reads the assert's own script_pubkey field.
The acceptance the bullet states is met: both puzzles reworked onto ASSERT_MY_TAPTREE, re-pinned, the saving measured in their docs. Whether ASSERT_MY_TAPROOT stays in the vocabulary is Evan's call at the rework PR's review, recorded in the bullet.
Owner
Author
Review pass (code-review skill, high, Fable finders, adversarial verify)Ten candidates went to independent verifiers. Nine confirmed, all folded in the two commits after the plan flip:
Refuted after verification: consolidating the Final: 815 tests, 1132 vector cases, lint clean, CI green. |
Review findings: the trigger figure is the no-revault path, so the sentence now names both trigger variants (6,704,696 with a revault claim), and the one-assert figure was a runner total called a parse cost. The condition charge is 200 against 1,300,200 per spec/COSTS.md section 10, and 220 against 1,300,220 is the minimal spend through evaluation and parsing together.
Review findings, all confirmed by adversarial verify. conftest.py is pytest's hook file and importing a library from it leans on conftest's special loading, so the helpers move to a plain support.py module and no conftest exists. The NUMS point and load_vector, previously retyped across three and two modules, join it, and one condition_inputs generator owns the files-cases-inputs walk the audit and both closure suites repeated. The audit's filler tuple is built from the three constants explicitly instead of through dict insertion order, and the duplicate-invariants taptree pool entries name the filler constants their satisfiability depends on. The PR 60 deferral prescribed a conftest.py home, and this amends the letter of that prescription while keeping its intent.
EvanWinget
force-pushed
the
puzzle-taptree-rework
branch
from
August 24, 2026 03:19
1b33fcc to
77c2372
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Both benchmark puzzles adopt ASSERT_MY_TAPTREE (0x38) in place of ASSERT_MY_TAPROOT (0x37), operand for operand, the corpus is re-pinned, the saving is measured in both puzzle docs, and the five review findings PR 60 deferred here are landed. This is tooling, tests, vectors, and docs only: nothing in
spec/orpython/bitlisp/changes.Spec authority.
spec/CONDITIONS.md, the ASSERT_MY_TAPTREE entry (0x38), andspec/COSTS.mdsection 10. Ratified as decision 28 and divergence C24 indocs/condition-record.md. The execution plan's cheap-identity-assert amendment names this rework as its acceptance step.Read the commits in this order
puzzles: adopt ASSERT_MY_TAPTREE, the cheap identity assert. The four sources swap the opcode name at six emit sites, comments reworded. The internal key stays the BIP341 NUMS point in both puzzles. The test suites re-pin the three changed mod hashes in the same commit, and the curried-identity closure reads the internal key and merkle root off the emitted assert, which carries no derived script_pubkey field.vectors: the puzzle corpus re-pins on the taptree assert. All five puzzle vector files regenerate. Method below.docs: the puzzle docs measure the taptree saving._optional_hexin the runner, HONEST_SPKS deleted.support.pywith NUMS,load_vector, and the corpus walk joining them.plan: the cheap identity assert amendment is accepted.The measured saving
Every spend of either puzzle costs exactly 1,300,000 less, the point multiplication the taptree assert does not run. Totals through the single-spend runner at the provisional constants:
The condition charge itself is 200 against 1,300,200, and a minimal spend emitting one identity assert and nothing else runs at 220 against 1,300,220 through evaluation and parsing together. Evaluation costs are unchanged, the opcode atom having the same width, so the whole saving is the condition charge.
What a reviewer should scrutinize
One corpus expectation changes.
program_on_wrong_coininvectors/validation/vault-core.jsonnow fails asunsatisfied_taptree_assertwhere it failed asunsatisfied_scriptpubkey_assert. CONDITIONS.md states the two asserts are satisfied together and fail together on an honest input, each with its own error, and this vector now pins the error swap.What the puzzles no longer read. The derivation assert compared the spent scriptPubKey bytes against a freshly derived tweak. The taptree assert compares against the identity triple the transaction view carries as authenticated and derives nothing, decision 28's stated stance. I probed the boundary directly (constructions in the review commands below):
unsatisfied_taptree_assertunsatisfied_taptree_assertThe third row is the recorded consequence of the authenticated-view stance: base consensus admits no input whose scriptPubKey is not the taproot output of its own control-block identity, so the shape exists only in hand-built model transactions. The stage 2 invariant in VALIDATION.md states the agreement. The vault's follower and leader paths still bind the literal scriptPubKey through ASSERT_MY_SCRIPTPUBKEY where the message ledger needs the bytes.
Vector regeneration method. The corpus was regenerated by a construction-for-construction generator keyed on the test modules' own objects. Before the swap was trusted, the generator had to reproduce all five files byte for byte from the unswapped sources, and did. After the swap it rebuilt every compiled program, condition payload, instance root, scriptPubKey, and singleton lifecycle txid, while every case name, shape, amount, and literal txid stayed. The closure tests then recompute every pinned program and payload from source, in both set directions.
The fate of ASSERT_MY_TAPROOT (0x37), your call
Decision 28 reserves this for you at this PR, with the measured saving in hand. Both sides:
Remove it. ASSERT_MY_TAPTREE subsumes it on every input base consensus admits, the tweak-collision exemption aside (decision 28). Nothing in the tree uses it any more. A vocabulary entry with no user is review burden and attack surface for a spec headed to skeptical readers, and the question "when do your two identity asserts diverge" invites exactly the tweak-collision conversation a reviewer will start. Pre-deployment removal is cheap, later removal is a governance event. The derivation code itself survives untouched in CREATE_OUTPUT_TAPROOT.
Keep it. It is the only self assert that binds the spent scriptPubKey bytes to components without trusting the view triple, the assert-side mirror of CREATE_OUTPUT_TAPROOT (D-CC2 symmetry in the condition comparison). Its spec entry, implementation branch, and vectors already exist and cost nothing to carry. Phase 4 re-prices the tweak constant, and a cheap derivation would leave the choice between the two harmless. Removing it is spec, implementation, and vector churn in a release that has no deployment pressure forcing the cut.
My recommendation: remove, in its own follow-up PR so this one stays at its stated scope. The subsumption is ratified, the last caller is gone as of this PR, and a one-fact-one-assert vocabulary is easier to defend than an exemption footnote. I have not touched 0x37 anywhere here.
Verify independently
The closure tests are the strongest check:
test_vm_vectors_match_sourceandtest_validation_vectors_match_sourcein both puzzle suites recompute every pinned payload from a fresh compile and curry, set-equal in both directions, andtest_validation_vector_identities_derive_their_scriptsre-derives every identity's scriptPubKey. The diff harness was not run: no VM or condition-layer behavior changes in this PR.Confidence
High, 9 of 10. The two remaining ASSERT_MY_TAPROOT mentions in the puzzle docs are the deliberate comparison sentences stating the saving. The residual risk is concentrated in the regenerated corpus, and it is fenced from three sides: the generator reproduced the old corpus byte for byte before the swap, the closure tests recompute every payload from source, and the corpus runner executes every case. The one point I could not close myself is the 0x37 decision above, which is yours.