Post a bounty

The judge and the replay

Build, replay, compare, verdict

Two identical machines fed the same instruction cards, drawn in white line
  • Build. The judge compiles the hunter's crate on its own machine with the pinned toolchain.
  • Replay. The original binary (read from mainnet at the snapshot) and the candidate are loaded in two identical local SVMs (LiteSVM 0.17.0 with the mainnet feature set). Both receive exactly the same inputs: the corpus instructions, the same account states, the same sysvars, then 10,000 fuzzed variants.
  • Compare. For every case: success or the exact error, logs line by line, return data, and the post-state of every writable account.
  • Verdict. A JSON report with its sha256, the source commitment, the corpus hash and both binary hashes.

Target figures: 1,000 real mainnet instructions and 10,000 fuzzed. When a program has fewer than 1,000 usable recent instructions, N is what exists (at least 200 to open a bounty) and every page prints the real N.

How a case runs

  • Both SVMs load every account of the corpus with the same lamports, owner, data and executable flag. Executable accounts referenced by the cases (CPI targets) are loaded as programs with their own loader. LiteSVM's default programs stay (System, SPL Token, Token-2022, ATA, Memo).
  • The target program is loaded at its own address with the loader of the snapshot, so program-derived addresses and self checks behave as on mainnet.
  • Each case is one transaction: the case's ComputeBudget instructions, then the target instruction with its original account metas. Signatures are not verified (the original signers' keys are not needed) and a fixed fee payer funds it.
  • Every case is simulated, never committed, so each one starts from the same state.
  • The log collector keeps 10,000 bytes, the same for both binaries.

Compute budget

Both binaries run under the same compute budget, taken from each case's own ComputeBudget instructions, or the default when the transaction set none. For v1 transactions, whose limits live in the message's transactionConfig, the snapshot rebuilds the equivalent instructions: heap frame, compute unit limit and loaded accounts data size.

A case where the original binary itself runs out of compute units is counted as budget-bound and not compared: compute units are a property of the compiler, not of the source, and no rebuild can match them. The reverse, a candidate that runs out where the original succeeds, is a mismatch. That is why an interpreter that embeds the original bytecode as data cannot pass.

What must match

FieldRule
ResultOk, or the exact transaction error, custom codes included, for example InstructionError(1, Custom(6001))
LogsLine by line, in order. Only the lines Program <id> consumed N of M compute units are removed. CPI invoke [n] lines and their order must match.
Return dataProgram id and bytes
AccountsOn success, for every account the instruction marks writable: lamports, owner, executable flag and every byte of data

Fuzzed cases

The 10,000 fuzzed cases are derived deterministically from the corpus. The seed is the first 8 bytes of sha256("decomp-fuzz/1:" + corpus sha256), read little endian, driving a SplitMix64 generator. Each fuzzed case takes a random real case and applies one of ten mutations:

  • flip one bit of the data
  • set one byte to a random value
  • truncate the data at a random length
  • append 1 to 16 random bytes
  • replace the data with another case's data
  • drop the signer flag of one signer account
  • drop the writable flag of one writable account
  • randomize every byte of the data
  • swap two accounts (or drop the only one)
  • write a boundary value (0x00, 0xff, eight zero bytes, eight 0xff bytes, i64 max, 0x80) at a random offset

The verdict

PASS requires three things: the original binary's sha256 matches the corpus snapshot, every compared case matches, and at least 100 cases succeed on the original, so a corpus of pure errors cannot pass. Anything else is FAIL, with the reason.

The report lists the totals (cases, compared, budget-bound, matched, real and fuzzed), the ok-path count, the fuzz seed, the first 20 mismatches with their first differing field (the first differing log line, the first differing data byte of an account), a sha256 over each binary's outcomes, and the rules it applied. It holds no timestamp and no host data: the same inputs give the same bytes, so the report sha256 is reproducible by anyone.

Memo record of each verdict
decomp:proof:<target>:<submission>:<source sha256>:<report sha256>:<matched>/<total>

Each verdict is recorded on Solana by the keeper as a Memo. Until the keeper is funded the verdict exists with all its hashes and the page says "Memo record pending"; the backfill records every pending verdict in one run.

The calibration

The engine was proven on SPL Memo v3, whose source is public. On the first local run against a 1,000-instruction Memo corpus, the plain source differed on 398 of 11,000 cases. The first differing log line said why: the deployed binary was compiled in 2020, and its standard library printed a NUL byte inside a memo as \u{0}, where today's compiler prints \0.

The calibration source carries a short function that reproduces the 2020 escaping (a NUL as \u{0}, a single quote as \', grapheme extenders escaped) and then matches every compared case. A copy with one changed log line ("length" instead of "len") fails on every memo case with that line shown. Those are Proof #0001 and Proof #0002, re-run live by the judge machine on a fresh corpus.

This is what the judge is for: a rebuild has to reproduce what the binary does, including what its compiler did.