Canary
Read RungState

Whitepaper

Canary

Funded discrete-log rungs on Solana as an on-chain quantum signal, with the markets and vaults that settle on it.

v0 · October 2026Download the PDF

Abstract

Canary is a protocol on Solana that turns progress against elliptic-curve cryptography into an account a program can read. It funds discrete-logarithm puzzles, called rungs, on six curves chosen so that a quantum computer reaches them before a classical one and in a known order: three prime-order curves of the secp256k1 family over 160-, 192- and 224-bit fields; a classical control over secp256k1's own 256-bit field with a 160-bit subgroup; and nothing-up-my-sleeve points on secp256k1 and Ed25519 themselves. Every rung's points are derived from a public seed by hash-to-curve under a published transcript, so nobody knows their discrete logarithm.

A solver who finds a rung's discrete logarithm claims it with a Schnorr proof whose challenge commits to the rung, the payout address and a job nonce. The proof never reveals the logarithm, and nobody who observes it can redirect the payout. On-chain verification is staged across transactions for the rungs whose field arithmetic Solana has no syscall for, and done in one transaction with native curve syscalls for the terminal rungs. When verification completes, the rung's RungState account changes from Open to Solved, recording the time the claim began, and the rung's bounty pays the recipient in the same instruction.

Three things are built on that one bit. Bounty vaults, funded by sponsors and irrevocable until 2040, pay solvers and give every capable lab a reason to disclose. Rung-event markets, binary YES and NO tokens fully collateralized in USDC, resolve permissionlessly by reading the rung, and their prices across expiries form a market-implied date curve for quantum capability. Q-Vaults hold funds under an Ed25519 key with a rate-limited instant budget and a delay, and a Falcon-512 key that can always spend and veto; when the vault's trigger rung reads Solved, the Ed25519 path is rejected permanently.

The protocol's token, CANARY, has a fixed supply launched in full to the public; the protocol's share of every settlement fee buys it and burns it, and it never touches the trigger, collateral or resolution.

This paper specifies the ladder and its parameter generation, the claim protocol and its verification, the three programs with their accounts and instructions, the market and fee mathematics, the token, the vault's key and authorization scheme, the security design, the off-chain infrastructure and the deployment model.

1. Introduction

1.1 A dated risk with no price

In 2026, estimates of the quantum resources needed to break elliptic-curve keys started to come with dates attached. Google Quantum AI estimates that solving the discrete logarithm on a 256-bit curve needs fewer than 1,200 logical qubits and 90 million Toffoli gates, or fewer than 500,000 physical qubits running for minutes on superconducting hardware [6][7]. Caltech and Oratomic estimate 10,000 to 26,000 neutral-atom qubits, with a P-256 run taking days [8]. Project Eleven places Q-Day possibly as early as 2030 [4]. Allianz counts about 6 million BTC, some 30 % of supply and about $400 billion, behind public keys that are already exposed on chain [3], and Project Eleven puts the value secured by elliptic-curve cryptography across digital assets above $2.5 trillion [5].

The risk has a size and, increasingly, a range of dates. It has no price. There is no market in which a holder can buy protection against it, no instrument whose value moves with the probability of a cryptographically relevant quantum computer (CRQC) arriving by a given year, and no market-implied date to anchor planning. Experts surveyed in the literature put the chance of a CRQC within ten years at 28 % to 49 % [3]; nobody can trade that range.

1.2 Signals a program can't read

The evidence that does exist arrives in forms that software cannot act on:

FormExampleWhy a program can't act on it
Papers and resource estimatesBabbush et al. (2026) [7], Cain et al. (2026) [8]Estimates, not events; they move expectations but trigger nothing
Judged prizesProject Eleven's Q-Day Prize, won with a 15-bit key in April 2026 [5]Judged off chain; toy sizes are classically trivial
Zero-knowledge proofs of cryptanalysisGoogle's 2026 proof, whose first version had a soundness bug found by Trail of Bits [7][11]A proof system's soundness becomes the trigger's soundness
Unclaimed classical puzzlesBitcoin puzzles #135–#160, ~916 BTC across 78 keys [14][15]Range-restricted keys measure classical progress only; Shor gains nothing from a range restriction [10]
A 256-bit canaryBitMEX Research's proposal for a NUMS address on Bitcoin [1]Fires at the moment theft is possible, leaving no lead time

A protocol that wants to react automatically — freeze a bridge, migrate a treasury, pay out a hedge — needs something objective to read. A committee's vote is not objective, an oracle's report adds a party to trust, and a 256-bit canary is objective but arrives too late to be useful.

1.3 What Canary provides

Canary provides an on-chain fact with three properties:

  1. Objective. A rung reads Solved if and only if its program verified a proof of knowledge of the rung's discrete logarithm. No party decides it.
  2. Ordered. The rungs are chosen so that a quantum computer reaches them in a known order, from Q-160 at 1,466 logical qubits to the terminal rungs at about 2,330, and so that the lower rungs fall before 256-bit keys are in danger.
  3. Funded. Each rung carries a bounty that pays its solver, so anyone who can solve a rung has a reason to claim it rather than keep the result quiet.

On top of that fact, Canary builds the two instruments the problem lacks: rung-event markets that price the risk at fixed dates, and Q-Vaults that act on it automatically.

1.4 Design goals

GoalHow Canary meets it
No oracle, no committeeMarkets and vaults read RungState directly; resolution is a permissionless instruction
Nobody knows the answerRung points come from hash-to-curve on public seeds with a published, reproducible transcript
A claim can't be stolenThe proof's challenge commits to the recipient; the discrete logarithm is never revealed
The signal carries its causeA classical control rung costs the same classically as the lowest quantum rung but needs the qubits of a full 256-bit break
Markets can't be gamed by brute forcePer-rung open interest is capped at no more than 1 % of the rung's low-end classical break cost
Collateral holds when it has to payUSDC only; crypto-native collateral would fall with the event it insures
Every wallet can signEvery instruction fits a legacy 1,232-byte transaction
Credible neutralityThe rungs program becomes immutable after audit; the CANARY token is never collateral, a resolution input or a vote on rungs and vaults

1.5 Structure of this paper

Section 2 sets out the cost models behind the ladder. Section 3 defines the ladder and how a fall is read; Section 4 specifies how rung parameters are generated. Sections 5 to 7 cover the claim protocol, the rungs program and the bounty vaults. Sections 8 and 9 cover rung-event markets and their program; Sections 10 and 11 cover Q-Vault and its program. Section 12 describes the security design, Section 13 the off-chain infrastructure, Section 14 integration, Section 15 the economics, Section 16 upgrade authority and deployment, and Section 17 the roadmap. The appendices give account layouts, the instruction reference, error codes, the rung parameters, the status feed schema and a glossary.

2. Background: what it costs to break a curve

The ladder's design rests on how the cost of the elliptic-curve discrete logarithm problem (ECDLP) grows with key size, classically and on a quantum computer. This section collects the models the rest of the paper uses.

2.1 The problem

Let E be an elliptic curve over a prime field 𝔽ₚ and G a point of prime order n. Given Q in the subgroup generated by G, the ECDLP asks for the integer k ∈ [0, n) with Q = k·G. Every elliptic-curve signature scheme in use on public blockchains, ECDSA on secp256k1 and EdDSA on Ed25519 among them, is secure only while this problem is infeasible at the sizes they use: a public key is k·G for a secret k.

Two parameters matter for cost:

For most curves these are the same number of bits. Canary's classical control rung, C-160, deliberately separates them.

2.2 Classical cost: Pollard's rho

The best known classical algorithms for a generic prime-order subgroup are Pollard's rho and its parallel variant by van Oorschot and Wiener, and Pollard's kangaroo for discrete logarithms known to lie in an interval. A random walk through the group finds a collision after about √(πn/2) steps. The negation map, which identifies P with −P at no cost, divides the effective group size by 2. On curves with j-invariant 0, of the form y² = x³ + b, an efficiently computable automorphism of order 3 divides it by a further 3. The expected step counts are therefore:

generic curve with negation map: √(πn/4) steps
j = 0 curve (y² = x³ + b): √(πn/12) steps

For the ladder's sizes:

RungSubgroup nCurveExpected stepslog₂
Q-160~2160 (161-bit)j = 0280 × √(π/12)79.0
Q-192~2192j = 0296 × √(π/12)95.0
Q-224~2224 (225-bit)j = 02112 × √(π/12)111.0
C-160~2160generic280 × √(π/4)79.8
T-256k~2256j = 02128 × √(π/12)127.0
T-255e~2252generic2126 × √(π/4)125.8

Each step is one elliptic-curve point addition plus bookkeeping. Parallel collision search distributes the walk across any number of processors with near-linear speedup, so the cost is a budget in group operations, not a wall-clock limit.

2.3 The classical frontier

The public record of what has been solved fixes how far classical computation reaches today:

EventSizeEffort
Academic ECDLP record, 2009112-bit prime fieldA PlayStation 3 cluster [12]
Academic ECDLP record114-bit, Barreto–Naehrig curve2,000 cores for about six months [13]
Bitcoin puzzle #130Key in a 2130 intervalAbout 265.5 kangaroo steps, on GPUs [14][15]
Bitcoin puzzle #135Key in a 2135 intervalAbout 268 steps; still open, 13.5 BTC bounty, public key exposed since 2019 [14][15]

The frontier sits around 265 to 268 group operations. Q-160, at about 279, is 211 times beyond puzzle #135.

Cost of brute force at the low rung. A modern GPU running a tuned kangaroo or rho kernel performs about 8 × 10⁹ group operations per second (RCKangaroo on an RTX 4090 [15]). At an assumed rental price of $0.30 per GPU-hour:

2^79 ≈ 6.0 × 10²³ operations
÷ 8 × 10⁹ per GPU-second = 7.6 × 10¹³ GPU-seconds = 2.1 × 10¹⁰ GPU-hours
× $0.30 = about $6 billion

Allowing for better kernels, cheaper hardware and the spread of rental prices, this paper uses $1 billion to $10 billion as the classical cost of Q-160 and C-160. The low end of that range sets the market caps of Section 8.6.

2.4 Quantum cost: Shor's algorithm

Shor's algorithm solves the ECDLP in polynomial time on a fault-tolerant quantum computer. Its cost is dominated by reversible modular arithmetic over the curve's base field. Roetteler, Naehrig, Svore and Lauter give explicit circuit counts for a curve over an n-bit prime field [9]:

logical qubits = 9n + 2⌈log₂ n⌉ + 10
Toffoli gates = 448 n³ log₂ n + 4090 n³
Field bits nLogical qubitsToffoli gatesScaled from Google 2026 (est.)
1601,4663.0 × 10¹⁰~750 qubits, ~21M Toffoli
1921,7545.3 × 10¹⁰~900 qubits, ~37M Toffoli
2242,0428.5 × 10¹⁰~1,050 qubits, ~59M Toffoli
2562,3301.3 × 10¹¹≤ 1,200 qubits, ≤ 90M Toffoli [7]

Google Quantum AI's 2026 estimate for 256-bit ECDLP is fewer than 1,200 logical qubits and 90 million Toffoli gates [7]; Google did not publish per-size figures, so the last column scales its 256-bit figure by Roetteler's ratios. Every quantum resource figure in this paper is a logical count; physical qubit counts depend on the error-correction scheme and hardware, and the references give them where they exist [7][8].

Two properties of these formulas shape the ladder:

  1. Qubits are dominated by the field term 9n. The subgroup size enters only through the length of the exponent register. A curve over a 256-bit field with a 160-bit subgroup needs nearly the qubits of a full 256-bit break, while costing classically what a 160-bit curve costs. This is the basis of the classical control rung.
  2. Quantum cost grows polynomially in n, classical cost exponentially. From 160 to 256 bits, logical qubits grow by a factor of 1.59 and Toffoli count by about 4.3, while classical cost grows by 248.

2.5 Why the ladder can't start lower

The second property sets a floor. A curve small enough to fall to a quantum computer long before 256-bit keys is also small enough to fall to classical computation: at 120 bits the classical cost is about 259, within reach of a modest GPU budget. Below about 150 bits, classical solvers would collect the bounty and fire the signal without any quantum progress at all.

The lowest trustless rung therefore sits where classical cost leaves the reach of any plausible budget, at 160 bits, and the ladder's lead time is the distance in logical qubits from there to 256 bits: from 1,466 to 2,330, a machine with 63 % of the qubits of a secp256k1 breaker. That is strictly more warning than a 256-bit canary gives, which is none.

2.6 Where 256-bit keys sit on Solana

On Solana, every address that is not a program-derived address is an Ed25519 public key [24]. A program-derived address (PDA) is a point that lies off the curve by construction and has no private key at all [16]. Funds held by a PDA can only move under the rules of the program that derives it. A program can therefore hold funds that no Ed25519 key can move and require a different signature scheme to release them. Section 10 builds Q-Vault on this property.

3. The ladder

3.1 Six rungs in three families

A rung is a curve, a generator G of a prime-order subgroup, and a target point Q whose discrete logarithm k with Q = k·G nobody knows. Canary's ladder has six:

RungFamilyCurveField bitsSubgroup bitsClassical stepsLogical qubits
Q-160Quantumsecp160k1: y² = x³ + 7 over a 160-bit prime, prime order160161~2791,466
Q-192Quantumsecp192k1: y² = x³ + 3 over a 192-bit prime, prime order192192~2951,754
Q-224Quantumsecp224k1: y² = x³ + 5 over a 224-bit prime, prime order224225~21112,042
C-160Classical controlA generic curve over secp256k1's 256-bit field with a ~160-bit prime factor in its order256~160~279–280~2,330
T-256kTerminalsecp256k1, NUMS target point256256~21272,330
T-255eTerminalEd25519, NUMS target point in the prime-order subgroup255253~2126~2,330
public classical record, 265–268 26428029621122128 1,4661,7542,0422,330 Q-160 Q-192 Q-224 C-160 T-256k T-255e
The ladder by classical cost (across) and logical qubits (up). The Q-rungs step together in both; C-160 shares Q-160's classical cost and the terminal rungs' qubit count.

3.2 Quantum rungs

The three Q-rungs are the SEC 2 curves secp160k1 (y² = x³ + 7), secp192k1 (y² = x³ + 3) and secp224k1 (y² = x³ + 5) [28]: prime-order curves with j-invariant 0, the same family as secp256k1, over 160-, 192- and 224-bit prime fields. A laboratory that builds arithmetic for secp256k1 builds it for these first. Their subgroups have 161, 192 and 225 bits. Canary uses the standard curve equations but not the standard base points: on every rung both G and Q are derived by hash-to-curve (Section 4), so no published generator with a known relation to anything else is involved. Dallaire-Demers, Doyle and Foo publish a deterministic suite of the same family stepping from 256 bits down to 6 [10], which places these sizes in the context of a public ladder of reference instances.

Because quantum cost is set by field size, each Q-rung is cheaper for a quantum computer than secp256k1: Q-160 needs 63 % of the logical qubits and 23 % of the Toffoli gates of a 256-bit break.

3.3 The classical control

C-160 is a generic curve over secp256k1's 256-bit base field whose order has a prime factor r of about 160 bits. G and Q are cofactor-cleared into the order-r subgroup. Classically, only the subgroup matters: C-160 costs about 279–280 steps, the same as Q-160 within a factor of 2. A quantum attack, however, must perform arithmetic over the 256-bit field, so its qubit count is that of a full secp256k1 break, about 2,330.

C-160 is the instrument that separates classical from quantum progress at the bottom of the ladder. Both Q-160 and C-160 cost the same to a classical attacker; only a quantum attacker finds Q-160 meaningfully cheaper. Its curve needs a certified point count (Schoof–Elkies–Atkin) over the 256-bit field and a certified ~160-bit prime factor; C-160 joins the parameter suite, and the quantum-signal market opens, when those certificates are published.

3.4 Terminal rungs

T-256k is a pair of nothing-up-my-sleeve points on secp256k1, the curve of Bitcoin and Ethereum keys, the counterpart of the canary BitMEX Research proposes [1]. T-255e is a pair of nothing-up-my-sleeve points in the 253-bit prime-order subgroup of Ed25519, the curve of every Solana address. A fall of either means keys on that curve can be broken.

3.5 Reading a fall

Markets settle on the fact that a rung fell. The ladder is arranged so that the pattern of falls also carries its cause:

ObservationReadingA 256-bit-keys-at-risk event?
Q-160 falls, C-160 standsQuantum, or an attack specific to small fieldsYes: the strong quantum signal
Q-160 and C-160 both fallClassical money or a classical algorithm; both cost ~279 classicallySays nothing about qubits
Q-192 or Q-224 fallsOnly a CRQC or a classical ECDLP breakthrough fits: 295 and more is beyond any budgetYes; a classical break endangers 256-bit keys as surely
T-256k or T-255e fallsTheft capability exists on that curveYes, terminal

Attribution matters only at the bottom, where enough money could buy a fall. Canary gives that case its own contract, the quantum-signal market, which resolves YES when Q-160 falls while C-160 still reads Open (Section 8.4).

3.6 The lead time the ladder buys

The ladder's lowest trustless rung is pinned by classical feasibility (Section 2.5), so its lead time is measured in qubits: the distance between a machine that can break Q-160, with 1,466 logical qubits, and one that can break secp256k1, with 2,330. Across that distance the markets carry information continuously: YES prices at each expiry move as capability estimates move, long before any rung falls, and the curve they draw is a market-implied date for each threshold.

3.7 Bounties and the decision to claim

A rung below 256 bits guards nothing but its own bounty. A solver who can break Q-160 gains nothing by keeping the result secret and gains the bounty, priority and recognition by claiming. A claim has no victim and is legal, can be pseudonymous and can name any recipient. The bounty for Q-160 and C-160 is set alike, so a classical solver gains nothing by claiming one to look like the other. If a rung's discrete logarithm is ever published anywhere, anyone can claim the rung with it, so the signal does not depend on the original solver choosing to act.

4. Parameter generation

A rung is only a canary if nobody knows its answer. Each rung's two points, the base point G and the target point Q, are therefore derived from a public seed by a deterministic procedure that anyone can rerun. This section specifies that procedure, the checks applied to its output, the transcript that records it, and how the transcript reaches the program.

4.1 Requirements

RequirementHow the generator meets it
Nobody knows logG QG and Q are independent hash-to-curve outputs; no party ever chooses a scalar
Anyone can check itThe procedure is deterministic: the same seed reproduces the same points, transcript and hash bit for bit
The curves are recognisableThe Q-rungs and T-256k use SEC 2 curve equations; T-255e uses Ed25519
The program verifies the published rungThe program's constants and the rung's transcript_hash both come from the same transcript
No hidden freedomThe only input is the seed text; the counters the procedure needed are recorded

4.2 The parameter suite

RungCurveEquationField bitsSubgroup bitsCofactor
Q-160secp160k1y² = x³ + 71601611
Q-192secp192k1y² = x³ + 31921921
Q-224secp224k1y² = x³ + 52242251
C-160joins when its point count is certified (Section 4.9)256~160
T-256ksecp256k1y² = x³ + 72562561
T-255eEd25519−x² + y² = 1 + d·x²y², d = −121665/1216662552538

The curve equations, field primes and group orders are those of SEC 2 [28][36] and RFC 8032 [32]; Appendix D gives them in full. The standard base points are not used. On every rung, both G and Q are produced by the procedure below, so the rung does not depend on how any standard's generator was chosen.

4.3 Hash to curve

For a rung id ρ, a role (the byte G or Q) and a counter c, the generator hashes:

H(ρ, role, c) = SHA-256( "canary.nums.v1" ‖ len(seed) ‖ seed ‖ ρ ‖ role ‖ c )

where len(seed) is the seed's UTF-8 length as a 32-bit big-endian integer, seed is its UTF-8 bytes, ρ is one byte and c is a 32-bit big-endian integer. The counter starts at 0 and increments until the candidate is accepted.

Short Weierstrass curves (the Q-rungs and T-256k).

  1. x ← H mod p.
  2. If x³ + b is not a square mod p, increment c and repeat.
  3. y ← the smaller of the two square roots, computed with Tonelli–Shanks.
  4. The point is (x, y). Since every curve here has cofactor 1, every point other than the identity generates the full group.
  5. For Q, the candidate must differ from G.

Twisted Edwards (T-255e).

  1. y ← H mod p.
  2. Solve for x; if no x exists, increment c and repeat. Take the even root.
  3. Multiply the point by the cofactor 8, which moves it into the prime-order subgroup.
  4. Reject the identity, require n·P = O, and for Q require Q ≠ G.
Public seedUTF-8 text Hash to curverole G, counter c Hash to curverole Q, counter c ChecksSection 4.5 Transcriptcanonical JSON SHA-256RungState, API Rust constantscompiled program
From a public seed to the constants a rung verifies against.

Try-and-increment is not constant-time, which is irrelevant here: every input is public. Its output is uniform enough for the purpose, and what matters is that it is a fixed function of the seed.

4.4 Why nobody knows the answer

G and Q are two outputs of SHA-256 mapped to the curve. Knowing k with Q = k·G for such a pair is exactly the discrete-logarithm problem the rung poses; no choice of seed changes that, because for every seed the relation between the two outputs is as unknown as it is for any other. The only freedom anyone had is the seed text, and grinding over seeds produces other uniformly random pairs, never a pair with a known logarithm. This is the nothing-up-my-sleeve property BitMEX Research proposes for a Bitcoin canary [1] and that the Brace-for-Impact suite applies to a ladder of curves [10].

Separate roles in the hash keep G and Q independent: neither is derived from the other, and Q is rejected if it equals G.

4.5 Checks

Every entry in the suite passes these checks before its transcript is written:

CheckMethod
p is primeBPSW probable-prime test (sympy.isprime)
n is primeBPSW probable-prime test
The group order is h·nn-torsion is non-trivial, and h·n is the unique multiple of n inside the Hasse interval p + 1 ± 2√p
Not anomalousThe trace of Frobenius t = p + 1 − h·n is not 1
Large embedding degreepk mod n ≠ 1 for every 1 ≤ k ≤ B, with B chosen at generation (128 or more for the published suite)
Non-singular curve4a³ + 27b² ≠ 0 mod p (Edwards: a ≠ d, both non-zero)
Points in the subgroupn·G = O and n·Q = O, G ≠ O, Q ≠ O
Independent pointsG ≠ Q
Edwards cofactor clearedPoints are multiplied by 8 before acceptance

BPSW has no known counterexample but is a probable-prime test, not a certificate. The SEC 2 and RFC 8032 primes are long-published values; the deployment's primality certificates for each p and n are listed with the parameter suite (RUNG_PRIME_CERTIFICATES in /v1/programs).

4.6 The transcript

Each rung's transcript is its manifest entry, every field except transcriptSha256 itself, serialized as canonical JSON: keys sorted, separators , and : with no spaces, ASCII only, no trailing newline. The entry records:

FieldContent
schemaVersion, networkcanary.parameters.v1, canonical
id, label, parameterSet, modelThe rung id and name, the curve and its form
pHex, nHex, aHex, bHex (or dHex), cofactorThe curve constants, as 32-byte hexadecimal
gHex, qHex, gMinusQHexThe two points and the precomputed G − Q used by the verifier
numsCountersThe counter at which G and Q were accepted
publicSeed, hashToCurveThe seed and the procedure's name
fieldBits, subgroupBits, scalarBitsSizes the verifier depends on
checksThe result of every check in Section 4.5, with its method
transcriptSha256 = SHA-256( canonical JSON of the entry without transcriptSha256 )

The hash names the transcript file, appears in the manifest, is written into the rung's RungState.transcript_hash at initialization, enters every claim challenge (Section 5.3), and the API serves the exact transcript bytes at GET /v1/transcripts/{hash}. Anyone can fetch the transcript, hash it, and compare the result with the 32 bytes stored on chain.

4.7 From transcript to program

The generator also writes the curve constants as Rust tables that the rungs program compiles in. For a Weierstrass rung they are p, n, a, b, G, Q, G − Q, the transcript hash, the scalar bit length and the cofactor; for the Edwards rung, G, Q, n and the transcript hash. There is no instruction that changes them.

Three things tie the compiled program to the published transcript:

  1. initialize_rung writes the compiled transcript hash into RungState, and refuses a rung id for which no parameters are compiled.
  2. begin_claim and the terminal claim instructions require the compiled transcript hash to equal the one in RungState.
  3. The verifier's setup recomputes G + (−Q) from the compiled points and requires it to equal the compiled G − Q, and requires the scalar bit length to equal the bit length of n.

The indexer checks every rung it reads against the published manifest and its program-derived address before it reports the rung, and the API reports the deployment as waiting until every rung on chain carries the published transcript hash.

4.8 Reproducing the suite

The generator is a single Python 3.13 program with one dependency, SymPy 1.14.0. From the backend source:

python canary-gen/generate.py --production --suite sec2 --seed "<published seed>" --embedding-bound <B>

It writes generated/canonical.json, its SHA-256, one transcript file per rung named by its hash, and the Rust tables. A reproduction matches when the manifest hash and every transcript hash equal the published ones; a single differing byte changes them.

Reproduction is deliberately cheap: it needs no special hardware and uses a general-purpose computer-algebra library rather than any Canary code beyond the generator itself. Independent reproductions are a launch condition (Section 17.2).

4.9 C-160

C-160 needs a curve over secp256k1's 256-bit field whose group order has a prime factor of about 160 bits. Finding one means counting points on random curves over a 256-bit field (Schoof–Elkies–Atkin) until the order has such a factor, then certifying the count and the factor. The rung's points are then cleared by the cofactor into the 160-bit subgroup, and the staged verifier runs over 160-bit scalars in 256-bit field arithmetic.

Until a certified curve is published, the parameter suite carries no C-160 entry, and initialize_rung for rung id 3 returns ParameterUnavailable. The quantum-signal market, which reads C-160 as its control, opens with it.

5. The claim protocol

A claim proves knowledge of a rung's discrete logarithm k, where Q = k·G, without revealing k, and binds the payout to one recipient. The proof is a Schnorr proof of knowledge made non-interactive with a domain-separated hash. This section specifies the proof, its encodings, the challenge, and how each family of rungs verifies it on Solana.

5.1 The proof

The solver holds k. To claim for a recipient A:

1. choose w uniformly in [1, n) and compute R = w·G
2. e = Hclaim(program, ρ, A, nonce, R, G, Q, transcript) mod n
3. s = w + e·k mod n
4. publish (R, s)

The program accepts when

s·G = R + e·Q, with 0 < s < n and R a valid point

which holds for an honest proof because s·G = w·G + e·k·G = R + e·Q. Only R and s go on chain; k does not, and w is discarded after use. Because k never leaves the solver, a claim leaves no artefact anyone else can reuse: the next rung is untouched, and Q's logarithm remains known only to whoever solved it.

5.2 Why a valid proof implies knowledge

Schnorr proofs are special-sound. Given two accepting proofs (R, s) and (R, s′) with the same R and different challenges e ≠ e′, anyone can compute

k = (s − s′) · (e − e′)−1 mod n

In the random-oracle model this means that anyone able to produce accepting proofs with noticeable probability can be turned into an algorithm that outputs k [33][34]. A rung that reads Solved therefore records that someone was able to compute its discrete logarithm.

The proof is also zero-knowledge: (R, s) can be simulated without k by choosing s and e first and setting R = s·G − e·Q, so a published proof reveals nothing about k beyond the fact that its author knew it.

5.3 The challenge

The challenge binds the proof to everything that must not change after the solver signs:

e = SHA-256( "canary.claim.v1" ‖ program_id ‖ ρ ‖ recipient ‖ nonce ‖ R ‖ G ‖ Q ‖ transcript_hash ), read as a big-endian integer and reduced mod n
FieldBytes (Weierstrass)Bytes (Edwards)What it binds
Domain tag canary.claim.v11515The purpose of the hash
Program id3232One deployment of the rungs program
Rung id ρ11One rung
Recipient3232The owner of the account that receives the bounty
Nonce8, little-endian8, little-endianOne claim attempt
R6432The commitment
G6432The rung's base point
Q6432The rung's target point
Transcript hash3232The exact published parameters
Total312216

Weierstrass points are encoded as x ‖ y, each coordinate 32 bytes big-endian (the smaller fields are left-padded). Edwards points use the 32-byte compressed encoding of RFC 8032 [32]. The scalar s is 32 bytes big-endian on Weierstrass rungs and 32 bytes little-endian on T-255e, matching each curve's convention. Reduction mod n is computed bitwise on chain, so no big-integer library is involved.

Because the recipient is inside the challenge, a proof cannot be redirected: changing A changes e, and producing the matching s for the new e requires k. Because the program id and transcript hash are inside, a proof made for one deployment or one parameter set is invalid for every other.

5.4 Proof hash and the claim job

Staged claims keep their progress in a VerifyJob account. Its address commits to the proof itself:

proof_hash = SHA-256( "canary.proof.v1" ‖ ρ ‖ recipient ‖ nonce ‖ R ‖ s )
job = PDA( "job", ρ, recipient, nonce, proof_hash )

A different proof maps to a different job, so a party who sees a solver's first transaction cannot create a job at that address with a modified proof. A copied valid proof maps to the same job and the same recipient, so copying it gains nothing. The proof hash is recorded in RungState when the rung falls.

The nonce is a random 64-bit value chosen by the client. A solver who needs to retry, for example after a job expires, uses a new nonce and so a new job; old jobs never block new ones.

5.5 Staged verification on the quantum rungs

The Q-rungs' fields have no Solana syscall, so the rungs program computes s·G − e·Q itself and compares it with R. A full check takes more compute than one transaction allows, so it runs as a sequence of transactions over a stored accumulator.

Field arithmetic. Every field element is eight little-endian 32-bit limbs in Montgomery form with R = 2256, whatever the field size. One representation serves all three curves, so the same audited code verifies Q-160, Q-192 and Q-224.

Point arithmetic. The accumulator is a point in Jacobian coordinates (X : Y : Z), representing (X/Z², Y/Z³). Each bit doubles the accumulator and, depending on the bits, adds an affine point with mixed addition. No step needs a field inversion.

Shamir's trick. Instead of computing s·G and e·Q separately, the engine scans the bits of s and e together, most significant first:

bit of sbit of eAdded after doubling
00nothing
10G
01−Q
11G − Q (precomputed in the parameters)

After the last bit the accumulator holds s·G − e·Q. The final comparison with R = (x, y) is projective, X = x·Z² and Y = y·Z³, so again no inversion is needed. The accumulator starts at the point at infinity (Z = 0).

Instructions.

  1. begin_claim(ρ, recipient, nonce, R, s) checks that the rung is Open, that R lies on the curve with coordinates below p, that 0 < s < n and that the transcript matches; it computes e, records begin_ts and begin_slot from the clock, and creates the job with the accumulator at infinity.
  2. step_claim(max_bits) advances the accumulator by up to 16 bits. It is permissionless: anyone can send it, and it only advances the stored computation. After the last bit, the job becomes Verified if the accumulator equals R, and Rejected otherwise.
  3. finalize_claim() requires a Verified job, pays the bounty and sets the rung Solved.
steps = ⌈ scalar_bits ÷ 16 ⌉ transactions = steps + 2
RungScalar bitsStepsTransactions
Q-16016111 (the last is 1 bit)13
Q-1921921214
Q-22422515 (the last is 1 bit)17
begin_claime, event time 161616161616161616161 finalize_claimpay, Solved step_claim × 11, bits per step begin_slot≤ begin_slot + 300
The thirteen transactions of a Q-160 claim, inside its claim window.

Sixteen bits per step is the measured fit: a 64-bit step exceeded the 1.4M compute-unit limit, and the heaviest 16-bit step, a scalar whose bits force an addition at every position, used 721,585 compute units. Measured Q-224 costs on a local validator:

TransactionCompute unitsBytes
begin_claim174,953–176,453455
step_claim, 16 bits469,585–714,170285
step_claim, final bit and comparison177,548–180,435285
finalize_claim22,334448

Each prepared transaction stays at or below 1,082 bytes, which leaves 150 bytes of the 1,232-byte limit for instructions a wallet may add.

5.6 Verification on the terminal rungs

T-256k uses sol_secp256k1_recover, the syscall that recovers an ECDSA public key. Recovery computes r−1·(s·P − z·Gstd), where P is the point whose x-coordinate is r and whose parity is given by the recovery id. Setting the message hash z = 0, r = x(P) mod n and s = r·t mod n turns it into a scalar multiplication:

recover(z = 0, r = x(P), s = r·t, v) = r−1 · (r·t) · P = t·P

The recovery id is the parity of y(P) plus 2 if x(P) ≥ n. The program performs two such multiplications, s·G and e·Q, rejects any case where r, t or s is zero, and checks e·Q + R = s·G projectively, in one transaction. Both G and Q are the rung's own hash-derived points; the standard generator Gstd cancels out because z = 0. Measured cost: 344,884 compute units, 553 bytes.

T-255e uses sol_curve_multiscalar_mul on Ed25519. The program rejects the identity encoding for R, s = 0 and s ≥ n, then computes one two-term multi-scalar multiplication and compares the compressed result with R's bytes:

s·G + (n − e)·Q = s·G − e·Q =? R

Measured cost: 51,949 compute units, 520 bytes.

Both terminal instructions are single transactions and both are permissionless: the transaction can be sent by anyone, because the payout goes to the recipient the proof names.

5.7 Event time and the claim window

Familyevent_ts, event_slotRecorded by
Q-rungsThe clock at begin_claimThe job, then copied to RungState at finalize
T-256k, T-255eThe clock at the claim transactionRungState directly

Recording the begin time means that a claim begun before a market's expiry counts for that market even if its verification finishes after (Section 8.3). It cannot be backdated: the program reads the clock; the solver supplies no time.

A staged claim must finish within the claim window: step_claim and finalize_claim require

Clock.slot ≤ begin_slot + 300

which is about two minutes at Solana's nominal 400 ms slot time. At finalize the program records finalize_slot, and every reader of a Solved rung re-checks that event_slot ≤ finalize_slot ≤ event_slot + 300. A job that missed its window can be closed by anyone; close_job returns the job's rent to the account that paid for it, and the solver begins again with a new nonce.

5.8 Payout

The payout instruction (finalize_claim, claim_secp256k1 or claim_ed25519) transfers the entire balance of the rung's bounty vault, at that moment, to a USDC token account owned by the recipient, and sets the rung Solved in the same instruction. The account need not be the recipient's associated token account; the browser plan creates the associated account in the finalize transaction if it does not yet exist. Once the rung reads Solved, every claim instruction fails, so a rung pays exactly once.

Claims remain possible after the 2040-01-01 sunset. A claim and a sponsor's reclaim (Section 7.6) then draw on the same vault, and whichever executes first is served first.

5.9 Making a claim

The solver needs k as a 32-byte big-endian integer, a funded Solana wallet and a recipient. Two clients build the proof locally:

ClientWhere k livesTransactions
The web app's claim pageIn the page's memory only; never sent to any server or written to storagePrepared together for one wallet approval
The claim CLIRead from a file or standard input, never from a command-line argumentSent in order, with one extra transaction that creates the recipient's token account if needed

Both draw w and the nonce from a cryptographically secure random source, check that the rung on chain is Open and carries the published transcript hash, and refuse to build a proof for parameters that do not match. If a sequence is interrupted, the CLI resumes the existing job from its stored cursor, and the browser starts a new job with a new nonce.

6. The canary_rungs program

canary_rungs holds the ladder. It owns one RungState account per rung, the rung's bounty vault and receipt mint, and the claim verifiers of Section 5. It is the only program whose state other programs need to trust, and it is kept small for that reason: nine instructions, no administrator after initialization, and constants compiled in.

6.1 Accounts and addresses

Every account the program owns sits at a program-derived address (PDA), so any reader can derive it from the rung id alone:

AccountSeedsSizePurpose
RungState"rung", ρ271 bytesThe public state of rung ρ
Bounty authority"bounty", ρno dataSigns bounty transfers and mints receipts
Bounty vault"collateral", ρSPL token accountHolds the rung's USDC; its authority is the bounty authority
Receipt mint"receipt", ρToken-2022 mintNonTransferable sponsor receipts, 6 decimals
SponsorPosition"sponsor", ρ, sponsor75 bytesOne sponsor's totals on rung ρ
VerifyJob"job", ρ, recipient, nonce, proof_hash360 bytesOne staged claim in progress

Sizes include Anchor's 8-byte discriminator, SHA-256("account:<Name>") truncated to 8 bytes. Account data is Borsh-encoded after it. Rung ids are 0 to 5 for Q-160, Q-192, Q-224, C-160, T-256k and T-255e.

6.2 RungState

RungState is the interface the rest of Canary, and any integrator, reads. Its fields in order:

FieldTypeMeaning
versionu8Layout version, 2
rung_idu8ρ
familyu80 quantum, 1 classical control, 2 terminal
statusu80 Open, 1 Solved
bump, bounty_bump, receipt_bumpu8 × 3PDA bumps
collateral_mintPubkeyUSDC
collateral_vaultPubkeyThe bounty vault
receipt_mintPubkeyThe receipt mint
transcript_hash[u8; 32]SHA-256 of the published parameter transcript
event_tsi64Unix time of the claim's begin (staged) or of the claim (terminal); 0 while Open
event_slotu64The matching slot
recipientPubkeyWho was paid
proof_hash[u8; 32]The hash of the accepted proof
total_fundedu64Cumulative sponsor deposits
total_paidu64The bounty paid on the fall
total_reclaimedu64Cumulative reclaims after the sunset
sponsor_count, receipt_countu32 × 2Distinct sponsors
initialized_slotu64When the rung was created
finalize_slotu64The slot of the payout

Appendix A gives byte offsets. A reader that only needs the signal reads four things: that the account is the rung's PDA under the rungs program, its discriminator and version, status, and, when Solved, event_ts with the claim-window check of Section 5.7.

6.3 Instructions

InstructionCallerEffect
initialize_rung(ρ)The program's upgrade authority, once per rungCreates RungState (Open), the bounty vault and the receipt mint
fund_bounty(ρ, amount)Any sponsorMoves USDC into the bounty vault and mints receipts 1:1
begin_claim(ρ, recipient, nonce, R, s)AnyoneStarts a staged claim on a Q-rung
step_claim(max_bits)AnyoneAdvances a staged claim by up to 16 bits
finalize_claim()AnyonePays a verified staged claim and sets the rung Solved
close_job()AnyoneCloses a finished or expired job and refunds its rent to its payer
claim_secp256k1(ρ, recipient, nonce, R, s)AnyoneVerifies and pays a T-256k claim in one transaction
claim_ed25519(ρ, recipient, nonce, R, s)AnyoneVerifies and pays a T-255e claim in one transaction
reclaim_after_sunset(ρ, receipts)A receipt holderAfter 2040-01-01, burns receipts for their share of the vault

Each instruction checks the rung's family: begin_claim accepts the staged rungs only, claim_secp256k1 only T-256k and claim_ed25519 only T-255e, and a mismatch fails with WrongClaimMethod. Appendix B lists every account and check; Appendix C lists the 22 error codes.

6.4 The life of a rung

Not createdno account initialize Open fund_bounty until the sunsetclaims accepted verified claimpays the vault Solved event_ts, recipient, proof_hashno further claim or funding reclaim_after_sunset: in either state, from 2040-01-01
A rung moves from Open to Solved once; reclaim depends only on the clock.

The transition is one-way. Every claim path requires Open, and the internal solve step requires Open again before it writes, so a Solved rung can never be paid twice or reopened. fund_bounty requires Open and a time before the sunset. reclaim_after_sunset depends only on the clock and the holder's receipts.

6.5 The life of a claim job

StatusValueReached byNext
Verifying0begin_claimstep_claim until the last bit
Verified1The last step_claim, when s·G − e·Q = Rfinalize_claim
Finalized2finalize_claimclose_job
Rejected3The last step_claim, when the check failsclose_job

close_job succeeds when the job is Finalized or Rejected, when the rung is already Solved by any claim, or when the claim window has passed. It refunds the job's rent to the account that paid for it, recorded in the job as payer, and to no one else.

Several jobs can be in flight on the same rung at once, for different recipients or nonces. The first to finalize sets the rung Solved; the others then fail at their next step and can be closed.

6.6 Initialization

initialize_rung(ρ) is the only privileged instruction, and it can succeed once per rung:

  1. Parameters must be compiled for ρ; otherwise ParameterUnavailable.
  2. The collateral mint must be the deployment's configured USDC mint, with 6 decimals.
  3. The signer must be the current upgrade authority of the rungs program, read from its ProgramData account.
  4. The receipt mint's address must be unused. The program allocates it, assigns it to Token-2022, initializes the NonTransferable extension, and then initializes the mint with 6 decimals, the bounty authority as mint authority and no freeze authority.
  5. The program writes RungState: Open, the compiled transcript hash and the current slot.

There is no instruction that changes a rung's parameters, collateral, vault or receipt mint after this. Once the upgrade authority is revoked (Section 16), even initialization of a new rung id is impossible.

6.7 Events

EventEmitted byFields
RungInitializedinitialize_rungrung_id, collateral_mint, transcript_hash
BountyFundedfund_bountyrung_id, sponsor, amount, total_funded
ClaimBegunbegin_claimjob, rung_id, recipient, nonce, begin_ts, begin_slot, proof_hash
ClaimAdvancedstep_claimjob, bit_cursor, status
RungSolvedthe three payout instructionsrung_id, event_ts, event_slot, recipient, proof_hash, amount, job (when staged)
SponsorReclaimedreclaim_after_sunsetrung_id, sponsor, receipt_amount, amount

RungSolved is 98 bytes for a terminal claim and 130 bytes for a staged one, after its 8-byte discriminator SHA-256("event:RungSolved")[0..8]. Events are a convenience for indexers; the account is the source of truth, and the indexer reads accounts.

6.8 Build profiles

The program is compiled in exactly one of two profiles:

ProfileProgram id and collateralParameters
productionTaken from the deployment's configuration at build timeThe published canonical suite only
localnetFixed local ids, a local test mintA local suite; optionally test rungs with known logarithms for automated tests

A production build fails if test rungs are enabled or if its parameter sources contain test material, so a deployed program cannot contain a rung with a known answer. The workspace builds with overflow checks on in release, fat link-time optimization and one codegen unit, on Anchor 0.31.1.

6.9 Measured costs

InstructionCompute unitsTransaction bytes
initialize_rung41,031–48,517550
fund_bounty58,550–64,550623
begin_claim (Q-224)174,953–176,453455
step_claim (16 bits)469,585–714,170285
finalize_claim22,334448
claim_secp256k1344,884553
claim_ed2551951,949520
reclaim_after_sunset25,993

Every transaction requests an explicit compute limit of 1.4M, the per-transaction maximum, and stays below 1,232 bytes as a legacy transaction, so every wallet can sign it.

7. Bounty vaults

Each rung has one bounty vault: a USDC token account owned by the rungs program's bounty authority for that rung. The vault is what makes a rung worth claiming. It receives sponsors' deposits and a share of market fees, pays its whole balance to the solver when the rung falls, and after the 2040-01-01 sunset lets sponsors reclaim what remains.

7.1 Where the money sits

AccountAddressControlled by
Bounty vaultPDA "collateral", ρThe bounty authority PDA "bounty", ρ, which only the rungs program can sign for
Receipt mintPDA "receipt", ρMint authority: the bounty authority; no freeze authority
A sponsor's receiptsThe sponsor's Token-2022 associated token accountThe sponsor, but the tokens cannot be transferred
SponsorPositionPDA "sponsor", ρ, sponsorWritten only by fund_bounty and reclaim_after_sunset

No instruction moves USDC out of a bounty vault except the three payout instructions of Section 5.8 and reclaim_after_sunset. There is no administrator withdrawal, no early withdrawal and no fee taken on deposits.

7.2 Funding

fund_bounty(ρ, amount) accepts a deposit while the rung is Open and the clock is before the sunset:

require amount > 0, status(ρ) = Open, now < 2,208,988,800 (2040-01-01T00:00:00Z)
transfer_checked(amount, 6 decimals): sponsor → bounty vault
mint amount receipts to the sponsor

The first deposit by a sponsor creates their SponsorPosition and receipt account and increments the rung's sponsor count. Every deposit adds to total_funded on the rung and on the position.

Field of SponsorPositionMeaning
ownerThe sponsor
total_fundedEverything this sponsor has deposited on the rung
total_reclaimedEverything this sponsor has reclaimed after the sunset
first_funded_ts, first_funded_slotWhen the sponsor first funded the rung

7.3 Receipts

Receipts are Token-2022 tokens with the NonTransferable extension, minted one receipt unit per USDC unit deposited, both at 6 decimals. A sponsor who deposits 25,000 USDC holds 25,000 receipts.

PropertyConsequence
NonTransferableA reclaim right stays with the sponsor who funded the rung; it cannot be sold, lent or pooled
Minted 1:1Receipts measure contribution exactly; together they are each sponsor's share of the vault
No freeze authorityNobody can freeze a sponsor's receipts
Mint authority is a PDANew receipts exist only against new deposits

A receipt is also a public, verifiable record: anyone can read who funded a rung, and how much, from the mint and the positions.

7.4 Growth from market fees

The markets program's sweep_bounty transfers each rung's accrued share of settlement fees (Section 8.7) directly into the rung's bounty vault. Fees mint no receipts. They raise the vault's balance and so both the bounty a solver receives and, after the sunset, every receipt's reclaim value. The API reports the cumulative amount a rung has received this way as feesReceived.

7.5 Payout on a fall

When a claim succeeds, the payout instruction transfers the vault's entire balance at that moment, deposits and fees together, to the recipient's USDC account, and sets the rung Solved:

paid = balance(vault) at the payout instruction total_paid ← paid

From then on the rung accepts no deposits, and the vault holds only what arrives later from fee sweeps. A Solved rung whose receipts are all redeemed is flagged by the markets program as having no exit for further fees, which then go to the other rungs (Section 8.8).

7.6 Reclaim after the sunset

From the sunset on, any receipt holder can call reclaim_after_sunset(ρ, r) to burn r of their receipts for a proportional share of the vault:

amount = ⌊ balance(vault) × r ÷ supply(receipts) ⌋ (computed in 128-bit arithmetic)

The receipts are burned and the amount is transferred to the holder's USDC account in the same instruction. A holder can reclaim part of their receipts and keep the rest. Rounding down means each reclaim leaves any fractional unit in the vault; the last holder to reclaim receives what remains.

ExampleValue
Deposits on the rung1,000,000 USDC, so 1,000,000 receipts outstanding
Fees swept in since200,000 USDC
Vault balance at the reclaim1,200,000 USDC
A sponsor's receipts250,000
Reclaim1,200,000 × 250,000 ÷ 1,000,000 = 300,000 USDC

Reclaim does not check the rung's status. If the rung has fallen, the vault holds only fees that arrived after the payout, and receipts reclaim those. If it is still Open, the rung remains claimable: a claim pays whatever is in the vault when it executes, so after the sunset claims and reclaims draw on the same balance.

7.7 What sponsors see

EndpointReturns
GET /v1/bountiesEvery rung's vault address and balance, totals funded and paid, fees received, sponsor and receipt counts, and the sunset
GET /v1/rungs/{rungId}The rung, its state and its bounty; /events and /claim-jobs give its fall and its claims in flight
GET /v1/wallets/{wallet}/receiptsA wallet's receipts per rung: amount funded and when, share units, and the reclaim's availability, estimated amount and amount already claimed

Share units are the wallet's remaining receipt balance. A sponsor's current share of a vault is therefore their receipts divided by the receipt supply, both of which are on chain.

7.8 Measured costs

InstructionCompute unitsTransaction bytes
fund_bounty58,550–64,550623
reclaim_after_sunset, partial25,993

8. Rung-event markets

A rung-event market is a binary contract on one question: does rung ρ fall by time T? It is two SPL tokens, YES and NO, collateralized one-for-one in USDC, and it resolves by reading the rung's RungState account. This section defines the contract precisely; Section 9 gives the program that implements it.

8.1 The contract

A market m is the tuple (ρ, T, c): a rung ρ, an expiry T (a Unix timestamp; v0 expiries are 23:59:59 UTC on 2027-12-31, 2028-12-31 and 2030-12-31) and a collateral mint c (USDC in v0). Let event_ts(ρ) be the rung's recorded event time, the time its successful claim began.

YES pays 1 unit of c ⟺ ρ reads Solved and event_ts(ρ) ≤ T
NO pays 1 unit of c ⟺ ρ reads Solved with event_ts(ρ) > T, or T has passed and ρ still reads Open 300 slots after the first post-expiry observation

Exactly one side pays. Because a claim may take several transactions to verify, the rung's event time is the start of the claim, not its end: a solver who begins before the expiry has fallen before the expiry, even if verification completes after it. The claim window, at most 300 slots between begin_claim and finalize_claim, bounds how long after T the outcome can remain undecided.

8.2 Pairs and collateral

Any holder of c can mint pairs while the market is open:

mint_pair(a): lock a units of c → receive a YES and a NO
merge_pair(a): burn a YES and a NO → receive a units of c

Both are exact at c's 6-decimal precision. The market's collateral account therefore always holds exactly the outstanding supply:

collateral(m) = supply(YES) = supply(NO) while m is unresolved

mint_pair is accepted only while the market is unresolved, the clock is at or before T and the target rung reads Open, and only within the rung's open-interest cap (Section 8.6). merge_pair has the same gates: once T has passed or the target rung reads Solved, a pair can no longer be merged, and the only exit is redemption after resolution. Closing merge at the event means the side that won cannot avoid the settlement fee by buying the losing side cheaply and merging at par.

YES and NO are classic SPL tokens with the market's program address as their sole mint authority and no freeze authority. They trade on any Solana venue that lists SPL tokens; Canary runs no exchange.

8.3 Resolution

resolve is permissionless: anyone may call it, and Canary's crank calls it for every market as soon as it can succeed. It reads the rung account and the clock and sets the outcome once:

Condition at the callOutcome
ρ reads Solved, its claim finalized within 300 slots of its event slot, and event_ts(ρ) ≤ TYES
ρ reads Solved and event_ts(ρ) > TNO
ρ reads Open and the clock is past T, on the first such callRecords the observation slot; no outcome yet
ρ reads Open and the slot is more than 300 past the recorded observationNO
OtherwiseNo change

The 300-slot barrier after expiry gives a claim that began before the expiry time to finish its verification before NO can be declared.

The rung account is read with full validation: its owner must be canary_rungs, its address must be the program-derived address of ρ, and its discriminator and layout version must match. A spoofed account cannot resolve a market. Once set, the outcome never changes.

8.4 The quantum-signal market

The quantum-signal market is a market on the pair (Q-160, C-160) rather than one rung. It answers the attribution question of Section 3.5:

YES ⟺ Q-160 fell by T, and C-160 had not fallen at or before Q-160's event (compared by event time, then event slot)

The market waits until 300 slots after Q-160's event slot before resolving YES, so a C-160 claim begun at the same moment can land first. Ties resolve NO. If Q-160 and C-160 fall together, classical money or a classical algorithm explains both, and the quantum-signal market pays NO even though both rung-event markets pay YES. The quantum-signal market is created with C-160 Open as its control rung and counts against Q-160's open-interest cap; it opens when C-160 is initialized.

8.5 Redemption and the settlement fee

After resolution, holders of the winning side redeem. Let g be the gross amount redeemed, in atomic units of c. The settlement fee is 1.0 %, that is 100 basis points of the winning redemption, computed on the market's cumulative winning redemptions so that splitting a redemption into pieces never adds fee:

R′ = R + g fee = ⌊R′ ÷ 100⌋ − F F′ = F + fee net = g − fee

where R is the market's total redeemed before this call and F its total fees. The losing side redeems for nothing. Minting and merging carry no fee. The fee stays in the market's collateral account as escrow; nothing leaves the market at redemption except the net payout.

8.6 Open-interest caps

Each rung has a cap on the open interest that markets on it may carry. The cap is set at no more than 1 % of the low end of the rung's classical break cost, so that buying YES and then funding a brute-force attack never pays:

cap(ρ) ≤ 1 % × classical cost(ρ)
Q-160: 1 % × $1 billion ≈ $10 million

Open interest counts only unresolved markets: once a market resolves, its pairs no longer count against the rung's cap, whether or not their holders have redeemed. A rung's cap is set once, when the markets program enables the rung, and can't be changed afterwards. Q-160's default is 10,000,000 USDC; the caps of rungs with a higher classical cost are set at deployment. The quantum-signal market counts against Q-160's cap.

8.7 Sweeping fees

At each redemption, the fee is split into a bounty share and a protocol share, both computed on cumulative totals across the whole protocol:

budget = ⌊(Φ + fee) × 30 ÷ 100⌋ − B protocol = fee − budget

where Φ is the protocol's total fees before this redemption and B the total bounty budget so far. The bounty share therefore rounds down and the protocol share is the remainder, and over many redemptions the split converges to exactly 30 % and 70 %.

ShareBasis pointsSwept byDestination
Protocol7,000sweep_treasuryThe buyback vault, the markets program's own USDC account under ["buyback"]; it buys CANARY and burns it (Section 15.5)
Bounties3,000sweep_bounty, per rungThe rung's bounty vault

Both sweeps are permissionless and independent, and both destinations are derived by the program, never supplied by the caller. The crank calls both. Pending amounts are tracked per market and per rung, so a sweep moves exactly what has accrued since the last one.

8.8 Splitting the bounty share by time-weighted open interest

The 30 % bounty share is split across rungs in proportion to the open interest their markets carried over time. For each rung ρ the markets program maintains an accumulator of open interest multiplied by the slots it was outstanding:

A(ρ) += OI(ρ) × (slot_now − slot_last(ρ)); slot_last(ρ) ← slot_now before every change of open interest
W = Σ A(σ) over enabled rungs σ not flagged no-exit
share(ρ) = ⌊budget × A(ρ) ÷ W⌋, the remainder (fewer than 16 units) to the largest fractional parts

The accumulators are 128-bit and run over each rung's whole lifetime in the markets program. A position opened and closed in the same slot adds nothing to any accumulator, so the split cannot be steered by flash positions. The split routes funds only to bounty vaults from which they can still leave: a rung that reads Solved and has no receipts outstanding is flagged no-exit, its pending share returns to a deferred pool, and allocate_deferred later re-splits that pool across the remaining rungs. If no rung has accumulated weight yet, the whole budget is deferred until one has.

8.9 Reading prices as a date

YES and NO are classic SPL tokens and trade wherever Solana tokens trade; the indexer reads their prices from Jupiter's price API (Section 13.2). A YES price at expiry T is the market's risk-neutral probability that the rung falls by T. Because winners on either side redeem net of the 1 % fee, the cleanest estimate normalizes by both sides' prices:

F(T) = p_YES(T) ÷ (p_YES(T) + p_NO(T))

Across expiries T₁ < T₂ < T₃, F traces a cumulative distribution for the rung's fall date. The chance of a fall within an interval, given none before it, and the corresponding constant hazard rate, are:

h(Tᵢ₋₁, Tᵢ) = (F(Tᵢ) − F(Tᵢ₋₁)) ÷ (1 − F(Tᵢ₋₁))
λᵢ = −ln((1 − F(Tᵢ)) ÷ (1 − F(Tᵢ₋₁))) ÷ (Tᵢ − Tᵢ₋₁)

With illustrative prices for Q-160 of 8.5 %, 14 % and 22 % at the three v0 expiries, measured from October 2026:

IntervalYearsF at endConditional chance hHazard λ per year
Now → 2027-12-311.210.0850.0850.073
2027-12-31 → 2028-12-311.000.1400.0600.062
2028-12-31 → 2030-12-312.000.2200.0930.049

The site's landing page draws these curves for each rung as an hourglass of bars whose widths are the YES prices. Read rung by rung, the curves give a market-implied date for each threshold of quantum capability; read across rungs, they give the market's view of how fast capability climbs the ladder.

8.10 Who can trade

Markets exclude US persons in v0. Eligibility is checked by the API from the deployment's configured source, and the app asks the user to confirm their status where no source is configured. Bounties, Q-Vaults and RungState carry no such restriction.

9. The canary_markets program

canary_markets implements the markets of Section 8. It holds one global configuration, one account per market, each market's collateral and YES and NO mints, and the fee escrow. It reads RungState from the rungs program and never writes to it. This section gives its accounts, its instruction set, the order in which state changes, and the accounting it maintains.

9.1 Accounts and addresses

AccountSeedsSizePurpose
MarketsConfig"config"1,215 bytesCollateral, the CANARY mint and buyback key, fee and buyback totals, and the per-rung weights
Market"mkt", ρ, expiry (i64 LE), kind, collateral mint477 bytesOne market
Market collateral"collateral", marketSPL token accountThe market's USDC, including its fee escrow
YES mint"yes", marketSPL mint, 6 decimalsMint authority: the market
NO mint"no", marketSPL mint, 6 decimalsMint authority: the market
Buyback vaultThe associated USDC account of "buyback"SPL token accountThe protocol share, until a buyback spends it
Buyback CANARY accountThe associated CANARY account of "buyback"SPL token accountThe CANARY a buyback buys, burned in the same transaction

Because the market's address includes the rung, the expiry to the second, the kind and the collateral mint, there is exactly one market for each combination, and anyone can derive it. YES and NO mints have no freeze authority.

9.2 MarketsConfig

FieldMeaning
rungs_programThe rungs program id the markets program reads
collateral_mintUSDC, a classic SPL mint with 6 decimals
administratorThe account that may enable rungs with configure_rung and set the buyback with configure_buyback
canary_mintThe CANARY mint, set once by configure_buyback and never changed
buyback_authorityThe key that may send a buyback; the administrator can replace it
pending_treasury, total_treasury_sweptProtocol share accrued, and swept to the buyback vault, across all markets
total_fees_accruedΦ, cumulative settlement fees across all markets
total_bounty_budgetB, the cumulative bounty share, ⌊Φ × 30 ÷ 100⌋ at the last redemption
total_bounty_sweptBounty share swept to vaults
total_buyback_usdc, total_canary_burnedUSDC spent by buybacks and CANARY burned, cumulative
weights[16]One RungWeight per enabled rung
unallocated_bountyBounty share deferred until a rung can receive it
buyback_*The open buyback, 37 bytes: written by buyback_begin, checked and cleared by buyback_finish in the same transaction

Each RungWeight holds the rung id, an enabled flag, the open-interest cap, the rung's current open interest, its pending bounty share, the amount swept to it so far, the 128-bit accumulator A(ρ) of open interest × slots, the slot of its last update and the no-exit flag. Up to 16 rungs can be enabled.

9.3 Market

A market's first 237 bytes form a discovery header that indexers scan without reading the rest:

Header fieldMeaning
rung_id, kindThe target rung; 0 rung-event, 1 quantum-signal
outcome0 unresolved, 1 YES, 2 NO
expiryUnix time T, at most 9999-12-31T23:59:59Z
no_observation_slotThe slot of the first post-expiry observation with the rung still Open
resolved_slotWhen the market resolved
rung, control_rungThe target RungState, and C-160's for a quantum-signal market
collateral_mint, collateral_vault, yes_mint, no_mintThe market's token accounts
open_interestOutstanding pairs while unresolved; the remaining winning liability after

The body records the market's totals and its share of the fee escrow: total_minted, total_merged, total_redeemed (R), total_fees (F), total_fees_swept, pending_treasury, pending_bounty[16] and unallocated_bounty, with its creator and creation time.

9.4 Instructions

InstructionCallerGatesEffect
initialize_config()Upgrade authority, once6-decimal mint; the configured USDCCreates the configuration
configure_rung(ρ, cap)AdministratorRung initialized with the same collateral; cap > 0; not yet enabled; a free slotEnables ρ with an immutable cap
create_market(ρ, T, kind)Anyonenow < T; ρ Open and enabled; for kind 1, ρ = Q-160 and C-160 OpenCreates the market and its mints
mint_pair(a)Anyone eligibleUnresolved; now ≤ T; ρ Open; OI(ρ) + a ≤ capTakes a USDC, mints a YES and a NO
merge_pair(a)A holderUnresolved; now ≤ T; ρ OpenBurns a YES and a NO, returns a USDC
resolve()AnyoneMarket unresolvedRecords the observation or sets the outcome (Section 8.3)
redeem(a)A holder of the winning sideResolvedBurns a winning tokens, pays a − fee
allocate_deferred()AnyoneRe-splits the market's deferred bounty share
sweep_bounty(ρ)AnyoneMoves the market's pending share for ρ into ρ's bounty vault
sweep_treasury()AnyoneMoves the market's pending protocol share to the buyback vault
sweep_fees(ρ)AnyoneBoth sweeps in one call
configure_buyback(mint, key)AdministratorA classic SPL mint other than USDC; no mint set yet, or the same one; no buyback openSets the CANARY mint for good, and the buyback key
buyback_begin(a, m)The buyback keyCANARY mint set; a, m > 0; a ≤ the vault's USDC; called directly by the transaction; a matching buyback_finish later in itRecords the balances and advances a USDC to the key's USDC account
buyback_finish()The buyback keyThe open buyback's pair; the key holds no more USDC than before; at least m CANARY arrivedBurns all CANARY in the program's account and adds to the totals

There is no instruction that moves collateral anywhere except to a token holder redeeming or merging, to a bounty vault, to the buyback vault, or out of the buyback vault into a swap whose CANARY is burned in the same transaction. No instruction can change a market's outcome once set, its expiry, or a rung's cap.

9.5 A market from creation to redemption

create_marketanyone Unresolved mint_pair: USDC in, YES + NO outmerge_pair: YES + NO in, USDC out resolve YES NO redeemwinner: a − feeloser: nothing Fee escrow in the marketpending buyback · pending bounty per rung · deferred sweep_treasury · sweep_bountypermissionless, sent by the crank Mint and merge close at expiry and when the target rung falls; redemption opens at resolution.
The instructions a market passes through, and where its fees wait.

9.6 Resolution in code order

resolve() reads the market, its target rung and, for a quantum-signal market, its control rung, and applies these rules in order:

  1. If the market is already resolved, do nothing.
  2. If the target rung reads Solved, require event_slot ≤ finalize_slot ≤ event_slot + 300 for it, and for a Solved control rung as well; then:
    • if event_ts > T, resolve NO;
    • else, for a rung-event market, resolve YES;
    • else, for a quantum-signal market, wait until the slot exceeds the target's event_slot + 300; then resolve NO if the control rung is Solved with (event_ts, event_slot) at or before the target's, and YES otherwise.
  3. If the target rung reads Open:
    • if now ≤ T, do nothing;
    • if no observation is recorded, record no_observation_slot = the current slot;
    • if the current slot exceeds no_observation_slot + 300, resolve NO.

On an outcome, the program settles the weight accumulators, subtracts the market's whole open interest from the rung's, and records the outcome, slot and time. From then on the market's open_interest counts the winning tokens still unredeemed.

9.7 Settlement arithmetic

Every redemption updates the market's and the configuration's cumulative totals:

R′ = R + a
fee = ⌊R′ ÷ 100⌋ − F F′ = F + fee
Φ′ = Φ + fee
budget = ⌊Φ′ × 30 ÷ 100⌋ − B B′ = B + budget
protocol = fee − budget
payout = a − fee

Because every quantity is computed from cumulative totals, splitting a redemption into pieces never changes the total fee or the total split. For example, a holder who redeems 1,000,000 units as 999,999 and then 1 pays 9,999 and then 1: a total of 10,000, exactly 1 %, of which 3,000 goes to the bounty share and 7,000 to the buyback vault.

The bounty share of each redemption is credited to rungs by weight at once (Section 8.8): each rung's part goes to the market's pending_bounty for that rung, and any amount that cannot yet be allocated goes to the market's unallocated_bounty. The protocol part goes to pending_treasury.

9.8 Accounting identities

The program maintains these identities at every instruction boundary:

IdentityHolds because
collateral(market) = open interest + fees not yet sweptMint and merge move exactly a USDC for a pairs; a redemption of a pays a − fee and keeps the fee in the market
total_fees = total_fees_swept + pending_treasury + Σ pending_bounty + unallocated_bountyEvery fee unit is credited to exactly one bucket and leaves only by a sweep
Σ OI(markets on ρ, unresolved) = OI(ρ) ≤ cap(ρ)Mint adds, merge subtracts and resolution subtracts each market's open interest once
payout + fee = a, for every redemptionThe fee is taken from the redemption itself

Counters only grow. A sweep moves exactly what its bucket holds and zeroes it, so repeating a sweep moves nothing.

9.9 Weight arithmetic

The accumulator A(ρ) is a 128-bit integer. The sum W over all eligible rungs is computed in a 256-bit accumulator built from four 64-bit words, and each share is computed by bitwise long division:

share(ρ) = ⌊ budget × A(ρ) ÷ W ⌋
leftover = budget − Σ share(ρ) < 16

The leftover, fewer units than there are rungs, goes one unit each to the rungs with the largest remainders, ties broken by the lower rung id, so the split is exact and deterministic. If W is zero, because no rung has accumulated weight yet, the whole budget is deferred to unallocated_bounty and allocate_deferred splits it later. A clock that moves backwards fails the instruction with InvalidWeightClock rather than corrupting an accumulator.

9.10 Measured costs

InstructionCompute unitsTransaction bytes
initialize_config22,520384
configure_rung14,392293
create_market55,529559
mint_pair44,495589
merge_pair44,986589
resolve17,368–17,671349
redeem39,810–40,459490
A crank sweep transaction25,429–28,842415–482

The program emits no events. Its accounts are the record, and the indexer reads them.

10. Q-Vault

10.1 Why a vault, and why on Solana

Every Solana address that is not program-derived is an Ed25519 public key [24]. Every balance held at such an address is behind a public key, exposed the moment Ed25519 can be broken. A program-derived address, by contrast, has no private key: funds held there move only under the rules of the program that derives it [16]. A program can therefore hold funds that no Ed25519 key alone can move and release them under a post-quantum signature.

Q-Vault combines that property with Canary's trigger. Before the trigger rung falls, the vault behaves much like a wallet with a safety delay: the owner's Ed25519 key spends within a budget, and larger spends wait. After the trigger falls, Ed25519 is rejected and only the owner's Falcon-512 key can move funds. The owner never has to notice the event or act in time: the vault reads the rung on every Ed25519 spend.

10.2 Keys and modes

A vault has an owner (an Ed25519 public key, the wallet), a Falcon-512 public key, a trigger rung, a delay, an instant budget and a mode:

ModeEd25519Falcon-512
Ed25519AndPqThe vault's instant asset only: instant spends within the budget; queued spends after the delaySpends any supported asset at any time; cancels queued Ed25519 spends; rotates the key
LockdownRejectedSpends at any time; rotates the key

A vault enters Lockdown in two ways, and never leaves it:

10.3 Falcon-512

Falcon is a lattice-based signature scheme over NTRU lattices, selected by NIST for standardization [35]. Canary uses Falcon-512:

Size
Public key897 bytes
Signature666 bytes (padded encoding)
Verification on Solana365,000–390,000 compute units, measured

Neither a key nor a signature fits beside the rest of a transaction within a legacy 1,232-byte transaction, so both are written to chain in chunks. The public key is stored in the vault's PqKey account. A signature is staged in an authorization account before the instruction that consumes it, so every transaction stays within the legacy size every wallet can sign [22].

10.4 Creating a vault and confirming its key

Creation commits to the hash of the Falcon public key, writes the key, and then proves possession with a Falcon signature over the vault itself:

#TransactionPurpose
1init_vaultCreates the vault: owner, index, trigger rung, delay, instant limit, instant asset, a hash of the Falcon public key, a random non-zero instance salt
2init_pq_keyCreates the PqKey account at epoch 0, committed to the key hash
3–4write_pq_key × 2Writes the 897-byte key in order; the account becomes immutable when complete and must match the committed hash
5init_authorizationCreates the staging account for a signature: the action (here, confirm) and the SHA-256 of the signature
6–7write_authorization × 2Writes the 666-byte signature
8confirm_pq_keyVerifies the Falcon signature over the vault's confirmation digest and marks the key confirmed

The app prepares all eight for one wallet approval. Until the key is confirmed, the vault refuses Falcon actions; possession is proven before any value depends on the key, so a mistyped or corrupted key can't brick the vault. A vault funded before confirmation is flagged by the API.

10.5 The instant budget

The Ed25519 path moves only the vault's one instant asset, chosen at creation (SOL in the app). Its instant spends draw on a budget that limits how much the wallet key can move without waiting:

previous = 0 if spent = 0 or now ≥ last_instant_ts + delay, else spent
accept a ⟺ 0 < a ≤ instant_limit and previous + a ≤ instant_limit
then spent ← previous + a, last_instant_ts ← now

The budget refills only after a full delay passes with no instant spend; a failed call changes nothing. Someone who steals the wallet key can take at most one budget's worth before the owner sees it. A spend within the limit that exceeds what is left must wait for the refill, and any spend larger than the limit must go through the queue, where the Falcon key can veto it.

10.6 The queue

An Ed25519 spend beyond the budget is queued:

queue_ed25519(amount > instant_limit, recipient, destination, asset) → QueuedSpend { unlock_ts = now + delay, status = Queued }
execute_queued: accepted ⟺ now ≥ unlock_ts ∧ status = Queued ∧ mode = Ed25519AndPq ∧ key confirmed ∧ trigger Open

Queuing does not draw on the instant budget, and execute_queued is permissionless: once the delay has passed, anyone can send it, and it moves exactly the destination, mint, amount and fee the queue recorded.

Execution rechecks the mode and the trigger at the moment it runs, so a spend queued before the trigger fell can't execute after. cancel_queued_pq, signed by the Falcon key, sets the status to Cancelled. A queued spend executes at most once.

10.7 Spending with Falcon

A Falcon spend is four transactions: initialize an authorization, write the signature in two chunks, then spend_pq, which verifies the signature against the vault's key and executes the transfer. The signature covers a digest of the exact action:

digest = SHA-256("canary.vault.action.v2" ‖ program id ‖ vault ‖ instance salt ‖ Action)
Action = operation ‖ key epoch ‖ nonce ‖ mint ‖ token program ‖ recipient ‖ destination ‖ amount ‖ queue ‖ new key ‖ new key hash ‖ expected fee

The preimage is 375 bytes. The nonce is consumed by the action, so a signature can be used once; Ed25519 activity uses a separate nonce, so it can never invalidate a pending Falcon veto. The instance salt is random and fixed at creation, so a signature made for a vault on one network, or for a vault since closed and re-created at the same address, is invalid for any other. The SDK's offline signer computes this digest itself from the action's fields and returns those fields alongside the signature, so an offline signer can show exactly what it is signing.

10.8 Rotation

rotate_pq_key replaces the Falcon key. It requires two signatures: the current key authorizes the rotation, and the candidate key proves possession over the vault, so the vault never ends up with a key nobody holds. The key epoch increments with each rotation, which invalidates any signature staged under the old key.

10.9 Assets

AssetDepositsEd25519Falcon
SOLPlain transfer to the vault's SOL accountIf it is the instant assetYes
Classic SPL tokens, USDC includedTransfer to the vault's associated token accountIf it is the instant assetYes
Token-2022 tokensTransfer to the vault's associated token accountIf it is the instant asset and its extensions are supportedWhere its extensions are supported

Token-2022 mints are checked against an extension allowlist: transfer fees, close authority, confidential-transfer configuration, default account state, interest-bearing configuration, permanent delegate, metadata and group pointers, and a transfer hook only when it names no program. A transfer fee must equal the fee the mint charges at the current epoch and is moved with transfer_checked_with_fee; every other mint moves with a zero fee. Any other extension, a non-transferable mint for example, is refused, as is a transfer hook pointing at a program, because executing it would require calling an arbitrary program. The API reports, per asset, whether a vault's balance is spendable.

10.10 What a vault reads

On every Ed25519 instruction, the vault reads its trigger rung's RungState with the full validation of Section 12.2. While a claim on the trigger is in flight, the rung still reads Open and Ed25519 still works; the API reports the in-flight claim on the vault, so an owner can choose to lock down early.

11. The canary_vault program

canary_vault implements Q-Vault (Section 10). It has no administrator and no instruction that can move a vault's funds without its owner's Ed25519 signature on the rung-gated path or its owner's Falcon-512 signature on the post-quantum path. This section gives its accounts, its seventeen instructions, the authorization format, and the transaction sequences of each flow.

11.1 Accounts and addresses

AccountSeedsSizePurpose
Vault"qv", owner, index (u64 LE)332 bytesOwner, trigger, policy, key commitment, nonces, budget
SOL custody"sol", vault0 bytes, System-ownedHolds the vault's SOL
Token custodyThe associated token account of (vault, mint, token program)Token accountHolds each SPL or Token-2022 asset
PqKey"pq", vault, payer, epoch (u64 LE), key hash1,014 bytesOne Falcon-512 public key, written in chunks
PqAuthorization"auth", vault, payer, auth id (u64 LE)1,040 bytesOne staged Falcon signature and the action it signs
QueuedSpend"queue", vault, Ed25519 nonce (u64 LE)227 bytesOne delayed Ed25519 spend

An owner can hold any number of vaults, distinguished by index. The SOL custody account keeps its rent-exempt minimum; only lamports above it are spendable.

11.2 Vault

FieldMeaning
ownerThe Ed25519 key: the owner's wallet
indexDistinguishes the owner's vaults
trigger_rung, trigger_rung_idThe RungState address and id the vault reads
mode0 Ed25519AndPq, 1 Lockdown
lockdown_reason0 none, 1 the owner locked it, 2 the trigger fell
asset_policy1, one instant asset
instant_mint, instant_token_programThe one asset the Ed25519 path can move; the default key with the System program means SOL
instant_limitThe instant budget, in the asset's atomic units
delay_secondsThe queue delay and the budget's quiet period
pq_key_hashSHA-256 of the 897-byte Falcon public key
active_pq_key, key_epoch, key_confirmedThe current PqKey account, its epoch, and whether possession has been proven
nonceThe Falcon nonce, consumed by each Falcon action
ed_nonceThe Ed25519 nonce, the index of the next queued spend
instance_salt32 random non-zero bytes fixed at creation
instant_spent, last_instant_tsThe budget checkpoint
created_ts, created_slot, locked_ts, locked_slotWhen the vault was created and locked

11.3 Instructions

PathInstructionSignerEffect
Lifecycleinit_vaultOwnerCreates the vault and its SOL account; starts in Lockdown if the trigger already fell
lockdownOwnerLocks the vault permanently (reason 1)
tripAnyoneLocks the vault permanently once the trigger reads Solved (reason 2)
Keyinit_pq_key(epoch, key_hash)PayerCreates a PqKey; epoch 0 for the first key, key_epoch + 1 for a candidate
write_pq_key(offset, bytes)The key's payerAppends up to 450 bytes in order; the last chunk must hash to key_hash
confirm_pq_keyAnyoneVerifies a possession proof by the key itself and marks it confirmed
rotate_pq_keyAnyoneSwitches to a confirmed candidate under the active key's signature
reset_initial_pq_key_hashOwnerReplaces the first key's commitment before it is confirmed
close_unconfirmed_pq_keyThe key's payerCloses an unconfirmed key account
Staginginit_authorization(auth_id, action, signature_hash)PayerFixes an action and the hash of its signature
write_authorization(offset, bytes)The payerAppends up to 450 signature bytes; the last chunk must match the hash
close_authorizationThe payerCloses a staging account
Ed25519spend_ed25519(amount, recipient, expected_fee)OwnerAn instant spend within the budget
queue_ed25519(…)OwnerQueues a spend above the instant limit
execute_queuedAnyoneExecutes a queued spend after its delay
Falconspend_pqAnyoneExecutes the staged, Falcon-signed spend
cancel_queued_pqAnyoneCancels a queued spend under a Falcon signature

Instructions marked "Anyone" carry their authority in a staged Falcon signature or in on-chain state, so the transaction itself can be sent by any fee payer. That lets an owner sign offline and have the result relayed, and lets anyone execute a matured queue or trip a vault whose trigger fell.

11.4 Gates on the Ed25519 path

Every Ed25519 instruction, including execute_queued, passes the same gate:

ed_allowed ⟺ trigger is the authenticated RungState ∧ key_confirmed ∧ mode = Ed25519AndPq ∧ trigger reads Open

and moves only the instant asset. The trigger is authenticated as in Section 12.2: owned by the rungs program, at the PDA for its id, with the current layout version, and, if it reads Solved, with a finalize slot inside the claim window. A Solved trigger blocks Ed25519 whether or not trip has been called.

The Falcon path has no mode or trigger gate. After the trigger falls, it is the vault's only way out, and it works the same before and after.

11.5 The authorization format

A Falcon signature signs a 32-byte digest of a 375-byte preimage:

BytesFieldNotes
0–21"canary.vault.action.v2"Domain tag
22–53Program idOne deployment
54–85Vault addressOne vault
86–117Instance saltOne instance of that address
118Operation0 confirm, 1 spend, 2 cancel, 3 rotate
119–126Key epochThe signing key's epoch
127–134NonceMust equal the vault's Falcon nonce
135–166MintSpend: the asset; SOL is the default key
167–198Token programSpend: System, Token or Token-2022
199–230RecipientSpend: the SOL recipient or the token account's owner
231–262DestinationSpend: the account credited
263–270AmountSpend: gross atomic units
271–302QueueCancel: the QueuedSpend
303–334New keyConfirm or rotate: the PqKey account
335–366New key hashConfirm or rotate: its SHA-256
367–374Expected feeSpend: the Token-2022 transfer fee, else 0
digest = SHA-256( preimage ) integers little-endian, unused fields zero

Every field an operation does not use must be zero, and the program checks the operation's fields against the accounts the instruction actually touches: a spend's destination, mint and token program must be the accounts passed in, a cancel's queue must be the queue being cancelled, and a confirmation's new key must be the key being confirmed. A signature therefore authorizes exactly one action, once.

11.6 Staging

A Falcon signature is 666 bytes and a public key 897 bytes; neither fits in a transaction beside its instruction. Both are staged:

  1. init_authorization fixes the action and the SHA-256 of the signature that will be written. It requires the action's nonce to equal the vault's current nonce.
  2. write_authorization appends the signature in order: 450 bytes, then 216. The final chunk must make the staged bytes hash to the committed value.
  3. The consuming instruction rechecks everything: not consumed, all 666 bytes written, the hash, the nonce, the key epoch, the key complete and matching its commitment, and the Falcon verification itself. It then marks the authorization consumed and increments the nonce.

Staging reserves nothing. Anyone may stage the same signed action under another payer or authorization id; only one can be consumed, because consumption increments the nonce. A staging account that is no longer needed can be closed by its payer to recover its rent; closing one does not restore a nonce.

11.7 Moving assets

The vault calls exactly three kinds of instruction in other programs:

CallProgramSigned by
transferSystemThe SOL custody PDA
transfer_checkedToken or Token-2022The vault PDA
transfer_checked_with_feeToken-2022The vault PDA

The source is always the vault's own custody account, checked by address. The destination's mint and owner are checked against the action. No remaining accounts are forwarded and no other program is invoked, so a token's extensions cannot route a transfer through code the vault did not choose.

11.8 Flows in transactions

FlowTransactionsInstructions
Create a vault and register its key8init_vault, init_pq_key, write_pq_key × 2, init_authorization, write_authorization × 2, confirm_pq_key
Deposit1A plain SOL or token transfer to the custody account
Instant Ed25519 spend1spend_ed25519
Queue an Ed25519 spend1queue_ed25519
Execute it after the delay1execute_queued
Falcon spend4init_authorization, write_authorization × 2, spend_pq
Falcon veto of a queued spend4init_authorization, write_authorization × 2, cancel_queued_pq
Stage and confirm a candidate key7init_pq_key, write_pq_key × 2, init_authorization, write_authorization × 2, confirm_pq_key
Rotate to it4init_authorization, write_authorization × 2, rotate_pq_key
Lock down1lockdown
Trip1trip

The client prepares each flow as an ordered list of transactions for one wallet approval. Every stage carries a distinct memo, so a wallet that signs all at once cannot deduplicate two stages with identical instructions.

11.9 Measured costs

InstructionCompute unitsTransaction bytes
init_vault21,497502
init_pq_key10,085357
write_pq_key (450 / 447 bytes)2,183 / 2,761708 / 705
init_authorization11,506–19,006615
write_authorization (450 / 216 bytes)3,154 / 3,617708 / 474
confirm_pq_key (Falcon verification)364,041–374,332316
spend_ed2551912,717433
queue_ed2551914,585–16,085503
execute_queued12,806417
spend_pq (Falcon verification)368,736–389,390417–481
cancel_queued_pq (Falcon verification)365,428–365,478349
trip7,029283

The largest vault transaction is 708 bytes, a full signature or key chunk; the Falcon verification dominates compute and stays well within the 1.4M limit.

12. Security design

Canary's programs move real money on a signal that, by construction, fires rarely and all at once. The security design follows from that: every reader of a rung must read the real rung, every payout must follow from verified state, and every path out of a vault must be bound to exactly the action its owner authorized. This section collects the properties the programs enforce and how they were checked.

12.1 Principles

PrincipleIn practice
Read the real accountEvery cross-program read checks owner, program-derived address, discriminator and layout version, and fails closed
One source of truthA market's outcome and a vault's mode follow from RungState and the clock only
State moves one wayOpen → Solved, unresolved → resolved, Ed25519-and-PQ → lockdown: none of these transitions can be reversed
Exact arithmeticChecked integer arithmetic in atomic units; the protocol's share of any split rounds down
No arbitrary callsVaults invoke only the System, Token and Token-2022 programs
Bound every signature to its actionSchnorr challenges commit to the recipient; Falcon digests commit to the program, the vault instance, the operation, a nonce and every field of the action
Minimal, immutable trigger codeThe rungs program's upgrade authority is revoked after audit

12.2 Reading RungState

Both the markets program and the vault program read rungs through a typed account constraint that requires:

  1. the account's owner to be the canary_rungs program;
  2. its address to be the program-derived address for the rung id, derived under the rungs program's id;
  3. its discriminator to be RungState's;
  4. its layout version to be the current one.

A rung that fails any check is not treated as Open or as Solved: the instruction fails. For the vault this fail-closed rule means a malformed trigger read blocks Ed25519 spending rather than allowing it. For markets it means no spoofed account can resolve YES. The markets program additionally requires that a Solved rung's claim finalized within the claim window measured from its event slot, so it does not rely on the rungs program alone for that bound.

12.3 The claim

PropertyHow it holds
The discrete logarithm is never revealedThe claim is a Schnorr proof; only R and s go on chain
A claim can't be retargetedThe challenge commits to the recipient; a different recipient needs a new s, which needs k
A proof can't be replayed on another rung or deploymentThe challenge commits to the domain tag canary.claim.v1, the program id, the rung id, G, Q and the rung's transcript hash
A proof can't be replayed for a second payoutA rung pays once: finalize sets Solved and a Solved rung accepts no claim
Verification can't be left half done to block othersEach claim job is its own account keyed by recipient and nonce; a stale job expires after the claim window and does not block a new one
Event time can't be backdatedevent_ts is the clock at begin_claim, recorded by the program, not supplied by the solver

For the terminal rungs, verification uses Solana's own curve syscalls (sol_secp256k1_recover and sol_curve_multiscalar_mul), which avoids instruction-introspection patterns and their offset-validation pitfalls.

12.4 Custody and accounting in markets

12.5 Bounty vaults

Deposits are irrevocable until the sunset; there is no instruction that withdraws before it. The payout on a fall is computed and transferred in the same instruction that sets the rung Solved, to the recipient the verified proof names. After the sunset, a receipt reclaims exactly its share-unit fraction of the vault, and receipts are Token-2022 NonTransferable, so a reclaim right can't be traded away from the sponsor who funded it.

12.6 Q-Vault

PropertyHow it holds
A stolen Ed25519 key can't drain the vaultInstant spends draw on a budget that refills only after a full delay passes without an instant spend; anything larger is queued for the delay, during which the Falcon key can veto it
The trigger closes Ed25519 permanentlyEvery Ed25519 path checks the live trigger rung; trip persists lockdown, and lockdown has no inverse
A queued spend executes at most onceIts account records status; execution rechecks mode and trigger after the delay
Falcon signatures can't be replayed or retargetedThe digest binds program id, vault, operation, key epoch, nonce, asset, recipient, amount, queue or new key, and a per-vault random instance salt fixed at creation; nonces are consumed
A Falcon signature can't be blocked mid-stagingA staged authorization commits to the signature's hash at creation and checks it on the final chunk
A wrong key can't brick the vaultPossession is proven by a Falcon signature over the vault before the key is confirmed; rotation needs both the current key's signature and the new key's proof of possession
Key bytes can't be alteredKey chunks are written in order, become immutable when complete, and are hash-checked against the commitment
Rent can't be spentThe SOL custody account keeps its rent-exempt reserve
Funds can only leave from the vault's own accountsSources are the vault's own token accounts, checked by address and owner
Tokens with unsafe extensions can't trap or redirectMint extensions are checked against a policy: extensions that need no arbitrary call are supported, transfer fees are handled exactly, and a transfer hook with a non-null program is refused

12.7 The Falcon-512 verifier

The vault verifies Falcon-512 with an SBF implementation (Blueshift's solana-falcon512, version 0.1.2, MIT licence) over raw 897-byte public keys. It rejects malformed key and signature headers (the padded signature header is 0x39), coefficients out of range, the encoding of negative zero and non-zero padding. It hashes to the lattice with SHAKE256 over the nonce and message and applies the standard norm bound of 34,034,726. Verification costs 365,000 to 390,000 compute units as measured, within the 1.4M per-transaction limit. Offline signing in the SDK uses @noble/post-quantum's padded Falcon-512.

12.8 Off-chain components

12.9 How the code was checked

Each program was reviewed independently after it was built, and every finding of medium severity or above was fixed with a test that failed before the fix:

ProgramReview outcome
canary_rungsAccount validation, claim binding and payout paths reviewed; findings fixed
canary_marketsNo critical or high findings; fee-avoidance, split-steering, cap-erosion and sweep-coupling findings fixed
canary_vaultOne high finding (an unbounded instant limit) fixed with the delay-gated budget; queue visibility, indexer isolation, Token-2022 handling, signature binding and staging findings fixed

A reference verifier in a second language checks the same proofs off chain, and the parameter generator's transcript is reproducible bit for bit. Before mainnet, the rungs program receives two external audits and the markets and vault programs one each, and the Q-rung verifier gains a second, independently written implementation that must agree with the first.

13. Off-chain infrastructure

Everything that decides an outcome or moves money runs on chain. The off-chain services exist to make that state easy to read and easy to act on: an indexer that mirrors the programs' accounts into a database, an API that serves them with an honest account of their freshness, a relay that lets browsers reach Solana without exposing an RPC key, a crank that sends the permissionless instructions nobody else has an incentive to send, and the web app. None of them holds a key that can move user funds, and any of them can be replaced by anyone who reads the chain directly.

13.1 Components

ComponentRunsReadsWritesHolds
The three programsSolanaAll protocol stateNothing off chain
IndexerA long-running workerSolana RPC, finalized; Jupiter's price APIPostgresNothing secret beyond the RPC URL
PostgresA managed databaseIndexed state, prices, checkpoints
APIAn HTTP servicePostgres, Solana RPC for readinessNothing on chainThe RPC URL, which never reaches a client
RPC relayInside the API, at /v1/rpcSolana RPCRelays signed transactions onlyNo signing key
CrankA long-running workerSolana RPC; Jupiter's swap APIPermissionless transactions and buybacksA fee-paying key, which is also the buyback key
Web appA Next.js serviceThe API and the relayTransactions the user's wallet signsNo key
Browserweb app + user's wallet API/v1 · /health RPC relay /v1/rpc Postgresindexed state Indexerfinalized reads Crankfee payer only SolanaRPC providerthree programs
Services and the direction of their reads and writes.

13.2 The indexer

The indexer runs a loop. Each pass reads the chain at finalized commitment, validates what it reads, and writes one consistent snapshot to Postgres inside a transaction that holds an advisory lock, so two indexers never interleave. A pass begins by reading each program's latest transaction signature and the Clock; only a program whose signature has changed is scanned again, so a quiet chain costs a few reads per pass.

ReadHow
The chain's identityGenesis hash, compared with the deployment's
RungsgetProgramAccounts filtered by discriminator for RungState, SponsorPosition and Verified or Finalized VerifyJob accounts
BalancesBatched getMultipleAccounts of at most 100 keys: the Clock sysvar, the USDC mint, bounty vaults, receipt mints and sponsors' receipt accounts
Program authorityEach program's ProgramData account
MarketsA scan of the 237-byte market headers, then bodies for 32 markets per pass (the 16 largest and 16 in rotation) and up to 512 holders per side
VaultsVault accounts and active QueuedSpend accounts, in pages of 32 vaults
PricesJupiter's price API, at most once a minute: the YES and NO mints of up to 24 markets in rotation, and USDC to convert them
Transaction signaturesBest effort: up to 64 recent signatures per account, at most 8 candidates, within 1.5 seconds

Every rung is checked against the published manifest and its PDA before it is stored. Two safety rules hold across passes: a rung recorded as Solved is never recorded as Open again, and two different Solved records for the same rung stop the indexer rather than overwrite either.

The poll interval is 10 seconds in production (2 seconds on a local validator), configurable up to 60 seconds. After a failure the delay doubles, up to 30 seconds by default. u64 values are stored as Decimal(20,0) so no amount is ever rounded through a floating-point type.

Table groupTables
Indexer stateIndexerCheckpoint, ProgramDeployment, Asset
RungsRung, GenerationTranscript, RungEvent, BountyBalance, SponsorReceipt, VerifyJob
MarketsMarket, MarketProtocol, MarketPrice, PositionBalance
VaultsQVault, PqKey, VaultBalance, QueuedSpend
Fee sweepsFeeSweep, FeeSweepAllocation

13.3 The API

The API serves JSON over HTTPS under /v1. Every response is an envelope {data, meta}; lists are pages {items, nextCursor} with a limit from 1 to 100 (default 50). Section 14.3 and the developer documentation list the routes; in summary:

GroupRoutes
Configuration/v1/app-config, /v1/programs, /v1/eligibility
Rungs and bounties/v1/rungs, /v1/rungs/{rungId}, /v1/rungs/{rungId}/events, /v1/rungs/{rungId}/claim-jobs, /v1/claim-jobs/{jobId}, /v1/bounties, /v1/transcripts/{hash}
Markets/v1/markets, /v1/markets/{marketId}, /v1/markets/{marketId}/prices, /v1/q-date, /v1/totals
Wallets and vaults/v1/wallets/{wallet}/receipts, /v1/wallets/{wallet}/positions, /v1/wallets/{wallet}/vaults, /v1/vaults/{vaultId}, /v1/vaults/{vaultId}/queued-spends
Feed and health/v1/status-feed, /v1/landing, /health/live, /health/ready
RelayPOST /v1/rpc

Responses are built from snapshots cached for one second with single-flight loading, so a burst of identical requests costs one database read. Each client address may make 1,200 requests per minute by default. Errors carry a stable code: VALIDATION_FAILED, NOT_FOUND, UPSTREAM_UNAVAILABLE, RATE_LIMITED, CONFIGURATION_REQUIRED, FORBIDDEN or INTERNAL_ERROR, with a request id and whether a retry may succeed.

Waiting and ready. In production the API checks, every ten seconds, that the chain it is pointed at matches the configured genesis hash, that the three programs are deployed and executable, that the USDC mint exists with 6 decimals, and that every rung in the published manifest is initialized with its transcript hash. Until all hold, /v1/app-config and /health/ready report state: "waiting" with the reasons, and the app shows the service as not yet open; once they hold, the state becomes ready without a restart. /health/ready answers 200 in both states, so a platform health check does not cycle the service while a deployment is still being initialized.

13.4 Freshness

Every snapshot reports how current it is:

FieldMeaning
commitmentfinalized
indexedSlot, indexedAtThe slot and time of the snapshot
chainHeadSlot, lagSlotsThe chain's latest slot and the distance to it
chainTimeThe Clock sysvar's time at the snapshot
rpcReachable, lastSuccessfulRpcAtWhether the last pass reached the RPC
statefresh, indexer-behind or rpc-unreachable

The state is rpc-unreachable when the last pass could not reach the chain, indexer-behind when the snapshot is older than 60 seconds or more than 150 slots behind, and fresh otherwise. If the database is unavailable, the API serves its last snapshot marked indexer-behind rather than nothing. Each vault carries its own indexed slot, so one unreadable vault does not change the freshness reported for the others.

13.5 The RPC relay

Browsers need an RPC endpoint to read accounts, fetch a blockhash and submit signed transactions. Production RPC endpoints carry an API key that must not be published. The relay at POST /v1/rpc forwards a fixed set of JSON-RPC methods to the deployment's RPC provider and returns the result, so the browser never sees the provider's URL.

LimitValue
MethodsgetVersion, getGenesisHash, getSlot, getBlockHeight, getLatestBlockhash, isBlockhashValid, getEpochInfo, getAccountInfo, getMultipleAccounts, getBalance, getTokenAccountBalance, getTokenAccountsByOwner, getTokenSupply, getSignatureStatuses, getTransaction, getMinimumBalanceForRentExemption, getFeeForMessage, simulateTransaction, sendTransaction, getSignaturesForAddress
Request bodyAt most 65,536 bytes; a batch of 1 to 5 requests
AddressesMust be valid public keys; getMultipleAccounts at most 100
SignaturesgetSignatureStatuses at most 32; getSignaturesForAddress limit 1 to 20
TransactionsBase64, 65 to 1,232 bytes, must deserialize as a Solana transaction
Rate600 requests and 60 transactions per client per minute by default

The relay has no signing key. It can only forward transactions a wallet has already signed, and it validates their size and shape before forwarding. A client that prefers its own RPC can use one; nothing in the protocol depends on the relay.

13.6 The crank

Several instructions exist to be sent by anyone: resolve, sweep_bounty, sweep_treasury and allocate_deferred. The crank sends them so that markets resolve and fees reach the bounty vaults and the buyback vault without anyone having to remember to. It also sends the buyback.

ActionWhen the crank sends it
resolveA market's target rung is Solved (a quantum-signal market waits 300 slots after the event); or the expiry has passed and the observation is unset or more than 300 slots old
sweep_bountyA market holds a pending bounty share for a rung
sweep_treasuryA market holds a pending protocol share
allocate_deferredA market holds deferred bounty share and some rung now has weight
A buybackThe buyback vault holds at least 100 USDC: one transaction of buyback_begin, a Jupiter swap of the whole balance quoted at 1 % slippage, and buyback_finish

Each pass plans at most 64 actions with a rotating cursor, so no market starves; sends them in batches of at most 4 instructions with at most 3 in flight; and polls every 10 seconds, backing off to 30 seconds on failure. The crank's key pays transaction fees and signs buybacks as the configured buyback key. Every other instruction it sends is permissionless and idempotent, so a duplicate, a crash or a second crank run by someone else never changes an outcome; a buyback that fails, or finds no route, leaves the USDC in the vault for a later pass. Claim steps and trip are left to the clients that need them: a solver's client drives its own claim, and anyone can trip a vault whose trigger has fallen.

13.7 The status feed

GET /v1/status-feed serves a compact, unwrapped document for integrators that poll:

{ schemaVersion: "canary.status.v1", contractVersion, mode, network, generatedAt, freshness, programId, rungs: [{ id, account, state, transcriptHash }] }

Each rung's state is either {status: "Open"} or {status: "Solved", eventTs, eventSlot, recipient, proofHash, finalizedSlot, finalizedAt, transactionSignature}. generatedAt is the time of the indexed snapshot, not of the request. Appendix E gives the full schema. The feed is a convenience; a program or a cautious integrator reads RungState directly (Section 14).

13.8 The app configuration

GET /v1/app-config tells the web app and any other client which deployment it is talking to:

FieldProduction value
schemaVersioncanary.app-config.v1
appEnv, state, waitingReasonsproduction, waiting or ready, and why it is waiting
cluster, genesisHash, commitmentThe Solana cluster, its genesis hash, finalized
rpcUrlThe API's relay, {API}/v1/rpc
programIdsThe three program ids
collateralThe USDC mint, 6 decimals
rungsPer rung: id, label, state address, transcript hash, the public parameters a claim needs, and whether it is claimable now
faucetDisabled

13.9 Market eligibility

Markets exclude US persons in v0. The API's /v1/eligibility answers from the deployment's location source: either a country header set by a trusted proxy and authenticated with a shared token, or the visitor's own attestation of their country. The web app asks before showing market actions. This is an interface policy; the markets program itself has no notion of location.

13.10 Hosting

The reference deployment runs on Railway as five services in one project:

ServiceStartHealthNotes
webnext start on the injected port/Static pages and the app; reads the API's public origin at build
apiMigrations, then the API server/health/livePublic; CORS limited to the web origin
indexerMigrations, then the indexer loopPrivate
crankMigrations, then the crank loopPrivate; holds the fee-paying key
PostgresManagedPrivate; referenced by the other services

Schema migrations run when a service starts, never during the build, because a build cannot reach the private network the database lives on. Every service reads its port from the platform, and cross-service values such as the database URL are platform references rather than copied secrets. Section 16 and the deployment guide give the exact settings.

14. Integration

Canary's output is one account per rung. A program that wants to react to a fall reads that account in its own instruction; a service that wants to watch the ladder reads it over RPC or through the API. This section shows all three, and the rules that make a read safe.

14.1 What to read

QuestionRead
Has the quantum signal fired?Q-160's RungState.status
Did it fire for quantum reasons?The quantum-signal market's outcome, once C-160 is on the ladder (Section 8.4)
How far up the ladder has it reached?The highest Solved rung among Q-160, Q-192 and Q-224
Can secp256k1 or Ed25519 keys be taken?T-256k's or T-255e's status
When did it happen?event_ts and event_slot of the Solved rung
What does the market expect?YES prices across expiries: /v1/q-date (Section 8.9)

The status says whether; event_ts says when. A reader that only needs a switch reads status. A reader that settles anything against a date, as the markets program does, also reads event_ts and checks the claim window.

14.2 The rules of a safe read

  1. Owner. The account must be owned by the deployment's canary_rungs program id.
  2. Address. It must be the PDA of ["rung", ρ] under that program, for the rung id you mean.
  3. Type. Its first 8 bytes must be RungState's discriminator, 67 56 8b 18 51 ff d0 fa.
  4. Version. Byte 8 must be 2, and byte 9 must be ρ.
  5. Window. If Solved, event_slot ≤ finalize_slot ≤ event_slot + 300.
  6. Fail closed. If any check fails, treat the read as an error, not as Open and not as Solved.

Rules 1 and 2 together rule out every spoofed account: only the rungs program can create an account at its PDA. Rule 6 is what makes the read safe in both directions. A vault that treated a malformed account as Open would let a stolen key spend; a market that treated it as Solved would pay the wrong side.

14.3 From a Solana program

The markets and vault programs read rungs through Anchor's typed account constraint, using the canary-rungs crate with its cpi feature:

#[derive(Accounts)]
#[instruction(rung_id: u8)]
pub struct WatchRung<'info> {
    #[account(
        seeds = [b"rung", &[rung_id]],
        bump = rung.bump,
        seeds::program = canary_rungs::ID,
    )]
    pub rung: Account<'info, canary_rungs::RungState>,
}

Account<RungState> checks the owner and the discriminator; the seeds constraint checks the address. The handler then checks the version, the id and, when Solved, the window. Without Anchor, the same read is a few lines over the raw account:

pub enum Rung { Open, Solved { event_ts: i64, event_slot: u64 } }

const RUNG_STATE: [u8; 8] = [0x67, 0x56, 0x8b, 0x18, 0x51, 0xff, 0xd0, 0xfa];

pub fn read_rung(account: &AccountInfo, rungs_program: &Pubkey, rung_id: u8) -> Result<Rung, ProgramError> {
    let (address, _) = Pubkey::find_program_address(&[b"rung", &[rung_id]], rungs_program);
    if account.key != &address || account.owner != rungs_program {
        return Err(ProgramError::InvalidAccountData);
    }
    let d = account.try_borrow_data()?;
    if d.len() != 271 || d[..8] != RUNG_STATE || d[8] != 2 || d[9] != rung_id {
        return Err(ProgramError::InvalidAccountData);
    }
    let u64_at = |o: usize| u64::from_le_bytes(d[o..o + 8].try_into().unwrap());
    match d[11] {
        0 => Ok(Rung::Open),
        1 => {
            let (event_slot, finalize_slot) = (u64_at(151), u64_at(263));
            if finalize_slot < event_slot || finalize_slot > event_slot + 300 {
                return Err(ProgramError::InvalidAccountData);
            }
            Ok(Rung::Solved { event_ts: u64_at(143) as i64, event_slot })
        }
        _ => Err(ProgramError::InvalidAccountData),
    }
}

Most of a read's cost is the address derivation; storing the bump (byte 12) and using create_program_address makes it cheaper. The account is passed read-only, so any number of programs can read the same rung in the same slot without contention.

14.4 Over RPC

import { Connection, PublicKey } from '@solana/web3.js'

const RUNG_STATE = Buffer.from('67568b1851ffd0fa', 'hex')

export async function readRung(connection: Connection, rungsProgram: PublicKey, id: number) {
  const [address] = PublicKey.findProgramAddressSync([Buffer.from('rung'), Buffer.from([id])], rungsProgram)
  const account = await connection.getAccountInfo(address, 'finalized')
  if (!account || !account.owner.equals(rungsProgram)) throw new Error('Not a rung')
  const d = account.data
  if (d.length !== 271 || !d.subarray(0, 8).equals(RUNG_STATE) || d[8] !== 2 || d[9] !== id) throw new Error('Unexpected layout')
  if (d[11] === 0) return { status: 'Open' as const }
  const eventSlot = d.readBigUInt64LE(151)
  const finalizeSlot = d.readBigUInt64LE(263)
  if (finalizeSlot < eventSlot || finalizeSlot > eventSlot + 300n) throw new Error('Outside the claim window')
  return {
    status: 'Solved' as const,
    eventTs: new Date(Number(d.readBigInt64LE(143)) * 1000),
    eventSlot,
    recipient: new PublicKey(d.subarray(159, 191)).toBase58(),
  }
}

Read at finalized commitment. A Solved status at confirmed can, in principle, be rolled back with its fork; at finalized it cannot. To watch rather than poll, subscribe to the six rung addresses with accountSubscribe; the accounts change only when a rung is funded, claimed or reclaimed from.

14.5 Through the API

For dashboards, alerts and services that do not want to run RPC infrastructure, the status feed returns every rung in one request, with its freshness:

curl -s "$CANARY_API/v1/status-feed"

where $CANARY_API is the deployment's API origin. Check freshness.state before acting: fresh means the snapshot is at most 60 seconds and 150 slots old. Each rung's account field is its RungState address, so a client can confirm anything important against the chain with the read in Section 14.4.

14.6 Client libraries

The web app is built on TypeScript clients that prepare transactions without signing or sending them; a wallet signs, and the caller sends. They are part of the Canary source and usable by any integrator:

ClientPrepares
RungsClientFunding a bounty and a claim from a secret scalar, which never leaves the caller; decoding rung, sponsor and job accounts
MarketsClientMinting, merging and redeeming pairs; decoding markets
VaultTransactionClientCreating a vault and registering its key, deposits, Ed25519 spends and queues, executing queues, Falcon spends and vetoes, tripping
Falcon SDKKey generation, the vault action digest and offline signing, with export and import of the secret key file
createCanaryClientTyped calls to the API's routes

Every prepared transaction carries an explicit compute limit, the wallet as fee payer, a recent blockhash and a distinct memo, and is at most 1,082 bytes so a wallet can add its own instructions within the 1,232-byte limit.

14.7 Patterns

IntegratorReadsReaction
A bridge or a lending protocolQ-160 or Q-192 in its own instructionPause withdrawals that rely on elliptic-curve signatures, or route them through a delay
A treasuryIts Q-Vault, which reads its chosen triggerEd25519 stops; the Falcon key moves the funds
A custodianThe status feed, then RungStateStart the planned key migration with the lead time the ladder gives (Section 3.6)
A fund or an insurerYES and NO tokens, which are plain SPL tokensHold, lend or price them like any other token; they redeem against the same rung
A multisigA trigger rung, through a voter that reads it (Section 17.3)Require a post-quantum approval after the fall

Each of these needs no permission from Canary and no account with it: the rung is a public account, and reading it is free.

15. Economics

15.1 Fee lines

LineRatePaid byNote
Settlement fee1.0 % of winning redemptionsThe winning side, at redemptionIn years without an event, mostly NO holders
Minting and merging pairsNone
Sponsorship0 % protocol cutEvery unit funds the bounty
Q-VaultFreeNo deposit, spend or assets-under-management fee
Status feedFreeThe public feed and API
Collateral yield skim (v1)10 % of yield on collateralWith yield-bearing collateral; winners receive principal plus the remaining yield
Status attestation (planned)Annual contractCustodians, fund issuersA service-level API and periodic monitoring attestations

15.2 Where fees go

Every settlement fee is swept 70 % to the buyback vault and 30 % to the bounty vaults, split across rungs by time-weighted open interest (Section 8.8). The buyback vault's USDC buys CANARY on the market and burns it (Section 15.5). A rung that the market hedges against grows its own bounty in proportion to how long and how heavily it is hedged, which strengthens the incentive to claim exactly where the demand for the signal is.

Winning redemptionsgross g Fee escrow⌊g × 1 %⌋ Buy and burn CANARY70 %, buyback vault Bounty vaults30 %, by OI × slots Q-160 · Q-192T-256k · …each rung's share
Settlement fees, from redemption to the CANARY burn and the bounty vaults.

15.3 Fee flow at three levels of open interest

The table assumes open interest turns over once a year at expiry and, for the v1 skim, a 4 % yield on collateral. These are assumptions for scale, not forecasts.

Open interestSettlement feesTo the CANARY burn (70 %)To bounties (30 %)v1 yield skimTotal per year
$5M$50k$35k$15k$20k~$70k
$50M$500k$350k$150k$200k~$0.7M
$500M$5M$3.5M$1.5M$2M~$7M

15.4 The bounty layer is a public good

Bounties are funded by sponsors and by the 30 % fee share. Sponsors — exchanges, custodians, treasury companies and foundations — receive what a public good can offer: a named, verifiable rung, early warning, and a Token-2022 NonTransferable receipt that records their contribution and carries their share of any unclaimed bounty after 2040-01-01. Because sponsorship carries no protocol cut, every sponsored unit is visibly at work in the bounty.

15.5 The CANARY token

CANARY is Canary's token. Its design follows from the same requirement as everything else in the protocol: the trigger must stay credibly neutral, so the token sits entirely on the revenue side.

Supply1,000,000,000, fixed; no mint authority remains after launch
Launch100 % to one public launch on equal terms
AllocationsNone: no presale, team, investor, airdrop or reserved pools, and no vesting
BuybackThe protocol's revenue buys CANARY on the market: the protocol's 70 % of every settlement fee, and in v1 the yield skim
BurnEvery CANARY bought is burned, so supply only falls

The buyback runs on the USDC in the buyback vault. sweep_treasury moves each market's pending protocol share there (Section 8.7). Whenever the vault holds at least 100 USDC, the crank sends one transaction that advances the whole balance with buyback_begin, swaps it for CANARY through Jupiter into the program's own CANARY account, and ends with buyback_finish, which requires every unit spent and at least the CANARY quoted at 1 % slippage, then burns it all (Section 9.4). Each burn shows on chain as a lower supply of the mint, and MarketsConfig counts the USDC spent and the CANARY burned. With fee F per settlement, the amount burned is

burned = CANARY bought with 0.70 × F, F = ⌊1 % × winning redemptions⌋

so the burn tracks settlement, not trading: minting, merging, sponsorship and Q-Vaults carry no fee. At the open-interest levels of Section 15.3, the protocol share is $35k, $350k and $3.5M a year.

CANARY has no role in the trigger or in any payout:

CANARY is neverWhy
CollateralA rung falling would most likely move crypto prices exactly when payouts come due; a crypto-native asset as collateral would lose value at the moment it is needed. Markets and bounties settle in USDC, and SOL, staked SOL and BTC are excluded for the same reason
A resolution inputMarkets resolve from RungState and the clock alone
A vote on rung parameters, resolution or vault rulesThe rungs program is immutable after audit; markets and vault move to a timelocked multisig and then freeze
A payout backstopBounties pay from their vaults and markets from their own collateral

The other functions a token is sometimes given are already filled without one:

FunctionFilled by
Funding bountiesSponsor deposits plus 30 % of settlement fees
UnderwritingNO holders, directly, in USDC
Recognizing sponsorsToken-2022 NonTransferable receipts carrying the post-sunset reclaim right
Incentivizing liquidityMarket-maker fee rebates in USDC

15.6 Market scale

AnchorFigureSource
BTC behind exposed keys~6M BTC (~30 %, ~$400bn) to ~6.9M BTC[3][4]
Value secured by elliptic-curve cryptography in digital assets> $2.5T[5]
Collateralized tail-risk capital (cat bonds and ILS)$64.7B outstanding[30]
On-chain cover precedentNexus Mutual: $7.00B protected, 13,000 covers since 2019[29]

The first market Canary serves directly is Solana-native: DAOs, foundations and funds holding Ed25519-exposed balances, who can both hedge with rung-event markets and move funds into Q-Vaults, and traders expressing a view on the date curve. Bitcoin-side proposals can reference RungState off chain, since rung state is public.

16. Upgrade authority and deployment

A Solana program is deployed with an upgrade authority, a key that can replace its code [25]. Canary's design goal of a credibly neutral trigger (Section 1.4) means that authority must end where it matters most. This section describes the authority each program has, how it is retired, how a deployment is built and initialized, and how anyone can check a deployment against its sources.

16.1 Authority in each program

ProgramWhat its upgrade authority can do in the programInstructions that need itPlan
canary_rungsInitialize a runginitialize_rungRevoked after audit; the rungs program becomes immutable
canary_marketsCreate the configuration; the configuration's administrator enables rungs with their caps, sets the CANARY mint once and names the buyback keyinitialize_config, configure_rung, configure_buybackA 3-of-5 multisig with a 7-day timelock; revoked in v1
canary_vaultNothing: the program has no privileged instructionnoneThe same multisig policy; later a Falcon-gated multisig voter

No instruction in any program lets an authority or administrator move user funds, change a rung's parameters, change an outcome, change a cap once set, or alter a vault. The authority's only other power is the platform's own: deploying new program code. That is why the rungs program's authority is revoked, and why every program's current authority is public.

16.2 Revocation and what it fixes

Revoking the rungs program's upgrade authority sets its ProgramData authority to none. From then on:

The indexer reads each program's ProgramData account on every pass, and /v1/programs reports whether each authority is revoked or still held. A revocation is a single transaction that anyone can see on chain.

16.3 Building a deployment

A production build compiles the deployment's identity into the programs:

Build inputWhere it ends up
The three program idsEach program's declare_id, and the rungs program id inside the markets and vault programs
The USDC mintRequired by initialize_rung and initialize_config
The canonical parameter suiteThe rungs program's curve constants and transcript hashes

The parameter suite is generated from the published seed (Section 4.8) as part of the build. The build refuses test rungs and any parameter source containing test material, so a production program cannot be built with a rung whose logarithm is known.

16.4 Initializing it

After the three programs are deployed, a one-time initialization brings the ladder up:

#StepInstructionSigner
1Create each rung in the suiteinitialize_rung(ρ)Rungs upgrade authority
2Create the markets configurationinitialize_configMarkets upgrade authority
3Create the buyback vault and the buyback CANARY accountAssociated token accounts of ["buyback"]Any payer
4Set the CANARY mint and the buyback keyconfigure_buyback(mint, key)Markets administrator
5Enable Q-160, Q-192 and T-256k with their capsconfigure_rung(ρ, cap)Markets administrator
6Create the rung-event markets at the three v0 expiriescreate_market(ρ, T, 0)Any payer

Q-160's cap defaults to 10,000,000 USDC; the Q-192 and T-256k caps are set explicitly at this step, and none can change afterwards. The deployment scripts are idempotent: each step checks whether its account already exists, so an interrupted initialization resumes where it stopped. When step 1 completes, the API's readiness check passes (Section 13.3) and the service opens.

16.5 The deployment record

Deployment writes a public record, canary.deployment.v1, that the API reads:

FieldContent
cluster, genesisHashThe Solana cluster and its genesis hash
programIdsThe three program ids
programsPer program: id, ProgramData address, SHA-256 and length of the deployed ELF, upgrade authority, and the slot at which it was verified
collateralThe USDC mint, 6 decimals
parameterManifestPathThe published parameter manifest
rungsEach rung's id, label, RungState address and transcript hash
policySettlement fee 100 bps, shares 7,000 / 3,000 bps, the sunset, the claim window of 300 slots, and the caps
buybackThe buyback signer and vault, the CANARY mint, the buyback key, and the USDC spent and CANARY burned so far

The record contains no RPC credential and no key. It is the same information anyone can read from the chain, collected in one place.

16.6 Checking a deployment

Anyone can check a Canary deployment without trusting its operators:

  1. Code. Fetch each program's ELF from its ProgramData account and compare its SHA-256 with a build from the published source and the published build inputs.
  2. Authority. Read each program's upgrade authority from its ProgramData account. For the rungs program after audit, it must be none.
  3. Parameters. Regenerate the parameter suite from the published seed (Section 4.8); every transcript hash must equal the one in the corresponding RungState.
  4. Collateral and buyback. Read MarketsConfig: the collateral mint must be USDC and canary_mint the published CANARY mint. The buyback vault is derived from ["buyback"], and total_buyback_usdc and total_canary_burned count every buyback so far.
  5. Caps. Read each enabled rung's cap from MarketsConfig.weights.
  6. Bounties. Read each rung's bounty vault balance and receipt supply.

Every value in this list is on chain or derived from published inputs; none depends on the API.

17. Roadmap

17.1 v0

Canary v0 consists of:

Markets run at three expiries on Q-160, Q-192 and T-256k with per-rung open-interest caps. C-160 and the quantum-signal market join when C-160's point count is certified.

17.2 Toward the mainnet launch

StepDone when
Independent reproductions of the rung parametersAt least three parties reproduce the transcript bit for bit
A second, independently written Q-rung verifierBoth verifiers accept the same proofs and reject the same malformed ones across differential fuzzing
AuditsThe rungs program twice; markets and vault once each
Launch sponsorsThree to five sponsors fund named rungs
Mainnet with the rungs authority revokedThe revocation is visible on chain

17.3 v1

ItemWhat it adds
Yield-bearing collateralMarkets collateralized in yield-bearing USD (for example USDY for non-US users [31]); winners receive principal plus yield, less a 10 % skim
A second post-quantum key in Q-VaultAn optional Winternitz one-time signature key alongside Falcon-512 [19]
A trigger-aware multisig voterA Falcon-gated Squads voter PDA that reads a trigger rung [17]
Frozen markets programcanary_markets upgrade authority revoked
Underwriting poolPool shares as plain SPL claims, with no governance rights
Readers on other chainsRung state referenced from other ecosystems

17.4 Metrics

HorizonTarget
Launch≥ 3 independent parameter reproductions; ≥ $250k in bounties from ≥ 3 sponsors; verification within the measured compute budgets
Six months≥ $2M open interest; ≥ 2 market makers; ≥ $5M in Q-Vaults; ≥ 1 integrator reading RungState

References

  1. BitMEX Research, "Avoiding An Unnecessary Quantum Freeze", April 2026. https://www.bitmex.com/blog/Avoiding-An-Unnecessary-Quantum-Freeze
  2. E. Aronesty, "Weak Quantum Bounty Ceremony", bitcoin-dev mailing list, 2026-05-31, with replies (secp160k1, QCAP). https://groups.google.com/g/bitcoindev/c/d7o74e-teNo/m/lLKjufQZAgAJ
  3. Allianz Research, quantum finance special, 2026-07-29. https://www.allianz.com/en/economic_research/insights/publications/specials_fmo/260729-quantum-finance.html
  4. The Block, Project Eleven Q-Day report, 2026-05-06. https://theblock.co/post/400274/project-eleven-warns-quantum-moment-all-at-once-q-day-early-2030
  5. The Quantum Insider, "15-Bit ECC Key Broken on Quantum Hardware Wins Q-Day Prize", 2026-04-24. https://thequantuminsider.com/2026/04/24/project-eleven-q-day-prize-quantum-ecc-attack/
  6. Google Research blog, "Safeguarding cryptocurrency by disclosing quantum vulnerabilities responsibly", 2026-03-31. https://www.research.google/blog/safeguarding-cryptocurrency-by-disclosing-quantum-vulnerabilities-responsibly/
  7. R. Babbush et al., "Securing Elliptic Curve Cryptocurrencies against Quantum Vulnerabilities", arXiv:2603.28846. https://arxiv.org/abs/2603.28846
  8. M. Cain et al., "Shor's algorithm is possible with as few as 10,000 reconfigurable atomic qubits", arXiv:2603.28627. https://arxiv.org/abs/2603.28627
  9. M. Roetteler, M. Naehrig, K. M. Svore, K. Lauter, "Quantum resource estimates for computing elliptic curve discrete logarithms", arXiv:1706.06752. https://arxiv.org/abs/1706.06752
  10. P.-L. Dallaire-Demers, W. Doyle, T. Foo, "Brace for impact: ECDLP challenges for quantum cryptanalysis", arXiv:2508.14011. https://arxiv.org/abs/2508.14011
  11. Trail of Bits, "We beat Google's zero-knowledge proof of quantum cryptanalysis", 2026-04-17. https://blog.trailofbits.com/2026/04/17/we-beat-googles-zero-knowledge-proof-of-quantum-cryptanalysis/
  12. J. Bos, "112-bit prime ECDLP solved", cryptography mailing list, 2009-07-09. https://www.metzdowd.com/pipermail/cryptography/2009-July/015476.html
  13. "Solving 114-bit ECDLP for a Barreto-Naehrig curve", Springer LNCS. https://link.springer.com/chapter/10.1007/978-3-319-78556-1_13
  14. Bitcoin.com News, Bitcoin puzzle status, 2026-06-23. https://news.bitcoin.com/key-hunters-eye-58-87m-bitcoin-puzzle-as-916-btc-sits-unsolved-in-78-addresses/
  15. Bitcoin-Puzzle-Info: RCKangaroo throughput, solved and unsolved list. https://github.com/roadhero/Bitcoin-Puzzle-Info
  16. Jump Crypto, "Quantum migration paths for Solana", 2026-04-27. https://jumpcrypto.com/resources/quantum-migration-paths-for-solana
  17. Solana Compass, Squads Falcon-512 multisig test and Blueshift Vector, 2026-10-13. https://solanacompass.com/news/squads-builder-says-falcon-512-post-quantum-multisig-voting-has-been-tested-on-solana-mainnet
  18. SIMD-0461, Falcon syscall proposal. https://github.com/solana-foundation/solana-improvement-documents/pull/461
  19. solana-winternitz-vault. https://github.com/deanmlittle/solana-winternitz-vault
  20. The Quantum Insider, Solana Winternitz vault, 2025-01-04. https://thequantuminsider.com/2025/01/04/solana-takes-a-step-toward-pqc-era-with-quantum-resistant-vault/
  21. Agave execution budget constants (compute-unit limit, syscall costs). https://github.com/anza-xyz/agave/blob/master/program-runtime/src/execution_budget.rs
  22. Solana documentation, Transactions (the 1,232-byte limit). https://solana.com/docs/core/transactions
  23. Solana documentation, Versioned transactions. https://solana.com/docs/core/transactions/versioned-transactions
  24. Solana documentation, Accounts (Ed25519 addresses). https://solana.com/docs/core/accounts
  25. Solana documentation, Programs (upgrade authority). https://solana.com/docs/core/programs
  26. Solana documentation, Syscall reference. https://solana.com/docs/core/programs/syscall-reference
  27. groth16-solana (compute-unit costs). https://github.com/Lightprotocol/groth16-solana
  28. SECG, SEC 2: Recommended Elliptic Curve Domain Parameters, version 2.0. https://www.secg.org/sec2-v2.pdf
  29. Nexus Mutual. https://nexusmutual.io/ and https://docs.nexusmutual.io/overview/
  30. Artemis, catastrophe bond and ILS market dashboard, viewed 2026-10-15. https://www.artemis.bm/dashboard/
  31. Ondo Finance, USDY. https://ondo.finance/usdy
  32. S. Josefsson, I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, 2017. https://www.rfc-editor.org/rfc/rfc8032
  33. C. P. Schnorr, "Efficient signature generation by smart cards", Journal of Cryptology 4, 1991. https://doi.org/10.1007/BF00196725
  34. D. Pointcheval, J. Stern, "Security arguments for digital signatures and blind signatures", Journal of Cryptology 13, 2000. https://doi.org/10.1007/s001450010003
  35. P.-A. Fouque et al., "Falcon: Fast-Fourier Lattice-based Compact Signatures over NTRU", specification v1.2, 2020. https://falcon-sign.info/
  36. SECG, SEC 2: Recommended Elliptic Curve Domain Parameters, version 1.0, 2000 (secp160k1). https://www.secg.org/SEC2-Ver-1.0.pdf

Appendix A. Account layouts

Offsets are in bytes from the start of the account data. Every account begins with Anchor's 8-byte discriminator, SHA-256("account:<Name>")[0..8]; fields follow in Borsh encoding, integers little-endian, Pubkey as 32 bytes, bool as one byte.

A.1 Discriminators

AccountProgramDiscriminator (hex)Size
RungStatecanary_rungs67568b1851ffd0fa271
SponsorPositioncanary_rungs87a4bc5ee0829c8f75
VerifyJobcanary_rungs22b650087c46810c360
MarketsConfigcanary_marketscce2e2a67c8f9e151,130
Marketcanary_marketsdbbed53700e3c69a477
Vaultcanary_vaultd308e82b02987577332
PqKeycanary_vault3d88b0e71ba215671,014
PqAuthorizationcanary_vaultf5ba432cf637f62f1,040
QueuedSpendcanary_vault81457801b66224c7227

A.2 RungState

Seeds ["rung", rung_id]. Version 2.

OffsetFieldType
0discriminator[u8; 8]
8versionu8
9rung_idu8
10familyu8
11statusu8
12bumpu8
13bounty_bumpu8
14receipt_bumpu8
15collateral_mintPubkey
47collateral_vaultPubkey
79receipt_mintPubkey
111transcript_hash[u8; 32]
143event_tsi64
151event_slotu64
159recipientPubkey
191proof_hash[u8; 32]
223total_fundedu64
231total_paidu64
239total_reclaimedu64
247sponsor_countu32
251receipt_countu32
255initialized_slotu64
263finalize_slotu64

family: 0 quantum, 1 classical control, 2 terminal. status: 0 Open, 1 Solved.

A.3 SponsorPosition

Seeds ["sponsor", rung_id, sponsor]. Version 1.

OffsetFieldType
8versionu8
9rung_idu8
10bumpu8
11ownerPubkey
43total_fundedu64
51total_reclaimedu64
59first_funded_tsi64
67first_funded_slotu64

A.4 VerifyJob

Seeds ["job", rung_id, recipient, nonce (u64 LE), proof_hash]. Version 2.

OffsetFieldTypeNotes
8versionu8
9rung_idu8
10bumpu8
11statusu80 Verifying, 1 Verified, 2 Finalized, 3 Rejected
12recipientPubkey
44nonceu64
52begin_tsi64Becomes the rung's event_ts
60begin_slotu64Becomes the rung's event_slot
68proof_hash[u8; 32]
100r[u8; 64]R as x ‖ y, big-endian
164s[u8; 32]Big-endian
196e[u8; 32]The challenge, computed at begin
228acc_x[u32; 8]Jacobian X, Montgomery form
260acc_y[u32; 8]Jacobian Y
292acc_z[u32; 8]Jacobian Z; 0 is the point at infinity
324bit_cursoru16Bits processed
326stepsu16Step instructions executed
328payerPubkeyReceives the rent at close_job

A.5 MarketsConfig

Seeds ["config"]. Version 3, 1,215 bytes.

OffsetFieldType
8versionu8
9bumpu8
10rungs_programPubkey
42collateral_mintPubkey
74administratorPubkey
106canary_mintPubkey
138buyback_authorityPubkey
170pending_treasuryu64
178total_treasury_sweptu64
186total_fees_accruedu64
194total_bounty_budgetu64
202total_bounty_sweptu64
210total_buyback_usdcu64
218total_canary_burnedu64
226weights[RungWeight; 16]
1170unallocated_bountyu64
1178buyback_activeu8
1179buyback_amountu64
1187buyback_min_outu64
1195buyback_pre_usdcu64
1203buyback_pre_canaryu64
1211buyback_begin_indexu16
1213buyback_finish_indexu16

canary_mint holds the default key until configure_buyback sets it. The seven buyback_ fields from offset 1178 describe an open buyback and are zero outside one.

A.6 RungWeight

59 bytes, sixteen of them from offset 226 of MarketsConfig. Offsets are within the element.

OffsetFieldTypeNotes
0rung_idu8
1enabledu8
2capu64Unresolved open-interest cap; immutable
10open_interestu64Current unresolved exposure
18pending_bountyu64
26total_bounty_sweptu64Fees received by the rung's bounty vault
34oi_slotsu128A(ρ), open interest × slots
50last_weight_slotu64
58no_exitu81 for a Solved rung with no receipts outstanding

A.7 Market

Seeds ["mkt", rung_id, expiry (i64 LE), kind, collateral_mint]. Version 3. Bytes 0–236 are the discovery header.

OffsetFieldTypeNotes
8versionu8
9bumpu8
10rung_idu8Target rung
11kindu80 rung-event, 1 quantum-signal
12outcomeu80 unresolved, 1 YES, 2 NO
13expiryi64
21no_observation_slotu64
29resolved_slotu64
37rungPubkeyTarget RungState
69control_rungPubkeyC-160's RungState for kind 1; else zero
101collateral_mintPubkey
133collateral_vaultPubkeySeeds ["collateral", market]
165yes_mintPubkeySeeds ["yes", market]
197no_mintPubkeySeeds ["no", market]
229open_interestu64
237total_mintedu64
245total_mergedu64
253total_redeemedu64
261total_feesu64
269created_slotu64
277resolved_tsi64
285total_fees_sweptu64
293pending_treasuryu64
301pending_bounty[u64; 16]Indexed like MarketsConfig.weights
429creatorPubkey
461created_tsi64
469unallocated_bountyu64

A.8 Vault

Seeds ["qv", owner, index (u64 LE)]. Version 2.

OffsetFieldTypeNotes
8versionu8
9bumpu8
10sol_bumpu8Seeds ["sol", vault]
11ownerPubkey
43indexu64
51trigger_rungPubkey
83trigger_rung_idu8
84modeu80 Ed25519AndPq, 1 Lockdown
85lockdown_reasonu80 none, 1 owner, 2 trigger
86asset_policyu81
87instant_mintPubkeyDefault key for SOL
119instant_token_programPubkey
151instant_limitu64
159delay_secondsu32
163pq_key_hash[u8; 32]
195active_pq_keyPubkey
227key_epochu64
235key_confirmedbool
236nonceu64Falcon nonce
244ed_nonceu64Ed25519 nonce
252created_tsi64
260created_slotu64
268locked_tsi64
276locked_slotu64
284instance_salt[u8; 32]
316instant_spentu64
324last_instant_tsi64

A.9 PqKey

Seeds ["pq", vault, payer, epoch (u64 LE), key_hash]. Version 1. The first 117 bytes form a scan header.

OffsetFieldType
8versionu8
9bumpu8
10vaultPubkey
42payerPubkey
74epochu64
82key_hash[u8; 32]
114writtenu16
116confirmedbool
117bytes[u8; 897]

A.10 PqAuthorization

Seeds ["auth", vault, payer, auth_id (u64 LE)]. Version 2.

OffsetFieldType
8versionu8
9bumpu8
10vaultPubkey
42payerPubkey
74auth_idu64
82actionAction (257 bytes)
339writtenu16
341consumedbool
342signature[u8; 666]
1008signature_hash[u8; 32]

A.11 Action

Embedded in PqAuthorization at offset 82; also the last 257 bytes of the Falcon digest preimage (Section 11.5).

OffsetFieldType
0operationu8
1key_epochu64
9nonceu64
17mintPubkey
49token_programPubkey
81recipientPubkey
113destinationPubkey
145amountu64
153queuePubkey
185new_keyPubkey
217new_key_hash[u8; 32]
249expected_feeu64

A.12 QueuedSpend

Seeds ["queue", vault, ed_nonce (u64 LE)]. Version 2.

OffsetFieldTypeNotes
8versionu8
9bumpu8
10vaultPubkey
42nonceu64
50recipientPubkey
82destinationPubkey
114mintPubkey
146token_programPubkey
178amountu64
186queued_tsi64
194unlock_tsi64
202queued_slotu64
210statusu80 Queued, 1 Executed, 2 Cancelled
211completed_slotu64
219expected_feeu64

Appendix B. Instruction reference

S marks a signer and W a writable account. "Permissionless" means the instruction needs no signer beyond the transaction's fee payer. Instruction discriminators are SHA-256("global:<name>")[0..8].

B.1 canary_rungs

initialize_rung(rung_id: u8)

AccountSWConstraint
payer✓✓The program's current upgrade authority
rung✓Created at ["rung", rung_id]
bounty_authority["bounty", rung_id]
collateral_mintThe configured USDC mint, 6 decimals
collateral_vault✓Created at ["collateral", rung_id], authority the bounty PDA
receipt_mint✓["receipt", rung_id], unused and System-owned
token_program, token_2022_program, system_program, rent
program, program_dataThe rungs program and its ProgramData

Checks: parameters compiled for rung_id; collateral decimals 6; family mapping. Effects: creates the NonTransferable receipt mint (6 decimals, mint authority the bounty PDA, no freeze authority) and RungState (Open, transcript hash, initialized slot). Emits RungInitialized.

fund_bounty(rung_id: u8, amount: u64)

AccountSWConstraint
sponsor✓✓
rung✓
collateral_mint
source✓The sponsor's USDC account
collateral_vault✓The rung's bounty vault
receipt_mint✓
receipt_account✓The sponsor's Token-2022 associated account, created if needed
sponsor_position✓["sponsor", rung_id, sponsor], created if needed
bounty_authority
token programs, associated token program, system_program

Checks: amount > 0; rung Open; now < sunset. Effects: transfer_checked(amount, 6) from source to vault; mints amount receipts; updates totals and, on the first deposit, the sponsor and receipt counts. Emits BountyFunded.

begin_claim(rung_id: u8, recipient: Pubkey, nonce: u64, r: [u8; 64], s: [u8; 32])

AccountSWConstraint
payer✓✓Pays the job's rent
rung
job✓Created at ["job", rung_id, recipient, nonce, proof_hash]
system_program

Checks: rung Open; a staged rung; transcript hash matches the compiled one; engine setup valid; 0 < s < n; R on the curve with coordinates below p. Effects: computes e; creates the job with the accumulator at infinity, begin_ts, begin_slot and payer. Emits ClaimBegun.

step_claim(max_bits: u16)

AccountSWConstraint
rung
job✓

Permissionless. Checks: slot ≤ begin_slot + 300; rung Open; job Verifying; 1 ≤ max_bits ≤ 16. Effects: processes min(max_bits, scalar_bits − cursor) bits; after the last, sets Verified if the result equals R, else Rejected. Emits ClaimAdvanced.

finalize_claim()

AccountSWConstraint
rung✓
job✓
bounty_authority
collateral_mint
collateral_vault✓
recipient_token✓A USDC account owned by the job's recipient
token_program

Permissionless. Checks: slot ≤ begin_slot + 300; rung Open; job Verified. Effects: transfers the vault's whole balance to recipient_token; sets the rung Solved with event time and slot from the job, the recipient, the proof hash, total_paid and finalize_slot; sets the job Finalized. Emits RungSolved.

close_job()

AccountSWConstraint
rung
job✓Closed
refund✓Equal to the job's payer

Permissionless. Checks: the job is Finalized or Rejected, or the rung is Solved, or slot > begin_slot + 300; otherwise JobStillActive. Effects: closes the job and returns its rent to its payer.

claim_secp256k1(rung_id, recipient, nonce, r: [u8; 64], s: [u8; 32])

AccountSWConstraint
rung✓
bounty_authority
collateral_mint
collateral_vault✓
recipient_token✓Owned by recipient
token_program

Permissionless. Checks: rung Open; T-256k; transcript; s < n; no degenerate scalar; e·Q + R = s·G. Effects: pays the vault's balance; sets Solved with event time and slot from the clock. Emits RungSolved.

claim_ed25519(rung_id, recipient, nonce, r: [u8; 32], s: [u8; 32])

Accounts as for claim_secp256k1. Permissionless. Checks: rung Open; T-255e; transcript; R not the identity; 0 < s < n; s·G + (n − e)·Q encodes to R. Effects as for claim_secp256k1.

reclaim_after_sunset(rung_id: u8, receipt_amount: u64)

AccountSWConstraint
sponsor✓
rung✓
sponsor_position✓Owned by the sponsor
bounty_authority
collateral_mint
collateral_vault✓
receipt_mint✓
receipt_account✓The sponsor's associated receipt account
destination✓A USDC account owned by the sponsor
token programs

Checks: now ≥ sunset; receipt_amount > 0; receipt_amount ≤ the sponsor's balance; supply > 0. Effects: amount = ⌊vault × receipt_amount ÷ supply⌋; burns the receipts; pays amount; updates total_reclaimed on the rung and the position. Emits SponsorReclaimed.

B.2 canary_markets

initialize_config()

AccountSWConstraint
payer✓✓The program's current upgrade authority; becomes the administrator
config✓Created at ["config"]
collateral_mintThe configured USDC mint, 6 decimals
system_program, program, program_data

configure_rung(rung_id: u8, cap: u64)

AccountSWConstraint
administrator✓Equal to config.administrator
config✓
rung["rung", rung_id] under the rungs program

Checks: rung version 2, matching id and collateral; cap > 0; rung not yet enabled; a free weight slot. Effects: enables the rung with its cap and starts its accumulator at the current slot.

create_market(rung_id: u8, expiry: i64, kind: u8)

AccountSWConstraint
payer✓✓
config
rungThe target rung
control_rungC-160 for kind 1
market✓Created at ["mkt", rung_id, expiry, kind, mint]
collateral_mint
collateral_vault✓Created
yes_mint, no_mint✓Created, 6 decimals, authority the market
token_program, system_program, rent

Permissionless. Checks: now < expiry ≤ 253,402,300,799; target Open and enabled; kind ≤ 1; for kind 1, target Q-160 and control C-160, authenticated and Open.

mint_pair(amount: u64) and merge_pair(amount: u64)

AccountSWConstraint
owner✓
config✓
market✓
rungThe market's target
collateral_mint
collateral_vault✓
yes_mint, no_mint✓
source✓The owner's USDC account
yes_token, no_token✓The owner's YES and NO accounts
token_program

Checks: amount > 0; unresolved; now ≤ expiry; target Open; for minting, OI + amount ≤ cap. Effects: settles weights; minting takes amount USDC and mints amount of each side; merging burns amount of each and returns amount USDC; updates open interest and totals.

resolve()

AccountSWConstraint
config✓
market✓
rungThe market's target
control_rungEqual to market.control_rung for kind 1

Permissionless. Applies Section 9.6.

redeem(amount: u64)

AccountSWConstraint
owner✓
config✓
market✓
collateral_mint
collateral_vault✓
winning_mint✓YES if the outcome is YES, NO if NO
winning_token✓
destination✓The owner's USDC account
token_program

Checks: amount > 0; resolved; the winning mint matches the outcome. Effects: Section 9.7.

allocate_deferred(), sweep_bounty(rung_id: u8), sweep_treasury(), sweep_fees(rung_id: u8)

InstructionAccountsEffect
allocate_deferredconfig W, market WRe-splits the market's deferred bounty share by weight
sweep_bountyconfig W, market W, collateral_mint, collateral_vault W, rung, bounty_vault W (the rung's), receipt_mint (the rung's), token_programPays the rung's pending share into its bounty vault, or flags it no-exit and defers the share
sweep_treasuryconfig W, market W, collateral_mint, collateral_vault W, buyback_signer (["buyback"]), buyback_vault W (its associated USDC account), token_programPays the pending protocol share into the buyback vault
sweep_feesThe union of the two aboveBoth

All four are permissionless.

configure_buyback(canary_mint: Pubkey, buyback_authority: Pubkey)

AccountSWConstraint
administrator✓Equal to config.administrator
config✓
canary_mintAn initialized classic SPL mint, equal to canary_mint

Checks: no buyback open; canary_mint is neither the default key nor the collateral mint; buyback_authority is not the default key; no CANARY mint is set yet, or the same one is. Effects: sets canary_mint, which can never change again, and buyback_authority, which the administrator can replace.

buyback_begin(amount: u64, min_out: u64) and buyback_finish()

Both take the same accounts, and they must appear in one transaction with the same accounts, begin first.

AccountSWConstraint
authority✓Equal to config.buyback_authority
config✓Version 3, with the CANARY mint set
collateral_mint
canary_mint✓Equal to config.canary_mint
buyback_signer["buyback"]
buyback_vault✓The signer's associated USDC account
authority_usdc✓The authority's associated USDC account
canary_vault✓The signer's associated CANARY account
instructionsThe instructions sysvar
token_program

buyback_begin checks: amount > 0; min_out > 0; no buyback open; amount ≤ the vault's balance; it is called directly by the transaction, not by another program; a later instruction of the transaction is a buyback_finish with the same accounts. Effects: records amount, min_out, the authority's USDC balance, the CANARY account's balance and both instruction positions, then transfers amount from the vault to authority_usdc.

buyback_finish checks: a buyback is open; this is the recorded finish, after a begin with the recorded arguments and the same accounts; authority_usdc holds no more than its recorded balance; the CANARY account gained at least min_out. Effects: burns the CANARY account's whole balance with burn_checked, adds amount to total_buyback_usdc and the burn to total_canary_burned, and clears the open buyback. Any failure reverts the whole transaction, including the swap between the two.

B.3 canary_vault

init_vault(index, trigger_rung_id, delay_seconds, instant_limit, asset_policy, instant_mint, instant_token_program, pq_key_hash, instance_salt)

AccountSWConstraint
owner✓✓
vault✓Created at ["qv", owner, index]
trigger_rungThe rung's PDA under the rungs program
sol_vault✓Created at ["sol", vault], 0 bytes
system_program
instant_mint_accountThe instant asset's mint, or the System program for SOL

Checks: trigger authenticated; policy 1; the asset's mint, owner, initialization and extensions; salt, delay and key hash non-zero. Effects: creates the vault, in Lockdown if the trigger already fell.

Key instructions

InstructionAccountsChecks and effects
init_pq_key(epoch, key_hash)payer S W, vault, pq_key W (created), system_programEpoch 0 with the committed hash for the first key, else key_epoch + 1
write_pq_key(offset, bytes)payer S (the key's payer), pq_key WIn order; 1–450 bytes; at most 897 in all; the final chunk must hash to key_hash
confirm_pq_key()vault W, pq_key W, authorization WPermissionless. A confirm action for this key, verified with this key; marks it confirmed and, for the first key, makes it active and increments the nonce
rotate_pq_key()vault W, pq_key (active), authorization W, new_pq_keyPermissionless. Candidate confirmed with epoch key_epoch + 1; a rotate action verified with the active key; switches the key and increments the nonce
reset_initial_pq_key_hash(key_hash)owner S, vault W, trigger_rungFirst key unconfirmed, vault not locked, trigger Open; replaces the commitment
close_unconfirmed_pq_key()payer S W, pq_key WThe key is unconfirmed; closes it to its payer

Staging instructions

InstructionAccountsChecks and effects
init_authorization(auth_id, action, signature_hash)payer S W, vault, authorization W (created), system_programOperation ≤ 3; action nonce equals the vault's; non-zero hash
write_authorization(offset, bytes)payer S, authorization WNot consumed; in order; at most 450 bytes; at most 666 in all; the final chunk must match the hash
close_authorization()payer S W, authorization WCloses it to its payer

Spending instructions

InstructionAccountsChecks and effects
spend_ed25519(amount, recipient, expected_fee)owner S, vault W, trigger_rung, sol_vault W, asset_mint, source W, destination W, token_programEd25519 gate; 0 < amount ≤ limit; the instant asset; the budget; transfers and updates the budget
queue_ed25519(queue_nonce, amount, recipient, destination, mint, token_program, expected_fee)owner S W, vault W, trigger_rung, queue W (created), system_programEd25519 gate; the instant asset; amount > limit; queue_nonce = ed_nonce; unlock at now + delay
execute_queued()vault, trigger_rung, queue W, sol_vault W, asset_mint, source W, destination W, token_programPermissionless. Ed25519 gate rechecked; Queued; now ≥ unlock; accounts match the queue; transfers and marks Executed
spend_pq()vault W, pq_key, authorization W, sol_vault W, asset_mint, source W, destination W, token_programPermissionless. A spend action verified with the active key; accounts match the action; transfers, consumes and increments the nonce
cancel_queued_pq()vault W, pq_key, authorization W, queue WPermissionless. Queue active; a cancel action for this queue verified with the active key; marks Cancelled

Lock instructions

InstructionAccountsChecks and effects
lockdown()owner S, vault WLocks with reason 1; a locked vault keeps its first reason
trip()vault W, trigger_rungPermissionless. Trigger authenticated and Solved; locks with reason 2

Appendix C. Error codes

Each program numbers its errors from 6000, Anchor's first custom error code. A failed instruction returns the code in the transaction's error, and the web app and the clients show the name.

C.1 canary_rungs

CodeNameRaised when
6000ParameterUnavailableNo parameters are compiled for the rung id
6001AlreadySolvedThe rung is Solved
6002InvalidCollateralThe mint is not the configured USDC, or its decimals are not 6
6003AlreadyInitializedThe rung or its receipt mint already exists
6004ZeroAmountAn amount is zero
6005SunsetReachedA deposit is attempted at or after the sunset
6006ArithmeticOverflowA checked operation overflows
6007WrongClaimMethodThe claim instruction does not match the rung's family
6008NonCanonicalScalars is zero or not below n
6009InvalidPointR is not a valid point, or a coordinate is not below p
6010JobNotVerifyingA step is sent to a job that is not Verifying
6011InvalidStepSizemax_bits is outside 1 to 16
6012InvalidJobThe job does not belong to this rung or is malformed
6013JobNotVerifiedFinalize is sent to a job that is not Verified
6014InvalidProofA terminal proof fails verification
6015DegenerateProofA scalar multiplication would use a zero scalar
6016InvalidRecipientThe payout account is not owned by the recipient
6017BeforeSunsetA reclaim is attempted before the sunset
6018InsufficientReceiptThe reclaim exceeds the holder's receipts, or no receipts exist
6019UnauthorizedInitializerThe signer is not the upgrade authority
6020ClaimExpiredThe claim window has passed
6021JobStillActiveA job is closed while still within its window

C.2 canary_markets

CodeNameRaised when
6000InvalidCollateralThe mint or its decimals do not match
6001UnauthorizedInitializerThe signer is not the upgrade authority or the administrator
6002CapUnsetThe rung has not been enabled with a cap
6003AlreadyConfiguredThe rung is already enabled
6004TooManyRungsAll sixteen weight slots are in use
6005InvalidRungThe rung account is not the authenticated RungState
6006ExpiryPassedThe expiry is in the past, or minting or merging after it
6007RungAlreadySolvedThe target rung is Solved
6008InvalidKindThe market kind is not 0 or 1
6009InvalidQuantumRungA quantum-signal market targets a rung other than Q-160
6010ControlAlreadySolvedThe control rung is Solved at creation
6011ZeroAmountAn amount is zero
6012AlreadyResolvedMinting or merging on a resolved market
6013OpenInterestCapMinting would exceed the rung's cap
6014NotResolvedRedeeming before resolution
6015WrongWinningMintThe redeemed token is not the winning side
6016ArithmeticOverflowA checked operation overflows
6017NoOpenInterestReserved
6018UnsupportedExpiryThe expiry is beyond 9999-12-31T23:59:59Z
6019InvalidClaimWindowA Solved rung's finalize slot is outside its claim window
6020InvalidWeightClockThe slot is earlier than a rung's last weight update
6021InvalidCanaryMintThe CANARY mint is the default key or USDC, or differs from the configured one
6022CanaryMintImmutableA different CANARY mint is set after one was configured
6023InvalidBuybackAuthorityThe buyback key is the default key
6024CanaryMintUnsetA buyback before the CANARY mint is set
6025UnauthorizedBuybackThe signer is not the configured buyback key
6026BuybackInProgressA buyback is already open
6027NoBuybackInProgressbuyback_finish without an open buyback
6028InsufficientBuybackBalanceThe amount exceeds the buyback vault's USDC
6029InvalidMinimumOutputThe minimum CANARY output is zero
6030MissingBuybackFinishNo matching buyback_finish follows in the transaction
6031InvalidBuybackPairBegin and finish differ in arguments, accounts or position
6032BuybackCpiForbiddenBegin or finish is called by another program
6033BuybackUsdcNotSpentThe buyback key kept some of the advanced USDC
6034InsufficientCanaryOutputLess than the minimum CANARY arrived
6035InvalidConfigVersionThe configuration is not version 3

C.3 canary_vault

CodeNameRaised when
6000AssetPolicyUnsetThe asset policy is not 1
6001InvalidDelayThe delay is zero
6002InvalidTriggerThe trigger account is not the authenticated RungState, or its window check fails
6003TriggerStillOpentrip is called while the trigger is Open
6004Ed25519DisabledAn Ed25519 instruction on a locked vault or after the trigger fell
6005InvalidKeyA key account or hash does not match
6006KeyNotConfirmedAn action needs a confirmed key
6007KeyAlreadyConfirmedA confirmed key is confirmed or reset again
6008InvalidKeyEpochThe epoch does not follow the active key's
6009InvalidChunkA chunk is out of order, empty or too long
6010ImmutableProofWriting to a complete key or a consumed authorization
6011InvalidActionThe action's fields do not match the instruction and its accounts
6012InvalidNonceThe action's nonce is not the vault's current nonce
6013InvalidAuthorizationThe staged signature is incomplete, consumed or does not match its hash
6014InvalidFalconProofFalcon-512 verification fails
6015AboveInstantLimitAn instant spend exceeds the instant limit
6016UseInstantPathA queued spend is not above the instant limit
6017DelayNotElapsedA queued spend is executed before it unlocks
6018QueueNotActiveThe queued spend is executed or cancelled already
6019InvalidAssetThe asset is not supported, or not the instant asset on the Ed25519 path
6020InvalidDestinationThe destination's owner or mint does not match
6021UnsupportedTokenExtensionThe mint has an extension outside the allowlist
6022ZeroAmountAn amount is zero
6023InsufficientBalanceThe custody account holds too little
6024ArithmeticOverflowA checked operation overflows
6025InstantBudgetExceededThe instant spend exceeds what is left of the budget
6026InvalidInstanceSaltThe salt is zero
6027InvalidTransferFeeThe expected fee does not equal the mint's fee for the current epoch

Appendix D. Rung parameters

The constants below are the curve parameters each rung verifies against, in hexadecimal. They are the standard values of SEC 2 [28][36] and RFC 8032 [32]. The points G and Q are not standard: each rung's pair is derived from the published seed by the procedure of Section 4.3 and recorded, with the counters used, in the rung's transcript. The transcript hashes are listed by /v1/programs, stored in each RungState, and served with their transcripts at /v1/transcripts/{hash}.

D.1 Summary

RungIdCurveField bitsSubgroup bitsCofactorScalar bitsClaim transactions
Q-1600secp160k1160161116113
Q-1921secp192k1192192119214
Q-2242secp224k1224225122517
C-1603joins when certified256~160
T-256k4secp256k125625612561
T-255e5Ed2551925525382531

D.2 Q-160 (secp160k1)

The curve y² = x³ + 7 over the prime field of order p, with a = 0 and a group of prime order n.

p = ffffffff ffffffff ffffffff fffffffe ffffac73
n = 1 00000000 00000000 0001b8fa 16dfab9a ca16b6b3
a = 0
b = 7
h = 1

D.3 Q-192 (secp192k1)

The curve y² = x³ + 3 over the prime field of order p, with a = 0 and a group of prime order n.

p = ffffffff ffffffff ffffffff ffffffff fffffffe ffffee37
n = ffffffff ffffffff fffffffe 26f2fc17 0f69466a 74defd8d
a = 0
b = 3
h = 1

D.4 Q-224 (secp224k1)

The curve y² = x³ + 5 over the prime field of order p, with a = 0 and a group of prime order n.

p = ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe ffffe56d
n = 1 00000000 00000000 00000000 0001dce8 d2ec6184 caf0a971 769fb1f7
a = 0
b = 5
h = 1

D.5 T-256k (secp256k1)

The curve y² = x³ + 7 over the prime field of order p, with a = 0 and a group of prime order n.

p = ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f
n = ffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141
a = 0
b = 7
h = 1

D.6 T-255e (Ed25519)

The curve −x² + y² = 1 + d·x²·y² over the prime field of order p = 2255 − 19, with a = −1 and a prime-order subgroup of order n.

p = 7fffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffed
n = 10000000 00000000 00000000 00000000 14def9de a2f79cd6 5812631a 5cf5d3ed
d = 52036cee 2b6ffe73 8cc74079 7779e898 00700a4d 4141d8ab 75eb4dca 135978a3
a = −1
h = 8

D.7 Classical and quantum costs

RungPollard rho steps, log₂Logical qubits [9]
Q-16079.01,466
Q-19295.01,754
Q-224111.02,042
T-256k127.02,330
T-255e125.8~2,330

The step counts use √(πn/12) for the j = 0 curves and √(πn/4) for Ed25519 (Section 2.2); the qubit counts use 9n + 2⌈log₂ n⌉ + 10 for an n-bit prime field (Section 2.4).

Appendix E. Status feed and app configuration

Both documents are served by the API as JSON. Integers that can exceed 253, such as slots and atomic amounts, are encoded as decimal strings; times are ISO 8601 in UTC.

E.1 canary.status.v1

GET /v1/status-feed returns the feed unwrapped, without the usual {data, meta} envelope.

FieldTypeMeaning
schemaVersion"canary.status.v1"
contractVersion"1.0.0"The API contract version
mode"live"
network"devnet" or "mainnet-beta"
generatedAttimeThe indexed snapshot's time
freshnessFreshnessSection 13.4
programIdConfigurationValue<address>The rungs program
rungsarrayOne entry per rung

Each entry of rungs:

FieldTypeMeaning
idstringQ-160, Q-192, Q-224, C-160, T-256k or T-255e
accountaddressThe RungState address
stateRungStateBelow
transcriptHashConfigurationValue<hex>The parameter transcript's SHA-256

state is one of:

{ "status": "Open", "eventTs": null, "eventSlot": null, "recipient": null, "proofHash": null }
{
  "status": "Solved",
  "eventTs": "2029-03-14T09:26:53.000Z",
  "eventSlot": "412345678",
  "recipient": "<address>",
  "proofHash": "<64 hex characters>",
  "finalizedSlot": "412345690",
  "finalizedAt": "2029-03-14T09:27:01.000Z",
  "transactionSignature": "<signature>"
}

The example values are illustrative.

A ConfigurationValue is either {"status": "set", "value": …, "provenance": …} or {"status": "unset", "key": …, "reason": …}, so a client can tell a value the deployment has not configured yet from a missing one.

E.2 Freshness

FieldTypeMeaning
commitment"finalized"
indexedSlotinteger stringThe slot the snapshot was read at
indexedAttimeWhen the snapshot was written
observedAttimeWhen the API served it
chainTimetimeThe Clock sysvar at the snapshot
chainHeadSlotinteger stringThe latest slot the indexer saw
lagSlotsinteger stringchainHeadSlot − indexedSlot
state"fresh", "indexer-behind" or "rpc-unreachable"
rpcReachableboolean
lastSuccessfulRpcAttime

E.3 canary.app-config.v1

GET /v1/app-config returns {data, meta} with data:

FieldTypeMeaning
schemaVersion"canary.app-config.v1"
mode"live"
appEnv"production"
state"waiting" or "ready"Section 13.3
waitingReasonsstring[]Empty when ready
cluster"devnet" or "mainnet-beta"
rpcUrlURLThe API's relay, {apiUrl}/v1/rpc
wsUrlnullClients poll signature status through the relay
genesisHashstringThe cluster's genesis hash
commitment"finalized"
apiUrlURLThe API's public origin
programIds{rungs, markets, vault}Addresses
collateral{mint, decimals: 6, symbol: "USDC"}
protocolPolicyobjectBelow
rungsarrayBelow
faucet{enabled: false}

Each entry of rungs:

FieldTypeMeaning
id, rungIdstring, integerQ-160 and 0, and so on
claimablebooleanTrue when the deployment is ready and the rung is Open
knownDiscreteLogfalseAlways false in production
stateAddressaddressThe RungState address
transcriptHashhex
parametersobjectid, label, model, pHex, nHex, aHex, bHex, gHex, qHex, transcriptSha256, parameterRole, scalarBits: what a client needs to build a claim

protocolPolicy:

FieldValue
settlementFeeBps100
treasuryShareBps7000
bountyShareBps3000
sunsetTs"2208988800" (2040-01-01T00:00:00Z)
maxClaimSlots300
capsAtomicPer enabled rung, keyed by rung id as a string ("0" is Q-160), the open-interest cap in atomic USDC

E.4 canary.deployment.v1

The deployment record (Section 16.5) is published with the deployment and read by the API:

{
  "schemaVersion": "canary.deployment.v1",
  "cluster": "mainnet-beta",
  "genesisHash": "<genesis hash>",
  "programIds": { "rungs": "<address>", "markets": "<address>", "vault": "<address>" },
  "collateral": { "mint": "<USDC mint>", "decimals": 6, "symbol": "USDC" },
  "parameterManifestPath": "<path to canonical.json>",
  "programs": {
    "rungs": {
      "programId": "<address>",
      "programDataAddress": "<address>",
      "elfSha256": "<64 hex characters>",
      "elfLength": 0,
      "upgradeAuthority": "<address or null>",
      "verifiedSlot": 0
    }
  },
  "rungs": [{ "rungId": 0, "id": "Q-160", "stateAddress": "<address>", "transcriptHash": "<64 hex characters>" }],
  "policy": {
    "settlementFeeBps": 100, "treasuryShareBps": 7000, "bountyShareBps": 3000,
    "sunsetTs": "2208988800", "maxClaimSlots": 300, "capsAtomic": { "0": "10000000000000" }
  },
  "buyback": {
    "authority": "<buyback signer address>", "vault": "<its USDC account>",
    "canaryMint": "<address or null>", "buybackAuthority": "<address or null>",
    "totalBuybackUsdcAtomic": "0", "totalCanaryBurnedAtomic": "0"
  }
}

The record never contains an RPC URL or a key; the API rejects a record that does. /v1/programs serves the same buyback object with the totals read from MarketsConfig at the indexed slot. treasuryShareBps keeps its name and is the protocol share that goes to the buyback vault.

E.5 Eligibility

GET /v1/eligibility returns:

FieldTypeMeaning
markets.allowedbooleanWhether the interface offers market actions
markets.reason"eligible-region", "us-restricted" or "location-unknown"
markets.countryCodestring or nullTwo letters
checkedAttime
source"trusted-header", "self-attestation" or "unconfigured"
policystringUS users excluded from markets in v0

With self-attestation, the client passes countryCode and attestNonUs=true as query parameters.

Appendix F. Glossary

TermMeaning
Atomic unitThe smallest unit of a token; USDC, receipts and YES and NO tokens have 6 decimals, so 1 USDC is 1,000,000 atomic units
Bounty vaultA rung's USDC token account, paid in full to the solver when the rung falls
Buyback vaultThe markets program's USDC account under ["buyback"]; it receives the protocol's 70 % of fees, and its USDC leaves only in a buyback that burns the CANARY bought
BPSWBaillie–PSW, a probable-prime test with no known counterexample
C-160The classical control rung: as costly classically as Q-160, as costly for a quantum computer as a 256-bit curve
CANARYCanary's token: 1,000,000,000 fixed supply, launched in full to the public; the protocol's 70 % of settlement fees buys and burns it
Claim window300 slots from a claim's begin slot, within which a staged claim must finish
CofactorThe ratio h of a curve's group order to its prime subgroup order n
CommitmentHow settled a Solana read is; finalized reads cannot be rolled back
Compute unit (CU)Solana's measure of execution cost; a transaction may use at most 1.4M
CRQCCryptographically relevant quantum computer: one that can solve discrete logarithms at the sizes used in practice
DiscriminatorThe first 8 bytes of an Anchor account or instruction, identifying its type
ECDLPThe elliptic-curve discrete-logarithm problem: given G and Q = k·G, find k
event_ts, event_slotThe time and slot of a rung's fall: when the winning claim began, or the terminal claim executed
ExpiryA market's settlement time T; a fall with event_ts ≤ T resolves YES
Falcon-512A lattice-based signature scheme selected by NIST; Q-Vault's post-quantum key
Jacobian coordinatesA projective representation (X : Y : Z) of the point (X/Z², Y/Z³), which avoids field inversions
LockdownA Q-Vault mode in which only the Falcon key can move funds; permanent
Logical qubitAn error-corrected qubit, the unit in which quantum resource estimates are stated
Montgomery formA representation of field elements that makes modular multiplication cheap
No-exitA Solved rung with no receipts outstanding, which receives no further fee share
NonTransferableA Token-2022 extension that makes a token impossible to transfer; used for sponsor receipts
NUMSNothing up my sleeve: values derived from a public seed by a fixed procedure, so no party could have chosen them with hidden knowledge
Open interestOutstanding YES-and-NO pairs in unresolved markets, counted per target rung
PDAProgram-derived address: an address with no private key, controlled only by the program that derives it
Pollard's rhoThe best known classical algorithm for a generic prime-order group, taking about √(πn/4) steps with the negation map
Q-dateThe date the market implies for a rung's fall, read from YES prices across expiries
Q-VaultA vault that accepts Ed25519 spends while its trigger rung is Open and only Falcon-512 after
Quantum-signal marketA market that resolves YES if Q-160 falls while C-160 has not
ReceiptA sponsor's NonTransferable token, minted one per USDC unit deposited, reclaimable after the sunset
RecipientThe owner bound into a claim's challenge; the bounty goes only to an account it owns
RungOne discrete-logarithm instance with its bounty: Q-160, Q-192, Q-224, C-160, T-256k or T-255e
RungStateThe on-chain account that says whether a rung is Open or Solved, and when it fell
Schnorr proofA proof of knowledge of a discrete logarithm: (R, s) with s·G = R + e·Q
Settlement fee1 % of winning redemptions, split 70 % to the buyback vault and 30 % to bounty vaults
Shor's algorithmThe quantum algorithm that solves discrete logarithms in polynomial time
SlotSolana's unit of time for block production, nominally 400 ms
Staged claimA claim on a Q-rung, verified over several transactions by begin_claim, step_claim and finalize_claim
Sunset2040-01-01T00:00:00Z, after which deposits stop and receipts can reclaim
Terminal rungT-256k or T-255e: the curves of Bitcoin and Ethereum keys and of Solana addresses
Time-weighted open interestOpen interest multiplied by the slots it was held, accumulated per rung; the basis of the bounty split
TranscriptThe canonical JSON record of a rung's parameters and the checks they passed; its SHA-256 is stored on chain
TripThe permissionless instruction that locks a Q-Vault after its trigger falls
Upgrade authorityThe key that can replace a Solana program's code; revoked for the rungs program after audit
YES, NOA market's two outcome tokens; a pair is minted for one USDC and merges back into one USDC