axmc docs

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_kind above 3, or a tier above 4;
  • a non-zero outcome_index when the kind is not Index;
  • a non-zero outcome_scalar when the kind is not Scalar;
  • any of the five unused flags bits 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.