Skip to content

Unit 7: the vault benchmark puzzle - #57

Merged
EvanWinget merged 6 commits into
mainfrom
unit-7-vault
Aug 21, 2026
Merged

Unit 7: the vault benchmark puzzle#57
EvanWinget merged 6 commits into
mainfrom
unit-7-vault

Conversation

@EvanWinget

@EvanWinget EvanWinget commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Unit 7, first puzzle: the vault, written in the v0 language as the acceptance artifact, resequenced ahead of the payment pool (decision 2026-08-16). Core semantics match BIP-345 (OP_VAULT and OP_VAULT_RECOVER) reproduced with the condition vocabulary, plus the keyless leader/follower consolidation path over the message ledger, the evaluation doc's section 3.2 benchmark, with its obligation 1 theft vectors. Tooling and vectors only: nothing in spec/ or python/bitlisp/ changes.

What changed

  • puzzles/ is new, establishing the source layout for the remaining benchmark puzzles: puzzles/lib/curry-hash.blib (generic curried-identity helpers), puzzles/vault/vault.bl (four paths), puzzles/vault/triggered.bl (two paths), puzzles/vault/vault-shared.blib.
  • docs/puzzles/vault.md: state machine, per-path conditions, the BIP-345 correspondence table with recorded divergences, the consolidation construction with its adversarial analysis and monotonicity argument.
  • Vectors: vectors/vm/vault-programs.json (17 compiled representatives), vectors/validation/vault-core.json (23 lifecycle cases), vectors/validation/vault-consolidation.json (15 cases, one theft vector per attack shape).
  • Tests: python/tests/test_vault_puzzles.py (20 tests), including drift guards that recompile the sources and byte-compare them against the pinned vectors.
  • docs/execution-plan.md unit 7 bullet updated with the dated decisions, and glossary rows for the new vocabulary.

Decisions recorded (Evan, 2026-08-16)

  • Vault first in unit 7, one PR, core matching BIP-345.
  • Recovery posture is a curried per-instance choice: RECOVERY_KEY nil is keyless, a 32-byte key demands an outpoint-bound signature.
  • The consolidation path is keyless, and the leader rejects an empty follower list, so every consolidation merges at least two coins and adversarial replacement still makes monotone progress.
  • The vault's dispatch helpers exist only to name a reconstructed root once: the pre-registered assign trigger firing, recorded in the plan bullet for the unit 7 ledger note.

Authorizing sections

  • CONDITIONS.md section 2 entries: CREATE_OUTPUT, CREATE_OUTPUT_TAPROOT, ASSERT_MY_TAPROOT, ASSERT_MY_SCRIPTPUBKEY, ASSERT_MY_AMOUNT, ASSERT_SIG_MY_OUTPOINT, ASSERT_SEQUENCE_HEIGHT, SEAL_OUTPUTS, SEND_MESSAGE, RECEIVE_MESSAGE.
  • VALIDATION.md rules 1 (exact-content injective counting, which is why merged batch triggers are declined scope), 2, 3 (the message ledger's exact balance, the consolidation security argument), and 8.
  • docs/lang/language.md and docs/lang/curry.md for the surfaces consumed. The unit 5 curry surfaces are consumed as planned: instances are built with curry, identity checked with -T, and uncurry read-back is pinned in a test.
  • BIP-345 (bip-0345.mediawiki) defines the lifecycle the core reproduces. The correspondence table and the recorded divergences are in docs/puzzles/vault.md.

Read the commits in this order

  1. docs: the vault benchmark design, unit 7 resequenced vault-first
  2. puzzles: the vault and triggered programs
  3. vectors: pin the vault lifecycle and the consolidation theft cases
  4. tests: compile-and-run coverage for the vault puzzles
  5. review fold-ins: domain guards, honest identity claims, closure drift guard
  6. review fold-ins: target-reuse divergence stated, recovery script and delay-encoding guards, signing-domain tags

Verify independently

.venv/bin/pytest python/tests -q                      # 784 passed
.venv/bin/python tools/run_vectors.py                 # 36 files, 1061 cases, 0 failures
.venv/bin/python tools/diff_clvm.py --count 10000 --seed 20260816   # 0 failures at this fresh seed
PATH=".venv/bin:$PATH" ci/lint/lint.sh                # all checks passed

# the pinned mod hashes
.venv/bin/bitlisp-compile -T puzzles/vault/vault.bl -I puzzles/lib -I puzzles/vault
.venv/bin/bitlisp-compile -T puzzles/vault/triggered.bl -I puzzles/lib -I puzzles/vault

Every vector case was executed against the validator at generation time, and the adversarial cases (signature replay, redirected and stale-signature targets, the ten consolidation theft shapes including the encoding-level sum-deflation attempts and the two-leaders double-claim, the merged recovery slot) were probed live: the test suite reproduces each with its exact error code.

Two review rounds are folded in as the final commits. The first (ten verified findings): in-program domain guards on the curried values so a malformed instance fails on first spend instead of quietly losing one path, trigger amount guards against misbehaving signers, corrected identity and monotonicity claims in the design doc, the per-group benchmark reading stated explicitly, four new adversarial vector pins, a de-tautologized identity test, and a full-closure drift guard over every conditions field in the validation vectors.

The second round (four findings from Evan's review): the same-target withdrawal merge is stated as a divergence with a consequence rather than as batching, pinned by the valid vector same_target_withdrawals_merge_second_burns, with the wallet rule (never reuse a target hash) and a count-committing seal variant flagged for a spec decision outside this unit. RECOVERY_SPK and minimal DELAY encoding joined the domain guards in both programs, closing the last quiet single-path failure modes. Both signed messages now open with a signing-domain tag so trigger and recovery authorizations stay in disjoint domains even under a shared key.

The unit 7 bullet records the dated decisions: vault ahead of the
payment pool, core semantics matching BIP-345 in one PR, the keyless
leader/follower consolidation path over the message ledger (the
evaluation doc's section 3.2 benchmark), and the per-instance
recovery posture. The dispatch helpers naming a reconstructed root
once are recorded as the pre-registered assign trigger firing.

docs/puzzles/vault.md states the state machine, both programs'
parameters and paths with their emitted conditions, the BIP-345
correspondence table with recorded divergences (literal recovery
scriptPubKey, no merged batch triggers under rule 1's exact-content
counting, the identity stand-in pending the Phase 4 commitment
scheme), the consolidation construction with its adversarial
analysis and the monotonicity argument, and the worked instance
with the pinned mod hashes.

Glossary rows for the vault vocabulary land with the terms per the
terminology rule.
The first benchmark puzzle, written in the v0 language: plain
functions, the fixed forms, and the unit 6b surfaces. vault.bl
carries four paths (trigger, recovery, consolidation follower and
leader), triggered.bl two (withdrawal, recovery), with the shared
recovery path and the triggered-root derivation in
vault-shared.blib.

puzzles/lib/curry-hash.blib is the generic identity helper: it
recomputes a curried program's tree hash from the mod hash and the
fixed values' digests, the digest bitlisp-curry -T prints, so every
path can bind itself to its coin through ASSERT_MY_TAPROOT. This
establishes the puzzles/ source layout and the lib/ include
directory for the remaining unit 7 puzzles.

Semantics authorized by docs/puzzles/vault.md. Conditions consumed:
CREATE_OUTPUT, CREATE_OUTPUT_TAPROOT, ASSERT_MY_TAPROOT,
ASSERT_MY_SCRIPTPUBKEY, ASSERT_MY_AMOUNT, ASSERT_SIG_MY_OUTPOINT,
ASSERT_SEQUENCE_HEIGHT, SEAL_OUTPUTS, SEND_MESSAGE,
RECEIVE_MESSAGE (CONDITIONS.md section 2). BIP-345 defines the
core lifecycle the vault reproduces, per the correspondence table
in the design doc.
vault-programs.json (suite vm) pins one compiled-and-curried
representative per spend path plus the in-program guard failures,
per VM.md sections 3 and 4, replayed byte-exact by the corpus
runner and round-tripped by the converter sweep.

vault-core.json pins the condition-level lifecycle under
VALIDATION.md rules 1, 2, and 8: triggers with and without revault,
the signature bindings (bad signature, replay onto another
outpoint, redirected target caught by the message binding),
sequence maturity and version gating, seal violations and the open
input side, recovery in both postures from both states, rule 1's
exact-content counting under batched recovery (two slots pass, one
merged slot fails), and the program-on-wrong-coin boundary.

vault-consolidation.json pins the message-ledger construction under
VALIDATION.md rules 1 and 3, the obligation 1 adversarial surface
from the evaluation doc's section 3.2: the happy paths including
equal-amount followers and two disjoint groups composing in one
transaction, and one theft vector per attack shape (omitted,
phantom, understated, overstated, and under-received followers, a
hostile leader at a foreign scriptPubKey, a hostile extra sender,
and the underpaid or missing merged output). Every case was
executed against the validator at generation time.
The source-to-bytes link: both mod tree hashes pinned as literals,
the in-program curried-hash reconstruction checked against the
curry tooling and uncurry read-back, every spend path run through
the single-spend runner, and every failure mode asserted by exact
error code live, including the parse-stage rejection of a negative
listed follower amount that would otherwise deflate the
consolidation sum before rule 3 runs.

The lifecycle test chains vault, trigger, and withdrawal
transactions by real txids, exercises the delay boundary, the open
input side of the seal against a grafted output, and recovery from
both states. Drift guards recompute the pinned vector programs and
the vector condition lists from source, so puzzle source and
corpus cannot drift apart silently.
… guard

Ten findings from the pre-merge review, all verified live, plus
three smaller cleanups.

Puzzle guards (mod hashes re-pinned, vectors regenerated and
re-verified case by case). Every path of both programs now checks
the fixed values' domains first: authorization key 32 bytes,
recovery key nil or 32, delay 0 to 65535, target hash 32 bytes.
Without this a 33-byte recovery key curried into an instance left
every path working except recovery, which failed forever at
condition parse, and a delay past 65535 triggered fine and then
bricked the withdrawal path, both reproduced end to end. The
trigger additionally rejects a non-positive trigger amount, a
negative revault amount, and a malformed target, closing the
signed-solution shapes that minted an oversized or worthless
triggered coin.

Corrected claims. The identity section no longer says a wrong
curry is unspendable, never theft: the taproot assert binds
program to coin and cannot check values against intent, stated
with the wallet's verification duty. The consolidation
monotonicity claim is withdrawn: rule 1 leaves unclaimed slots
unconstrained, so funded dust at the vault scriptPubKey can always
appear, and the accurate statements are that conflicting
consolidations still merge what they consume and that no keyless
path splits value. The per-group reading of the section 3.2
benchmark predicate is now stated explicitly as the
composition-compatible reading.

Vectors. New pins: negative and non-minimal listed follower
amounts (bad_condition_arg at parse, the sum-deflation hole), the
two-leaders double-claim (unbalanced_message), the stale-signature
redirected target (unsatisfied_sig_assert where the output claim
alone would pass), the malformed-instance and amount-guard
failures as vm cases. Spec citations unchanged: VALIDATION.md
rules 1 and 3 and the CONDITIONS.md argument domains.

Tests. The identity test now cross-checks three independent
computations (tree_hash, a spec-rule reimplementation, the
program's own reconstruction) and checks uncurry's inner program,
replacing an assertion that compared tree_hash to itself. The
validation drift guard is a full closure: every conditions field
in both validation files recomputed, set-equal in both directions.
Guard tests cover every new rejection. Ruff findings fixed,
solution_items replaced by the library's iter_proper_list.

Docs and repo. Glossary row for mod hash. Redundant include
dropped from triggered.bl. Stray probe artifact removed.
…delay-encoding guards, signing-domain tags

Four findings from Evan's review of the fold-in commit.

The same-target withdrawal merge is a real divergence with a
consequence and the doc now says so instead of calling it batching.
SEAL_OUTPUTS drops the input-side commitment CTV's template hash
carries, the withdrawal path is keyless, so two matured triggered
coins with one target hash satisfy a single committed output set
together and the second coin's value burns to fees, profitably for
a miner. CTV's input-count commitment is the known half-spend
protection and this construction does not have it. The divergence
entry states the attack, the wallet rule (never reuse a target),
and flags a count-committing seal variant for a spec decision
outside this unit. The merge transaction is pinned as the valid
vector same_target_withdrawals_merge_second_burns and a runner
test, and the vault value never pays fees claim now carries the
recorded exception.

RECOVERY_SPK joins the domain guards in both programs, 1 to 10000
bytes. It was the one curried value without a guard and its
failure is the guard block's own stated worst case: nil is the
legitimate spelling for the adjacent RECOVERY_KEY, and a nil
recovery script triggered, revaulted, and consolidated perfectly
while only the panic path was dead.

DELAY must be minimally encoded, guarded by (= DELAY (+ DELAY 0)),
byte-exact equality against the normalizing sum. A padded encoding
read as the same number in the numeric guards but
ASSERT_SEQUENCE_HEIGHT rejects it, bricking withdrawal alone,
exactly the asymmetric-path failure the guards exist to remove.
Probed live: = is byte-exact, + normalizes.

Both signed message lists now open with a signing-domain tag, 1
for trigger and 2 for recovery, so the two authorizations live in
disjoint digest domains even when one key is curried into both
roles, instead of resting on the lists' shapes differing.

Mod hashes re-pinned (vault 15884715, triggered 214b0347), vectors
regenerated and executed case by case, new vm pins for the nil
recovery script and padded delay instances. 784 tests, 1061
vector cases.
@EvanWinget
EvanWinget merged commit 33fcdc5 into main Aug 21, 2026
2 checks passed
@EvanWinget
EvanWinget deleted the unit-7-vault branch August 21, 2026 02:40
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