Skip to content

Unit 7: the singleton wrapper - #59

Merged
EvanWinget merged 7 commits into
mainfrom
unit-7-singleton
Aug 22, 2026
Merged

Unit 7: the singleton wrapper#59
EvanWinget merged 7 commits into
mainfrom
unit-7-singleton

Conversation

@EvanWinget

@EvanWinget EvanWinget commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Unit 7, second puzzle: the singleton wrapper, written in the v0 language, resequenced ahead of the payment pool and the async offer (Evan, 2026-08-22). Tooling and vectors only: nothing in spec/ or python/bitlisp/ changes.

What this PR is, honestly

Three things landed, in decreasing order of value:

  1. A finding about the vocabulary. Chia's singleton_top_layer does not port. A Bitcoin txid commits input outpoints and output scripts but never input scripts, so proving "my parent sat at a singleton scriptPubKey" needs two transaction preimages, and recognizing that scriptPubKey needs the taproot tweak, which no VM operator performs (secp_verify is Schnorr-only). Both facts apply equally to a fungible asset token's lineage proof.
  2. Reusable machinery. puzzles/lib/tx-wire.blib rebuilds a transaction's non-witness serialization from fields with canonical compact-size prefixes, the two-preimage lineage proof, the output-index rule, and the condition morph. All of it is what the asset token benchmark (BAT1 if it becomes a standard) needs.
  3. The singleton itself, whose value on Bitcoin is thin: Ark, Lightning, vaults, and payment pools get identity from their root outpoint and authority from signatures, and a funded imitation is self-funded. The doc's "Is a singleton worth having on Bitcoin" section says so, with a census of the vendored Chia corpora showing every Chia use is a token, an identity, or a coin governing other coins.

The construction keeps one scriptPubKey per lineage and commits the state in a zero-value OP_RETURN output of the creating transaction, whose payload is the tree hash of (inner program hash, child amount, child output index). The census also found no Chia dependent needs the puzzle hash to move with the inner puzzle, so the constant scriptPubKey is the better shape on Chia's own evidence, not a compromise. The taproot-tweak operator is flagged for the Phase 4 gate against condition-record decision 3, which declined it on an objection this puzzle answers.

The token benchmark is a capability-and-risk study, not a goal: BitLisp does not want fungible asset tokens on Bitcoin. It wants to know what the vocabulary can build and what that exposes, and to say so before anyone discovers it after deployment.

The review pass (folded in as the last commit)

Ten verified findings, three of them holes: an ending spend left its slot refillable (a hijack), placement hung on the parent's input index (any observer could brick a lineage, and a substituted child stayed live), and a curried internal key would have let a key holder bypass the inner program. Fixes: the ending spend claims a zero-payload state output, the state output commits (inner hash, amount, index) and the owner inner program seals the outputs, and the NUMS point is a wrapper constant. The rest: odd CREATE_OUTPUT_TAPROOT rejected, scriptPubKey derived rather than supplied, list.blib, serializer guarantee stated precisely with empty lists rejected, cost leaks closed, missing negative tests and serialization boundaries added.

Review guide

Read in this order. Each step says what to check and how long it should take.

1. docs/puzzles/singleton.md, sections 1 to 3 (10 minutes). Read "Is a singleton worth having on Bitcoin" as the record of the value question. Check the funded-imitation table against your own sense of Ark, Lightning, vaults, and pools: the claim is that an imitation hurts only the faker in every one of them. Then "Why the Chia construction does not port directly": two facts, the txid's contents and the missing tweak. If either fact is wrong the whole design is wrong, so this is the place to push.

2. The lineage diagram, "The lineage proof", and "The state output" (8 minutes). The chain argument: an outpoint is spent once, so one transaction creates any singleton's child, and that transaction's one tagged state output names the child by amount and index. The induction step is the parent check: a coin at the lineage scriptPubKey can only be spent by running this program (the internal key is the NUMS constant, no key path), so a transaction that spent one spent a genuine singleton. Ask two things: can a funded fake at spk_L ever be spent (no, the input it names is neither the launcher nor a coin at spk_L), and can an observer who re-assembles a broadcast spend end up with a live coin of their own (no, every misplacement is a dead coin, and the ending state's zero payload means an ended slot cannot be refilled).

3. puzzles/lib/tx-wire.blib (5 minutes). The claim to check: the byte string it builds parses back, under Bitcoin's parser, to exactly the fields it was built from. Canonical compact-size (Core rejects non-canonical), width checks on every fixed field. test_wire_serialization_matches_model compares it to the model's Transaction.txid across the compact-size widths the model reaches.

4. puzzles/singleton/singleton.bl (15 minutes). Read the top comment, then bottom up: the assert block of domain guards, spend (names the scriptPubKey, txid, and inner hash once), emit (parent proven, state proven, the three self asserts, then the morph). Four things to verify against the doc: parent-proven handles the launcher case by outpoint equality and everything else through the grandparent's txid, index, and script; state-proven requires exactly one tagged output whose payload hashes this coin's (inner hash, amount, index); morph raises on a second odd output, on none, and on an odd CREATE_OUTPUT_TAPROOT, and turns the -113 marker into the zero-payload ending state output; INTERNAL_KEY is a constant. END_AMOUNT is singleton_top_layer's own escape value, not CAT's melt.

5. puzzles/singleton/owner-inner.bl (3 minutes). The simplest useful inner program. The signature covers the next inner hash, the amount, the extra conditions, and the outputs hash, bound to the coin's outpoint, and the program emits SEAL_OUTPUTS: that seal is what makes a signing owner's placement immutable while fee inputs still attach.

6. "Chia correspondence" and "Adversarial cases" (5 minutes). Five recorded divergences. The one to weigh: placement is committed, not created. With a signing owner nothing is exposed. For an inner program with a public path and no signer, the assembler can end the lineage (a brick) but never redirect it. Chia's exact coin creation has no brick vector.

7. "Authoring observations across the vault and the singleton" (5 minutes). Eight patterns, none implemented. The decision they set up is let first, with the domain-guard blocks as the typing evidence for the v1 gate. Nothing to approve here, only to read.

8. Tests and vectors (5 minutes, mostly commands). python/tests/test_singleton_puzzles.py builds the lifecycle through the transaction model so every txid is real, reproduces each adversarial case with its error code, and recomputes every pinned program, solution, and conditions field from source. Skim test_lifecycle, test_substituted_child_is_dead_and_so_is_the_displaced_one, test_ended_lineage_cannot_be_refilled, and test_fee_input_prepended_keeps_spend_valid, then run the commands below.

Authorizing sections

  • CONDITIONS.md entries CREATE_OUTPUT, ASSERT_MY_TAPROOT, ASSERT_MY_AMOUNT, ASSERT_MY_OUTPOINT, ASSERT_SIG_MY_OUTPOINT, SEAL_OUTPUTS (and its author guidance on composing a seal into a signed message).
  • VALIDATION.md transaction view (the txid derivation the library reproduces byte for byte), rules 1, 2, 4, and 8.
  • VM.md section 4: the operator table, secp_verify Schnorr-only being the fact that decides the construction.
  • docs/lang/language.md and docs/lang/curry.md for the surfaces consumed, including computed defconstant for the state tag length.

Commits in order

  1. docs: the singleton wrapper design, unit 7 resequenced singleton-second
  2. puzzles: the singleton wrapper, the owner inner program, and the wire serialization library
  3. vectors: pin the singleton lifecycle and its lineage, state, and morph failures
  4. tests: compile-and-run coverage for the singleton puzzles
  5. docs: the singleton's value on Bitcoin, the Chia census, and eight authoring observations
  6. docs: the token benchmark is a capability-and-risk study, not a goal
  7. review fold-ins: committed placement, the ending state output, the fixed internal key, sealed owner spends

Verify independently

.venv/bin/pytest python/tests -q                      # 809 passed
.venv/bin/python tools/run_vectors.py                 # 38 files, 1108 cases, 0 failures
PATH=".venv/bin:$PATH" ci/lint/lint.sh                # all checks passed

# the pinned mod hashes
.venv/bin/bitlisp-compile -T puzzles/singleton/singleton.bl -I puzzles/lib -I puzzles/singleton
.venv/bin/bitlisp-compile -T puzzles/singleton/owner-inner.bl -I puzzles/lib -I puzzles/singleton

Every vector case was executed against the VM and the validator at generation, and every adversarial case is reproduced live in the test suite with its exact error code.

The fourth benchmark puzzle's design record: why Chia's singleton_top_layer does not port (a txid commits input outpoints but not input scripts, and no VM operator performs the taproot tweak, secp_verify being Schnorr-only), the constant-scriptPubKey construction with the state committed in a tagged OP_RETURN output of the creating transaction, the two-preimage lineage proof, the output-index rule, the morph, the Chia correspondence table with its recorded divergences, and the adversarial analysis (funded fakes, dust, assembler substitution versus bricking, bloat griefing). The deviation from the Chia shape is stated with both sides and awaits Evan's ratification. A taproot-tweak VM operator is recorded as the Phase 4 candidate that would allow the Chia shape.

Authorizing sections: CONDITIONS.md entries CREATE_OUTPUT, ASSERT_MY_TAPROOT, ASSERT_MY_SCRIPTPUBKEY, ASSERT_MY_AMOUNT, ASSERT_MY_OUTPOINT, ASSERT_SIG_MY_OUTPOINT. VALIDATION.md transaction view (the txid derivation the serialization library reproduces), rules 1, 2, and 8. VM.md section 4 (the operator table, secp_verify Schnorr-only, sha256, substr, concat, logand, ash).

Glossary rows for singleton, launcher outpoint, lineage proof, state output, inner program, and the output-index rule. Execution plan unit 7 bullet records the resequencing and the decision.
… serialization library

puzzles/lib/tx-wire.blib rebuilds a transaction's non-witness serialization from its fields with canonical compact-size prefixes and width-checked fixed fields, so the double SHA-256 proves the fields, not only the hash (VALIDATION.md transaction view, the txid derivation).

puzzles/singleton/singleton.bl: curried (SINGLETON_MOD_HASH INTERNAL_KEY LAUNCHER_OUTPOINT), constant scriptPubKey per lineage, ASSERT_MY_TAPROOT over the recomputed root plus ASSERT_MY_SCRIPTPUBKEY, ASSERT_MY_AMOUNT, and ASSERT_MY_OUTPOINT over the creating txid and the coin's output index (CONDITIONS.md self asserts). The parent is the creating transaction's input at the coin's own output index and must be the launcher or proven at the lineage scriptPubKey through the grandparent. The state is the supplied inner program, committed by exactly one OP_RETURN output tagged with the lineage. The morph rewrites the one odd-amount CREATE_OUTPUT into the child claim and the state claim (CONDITIONS.md CREATE_OUTPUT, VALIDATION.md rule 1), or drops it at -113.

puzzles/singleton/owner-inner.bl: a curried owner key signs the next inner hash, amount, and extra conditions under ASSERT_SIG_MY_OUTPOINT (CONDITIONS.md signature asserts, VALIDATION.md rule 8).

Design references read, per the reference policy: Chia's singleton_top_layer (the clvm_tools_rs test copy), singleton_launcher and p2_singleton (tibetswap includes). The morph shape, the odd-amount marker, the -113 escape value, and the truths-prepended inner call follow singleton_top_layer. Nothing is copied.
…h failures

vm/singleton-programs.json: 15 compiled representatives, the three lifecycle spends and a second lineage's first spend, then every in-program failure: funded fakes with and without a grandparent, extra outputs indexing a fee input or no input, an uncommitted inner program, two state outputs, no odd output, two odd outputs, an even amount, a malformed launcher outpoint, a truncated version field.

validation/singleton-lineage.json: 9 cases, the lifecycle and two lineages composed in one transaction valid, then the coin at the wrong outpoint, the child output missing or re-amounted, the state output missing, and an invalid owner signature. Every case executed against the VM and the validator at generation.

Pins VALIDATION.md rules 1 and 8 and the self asserts of CONDITIONS.md over the singleton's condition lists.
Mod hashes pinned, uncurry read-back, the serialization library checked against the transaction model's txid across every compact-size width the model reaches, the lifecycle run through the single-spend runner over real txids, each adversarial case reproduced with its exact error code, signature replay and rewrite, two lineages composed, and drift guards that rebuild every vm vector's program and solution and every validation vector's conditions field from source.
…thoring observations

Answers the question owed before review: whether a singleton earns a place on Bitcoin. The funded-imitation test over Ark, Lightning, channel factories, vaults, payment pools, and fixed-key oracles finds none of them needs one, since the root outpoint and the parties' signatures give identity and uniqueness and an imitation is self-funded. The census of the vendored Chia corpora (chia_rs puzzle types, tibetswap, chia-gaming, the clvm_tools_rs puzzle sources) finds every dependent names a singleton by launcher id and none needs the puzzle hash to move with the inner puzzle, so the constant scriptPubKey is the better shape on Chia's own evidence. The lineage machinery is what the fungible asset token, the suite's own benchmark, needs, and the remaining effort should point there, named BAT1 if it becomes a standard (decision by Evan, 2026-08-22).

Also records eight authoring observations across the vault and the singleton for the typed v1 ledger: let first (the assign trigger fired in both puzzles), curried-value domain guards as the typing evidence, a standard list library owning generic names, a list* form, the identity-reconstruction repetition, a self-assert helper, a Phase 4 encoding-operator cost question, and a shared puzzle test harness. None implemented here.
BitLisp does not set out to put fungible asset tokens on Bitcoin. The token benchmark exists to know what the vocabulary can build, what it costs, and what it exposes (decision by Evan, 2026-08-22). Stated in the singleton doc's verdict and the unit 7 plan bullet.
…xed internal key, sealed owner spends

Ten verified findings from the code-review pass, three of them holes in the construction:

1. An ending spend claimed no state output, so a re-assembler could add a coin and a state output of their own and resurrect the lineage under their inner program. The ending spend now claims a state output with the all-zero payload, which no inner program matches, so a refill is a second tagged output and the refilled coin dies.
2. Placement hung on the parent's input index, so any mempool observer could brick a lineage by prepending a fee input or reordering outputs, and a substituted child at the index stayed live. The state output now commits the tree hash of (inner hash, amount, child index), the child verifies its own triple, the input-index rule is gone, and the owner inner program signs and emits SEAL_OUTPUTS so a signing owner's placement is immutable while fee inputs still attach freely (CONDITIONS.md SEAL_OUTPUTS, the author guidance on composing a seal into a signed message).
3. INTERNAL_KEY was curried, so a lineage with a spendable key path would let the key holder replace the state without running the inner program. The NUMS point is now a constant of the wrapper.

Also: an odd-amount CREATE_OUTPUT_TAPROOT is rejected so one spend creates one odd coin and no more, the scriptPubKey is derived from the creating transaction's output at the coin's index instead of supplied and separately asserted, puzzles/lib/list.blib owns length and nth, the serializer rejects empty input and output lists and states its guarantee precisely, state-proven is a defun and the inner hash is computed once (about 15 percent cheaper), the no-grandparent fake raises deliberately, and the index domain asserts that nth already enforces are gone.

Tests: the grandparent's script and index clauses each have a negative case on a real chain, the serializer is checked at the 252, 253, 65535, and 65536 script widths and a 253-input list, the substitution and refill re-assemblies are reproduced at both the validator (seal) and the wrapper (amount, index, second tagged output), and iter_proper_list replaces a local copy. Vectors re-pinned: 20 vm cases, 13 validation cases, all single-rule violations. Doc: the lineage section describes committed placement, divergence 4 is rewritten around what the mechanisms actually do, the tweak operator is stated against condition-record decision 3, the built shape carries its decision line, Chia's truths are named the coin's own data, and the two-OP_RETURN relay-policy dependency is recorded.
@EvanWinget
EvanWinget merged commit ddc93ee into main Aug 22, 2026
2 checks passed
@EvanWinget
EvanWinget deleted the unit-7-singleton branch August 22, 2026 20:32
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