The receipt on the wire
The byte layout of an axmc resolution receipt as it leaves Solana for a market on another chain. Use it to decode a receipt yourself, or to check a decoder against the shared fixtures.
Three implementations read and write this format and are tested against the same fixtures: the emitter program on Solana (truth_emitter), the SDK (encodeReceipt and decodeReceipt) and the spoke contract (ReceiptCodec). How the receipt reaches your chain is in The EVM spoke.
Version 1
A version 1 receipt is exactly 287 bytes. Every integer is big-endian. Every Solana key is its raw 32 bytes. There is no length prefix and no optional field, so a decoder checks the length and the version byte first.
| Offset | Size | Field | Type | Notes |
|---|---|---|---|---|
| 0 | 1 | version |
u8 | 1 |
| 1 | 32 | request |
pubkey | The request account the answer belongs to |
| 33 | 32 | market |
pubkey | The market on axmc. A spoke consumer filters on it |
| 65 | 8 | index |
u64 | The request's index within its market |
| 73 | 2 | chain |
u16 | Where it was decided. 1 is Solana, in Wormhole numbering |
| 75 | 1 | outcome_kind |
u8 | 0 Index, 1 Scalar, 2 TooEarly, 3 Unresolvable |
| 76 | 1 | outcome_index |
u8 | The winning position when the kind is Index, else 0 |
| 77 | 16 | outcome_scalar |
i128 | Two's complement when the kind is Scalar, else 0 |
| 93 | 1 | cause_code |
u8 | See Cause codes |
| 94 | 1 | tier |
u8 | 0 to 4 for T0 to T4 |
| 95 | 2 | score |
u16 | The scorer's risk score, 0 to 1000 |
| 97 | 8 | proposer_bond |
u64 | In collateral base units |
| 105 | 4 | window_secs |
u32 | The challenge window the tier set |
| 109 | 8 | premium |
u64 | What the question paid the insurance fund |
| 117 | 32 | judge_commitment |
bytes32 | Zero when no judge was committed |
| 149 | 32 | evidence_hash |
bytes32 | Zero when the standing proposal brought no evidence bundle |
| 181 | 1 | proposal_kind |
u8 | 0 an open proposal, 1 the committed judge's |
| 182 | 1 | flags |
u8 | Bit 0 disputed, bit 1 overturned, bit 2 corrected. The other five bits are zero |
| 183 | 32 | deciding_round |
pubkey | The court round whose jurors earned the settlement share. Zero when none did |
| 215 | 32 | rules_hash |
bytes32 | Hash of the question's rules text |
| 247 | 32 | sources_hash |
bytes32 | Hash of the question's source list |
| 279 | 8 | finalized_at |
i64 | Unix seconds, Solana's clock |
Every field comes from the request account on Solana. Anyone can rebuild the receipt from that account and compare it byte for byte. The SDK's receiptFromRequest does exactly that.
Canonical form
One receipt has exactly one encoding. A decoder rejects:
- a payload that is not 287 bytes, or whose first byte is not
1; - an
outcome_kindabove3, or atierabove4; - a non-zero
outcome_indexwhen the kind is not Index; - a non-zero
outcome_scalarwhen the kind is not Scalar; - any of the five unused
flagsbits set.
A decoder does not reject an unknown cause_code. The table below can grow, and a consumer that reverts on a new code breaks the day it appears. Store the code, expose it, and let the market decide.
Because the form is canonical, keccak256(payload) identifies a receipt's content. The same receipt hashes the same however it travelled, and a correction hashes differently from the answer it replaced.
Cause codes
| Code | Name | When it is set |
|---|---|---|
| 0 | none | An index or scalar answer, or an Unresolvable nobody explained |
| 1 | event cancelled | The proposer answered Unresolvable with this reason, and the answer stood |
| 2 | participant withdrew | The proposer answered Unresolvable with this reason, and the answer stood |
| 3 | source contradiction | The proposer answered Unresolvable with this reason, and the answer stood |
| 4 | not yet observable | The proposer answered Unresolvable with this reason, and the answer stood |
| 5 | criteria ambiguous | The proposer answered Unresolvable with this reason, and the answer stood |
| 6 | no proposal | Set by the protocol: nobody answered before the deadline. The market refunds at par |
| 7 | court ruled | Set by the protocol: the court ruled Unresolvable over an answer that said otherwise |
| 8 | corrected | Set by the protocol: a correction petition changed a final answer |
TooEarly never reaches a receipt. It re-opens the request instead of ending it, and the emitter refuses a request that is not final. The format keeps the kind so the outcome enum is complete.
How it travels
The emitter publishes the 287 bytes as the payload of a Wormhole message, from the ["emitter"] PDA of truth_emitter, at Solana's finalized commitment. A spoke receives it inside a VAA (the guardians' signed message) whose body is:
| Field | Type and value |
|---|---|
timestamp |
u32, when Solana saw the transaction |
nonce |
u32, chosen by whoever called emit_receipt |
emitter_chain |
u16, 1 (Solana) |
emitter_address |
bytes32, the ["emitter"] PDA |
sequence |
u64, Wormhole's counter for this emitter |
consistency_level |
u8, 32 (Solana finalized, as the core bridge records it) |
payload |
the 287-byte receipt |
The guardians sign keccak256(keccak256(body)). The spoke checks emitter_chain and emitter_address before it reads a byte of the payload: Wormhole proves a message is authentic, not that it came from axmc.
Anyone can call emit_receipt on a final request, as often as they like. A second emission is the same receipt under a new sequence, and the spoke treats it as a redelivery. After a correction, the emitted receipt is the corrected one.
Fixtures
The fixtures/receipt-v1 folder holds one JSON file per case. Each file has the receipt's fields and the expected payload in hex. All three implementations must agree with every file.
| Cases | Files |
|---|---|
| Undisputed answers | undisputed-index, judged-index (the committed judge, with evidence) |
| Court rulings | court-upheld, court-overturned, court-unresolvable (cause 7) |
| Protocol causes | expired (cause 6), corrected-index and corrected-unresolvable (cause 8) |
| Stated causes | stated-cause-event-cancelled, stated-cause-participant-withdrew, stated-cause-source-contradiction, stated-cause-not-yet-observable, stated-cause-criteria-ambiguous, unresolvable-no-cause (cause 0) |
| Scalars | scalar-positive, scalar-negative, scalar-min (smallest i128 and a zero clock) |
| Edge cases | max-values (every width at its maximum, every flag set), unknown-cause (stored, never refused), too-early (the codec carries the kind; the oracle never emits it) |
generate.mjs in the same folder writes the JSON files and the spoke's Solidity copy of them. After you change the generator, regenerate with node fixtures/receipt-v1/generate.mjs and each implementation's tests will show what changed.
A change to this format is a change to a deployed interface. Bump the version byte. Never reinterpret an existing one.
Source: docs/RECEIPT_WIRE.md in the axmc repo. Edit the file; the next deploy rebuilds this page.