Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,19 @@ In practice this means:
touching semantics references its spec section. The spec states
behavior only, and stays complete enough on its own to predict
every vector's outcome. Rationale, oracle provenance, and decision
records live in `docs/` (for the VM, `docs/vm-record.md`).
records live in `docs/` (for the VM, `docs/vm-record.md`). One
recorded exception (decision by Evan, 2026-07-29): vocabulary
entries in `spec/CONDITIONS.md` carry a brief curation note, why
the entry is in v0 and what was declined, with the full rationale
still in `docs/condition-record.md`.
2. **Vectors are the source of truth between sessions.** Sessions are
stateless, the vector corpus is not. Any behavior worth keeping becomes
a vector in `vectors/` the same day.
3. **Divergence is documented, never silent.** Anywhere BitLisp differs
from CLVM, the divergence table in `docs/vm-record.md` says so and
why.
why. Anywhere the condition layer differs from Chia's deployed
condition semantics, the table in `docs/condition-record.md` says
so and why.
4. **The novel layer gets adversarial treatment first.** The matching rules
(`spec/MATCHING.md`) have no external reference. They get
property-based invariants and theft-bug regression vectors before any
Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- [vm-record.md](vm-record.md): the VM record behind spec/VM.md, the
divergence table, the oracle provenance, and the design decision
record. Cited by CLAUDE.md ground rule 3.
- [condition-record.md](condition-record.md): the Phase 2 counterpart
behind spec/CONDITIONS.md and spec/MATCHING.md, the
divergence-from-Chia table, reference provenance, decision record,
and the novel-layer register. Cited by CLAUDE.md ground rule 3.
- [opcode-comparison.md](opcode-comparison.md): informative side-by-side
of the CLVM, bllsh, and BitLisp operator sets.
- Essay drafts land here in Phase 4.
148 changes: 148 additions & 0 deletions docs/condition-record.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Condition and matching record

The rationale, reference provenance, and decision record for the
condition layer (`spec/CONDITIONS.md`) and the matching layer
(`spec/MATCHING.md`). The specs state behavior only. This record says
why, and what the evidence was. It is the Phase 2 counterpart of the
VM record (`docs/vm-record.md`), with one structural difference: the
VM record's divergence rows are each diff-tested against a consensus
binary, while the rows here are established against Chia's deployed
condition semantics by translated consensus tests and source reading,
because the transaction models differ and no binary diff is possible.
Section 4 registers the rules that have no external reference at all.

## 1. Divergence from Chia conditions

| id | area | Chia (deployed) | BitLisp | rationale | vectors |
| --- | --- | --- | --- | --- | --- |
| C1 | CREATE_COIN target | 32-byte puzzle hash | full scriptPubKey bytes, 1 to 10,000 | A Bitcoin output carries any script, and exits to non-BitLisp outputs are ordinary. A hash would also block the validator from comparing against the transaction's actual outputs without a reveal. Ratified 2026-07-29. | `matching/create-coin.json` |
| C2 | CREATE_COIN memos | optional third argument, wallet-discovery hints | declined, strict arity two | The discovery job does not exist under output-script scanning. Consensus-carried bytes with no consensus meaning are a deliberate non-affordance (design obligation 4, inscription counterargument recorded there). A memo-bearing variant stays reachable through the reserved tier. Ratified 2026-07-29. | `conditions/encoding.json` arity cases |
| C3 | duplicate CREATE_COIN within one spend | rejected (child coin ids would collide) | valid, two claims requiring two distinct slots | Chia's rejection exists because its content-derived coin ids make identical children the same coin. Bitcoin output identity is positional, so identical slots are meaningful and routine (batch payouts). Counting under rule 1 handles them. Ratified 2026-07-29. | `matching/create-coin.json` duplicate cases |
| C4 | unknown condition opcodes | ignored and unenforced, zero cost for one-byte opcodes, a computed cost table for larger opcodes (verified in chia_rs `compute_unknown_condition_cost`, 2026-07-29) | three tiers: assigned, invalid, reserved 0x80 to 0xff with declared cost and a floor | Invalid-by-default matches the consensus mindset (reject the ambiguous case). The reserved tier is the deliberate forward-compatibility hatch, priced so old and new validators agree forever. Ratified 2026-07-29, four sub-decisions in section 3. | `conditions/encoding.json` tier cases |

## 2. Reference provenance

- **Chia condition semantics.** Established from the deployed
behavior of the pinned oracle wheels where portable, and from
translated Chia consensus tests for semantics that overlap
(the cross-check subset lands with the timelock family). No
binary diffing: the transaction models differ.
- **CHIP-0025 (message conditions)** and **CHIP-0049 (Chia 3.0
cost revisions)** are the recorded costing precedents for
matching rule 5. CHIP-0049's per-condition base cost of 500 is
the provisional value of RESERVED_COST_FLOOR in rule 6, to be
revisited when rule 5 lands. Two decisions are pre-registered as
deliberate rather than inherited: whether a per-spend free tier
is acceptable, and which precedent prices tx-scoped
SEND_MESSAGE and RECV_MESSAGE (the CHIP-0049 precedent is split,
see `docs/execution-plan.md` Phase 2 notes).
- **bllsh** (AJ Towns' introspection Lisp) was cloned into
git-ignored `references/` on 2026-07-29 and read for the
CREATE_COIN_TAPROOT evaluation, under the reading guardrails
(no code copied, spec statements established by our own
evidence, influence disclosed). Findings recorded in the D-CC2
entry below. `tools/fetch-references.sh` clones it alongside the
Chia repos.

## 3. Design decision record

1. **Condition-list encoding (decision zero).** RATIFIED (decisions
by Evan, 2026-07-29). Five parts:
- Chia-shaped lists: a proper list of conditions, each a proper
list with a one-byte opcode atom first. No Chia opcode-value
compatibility (the code space is laid out fresh, C4).
- Strict arity and minimal integer encodings for assigned
conditions, every deviation rejected. Strictness is the
loosenable direction post-deployment.
- Reserved tier with declared cost: the first argument is the
cost, charged as declared by validators before and after any
future assignment, which is what keeps them in consensus. The
cost argument itself is strict. Everything after it is
unconstrained forever, because the future assignment defines
the shape and old validators must not reject what it needs.
RESERVED_COST_FLOOR prevents free spam.
- Code-space layout: 0x00 invalid, family blocks 0x01 to 0x5f
with intra-block gaps invalid (typos near real opcodes fail
loudly), 0x60 to 0x7f unallocated invalid, 0x80 to 0xff
reserved. More than 128 future conditions means a new leaf
version, accepted deliberately.
- Policy stance: reserved conditions are consensus-valid and
policy-discouraged until assigned, the upgradable-NOP
precedent. The spec marks the policy note as non-consensus.
2. **CREATE_COIN shape.** RATIFIED (decisions by Evan, 2026-07-29).
Script bytes not hash (C1), memos declined (C2), duplicates
allowed as distinct claims (C3), empty script rejected as
burn-or-bug material, amount 0 to MAX_MONEY with zero-amount
outputs left to policy exactly as Bitcoin base rules leave them.
3. **CREATE_COIN_TAPROOT (D-CC2).** RATIFIED (decision by Evan,
2026-07-29). Covenant recursion needs successor scriptPubKeys of
the form taproot(internal key, tree root), and the BIP341 tweak
is elliptic-curve arithmetic the VM deliberately lacks (the D2
curation in `docs/vm-record.md`). Resolved as a condition:
`CREATE_COIN_TAPROOT(internal_key, merkle_root, amount)`, strict
arity three, the validator computes the tweak natively and the
condition then matches as an ordinary rule 1 claim. Two
alternatives declined:
- `secp256k1_muladd` (bllsh's general linear-combination
assert). Reading bllsh's examples on 2026-07-29 found three
usage patterns: re-implementing BIP340 (covered by
`secp_verify` and the AGG_SIG family), verifying the current
input's own taproot construction (impossible as a VM operator
in a pure VM, flagged as ASSERT_MY_TAPROOT for the ASSERT_MY_*
design), and covenant recursion (test-flexmarks), which is
exactly the computation the condition form performs. The
honest residual: muladd also enables adaptor-signature-class
and Pedersen-class equation verification. That capability is
named here, not silently dropped, and the reserved tier is its
priced future path.
- A narrow `taptweak` VM operator. Strictly weaker than the
condition form in this architecture: a pure VM has no
transaction access, so the operator could only check
solution-supplied claims, while costing a VM operator slot, a
no-oracle divergence row, an extra witness element (the
claimed key plus a parity bit that compute-mode never needs),
and the first exception to "the VM's only curve door is
signature verification."
The condition enters the v0 vocabulary in its own PR immediately
after the opening one. Its commit discloses the bllsh reading.
4. **Rule 1 equality-only matching.** RATIFIED (decision by Evan,
2026-07-29, as part of the rule 1 draft). Claims match slots by
exact content equality only, which collapses injective matching
to multiset containment (counting) and keeps graph algorithms
out of consensus. The spec makes the restriction normative text
so relaxing it requires amending visible prose plus a recorded
decision here.
5. **Curation notes stay in the spec.** RATIFIED (decision by Evan,
2026-07-29). The Phase 0 stub planned a curation note on every
vocabulary entry, and the later spec-purity rule (spec states
behavior only, rationale in docs) arguably forbade it. Resolved
in favor of the notes: obligation 4 wants the curation visible
where the vocabulary is, so entries keep a brief note and this
record keeps the full rationale. CLAUDE.md ground rule 1 records
the exception.
6. **RESERVED_COST_FLOOR stays at 500.** RATIFIED (decision by Evan,
2026-07-29). The CHIP-0049 per-condition base cost stands as the
provisional floor, revisited when rule 5's costing design lands.
7. **Invariant direction correction.** The Phase 0 stub stated that
removing a condition never turns an invalid transaction valid.
Under rule 1 that is false (removing one of two over-claims
restores validity) and the true property is the reverse
monotonicity: constraints only tighten, so removing a condition
never invalidates a valid transaction. Corrected in the spec
commit that made the invariants normative, 2026-07-29, flagged
in that PR for review.

## 4. Novel-layer register

The matching rules have no external reference: no deployed system
checks a condition list against a Bitcoin transaction. What stands in
for an oracle, per ground rule 4:

| rule | status | oracle substitute |
| --- | --- | --- |
| 1. Injective multiset output matching | normative | hypothesis invariant suite (injectivity, reorder invariance, monotonicity, metamorphic mutations) plus the adversarial corpus in `vectors/matching/`, opening with the duplicate-CREATE_COIN theft vector |
| 2. Mixed-transaction rule | pending | same treatment on landing |
| 3. Message scoping | pending | same treatment on landing |
| 4. Dedup and multiplicity | pending | same treatment, plus translated Chia dedup tests where semantics overlap |
| 5. Per-condition costing | pending | CHIP-0049 precedent comparison plus cost-conservation properties |
| 6. Reserved conditions | normative | encoding vectors in `vectors/conditions/`, every error path pinned |
16 changes: 14 additions & 2 deletions python/bitlisp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
"""BitLisp reference implementation.

This package is the executable specification: small, boring, and
readable whole. Behavior is pinned by the vector corpus and by
differential testing against the consensus oracle.
readable whole. Behavior is pinned by the vector corpus, by
differential testing against the consensus oracle where one exists,
and by property-based invariants where none does.
"""

from .conditions import MAX_MONEY, CreateCoin, Reserved, parse_conditions
from .errors import CODES, BitLispError
from .machine import run, run_serialized
from .matching import validate_transaction
from .serialize import deserialize, serialize
from .sexp import NIL, TRUE, atom_to_int, int_to_atom, is_atom, is_pair
from .tx import Transaction, TxInput, TxOutput

__version__ = "0.0.1"

__all__ = [
"BitLispError",
"CODES",
"CreateCoin",
"MAX_MONEY",
"NIL",
"Reserved",
"TRUE",
"Transaction",
"TxInput",
"TxOutput",
"atom_to_int",
"deserialize",
"int_to_atom",
"is_atom",
"is_pair",
"parse_conditions",
"run",
"run_serialized",
"serialize",
"validate_transaction",
]
128 changes: 128 additions & 0 deletions python/bitlisp/conditions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Condition-list parsing and validation.

A successful puzzle evaluation yields a condition list: a proper list
of conditions, each a proper list opening with a one-byte opcode atom.
Opcode values fall in three tiers. Assigned values carry the
vocabulary's semantics under strict arity and minimal integer
encodings. Values 0x80 and above are reserved: accepted with a
declared cost and no enforced semantics, the forward-compatibility
hatch a later soft fork can tighten into real conditions. Everything
else is invalid, so a typo near a real opcode fails loudly instead of
becoming an accidental no-op.
"""

from dataclasses import dataclass

from .errors import BitLispError
from .sexp import NIL, atom_to_int, int_to_atom, is_atom, is_pair

MAX_MONEY = 2_100_000_000_000_000
MAX_SCRIPT_PUBKEY_SIZE = 10_000
RESERVED_COST_FLOOR = 500

CREATE_COIN = 0x01
_RESERVED_START = 0x80


@dataclass(frozen=True)
class CreateCoin:
"""Claims one output slot with exactly this content."""

script_pubkey: bytes
amount: int

opcode = CREATE_COIN


@dataclass(frozen=True)
class Reserved:
"""No enforced semantics, only the declared cost. args holds the
raw argument nodes after the cost, unconstrained by design."""

opcode: int
cost: int
args: tuple


def _iter_conditions(node, what):
"""Yields the elements of a proper list, else bad_condition_list."""
while node != NIL:
if not is_pair(node):
raise BitLispError("bad_condition_list", f"{what} is an improper list")
yield node[0]
node = node[1]


def _parse_int(atom, what):
if not is_atom(atom):
raise BitLispError("bad_condition_arg", f"{what} must be an atom")
value = atom_to_int(atom)
if int_to_atom(value) != atom:
raise BitLispError("bad_condition_arg", f"{what} not minimally encoded")
return value


def _parse_create_coin(args):
if len(args) != 2:
raise BitLispError(
"bad_condition_arity", f"CREATE_COIN takes 2 arguments, got {len(args)}"
)
script_pubkey, amount_atom = args
if not is_atom(script_pubkey):
raise BitLispError("bad_condition_arg", "scriptPubKey must be an atom")
if not 1 <= len(script_pubkey) <= MAX_SCRIPT_PUBKEY_SIZE:
raise BitLispError(
"bad_condition_arg",
f"scriptPubKey must be 1 to {MAX_SCRIPT_PUBKEY_SIZE} bytes, "
f"got {len(script_pubkey)}",
)
amount = _parse_int(amount_atom, "CREATE_COIN amount")
if not 0 <= amount <= MAX_MONEY:
raise BitLispError("bad_condition_arg", f"amount out of range: {amount}")
return CreateCoin(script_pubkey, amount)


def _parse_reserved(opcode, args):
if not args:
raise BitLispError(
"bad_condition_arity", "reserved condition missing its declared cost"
)
cost = _parse_int(args[0], "declared cost")
if cost < 0:
raise BitLispError("bad_condition_arg", f"declared cost negative: {cost}")
if cost < RESERVED_COST_FLOOR:
raise BitLispError(
"reserved_cost_too_low",
f"declared cost {cost} below floor {RESERVED_COST_FLOOR}",
)
return Reserved(opcode, cost, tuple(args[1:]))


def _parse_condition(node):
if not is_pair(node):
raise BitLispError("bad_condition_list", "condition is not a list")
items = list(_iter_conditions(node, "condition"))
opcode_atom = items[0]
if not is_atom(opcode_atom) or len(opcode_atom) != 1:
raise BitLispError(
"bad_condition_opcode", "opcode must be an atom of exactly one byte"
)
opcode = opcode_atom[0]
args = items[1:]
if opcode >= _RESERVED_START:
return _parse_reserved(opcode, args)
if opcode == CREATE_COIN:
return _parse_create_coin(args)
raise BitLispError("bad_condition_opcode", f"invalid opcode {opcode:#04x}")


def parse_conditions(node):
"""Parses an evaluation result into a tuple of conditions.

Raises BitLispError on any encoding violation. Order is the
emitted order.
"""
return tuple(
_parse_condition(element)
for element in _iter_conditions(node, "condition list")
)
11 changes: 9 additions & 2 deletions python/bitlisp/errors.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Error taxonomy.

Errors are consensus-relevant only as "the spend is invalid". The
classes exist so vectors and the diff harness can assert that BitLisp
fails for the same reason as the oracles.
classes exist so vectors can pin the reason a spend fails: oracle
parity for the VM codes, the named rejection rule for the condition
and matching codes, which have no oracle.
"""

CODES = frozenset(
Expand All @@ -24,6 +25,12 @@
"secp_verify_failed",
"user_raise",
"cost_exceeded",
"bad_condition_list",
"bad_condition_opcode",
"bad_condition_arity",
"bad_condition_arg",
"reserved_cost_too_low",
"unsatisfied_output_claim",
}
)

Expand Down
Loading