Beersy
BRC-173

Bitcoin Script to and from Bitcoin BASIC — a compiler and a decompiler

Bitcoin Script is a low-level stack machine with no names or structure, so a script of any real size is nearly impossible to read, review, or safely edit once written. Because a published covenant can never be changed after it is funded, mistakes hidden in that unreadable stack manipulation are typically found only after it is too late to fix. This gives developers a way to write covenant logic in a readable form and, just as importantly, to read back and verify a script someone else already deployed.

sun-dive18 min read

Reference for an AI

Everything an assistant needs to answer questions about BRC-173 accurately, including what it depends on.

Summary

Why
Bitcoin Script has no names or structure, so once a covenant is published its logic cannot be reliably checked or safely amended, and errors surface only after funds are committed.
What
BRC-173 defines Bitcoin BASIC, a readable BASIC-like dialect that maps one-to-one to Bitcoin Script opcodes, along with a compiler that emits Script from it and a decompiler that renders any deployed script back into it.
How
A developer writes or pastes script into a browser tool (or an on-chain published copy) that compiles the BASIC dialect down to Script opcodes, or decompiles existing Script chunks back into the dialect using named annotations while leaving the underlying bytes untouched.

What this lets you do

  • Write covenant logic in a readable BASIC-like dialect instead of raw stack opcodes
  • Compile that dialect to Bitcoin Script with branch stack-shape checking built in
  • Decompile a deployed back into readable structured code, even one you did not write
  • Recompile a decompiled program and get a semantically identical, often byte-identical, script
  • Unroll bounded FOR loops and named DIM fields into correct opcode sequences

Written by claude-sonnet-5 from the specification text. Where the two differ, the original is correct.

Bitcoin BASICScript opcodes

The specification

Abstract

Bitcoin Script is a stack machine with no names, no types and no structure a reader can hold. It is also, almost exactly, the machine that sits underneath every infix expression evaluator written for an 8-bit microcomputer: v = v + f/m - v*d becomes the same sequence of stack operations in either.

This standard defines Bitcoin BASIC, a BASIC dialect in one-to-one correspondence with the Script opcode set, and the two translations between them — a compiler that emits Script from the dialect, and a decompiler that renders a deployed back into it. The dialect adds no runtime: every construct either emits opcodes or is resolved at compile time.

The decompiler is the half with no precedent. A compiler reports what an author meant; a decompiler reports what the bytes say — including bytes the reader did not write, fetched from the chain, belonging to somebody else.

Both directions run in a browser, with nothing to install:

https://grafverse.com/basic.html

Paste a locking script and read it back as a program, or write the dialect and compile it. The same implementation is also published on chain, which is for permanence and verification rather than for use.

Motivation

A covenant cannot be amended. Its constants are permanent, its branches are permanent, and the transaction that publishes it is the last moment at which any of it can change. Yet a covenant of useful size cannot be checked by eye: a working one is a few thousand opcodes of stack manipulation, a single significant constant is a two-byte push somewhere inside it, and one edit moves every offset and depth that follows. What gets verified in practice is a mental model of the script rather than the script — and the model is invalidated by the very change it was meant to check.

The consequence is that errors are discovered after publication and funding, when nothing can be altered.

Both halves of the answer are needed, and only one of them is conventional:

  • Compiling removes a class of defect at the source. Branch arms that disagree about stack depth, a value left on the altstack in one arm only, a depth counted by hand — none of these are expressible in a higher-level language, so a compiler cannot emit them.
  • Decompiling is what makes verification possible at all, and it applies to scripts nobody in possession of the tool wrote. It is the only route by which a third party can review a covenant before trusting value to it.

Relationship to BRC-15 and BRC-106

This standard sits above the existing representations rather than competing with them.

BRC-15 defines an assembly language for Script, and BRC-106 standardises deterministic translation of Script to and from . Both operate at one token per opcode: they are lossless, they preserve the opcode sequence exactly, and they recover no structure. OP_ADD states that an addition occurs; it does not state that a line reads v = v + f/m - v*d, nor which bytes of a script are its mutable state.

Bitcoin BASIC operates on sequences of opcodes, and recovers structure: infix expressions, conditional blocks, named state fields, and loop bodies where the loop survives in the script. It is therefore lossy in spelling and exact in behaviour — a decompiled program recompiles to a script with identical semantics, and frequently to identical bytes.

The layers compose: Bitcoin BASIC to Script, and Script to ASM under BRC-15 or BRC-106.

Scope

Defines the correspondence between the dialect and the opcode set, the obligations of a conforming compiler, the obligations of a conforming decompiler, and the round trip between them.

Does not define a covenant's funding, its authorisation, or where its state lives — those are separate proposals. Does not standardise a source-file format, an editor, or a package layout.

⚠ This standard does not claim the dialect is the only readable representation of Script, nor that BASIC is the best available syntax. It claims that the correspondence is exact, that both directions are implementable, and that the reverse direction is the one currently missing.

Specification

The key words "MUST", "MUST NOT", "SHOULD" and "MAY" are as described in RFC 2119.

1. The correspondence

Every infix language is compiled to a stack machine. An expression parser converts infix to postfix, and the postfix form is a sequence of pushes and operators applied to a stack. Script is that stack machine, with the front end absent: it has the operators, the stack, and no parser.

The correspondence therefore requires no invention. A conforming implementation MUST NOT introduce runtime machinery that Script does not have — no call stack, no heap, no return addresses, no runtime types. Every construct in the dialect MUST either emit opcodes directly or be resolved entirely at compile time.

2. The dialect

A conforming dialect MUST provide, and its decompiler MUST recognise:

  • AssignmentLET v = expr, with LET optional. Infix expressions with conventional precedence.
  • ConditionalsIF cond THEN … ELSE … on one line, or IF cond THEN opening a block closed by END IF (or ENDIF). The spellings MUST compile to identical bytes. ⚠ The block form is not sugar: a line-scoped arm ends where its line ends, so a loop cannot live in one.
  • VerificationVERIFY expr, emitting OP_VERIFY; the covenant's refusals are statements, not side effects.
  • Counted loopsFOR i = a TO b [STEP s] … NEXT i, unrolled at compile time (§4.2).
  • Fixed-width stateDIM name%width for numeric fields, DIM name$width for opaque bytes, with 1 ≤ width ≤ 75 (§4.3). ⚠ DIM belongs to a distinct compilation mode, not to a free-standing program: a layout is meaningless without the field offset and the peel and rebuild generated around it, and a conforming compiler MUST refuse it outside that mode rather than emit a bare push.
  • Two-value resultsl, r = SPLIT(x, n), since OP_SPLIT returns two values and no BASIC needed to.
  • Line numbers, optional in source and emitted by the decompiler, and &H hexadecimal literals.

⚠ Implementations MAY extend the dialect, but a decompiler MUST NOT emit a construct its own compiler cannot parse. A reader that prints a dialect the writer rejects is two tools, not one.

3. Opcode correspondence

The machine's own words are its opcodes, exactly as every BASIC had words for the machine beneath it. The reference implementation maps 35 of them; a conforming implementation MUST use these names for these opcodes, so that a listing produced by one tool is parsed by another.

operatorscomparisons (numeric)
+OP_ADD 0x93=OP_NUMEQUAL 0x9c
-OP_SUB 0x94<>OP_NUMNOTEQUAL 0x9e
*OP_MUL 0x95<OP_LESSTHAN 0x9f
/OP_DIV 0x96>OP_GREATERTHAN 0xa0
ANDOP_BOOLAND 0x9a<=OP_LESSTHANOREQUAL 0xa1
OROP_BOOLOR 0x9b>=OP_GREATERTHANOREQUAL 0xa2
one argumenttwo arguments
ABSOP_ABS 0x90MINOP_MIN 0xa3
NOTOP_NOT 0x91MAXOP_MAX 0xa4
NEGATEOP_NEGATE 0x8fCATOP_CAT 0x7e
ISTRUEOP_0NOTEQUAL 0x92NUM2BINOP_NUM2BIN 0x80
BIN2NUMOP_BIN2NUM 0x81MODOP_MOD 0x97
INVERTOP_INVERT 0x83BITANDOP_AND 0x84
HASH256OP_HASH256 0xaaBITOROP_OR 0x85
HASH160OP_HASH160 0xa9BITXOROP_XOR 0x86
SHA256OP_SHA256 0xa8LSHIFTOP_LSHIFT 0x98
SHA1OP_SHA1 0xa7RSHIFTOP_RSHIFT 0x99
RIPEMD160OP_RIPEMD160 0xa6SAMEBYTESOP_EQUAL 0x87
CHECKSIGOP_CHECKSIG 0xac

⚠⚠ = and SAMEBYTES MUST NOT be given one name. = is OP_NUMEQUAL and compares numbers; SAMEBYTES is OP_EQUAL and compares byte strings. 0, -0 and a zero padded to four bytes are one number and three different byte strings. Merging them erases a distinction that decides whether a covenant accepts a spend.

BITAND, BITOR and BITXOR are named apart from AND and OR for the same reason: OP_BOOLAND asks whether both operands are non-zero, and is not a bitwise operation.

4. What a covenant compiler must do that no BASIC had to

4.1 Balance the branches

Both arms of an OP_IF MUST leave the stack in the same shape. A conforming compiler MUST verify this and MUST refuse to emit otherwise. Hand-written Script gets this wrong silently, and the resulting failure surfaces far from its cause — typically where a later OP_SPLIT complains about a size.

4.2 Unroll the loops

Script has no backward jump, so there is nothing for a loop to compile to. FOR … NEXT MUST be unrolled at compile time, with the bounds known statically, and a conforming compiler MUST bound the expansion and fail rather than emit an unbounded one. Iteration in a script is spatial; iteration over time is a chain of spends, and is outside this standard.

⚠ A decompiler cannot recover a FOR that was unrolled — the loop is not in the script. It MUST render the unrolled body rather than infer a loop that may not have existed.

4.3 Make the widths the layout

DIM MUST declare a field's byte width, and that width MUST be the width the script carries. A conforming compiler generates both the reading of the state out of the script and its rebuilding, from the declaration alone.

Widths MUST be whole bytes in the range 1 to 75, and a compiler MUST refuse anything outside it. Above 75 a data push requires OP_PUSHDATA1 and a separate length byte, which shifts every offset after that field and breaks the property that the push opcode equals the width — the invariant a third-party reader depends on (see BRC-171).

⚠ The rebuild MUST gather fields by name, not by position: a compiler that reuses or coalesces slots will not leave them in declaration order, and a positional rebuild then assembles a valid-looking script with its state transposed.

4.4 Refuse rather than degrade

Where a construct cannot be expressed in opcodes, a conforming compiler MUST fail with a statement of why, and MUST NOT emit an approximation.

Exponentiation is the worked case. ^ is resolved at compile time in arbitrary-precision integers, is right-associative, and MUST reject a negative exponent and an exponent large enough to exceed what a script number can carry. If either operand is unknown until the script runs, the compiler MUST fail — Script has no power opcode, so there is nothing to emit. The idiom that replaces it is an unrolled loop whose counter supplies the exponent, which is also how the dialect supplies an array:

FOR k = 0 TO 23 : IF slot = k THEN bit = 2 ^ k : NEXT k

Twenty-four comparisons, each with its constant already folded. Script has no arrays; that is one.

5. Reading a script back

A conforming decompiler MUST operate on a script's parsed chunks, MUST NOT require the script to have been produced by the compiler, and MUST NOT modify the script in any way. Names are annotation supplied by the caller — the bytes remain the source of truth.

It MUST accept a declaration of what the stack already holds when the script begins, since a locking script is entered with the unlocking script's pushes already present, and without them a reader cannot know what it is looking at. Where no names are supplied it MUST still produce a correct listing with generated names.

It MUST report, rather than guess, when it can no longer track the stack — a script that reads below the bottom of the stack as the reader models it indicates either a malformed script or a wrong stack declaration, and both are results worth having.

6. Two examples, in both languages

One expression. The stack already holds v, force, mass and nv.

10 nv = v + force / mass
010379  push 3, OP_PICK      v
010379  push 3, OP_PICK      force
010379  push 3, OP_PICK      mass
96      OP_DIV               force / mass
93      OP_ADD               v + that
77      OP_NIP               discard the slot nv used to occupy

12 bytes, 9 opcodes. Infix became postfix, which is the whole of the correspondence in §1: the parser is the only thing Script was missing, and precedence decided the order of two opcodes.

A balanced conditional. The stack holds grip, demand and force.

10 force = 0
20 IF demand > grip THEN force = grip ELSE force = demand
00 77                     OP_0, OP_NIP            force = 0
010179 010379 a0          pick demand, pick grip, OP_GREATERTHAN
63                        OP_IF
  010279 0079 6b 75 6c      pick grip   → assign force
67                        OP_ELSE
  010179 0079 6b 75 6c      pick demand → assign force
68                        OP_ENDIF
77                        OP_NIP

29 bytes, 25 opcodes. ★ that both arms emit the same seven-opcode shape, differing only in which value they pick. That is §4.1 in the output: the compiler takes the union of what either arm assigns and makes both arms produce all of it, so the stack leaves the conditional identical either way. Hand-written Script is where this goes wrong, and it goes wrong silently — a mismatch surfaces much later, wherever the first opcode happens to read a slot that has moved.

Indexing without arrays. Noughts and crosses packs nine squares base 3 into two bytes — 0 empty, 1 for X, 2 for O — so a square is read by arithmetic rather than by lookup:

DIM board%2      REM  nine squares packed base 3
DIM turn%1       REM  whose move it is — 1 or 2

place = 0
IF move = 0 THEN place = 1
IF move = 1 THEN place = 3
IF move = 2 THEN place = 9
REM  … through to 6561
VERIFY place > 0                      REM  ★ the bounds check is free

VERIFY MOD(board / place, 3) = 0      REM  the square is empty
p = turn
board = board + p * place             REM  playing a move is an addition
turn = 3 - p                          REM  and the turn flips by arithmetic

★ Two properties fall out of the encoding rather than being enforced by extra code. A move outside 0…8 selects no constant, so place stays 0 and the bounds check costs nothing — and because the turn is read from state and written back as 3 - p, no party can play twice or play as the other. A base-4 packing of the same game is 105 bytes smaller, and gives up exactly this: a shift accepts an out-of-range index that a table of comparisons refuses.

Bounded and unbounded, from one source. Rule 110 is the same program with one number changed:

DIM cells%4      REM  31 cells, one bit each, wrapped into a ring
DIM gen%2

FOR g = 1 TO <generations>
  new = 0
  FOR i = 0 TO 30
    l = MOD(cells / 2 ^ MOD(i + 1, 31), 2)
    c = MOD(cells / 2 ^ i, 2)
    r = MOD(cells / 2 ^ MOD(i + 30, 31), 2)
    new = new + ((c OR r) AND NOT(l AND c AND r)) * 2 ^ i
  NEXT i
  cells = new
  gen = gen + 1
NEXT g

<generations> = 1 gives one generation per transaction; = 8 gives eight generations in one script, at 19,655 bytes. Both were run and arrive at the same state. Every 2 ^ … is folded at compile time because i is a loop counter, so the divisors are constants and there is no runtime index at all — which is why this program, alone among the four, needs neither a comparison table nor a shift. The eight-row rule table collapses to one line of boolean algebra.

7. The round trip

A program produced by the decompiler MUST recompile. The recompiled script MUST be semantically identical to the original; it SHOULD be byte-identical where the source construct survives in the script.

A disagreement between the two representations is the deliverable, not a defect in the tools. The static model held by the compiler is a stated hypothesis about a stack; the script interpreter is the referee. Where they differ, the difference is reported at the instruction that causes it rather than at the opcode that eventually fails — which is the shortest available path from a symptom to a cause.

8. Conformance

  1. Every name in §3 maps to its stated opcode, and = / SAMEBYTES are distinct. (§3)
  2. Both spellings of a conditional compile to identical bytes. (§2)
  3. Branch arms are checked for equal stack shape, and emission is refused otherwise. (§4.1)
  4. FOR is unrolled with static bounds and a bounded expansion. (§4.2)
  5. DIM widths are the widths carried, and the rebuild gathers by name. (§4.3)
  6. A construct with no opcode is refused with a reason, never approximated. (§4.4)
  7. The decompiler reads scripts it did not produce, modifies nothing, and reports loss of stack tracking rather than guessing. (§5)
  8. Decompiled programs recompile to semantically identical scripts. (§7)

⚠ Item 8 is the one that is worth measuring rather than asserting, and the measurement MUST include the number that recompile byte-identically — an implementation that recompiles to equivalent-but-different scripts every time has a defect its own test suite will not report.

Implementations

There are two copies of the reference implementation, and they exist for different reasons.

To use it — the workbench: https://grafverse.com/basic.html · compiles, decompiles, and reads a script straight off the chain. It requires no installation and no build; the page carries a prebuilt .

To verify it, or to have it outlast this document — the archive. Published on chain so that a standard's reference implementation is a payload rather than a link:

payload   8e0a79e00ef3c38cf3c4fa33c7d8032cc5def3853c0e798730c48217f00a369a
archive   f308f18b952360b46d207beb8539bb202d704ab80e23c39425c76f024d908175   200,280 B · 15 files

The archive was recovered from that transaction, unpacked, and its test suites run: 42/42, 18/18 and 10/10. Verification is therefore reproducible from the txid alone rather than from a repository that may move.

★ The compiler and the decompiler import only an opcode-number table and a type that erases at runtime. An implementation in another language needs a list of opcode numbers and nothing more. Cryptography appears in the reference implementation solely where covenants authorise spends, which is not part of this standard.

The round trip, measured: of 15 decompiled programs, 15 recompile and 9 are byte-identical. The six that differ contain unrolled loops, which cannot read back as loops (§4.2). A hand-written covenant of 1,672 bytes and 1,108 opcodes renders as a listing; a 1,428-byte covenant renders as 216 lines.

Four covenants are included as tests, each answering the absence of arrays differently, and all deployed or interpreter-validated as real spends:

lockopcodesapproach
Noughts and crosses, base 31,330 B957nine comparisons; a board packed base 3, so reading a square is arithmetic
the same game, base 41,226 Bone extra byte of state buys 105 fewer bytes of program — 1.16×
Space Invaders, 55 aliens1,082 B7432 ^ k folded inside an unrolled loop is the array
Rule 110, 31 cells2,974 Bno runtime index at all

⚠ The base-4 result is not free: a shift does not supply the bounds check that a comparison did.

Turing completeness, stated exactly

Rule 110 is Turing complete by Cook's proof. Compiled from this dialect it runs two ways, and both were measured: eight generations unrolled into one script (19,655 bytes), and eight generations as eight chained spends (23,792 bytes across eight scripts). Both arrive at exactly the same state.

The distinction that matters is between a script and a chain of them, and it is usually stated backwards.

A single script is total: it provably halts. Its step count is bounded before it runs.

⚠ But it is not true that a script cannot iterate. A script iterates perfectly well — Rule 110 advances thirty-one cells every generation, unrolled, inside one script. What a script cannot do is run an unknown number of times, and that is the only thing the chain is needed for. The chain does not supply the looping. It supplies the not knowing when to stop.

A chain of covenant spends is unbounded in that count, so Rule 110 compiled from this dialect and advanced by such a chain is a universal computation: covenant-chained Script is Turing complete in the same qualified sense that any physical computer is.

The qualification is the tape, not the loop. Universality requires unbounded working storage; the deployed instance carries 31 cells and is therefore a finite automaton, however long it runs. The construction generalises — a covenant may carry more state at each spend than it carried at the last, and nothing in the transaction model bounds that — but a conforming implementation SHOULD state which of the two it has built, because a fixed-width state machine and an unbounded one are different claims.

And the absence of an in-script loop follows from what the machine is for, rather than from a shortfall in its language. Bitcoin is a timestamp server: its function is to order events in time. A program that never finishes produces no event to order — the transaction carrying its result is never built, and a script that failed to terminate would never be validated into a block. A timestamp server has no unbounded time to offer a computation that will never finish.

So the model does not exclude unbounded computation. It requires unbounded computation to be presented one halting step at a time, which is the condition under which each step can be validated by everybody and paid for by somebody. What reads as a missing language feature is the same property that makes the result verifiable.

⇒ Where an iteration is placed is then an economic question rather than a question of capability, and the answer differs per program. Rule 110's body dominates its frame 4.3 to 1, so unrolling eight generations into one script buys only 1.21×. A covenant whose frame dominates its body by 13 to 1 gained roughly tenfold from the same change.

References

  • BRC-15 — Bitcoin Script Assembly Language, the token-level representation this composes above
  • BRC-106 — Bitcoin Script ASM Format, for deterministic Script ↔ ASM translation
  • BRC-21 — Push TX, used by the covenants included as tests, though not required by this standard
  • Matthew Cook, Universality in Elementary Cellular Automata (2004), for Rule 110
  • Bitcoin SV opcode semantics for the opcodes named in §3

The reference implementation and its test suite were written by Claude (Opus 5).

Was this helpful?

Search Beersy

Search standards by number, title, author or topic