Alkanes protocol docs

Normative

Alkanes specification

Each rule below states behaviour that alkanes-rs 2.2.1-rc.4 implements, with the source file that implements it. Where the reference implementation and the upstream prose disagree, this page follows the code, because the code is what determines the indexed state.

Scope and conformance

This document specifies how a conforming indexer derives alkanes state from Bitcoin blocks. It does not specify wallet behaviour, RPC surfaces, or contract semantics beyond the protocol boundary.

The words must, must not, and may describe required behaviour of a conforming indexer. An implementation that disagrees with any rule here will fork away from the reference index, which in a derived-state protocol means it is simply wrong.

Layering

Alkanes is a sub-protocol of protorunes, which is an extension of Runes. Runes and protorunes originated outside Bitcoin Universe. Rules tagged ENV and parts of EDT describe behaviour inherited from those layers as alkanes-rs implements it.

ENV. The carrier

  1. ENV-1
    Alkanes data is carried in a Bitcoin transaction output whose scriptPubKey begins with OP_RETURN (0x6a) immediately followed by OP_PUSHNUM_13 (0x5d). alkanes-rs matches this as the little-endian u16 constant 0x5d6a.
  2. ENV-2
    An output that does not match ENV-1 is not a runestone. An indexer must not attempt to read protocol data out of it, and must not treat any other payload format, including JSON, as an alkanes instruction.
  3. ENV-3
    If several outputs match ENV-1, the first in output order is the runestone.
  4. ENV-4
    Everything after OP_PUSHNUM_13 must be data pushes. Push payloads are concatenated in order to form the runestone payload. Any non-push opcode makes the runestone a cenotaph.
  5. ENV-5
    A single push carries at most 520 bytes (MAX_SCRIPT_ELEMENT_SIZE). Longer payloads must be split across consecutive pushes.
  6. ENV-6
    The payload is a sequence of LEB128 varints. A varint longer than 18 bytes, or one whose final byte sets disallowed bits, is a flaw and makes the runestone a cenotaph.
  7. ENV-7
    Varints are read as tag and value pairs until tag 0 (Body) appears, after which all remaining values are Runes-layer edicts. Protostone data is carried in fields with tag 16383, repeated as needed.
  8. ENV-8
    The concatenated tag 16383 values are expanded to the protostone byte stream by writing each value as 16 little-endian bytes and discarding the 16th, so every value contributes exactly 15 bytes. The inverse operation, used when encoding, reads 15 bytes at a time little-endian.

Source: crates/ordinals/src/runestone.rs, crates/ordinals/src/varint.rs, crates/ordinals/src/runestone/tag.rs, crates/protorune-support/src/protostone.rs.

PST. Protostone records

  1. PST-1
    The protostone byte stream is LEB128-decoded into a flat integer list and read as consecutive records of the form [protocol_tag, length, value × length].
  2. PST-2
    A protocol_tag of 0 terminates the record list. This absorbs the zero padding produced by ENV-8.
  3. PST-3
    If length exceeds the number of remaining values, decoding of the entire protostone list fails.
  4. PST-4
    Alkanes is protocol tag 1. Records with any other tag are decoded but are not alkanes records.
  5. PST-5
    Within a record, values are tag and value pairs until tag 0 (Body), after which all remaining values are edict data. Recognised tags are Message 81, Burn 83, ProtoPointer 91, Refund 93, and From 95. Unrecognised tags are consumed as pairs and ignored.
  6. PST-6
    Each protostone occupies a virtual output. For a transaction with N real outputs, the protostone at index i (zero based) occupies virtual output N + 1 + i. Edicts may target these indexes.
  7. PST-7
    All alkane balances carried by the transaction's inputs are assigned to the virtual output of the first record whose protocol_tag is 1. If no record carries tag 1, no such assignment happens.
  8. PST-8
    Protoburns (Burn 83 and From 95) are parsed but not processed by the released indexer. The call to process_burns is compiled only under cfg(test), with a source comment deferring activation to a future block. An implementation must not process protoburns on mainnet today.

Source: crates/protorune-support/src/protostone.rs, crates/protorune/src/lib.rs (index_protostones).

ID. Identifiers

  1. ID-1
    An alkane is identified by an AlkaneId, a pair of u128 values written block:tx and serialised as 32 bytes, two little-endian u128s.
  2. ID-2
    An id with block == 0 and tx > 0 is not constructible and must be rejected.
  3. ID-3
    The block value carries meaning:
    blockMeaningtx
    0Null, non-contract caller0
    1CREATE: deploy the binary in the transaction witness0
    2Sequentially numbered contractsequence number
    3CREATERESERVED: deploy to reserved idreserved number
    4Reserved-number contractreserved number
    5Factory clone of a sequence-numbered templatetemplate sequence
    6Factory clone of a reserved-number templatetemplate reserved number
    32System precompiled contract0 fr-BTC, 1 fr-Sigil
    800000000Virtual precompile0 block header, 1 coinbase tx, 2 diesel mint count, 3 total miner fee
  4. ID-4
    Sequence numbers are allocated from a single global counter at /alkanes/sequence, incremented once per successful CREATE or factory deployment.

Source: crates/alkanes-support/src/id.rs, src/vm/utils.rs.

EDT. Edicts

  1. EDT-1
    The edict body is a flat run of values read in groups of four: block_delta, tx_delta, amount, output.
  2. EDT-2
    Ids are delta encoded from 0:0. The next id is previous.block + block_delta, and its tx is previous.tx + tx_delta when block_delta is 0, otherwise tx_delta taken absolutely.
  3. EDT-3
    A body whose length is not a multiple of four produces the error edict values did not appear in sets of four. The reference implementation discards this error and substitutes an empty edict list, so the protostone remains valid and the transfers do not happen. A conforming indexer must reproduce this, including the silence.
  4. EDT-4
    amount is a raw u128 in the alkane's own base units. The protocol layer does not apply divisibility.
  5. EDT-5
    An amount of 0 transfers the entire remaining balance of that alkane at that point in edict processing.
  6. EDT-6
    An output equal to the number of transaction outputs spreads the amount across all non-OP_RETURN outputs. Values above that address protostone virtual outputs per PST-6.
  7. EDT-7
    Edicts are applied in encoded order, each drawing from the running unallocated balance. An edict for more than the remaining balance transfers what remains.
  8. EDT-8
    Within a protostone, the message is processed before the edicts. Edicts then operate on the balance sitting at the protostone's pointer output. If the message failed and refunded, the edicts of that protostone are skipped entirely.
  9. EDT-9
    Balance arithmetic is checked. Overflow on addition is an error that rolls the enclosing scope back.

Source: crates/protorune-support/src/protostone.rs, crates/protorune/src/lib.rs (process_edicts, handle_transfer_runes_to_vout).

MSG. Messages and cellpacks

  1. MSG-1
    A record whose Message field (81) is non-empty is a message. Its chunks are expanded per ENV-8 into the calldata byte string.
  2. MSG-2
    Calldata is LEB128-decoded into a u128 list. The first two values are target.block and target.tx; the remainder are the inputs. By convention inputs[0] is the opcode the contract dispatches on.
  3. MSG-3
    A calldata list of fewer than two values is rejected before execution. This guard is explicit in the reference implementation because the alternative was a panic that would halt indexing network wide.
  4. MSG-4
    Because ENV-8 always emits whole 15-byte chunks, calldata arrives zero-padded and the padding decodes to additional zero-valued inputs. Encoders must pass every argument explicitly.
  5. MSG-5
    A message record must carry both ProtoPointer (91) and Refund (93). A missing one is the error Missing pointer or Missing refund pointer, which aborts protostone processing for the transaction rather than refunding.
  6. MSG-6
    Both pointers must be no greater than num_outputs + num_protostones. Otherwise the error is Invalid output pointer.
  7. MSG-7
    The protostone's virtual output must be strictly less than num_outputs + 100. This bounds the number of message-bearing protostones in one transaction.
  8. MSG-8
    A message is only executed when its record's protocol_tag equals the indexer's protocol tag. For alkanes that is 1.

Source: crates/protorune/src/protostone.rs (process_message), crates/alkanes-support/src/cellpack.rs, src/message.rs.

DEP. Deployment

  1. DEP-1
    A cellpack whose target is 1:0 is a CREATE. The WASM binary is read from the envelope in the transaction's first input witness, stored gzip-compressed at /alkanes/<id>, and the contract is assigned the id 2:<next_sequence>.
  2. DEP-2
    A cellpack whose target has block == 3 is a CREATERESERVED. The contract is deployed at 4:<tx>. It fails if a binary already exists at that id.
  3. DEP-3
    A cellpack whose target has block == 5 or block == 6 is a factory deployment. The new contract receives 2:<next_sequence> and stores a 32-byte pointer to the template's id instead of a copy of the binary. It shares the template's code but has its own storage and balances.
  4. DEP-4
    Deployment maps the new id to its creation outpoint at /alkanes_id_to_outpoint/<id>.
  5. DEP-5
    For fuel accounting, transactions containing a deployment cellpack have their first witness stripped before virtual size is computed, so a large binary does not consume a disproportionate share of the block's fuel.

Source: src/vm/utils.rs (run_special_cellpacks), src/vm/fuel.rs.

EXE. Execution

  1. EXE-1
    Contracts execute in wasmi, a deterministic interpreter, with fuel metering enabled. Given the same binary, fuel, and context, execution always produces the same result.
  2. EXE-2
    A contract must export __execute. It may export __meta for ABI description. It reads its context through host functions and returns a pointer to a response buffer.
  3. EXE-3
    The sandbox provides no network, no filesystem, and no randomness. The only interface is the fixed set of host functions in the env namespace.
  4. EXE-4
    Memory is bounded by MEMORY_LIMIT = 43,554,432 bytes. Allocation beyond it traps.
  5. EXE-5
    Nested contract calls are limited to a checkpoint depth of 75.
  6. EXE-6
    Three call forms exist. __call sets caller to the current contract and myself to the target, committing state on success. __delegatecall leaves caller and myself unchanged. __staticcall always rolls back state changes.
  7. EXE-7
    A nested call that reverts returns a negative value to its caller and does not abort it. The caller may inspect the result and continue.
  8. EXE-8
    Revert data begins with the four bytes 08 c3 79 a0 followed by a UTF-8 error message.
  9. EXE-9
    Each block has a total fuel budget, split across transactions in proportion to virtual size with a per-transaction floor. Unused fuel returns to the block pool. On failure all fuel allocated to the transaction is drained rather than returned.
Fuel budget by network, from src/vm/fuel.rs.
NetworkInitial block fuelAfter change heightChange height
Bitcoin mainnet100,000,0001,000,000,000899,087
Regtest100,000,0001,000,000,0000

Source: src/vm/instance.rs, src/vm/extcall.rs, src/vm/host_functions.rs, src/vm/fuel.rs, src/vm/constants.rs.

BAL. Balances

  1. BAL-1
    Balances are held per outpoint, namespaced by protocol tag, at /runes/proto/1/byoutpoint/<outpoint>. Contract-held balances live at /alkanes/<what>/balances/<who>.
  2. BAL-2
    Incoming balances are credited to the target contract before execution and debited after, according to the transfers the contract returns.
  3. BAL-3
    A contract may transfer more of a token than it holds only when that token is the contract itself. This is how an alkane mints its own supply. Any other shortfall is a balance underflow error. Supply control is the contract author's responsibility, not the protocol's.
  4. BAL-4
    After a successful message the indexer reconciles: the virtual output's sheet is removed, outgoing transfers are added to the pointer output, and runtime balances are stored at virtual index u32::MAX.
  5. BAL-5
    When protostones are present, every transaction input's stored balance sheet is cleared at the end of processing. The source comment is explicit that all inputs must be used up, even in cenotaphs.

Source: src/utils.rs, crates/protorune/src/balance_sheet.rs, crates/protorune/src/lib.rs.

ST. State transitions

Order of operations for one transaction Six steps in sequence: decipher the runestone, decode protostones, load and concatenate input balance sheets, assign all input balances to the first protostone with protocol tag 1, then for each protostone run the message and then the edicts, and finally save the output balance sheets and clear every input sheet. 1 decipher runestone (6a 5d) 2 decode protostone records 3 load + concat input sheets 4 assign all to first tag-1 record 5 per protostone, in order: a. message, if tag matches b. edicts, unless message refunded 6 save output sheets 7 clear every input sheet
Steps 3 through 7 only run when at least one protostone record was decoded. If none was, nothing on this diagram happens and alkane balances are untouched.
  1. ST-1
    All state changes for one message are wrapped in a checkpoint. On failure the checkpoint is rolled back and the in-memory balance map is restored from a snapshot taken before the message, so the two stores unwind together.
  2. ST-2
    On message failure, balances are directed to the refund pointer and the protostone's edicts are skipped.
  3. ST-3
    Blocks are processed in order. Each block's fuel tank is initialised fresh.
  4. ST-4
    One transaction is hardcoded as blacklisted and skipped entirely: 5cbb0c466dd08d7af9223d45105fbbf0fdc9fb7cda4831c183d6b0cb5ba60fb0.

INV. Invalid conditions

These are the outcomes an implementer most often gets wrong. None of them produce a visible failure on chain.

ConditionOutcomeEffect on alkane balances
No output matches 6a 5dNo runestone; protostone processing never runsUntouched in the index, so stranded at the now-spent outpoint
Runestone present, no tag 16383 fieldNo protostones decodedUntouched, stranded
Protostone present but no record has tag 1Protostone processing runs; nothing claims the balancesCleared from the inputs, so destroyed
Record length exceeds remaining valuesless values than expected; whole protostone list rejectedUntouched, stranded
Edict body not a multiple of fourEmpty edict list, no error surfacedFollow the default assignment, not the intent
Message without pointer or refund pointerMissing pointer; protostone processing aborts for the transactionUntouched, stranded
Pointer above num_outputs + num_protostonesInvalid output pointerUntouched, stranded
Cellpack shorter than two varintsRejected before execution; protostone skipped and refundedRefunded to the refund pointer
Contract traps, reverts, or runs out of fuelRollback, revert trace saved, remaining transaction fuel drainedRefunded to the refund pointer
Non-push opcode or oversized varint in the payloadCenotaphCleared from the inputs, so destroyed

ACT. Activation

  1. ACT-1
    Alkanes is inactive below the network's genesis height. On Bitcoin mainnet that height is 880,000. On regtest it is 0.
  2. ACT-2
    Below the genesis height the indexer still processes the Runes layer, which activates earlier, but performs no alkanes setup or execution.
  3. ACT-3
    Three system contracts are bootstrapped: the genesis alkane at 2:0, fr-BTC at 32:0, and fr-Sigil at 32:1.
  4. ACT-4
    The genesis alkane is upgraded at fixed mainnet heights 908,888 and 917,888. Fuel accounting changes at mainnet height 899,087.

Source: src/network.rs, src/indexer.rs, src/vm/fuel.rs.