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
- Feed the script hex to your decoder and compare against the decoded integers column.
- Feed the inputs to your encoder and compare against the script hex.
- For the invalid cases, check that your implementation reproduces the stated outcome, including the cases
where the correct behaviour is to produce no error.
- Any of them can be pasted into the decoder on this site.
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 LEB128 | 010500020180c2d72f01 |
| Runestone integers | [16383, 5604910583939447391489] |
| Script hex | 6a5d0dff7f818a80909080a0e1d7df04 |
| Expected | 100000000 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 hex | 6a5d0cff7f818a80909080a0e1d75f |
| Expected | Same 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 LEB128 | 01050002010000 |
| Script hex | 6a5d07ff7f818a809010 |
| Expected | The 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 hex | 6a5d11ff7f819280909080a0e1d7df8080808040 |
| Expected | 100000000 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 LEB128 | 010900020180e1eb17010004c0f0f50b01 |
| Chunks | [1276995721691057111568520951759898881, 267] |
| Script hex | 6a5d18ff7f819280909080e0f0ebaf8480c080b0f8f501ff7f8b02 |
| Expected | 50000000 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 LEB128 | 02014d |
| Protostone integers | [1, 6, 91, 0, 93, 1, 81, 5046530] |
| Script hex | 6a5d0eff7f818cec82d0abc0a88285d215 |
| Expected calldata | 02014d000000000000000000000000, decoding to target 2:1 and inputs [77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]. |
| Expected | Message 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 hex | 6a5d0bff7f818cec82d08bc0a801 |
| Expected | New 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 hex | 6a5d0fff7f818cec82d0abc0a886b5ffff01 |
| Expected | New 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 hex | 6a5d19ff7f818a80909080fa83808298d885a0d78051ff7f8285d215 |
| Expected | Two 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 hex | 6a5d101600ff7f818cec8ad0abc0a8a081d215 |
| Runestone integers | [22, 0, 16383, 3270250341403247458846209] |
| Runes-layer fields | Tag 22 Pointer = 0 |
| Protostone integers | [1, 6, 91, 1, 93, 1, 81, 5046304] |
| Cellpack | target 32:0, opcode 77 |
| Expected | Message 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.
| Structure | burn 1, pointer 3, protocol tag 13, no edicts, no message |
| Protostone integers | [13, 4, 83, 1, 91, 3] |
| Inner LEB128 | 0d0453015b03 |
| Script hex | 6a5d08ff7f8d88cc8ab06b |
| Expected | Decodes 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
| Structure | burn 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 LEB128 | 010953005b030080d9800401959aef3a02 |
| Script hex | 6a5d18ff7f8192cc82b0eb808080b383a490a0a5cdef01ff7fba04 |
| Expected | Edict 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
| Structure | pointer 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 LEB128 | 01045b035180828898c0a081838710 |
| Script hex | 6a5d13ff7f8188ec9a908aa0c188b182869af0e0c310 |
| Expected | One 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
| Structure | tag 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 hex | 6a5d20ff7f8d88cc8ab0ebc08186a685d8a5a094f29601ff7fc0979ef5ced3eefaec03 |
| Expected | Two 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 hex | 6a347b2270223a22616c6b616e6573222c226f70223a227472616e73666572222c226964223a22323a31222c22616d74223a2231227d |
| Expected | Second 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 hex | 6a5d021600 |
| Runestone integers | [22, 0] |
| Expected | A 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 hex | 6a5d0dff7f8d8a80909080a0e1d7df04 |
| Expected | One 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 hex | 6a5d09ff7f81888090908019 |
| Expected | protostone_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 hex | 6a5d09ff7f81a88090908059 |
| Expected | The 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 hex | 6a5d0cff7f8188ec8290caa0c1b405 |
| Expected | The 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 hex | 6a5d20ff7f818cec82d08bc0a880838aa48891a4d0c001ff7f80838aa48891a4d0c003 |
| Expected | Rejected 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 hex | 6a5d51ff7f |
| Expected | 0x51 is OP_1, not a data push. The runestone is a cenotaph. Input balances are burned. |
I9. Not an OP_RETURN at all
| Script hex | deadbeef |
| Expected | First 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.