Beersy
BRC-171

Fixed-Width State in a Covenant's Own Locking Script

A Bitcoin contract that needs to change some stored value on every spend has only one trustworthy place to keep that value: inside its own . But Bitcoin Script can only cut a script apart at fixed byte positions, so two developers solving this independently end up with two incompatible layouts, and neither their tools nor anyone else's contracts can read the other's state. This describes a shared, self-describing way to lay out that state so any script, wallet, or third-party contract can find and read it without being told anything else about it.

sun-dive12 min read

Reference for an AI

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

Summary

Why
Bitcoin Script can only split a script at constant byte offsets, so any covenant that needs to update its own state on every spend needs a shared, predictable way to lay that state out or no other script can find it.
What
BRC-171 defines a byte layout for storing mutable state directly inside a Bitcoin covenant's locking script as a run of fixed-width data pushes.
How
A covenant lays its state out as a header followed by adjacent fixed-width data pushes, where each push opcode equals the width of the field it precedes, so any reader can walk the script with OP_SPLIT at known offsets, convert numeric fields with OP_NUM2BIN/OP_BIN2NUM, and rebuild the exact script from the pieces cut…

What this lets you do

  • Read a covenant's current state directly out of its own locking script
  • Rewrite that state and reproduce the rest of the script byte for byte
  • Let one covenant verify and read another covenant's state with no shared code
  • Distinguish numeric fields from opaque fields so hashes and counters aren't confused
  • Render or index any conforming covenant's state without prior knowledge of it

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

headerwidthfieldSUF

The specification

Abstract

This standard defines how a covenant carries mutable state inside its own , as a run of fixed-width data pushes, such that the state can be located, read and rewritten at constant byte offsets — by the covenant itself, and by any third party holding only the script.

The layout is self-describing: for every field, the single-byte push opcode that precedes the data is numerically equal to the field's width. A reader that knows nothing but the list of widths can walk the state, and a reader that knows nothing at all can still verify that each push agrees with the next offset it implies.

The convention is already carried by three distinct covenant families on mainnet, and read across the boundary by a fourth covenant that shares no code with them.

Motivation

A covenant that changes state must do two things on every spend: read its current state, and prove that its successor's state is the correct one. Bitcoin Script offers exactly one honest source for the former — the scriptCode field of the sighash preimage, which is the only copy of the script a miner has verified. So the state has to live in the script, and the script has to be able to cut it out again.

OP_SPLIT cuts at a constant offset. It cannot search, and it cannot follow a length prefix computed at runtime without spending opcodes to do so. That single fact forces the shape of any workable answer: fixed widths, in a fixed order, at a known starting offset. Every covenant that carries state arrives at this independently.

The cost of arriving at it independently is that nothing can read anything else. Two covenants written by two teams cannot inspect one another's state, tooling cannot render either of them, and an indexer must special-case each one. That is the gap this document closes, and the gap is not hypothetical:

  • A fuel depot covenant needs to recognise a racing car covenant well enough to refuel it — which means locating the car's owner field inside the car's script, with no shared code path and no ability to run the car's own logic. It walks the layout defined here and validates every push opcode against the width the layout declares. If any disagrees, it refuses. That is a third-party read of another covenant's state, in production, and it is only possible because the layout is a convention rather than a private detail.

  • A reader tool can render any conforming covenant's state without being told anything about the covenant, because the field boundaries are recoverable from the script.

Scope

This standard is deliberately narrow. It defines where the state sits and how it is encoded — and nothing else. It does not define what the fields mean, how the preimage is authorised, what the successor state must be, or what the covenant's output value may become. Those differ between every covenant that has ever needed them, and a standard that guessed at them would be wrong for all of them.

What it buys is that any covenant, any wallet, any indexer and any tool can find and read the state of any conforming script without being told anything else about it.

Specification

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

1. Terminology

  • Field — one unit of covenant state: a single data push of a fixed byte width.
  • PRE — every byte of the script before the first field's data, including the first field's own push opcode.
  • SUF — every byte of the script after the last field's data. In practice this is the covenant's code, and it is never inspected by this standard.
  • scriptCode field — the BIP143 preimage member holding varint(scriptLen) ‖ script.

2. Script layout

A conforming locking script MUST begin with:

PRE ‖ ⟨w₀⟩ f₀ ‖ ⟨w₁⟩ f₁ ‖ … ‖ ⟨wₙ⟩ fₙ ‖ SUF

where ⟨wᵢ⟩ is a single byte whose value is wᵢ, and fᵢ is exactly wᵢ bytes of data. The fields MUST be contiguous: no opcode may appear between the end of fᵢ and the push opcode of fᵢ₊₁.

3. Field width

1 ≤ wᵢ ≤ 75.

⚠ 75 is not a stylistic limit. At 76 a data push is encoded as OP_PUSHDATA1 followed by a separate length byte, the push opcode ceases to equal the width, every offset after that field shifts by one, and the self-describing property in §5 is lost. Implementations MUST reject a declared width outside this range rather than emit a script whose offsets silently disagree with its declaration.

A field wider than 75 bytes MUST be expressed as two or more adjacent fields.

4. Field encoding

Two of field are defined:

  • Numeric. The value is encoded in exactly wᵢ bytes, little-endian, sign-magnitude — the encoding produced by OP_NUM2BIN and consumed by OP_BIN2NUM. The top bit of the most significant byte is the sign. The representable magnitude is therefore ±(2^(8wᵢ − 1) − 1).

  • Opaque. The value is wᵢ raw bytes and MUST NOT be converted to or from a script number.

⚠ The distinction is load-bearing and is not decoration. OP_BIN2NUM applied to a 20-byte hash is meaningless, and OP_NUM2BIN will not reliably put it back. Separately, a byte string whose only set bit is the sign bit of its most significant byte reads as negative zero — numerically indistinguishable from empty. Implementations comparing opaque fields MUST use a byte comparison (OP_EQUAL), never a numeric one (OP_NUMEQUAL).

An implementation MUST record, per field, which kind it is. This standard does not define how that record is transmitted; see §9.

5. The self-describing invariant

For every field, the byte immediately preceding its data MUST be numerically equal to that field's width.

This follows automatically from §2 and §3 when the script is built with minimal data pushes, and it is the property that makes the layout walkable. A reader holding the ordered list of widths can traverse the state and verify at every step that the script agrees with the list. A reader holding no list at all can still recover the field boundaries by treating each byte at a field position as a length.

Verification, given widths w₀ … wₙ and the byte offset h at which the first field's push opcode sits, is:

off ← h
for i in 0 … n:
    assert script[off] = wᵢ          # the push opcode is the width
    off ← off + 1 + wᵢ

A covenant that reads another covenant's state MUST perform this check and MUST refuse the spend if any push opcode disagrees. Skipping it permits a counterfeit script whose fields sit at the expected offsets but describe a different layout.

6. The header

Fields SHOULD be preceded by a three-push header identifying the protocol and the record:

01 50    push "P"           protocol prefix
01 vv    push <version>     header format version, currently 0x01
01 rr    push <record>      record type — what the fields mean

This occupies six bytes, so with the first field's push opcode PRE is seven bytes plus the scriptCode field's own varint. The header is what allows a reader to recognise a conforming script before it has any idea which covenant produced it, and the record type is what tells it which field list to use.

7. Deriving the field offset

The offset at which the covenant splits its own scriptCode is:

fieldOffset = varIntSize(scriptLen) + headerBytes + 1

⚠⚠ This is circular, and the circularity is the most common way to get this wrong. BIP143 prefixes the scriptCode with its own length as a varint, so fieldOffset depends on the finished script's length — and the finished script contains fieldOffset as a literal. A one-byte difference in the varint moves every field.

Implementations MUST resolve it by construction, not by assumption:

  1. Build the script once with any placeholder offset.
  2. Measure the resulting length.
  3. Compute varIntSize from that length.
  4. Build again with the real offset.

A second pass is sufficient because the offset is a small literal whose own encoded size does not vary across the values it can take in step 4. Implementations SHOULD assert that the second build's length yields the same varIntSize as the first.

8. Reading and writing

Reading, with the scriptCode field on the stack:

push fieldOffset · OP_SPLIT              → PRE, rest
for i in 0 … n:
    if i > 0:  OP_1 · OP_SPLIT · OP_NIP  → discard the push opcode
    push wᵢ · OP_SPLIT                   → fᵢ, rest
                                          rest is SUF after the last field

Numeric fields are converted with OP_BIN2NUM at this point. Opaque fields are not converted at all.

Writing is the inverse, and MUST reproduce the script exactly:

numeric fields: push wᵢ · OP_NUM2BIN
PRE ‖ f₀                                 (f₀'s push opcode is already inside PRE)
for i in 1 … n:  ‖ ⟨wᵢ⟩ ‖ fᵢ
‖ SUF

OP_NUM2BIN fails if the value does not fit wᵢ. That is the desired behaviour: the alternative, truncating to fit, yields a well-formed script carrying a different number. Implementations that construct state off-chain MUST range-check before encoding rather than truncate.

The covenant then binds the rebuilt script by comparing HASH256(value ‖ rebuilt scriptCode field ‖ any further outputs) against the hashOutputs member of its own verified preimage. The mechanism by which the preimage is verified is out of scope; OP_PUSH_TX is one.

that the rebuilt script is assembled from PRE and SUF taken out of the preimage. A conforming covenant therefore recreates itself without containing a copy of itself.

9. Record types

The record type byte in §6 selects the field list — the ordered widths and their kinds. This standard reserves no values and defines no registry; a record type is meaningful only alongside a published description of its fields, which SHOULD accompany the covenant that uses it.

Values in use by the reference implementations at the time of writing:

0x06   live counter
0x07   battery
0x08   racing shell

10. A worked example, in full

Two fields — phase at one byte and n at four — under the header in §6, with n incremented. The opcodes below are not illustrative: they were emitted by a conforming implementation and run through a script interpreter, which rebuilt the script byte for byte.

scriptCode before   fd0003 015001010106 01 02 04 29000000 51529387
scriptCode after    fd0003 015001010106 01 02 04 2a000000 51529387
                    └varint┘└─ header ─┘ ▲  ▲  ▲  ▲          └─ SUF ─┘
                                         │  │  │  └ n = 41 → 42, four bytes little-endian
                                         │  │  └─ w₁ = 4, and this byte IS the push opcode
                                         │  └─ phase = 2
                                         └─ w₀ = 1  (this byte is the last byte of PRE)

fieldOffset = varIntSize(3) + headerBytes(6) + 1 = 10

Reading — 18 bytes:

01 0a    push 10          the field offset from §7
7f       OP_SPLIT         → PRE, rest
01 01    push 1           w₀
7f       OP_SPLIT         → phase, rest
7c 81 7c SWAP BIN2NUM SWAP  phase is numeric, so convert it in place
51       OP_1
7f       OP_SPLIT
77       OP_NIP           discard the push opcode in front of field 1
01 04    push 4           w₁
7f       OP_SPLIT         → n, rest — and rest is now SUF
7c 81 7c SWAP BIN2NUM SWAP

010a7f01017f7c817c517f7701047f7c817c

Writing — 23 bytes:

01 04 80 push 4, OP_NUM2BIN     n back to four bytes
6b       OP_TOALTSTACK
01 02 7a push 2, OP_ROLL        bring PRE up
01 02 7a push 2, OP_ROLL        bring phase up
01 01 80 push 1, OP_NUM2BIN
7e       OP_CAT                 PRE ‖ phase
01 04    push 4                 ★ the push opcode for field 1 — written as a LITERAL, and it is the
7e       OP_CAT                   same byte as the width. That single line is §5.
6c 7e    FROMALTSTACK, OP_CAT   ‖ n
01 01 7a push 1, OP_ROLL        SUF
7e       OP_CAT                 ‖ SUF

0104806b01027a01027a0101807e01047e6c7e01017a7e

⚠ Note what is NOT here: no copy of the script, and no length that had to be computed while running. The covenant reassembles itself from two pieces it cut out of its own scriptCode, and every number it pushes is a constant known when the script was written.

11. Conformance

An implementation conforms if:

  1. Every field's push opcode equals its width, and every width is in 1 … 75. (§2, §3, §5)
  2. Numeric fields round-trip through OP_NUM2BIN / OP_BIN2NUM at their declared width, and opaque fields are never converted. (§4)
  3. Opaque fields are compared as bytes, never as numbers. (§4)
  4. fieldOffset is derived by building, measuring and rebuilding — not assumed. (§7)
  5. Rebuilding an unmodified state reproduces the original script byte for byte. (§8)
  6. A reader that walks another script's fields verifies every push opcode against the expected width and refuses on any disagreement. (§5)

⚠ Item 5 is the cheapest and most valuable test to write first. A covenant that cannot read its own state and write it back unchanged has nothing worth debugging on top of it, and the failure is silent until an output comparison fails hundreds of opcodes later.

Implementations

All of the following are deployed on BSV mainnet.

Covenants carrying state in this layout

covenantfieldsstate byteslock
live counter224674 B
battery9391,428 B
racing shell13981,744 B

The header and the invariant are identical across all three. Their opening bytes:

live counter   01 50 01 01 01 06 04 07 00 00 00 14 …
battery        01 50 01 01 01 07 05 00 fe ff ff 81 …
racing shell   01 50 01 01 01 08 01 00 14 14 ae a2 …
               └── "P" ──┘ └ver┘ └rec┘ └w₀┘└─ f₀ …

Each was walked programmatically for this document, and in every case every push opcode equalled its declared width across the full field list.

A covenant reading another covenant's state

The fuel depot carries no state of its own — its script is constant, which is what lets a car name it by a single hash. It nevertheless locates the owner field inside a racing shell's script by walking the layout in §5 and validating each push opcode against the expected width, refusing the spend on any disagreement. This is the interoperability case, in production, between two covenants with no shared execution path.

Tooling

A browser workbench compiles this layout from a source declaration and reads it back out of an arbitrary locking script, rendering the covenant as a program. It reads all four covenants above.

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

References

  • BIP 143 — Transaction Signature Verification for Version 0 Witness Program (the scriptCode, hashOutputs and value members referenced throughout)
  • BRC-224 — Block Media Format (BMF), for the "P" protocol-prefix and record-type convention reused here
  • BRC-226 — Miner-Enforced Resale-Royalty Covenant Tokens, which reconstructs its own locking script from the preimage's scriptCode rather than embedding a second copy, exactly as §8 describes. ★ It is the useful contrast: BRC-226's template is IMMUTABLE, so it needs no field layout at all. This standard is what a covenant needs when its script must legitimately DIFFER between one spend and the next — everything else about the two techniques is the same
  • Bitcoin SV opcode semantics for OP_SPLIT, OP_NUM2BIN, OP_BIN2NUM, OP_CAT and OP_EQUAL

⚠ This standard is intended to be cited by, rather than to contain, the covenants that use it. A forthcoming proposal describing a self-funding covenant that performs bounded computation for an autonomous agent carries its state as record type 0x07 under this layout.

Was this helpful?

Search Beersy

Search standards by number, title, author or topic