Alkanes protocol docs

Conformance

Test vectors

Every vector below was produced by encoding the stated inputs with the alkanes-rs encoding rules and then decoding the result back, so the script hex and the decoded integers are known to agree. The invalid vectors state what the reference implementation actually does, which in several cases is to fail silently.

How to use these

All vectors use protocol tag 1 unless stated otherwise. Amounts are raw base units.

Valid vectors

V1. Single edict transfer

Move 100000000 of alkane 2:1 to output 1.

Protostone integers[1, 5, 0, 2, 1, 100000000, 1]
Inner LEB128010500020180c2d72f01
Runestone integers[16383, 5604910583939447391489]
Script hex6a5d0dff7f818a80909080a0e1d7df04
Expected100000000 of 2:1 assigned to output 1; any remaining balance follows the default assignment.

V2. Same transfer to output 0

Protostone integers[1, 5, 0, 2, 1, 100000000, 0]
Script hex6a5d0cff7f818a80909080a0e1d75f
ExpectedSame transfer, output 0. Included because it differs from V1 in both tail bytes and total length, which catches encoders that assume a fixed size.

V3. Sweep the whole balance

Amount 0 means the entire remaining balance.

Protostone integers[1, 5, 0, 2, 1, 0, 0]
Inner LEB12801050002010000
Script hex6a5d07ff7f818a809010
ExpectedThe entire balance of 2:1 goes to output 0.

V4. Transfer plus change sweep

Two edicts on the same id, so the second delta pair is 0, 0.

Protostone integers[1, 9, 0, 2, 1, 100000000, 0, 0, 0, 0, 1]
Script hex6a5d11ff7f819280909080a0e1d7df8080808040
Expected100000000 of 2:1 to output 0, then everything remaining to output 1.

V5. Two edicts on different ids

Ids are sorted then delta encoded, so 2:5 after 2:1 encodes as 0, 4.

Protostone integers[1, 9, 0, 2, 1, 50000000, 1, 0, 4, 25000000, 1]
Inner LEB128010900020180e1eb17010004c0f0f50b01
Chunks[1276995721691057111568520951759898881, 267]
Script hex6a5d18ff7f819280909080e0f0ebaf8480c080b0f8f501ff7f8b02
Expected50000000 of 2:1 and 25000000 of 2:5, both to output 1. Two chunks, so two tag 16383 fields.

V6. Contract call

Cellpack [2, 1, 77], pointer 0, refund pointer 1.

Cellpack LEB12802014d
Protostone integers[1, 6, 91, 0, 93, 1, 81, 5046530]
Script hex6a5d0eff7f818cec82d0abc0a88285d215
Expected calldata02014d000000000000000000000000, decoding to target 2:1 and inputs [77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].
ExpectedMessage runs against 2:1 with opcode 77. Result to output 0, refund to output 1.

V6 is the vector to check zero padding against. An implementation that trims trailing zeros from the calldata will disagree with alkanes-rs.

V7. CREATE deployment

Cellpack [1, 0, 0]: target 1:0, opcode 0. The binary travels in the witness.

Protostone integers[1, 6, 91, 0, 93, 0, 81, 1]
Script hex6a5d0bff7f818cec82d08bc0a801
ExpectedNew contract at 2:<next sequence>, initialised with opcode 0.

V8. Factory clone

Cellpack [6, 65517, 0]: clone reserved template 65517.

Protostone integers[1, 6, 91, 0, 93, 1, 81, 67104006]
Script hex6a5d0fff7f818cec82d0abc0a886b5ffff01
ExpectedNew contract at 2:<next sequence> storing a pointer to the template rather than a copy of the code.

V9. Two protostones in one runestone

An edict record followed by a message record.

Protostone integers[1, 5, 0, 2, 1, 1000, 0, 1, 6, 91, 0, 93, 1, 81, 5046530]
Script hex6a5d19ff7f818a80909080fa83808298d885a0d78051ff7f8285d215
ExpectedTwo records decode. All input balances go to the first record's virtual output, because it is the first with tag 1. Records then process in order.

V10. Real mainnet payload

Captured in the alkanes-rs corpus. A call to the fr-BTC system contract.

Script hex6a5d101600ff7f818cec8ad0abc0a8a081d215
Runestone integers[22, 0, 16383, 3270250341403247458846209]
Runes-layer fieldsTag 22 Pointer = 0
Protostone integers[1, 6, 91, 1, 93, 1, 81, 5046304]
Cellpacktarget 32:0, opcode 77
ExpectedMessage to fr-BTC with opcode 77. Pointer and refund pointer both output 1. Note that a Runes-layer field can sit alongside a protostone in the same runestone.

From the alkanes-rs tests

These four are the protostone round-trip cases in crates/protorune/src/protostone.rs. The upstream tests assert that encipher followed by decipher returns the same structure; the script hex below is what those structures encode to when wrapped in a runestone.

U1. Protoburn record, protocol tag 13

The doc comment above these tests spells out the intended byte layout as [1 4 83 0 91 3 0 0 0 0 0 0 0 0 0 0] for a tag 1 variant, which is the clearest statement in the repository of the 15-byte rule.

Structureburn 1, pointer 3, protocol tag 13, no edicts, no message
Protostone integers[13, 4, 83, 1, 91, 3]
Inner LEB1280d0453015b03
Script hex6a5d08ff7f8d88cc8ab06b
ExpectedDecodes as one record with tag 13, Burn = 1, ProtoPointer = 3. The alkanes indexer does not act on it: tag 13 is not 1, and protoburn processing is not compiled into the released indexer.

U2. Edict record with a burn field

Structureburn 0, pointer 3, tag 1, edict 8400000:1 amount 123456789 to output 2
Protostone integers[1, 9, 83, 0, 91, 3, 0, 8400000, 1, 123456789, 2]
Inner LEB128010953005b030080d9800401959aef3a02
Script hex6a5d18ff7f8192cc82b0eb808080b383a490a0a5cdef01ff7fba04
ExpectedEdict for 8400000:1, amount 123456789, output 2. Field order inside a record is burn, pointer, refund, from, message, then body.

U3. A message that fills exactly one chunk

Structurepointer 3, tag 1, message bytes [0,1,2,3,4,5,6,7,8,0,0,0,0,0,0]
Protostone integers[1, 4, 91, 3, 81, 148080050112590643456]
Inner LEB12801045b035180828898c0a081838710
Script hex6a5d13ff7f8188ec9a908aa0c188b182869af0e0c310
ExpectedOne message chunk. The trailing zeros in the message are part of the payload here, which is exactly why the decoder must not trim them.

U4. Two records with different protocol tags

Structuretag 13 with burn 1 and pointer 3, then tag 3 with burn 1, pointer 2, and a 15-byte message
Protostone integers[13, 4, 83, 1, 91, 3, 3, 6, 83, 1, 91, 2, 81, 2185179317232829729636]
Script hex6a5d20ff7f8d88cc8ab0ebc08186a685d8a5a094f29601ff7fc0979ef5ced3eefaec03
ExpectedTwo records decode cleanly. Neither has tag 1, so from the alkanes indexer's point of view nothing claims the input balances.

Invalid vectors

Most of these do not raise an error

That is the point. An implementation is only correct if it reproduces the silence too. The "expected" column below is the reference implementation's real behaviour, not the behaviour you might wish for.

I1. JSON in a bare OP_RETURN

Payload{"p":"alkanes","op":"transfer","id":"2:1","amt":"1"}
Script hex6a347b2270223a22616c6b616e6573222c226f70223a227472616e73666572222c226964223a22323a31222c22616d74223a2231227d
ExpectedSecond byte is 0x34, a data push, not 0x5d. Not a runestone. No protostone is read, no protorune balance moves, and no input balance is cleared. The alkanes stay recorded against the spent outpoint and are unreachable.

I2. Runestone with no protocol field

Script hex6a5d021600
Runestone integers[22, 0]
ExpectedA valid runestone with a Runes-layer pointer and no tag 16383 field. No protostones decode, so alkanes processing does not run and input balances are not cleared. Stranded, not burned.

I3. Right envelope, wrong protocol tag

Protostone integers[13, 5, 0, 2, 1, 100000000, 1]
Script hex6a5d0dff7f8d8a80909080a0e1d7df04
ExpectedOne record decodes with protocol tag 13. Protostone processing runs because a protostone exists, but no record has tag 1, so no virtual output receives the input balances. At the end of processing every input's balance sheet is cleared regardless. The alkanes are destroyed, not stranded.

I4. Edict body of three values

Protostone integers[1, 4, 0, 2, 1, 100]
Script hex6a5d09ff7f81888090908019
Expectedprotostone_edicts_from_integers returns edict values did not appear in sets of four. from_fields_and_tag discards that error, so the record is valid with an empty edict list. No error surfaces anywhere. The intended transfer does not happen and the balance follows the default assignment.

I5. Record length overruns the payload

Protostone integers[1, 20, 0, 2, 1, 100, 1]
Script hex6a5d09ff7f81a88090908059
ExpectedThe record declares 20 values but fewer remain after the zero padding is counted. Decoding fails with less values than expected and the entire protostone list is rejected. Nothing is processed and no balance is cleared.

I6. Message without a refund pointer

Protostone integers[1, 4, 91, 0, 81, 5046530]
Script hex6a5d0cff7f8188ec8290caa0c1b405
ExpectedThe record decodes with ProtoPointer but no Refund. process_message returns Missing refund pointer, which propagates and aborts protostone processing for the transaction rather than refunding. Not a soft failure.

I7. Cellpack shorter than two values

The calldata is fifteen bytes of continued LEB128, [0x80 × 14, 0x01], which decodes to a single value.

Protostone integers[1, 6, 91, 0, 93, 0, 81, 7798626262034662673439921937612928]
Script hex6a5d20ff7f818cec82d08bc0a880838aa48891a4d0c001ff7f80838aa48891a4d0c003
ExpectedRejected by the explicit length guard in Cellpack::try_from with cellpack: need at least 2 varints. The protostone is skipped and balances go to the refund pointer. This guard exists because the alternative was a panic, and a panic stops the indexer for everyone.

I8. Non-push opcode in the payload

Script hex6a5d51ff7f
Expected0x51 is OP_1, not a data push. The runestone is a cenotaph. Input balances are burned.

I9. Not an OP_RETURN at all

Script hexdeadbeef
ExpectedFirst byte is not 0x6a. A decoder must reject it rather than guessing.

Reproducing these

Each script hex above was produced by encoding the stated protostone integers with the layered encoding in the encoding page, then decoding the result and confirming the integers came back unchanged. The decoder used is protostone.js, which follows crates/protorune-support/src/protostone.rs and ts-sdk/src/protostone/ in alkanes-rs.

To check your own implementation quickly: encode V1, compare the hex, then decode I4 and confirm you get a valid record with zero edicts and no error. Those two cover the most common encoding bug and the most common decoding bug.