Skip to main content

The signed Order message (interface contract §1)

The canonical borsh-serialized struct the user signs client-side. The backend stores it, the crank passes it into settle_match, and the program verifies the signature on-chain:
Both sides enforce the collateral rules at entry: BUY requires available_USDC ≥ price·size + fee; SELL requires token_balance(outcome) ≥ size. There is an analogous signed ComboQuote message (interface contract §2) for RFQ combos: {maker, legs[], stake, payout, expiry, salt}, single-use by hash-marked salt.

Five encoders, one golden vector

borsh(Order) is independently implemented in: All five are pinned byte-identical by the same golden vector. If you touch any encoder, update that vector in every suite and run them all — a one-byte divergence means every signature fails verification.
A subtle encoder hazard this discipline catches: JavaScript’s Number(bigint) silently rounds values ≥ 2⁵³. A 63-bit random salt can be signed as one value and transmitted as another, so signature verification fails on nearly every order. randomSalt() draws below 2⁵³ so the bigint → Number → JSON → uint64 round-trip is exact.

The pinned settle_match transaction (interface contract §6.5)

Ed25519 verification on Solana is instruction introspection, not a program call. The crank must build every settle_match transaction as exactly three instructions:
settle_match reads ix[0]/ix[1] via the instructions sysvar and asserts each one’s (pubkey, message) matches (order.maker, borsh(order)) exactly (programs/pitchmarket/src/sig_verify.rs). Wrong order or omitted ed25519 instructions fail closed with BadSignature — verification is never silently skipped.

Why it must be a v0 transaction

The full 3-instruction transaction measures ~1453 bytes — over the 1232-byte legacy limit. It is therefore pinned as a v0 transaction with a per-market Address Lookup Table holding the market’s static accounts and each trading wallet’s vault/ATAs. The operator (fee payer) and the two per-order OrderStatus PDAs stay static keys — OrderStatus PDAs are unique per order, and keeping them inline avoids a table-extend + activation wait on every settle. Measured in the crank’s tests: the legacy encoding is 1421 B (over the limit); the v0 encoding is 1116 B (within it). Reference implementations: backend/internal/crank/{builder,lut}.go (Go, production) and tests/helpers.ts (TypeScript). builder_test.go re-implements the on-chain byte checks against the same golden vectors.