Goal: the final score recorded on-chain is exactly what the configured verifier accepted. The chain signature guarantee is cryptographic. The current off-chain gameplay guarantee is a strict, persistent event ledger with bounded validation; it is not yet a full authoritative physics replay.
Trust model (two layers)
- Off-chain authority — the live anti-cheat ledger (Gate D). Ledger v2 explicitly accounts for kills, Galaxian dive values, boss-hit points, wave bonuses, gifts, bombs and extra lives. It rejects any checkpoint/final score that differs from its event total, permanently binds one verified entry transaction to one run ticket, and persists active/finalized sessions across restarts. A modified client can still fabricate a plausible event stream; authoritative replay or server-hosted simulation is required before real-value Mainnet rewards.
All score-producing gameplay paths and verifier checks consume src/scoring_rules.js. The browser
has one score mutation entrypoint, and that entrypoint emits the corresponding ledger event. Burst
limits accept a complete 48-enemy carried formation in one frame and reset at each canonical wave,
while duplicate enemy ids, per-wave budgets, exact point values and final score equality remain
strict. Any rejected event invalidates browser finalization and reports the first rejected type,
wave and frame instead of allowing a later generic score mismatch.
2. On-chain enforcement — verifier ed25519 signature. submit_verified_run requires the verifier
to have signed the exact score being recorded, so even though anyone may send the submit
transaction, only a verifier-signed score is accepted.
How the on-chain check works
-
set_verifier(pubkey)(admin) stores the trusted verifier ed25519 key inconfig.verifier_pubkey. Once set (non-default), enforcement is mandatory. -
The verifier signs a canonical, fixed-layout message bound to the run:
"GKD_RUN_V1" | run_hash(32) | player(32) | season_id(2 LE) | score(8 LE) | fee(8 LE) | entry_slot(8 LE) -
The submit transaction includes a self-contained Solana Ed25519 precompile instruction over
(verifier_pubkey, canonical_message, signature)beforesubmit_verified_run. The runtime rejects the whole tx if that signature is invalid. -
submit_verified_runscans only earlier, already-executed instructions. This safely tolerates wallet-added ComputeBudget instructions while still requiring an exact binding toconfig.verifier_pubkeyand the canonical message rebuilt from the submit args. Mismatched pubkey/message ⇒WrongVerifier/VerifierMsgMismatch.
Result: to record a score on-chain you need the verifier's private key over that score — which a cheating client does not have. Changing any signed field (score, player, run, fee, slot) invalidates the signature.
Cross-component layout (must stay in sync)
| Piece | Location |
|---|---|
| canonical message + ed25519 introspection | programs/gkd_chain/src/lib.rs (canonical_run_message, verify_verifier_signature) |
| verifier signs canonical message | verifier/server.js (buildChainMessage, /verify → chain_msg_b64 / chain_sig_b64) |
| browser submit tx attaches Ed25519 ix + sysvar | src/chain/chain_client.js (submitVerifiedRun) |
| CLI submit parity | tools/gate_e_submit_run.js |
The Ed25519 instruction byte layout was verified to match the on-chain parser (pubkey @ offset 16, message @ offset 112). The signed score is the ledger-accepted score, so on-chain == validated.
Deploy checklist
- Generate the ignored local key with
npm run verifier:key:init; strict/production startup fails closed ifVERIFIER_SECRET_KEY_B64is absent. - Keep
REQUIRE_ENTRY_TX=true,STRICT_GATE_D=true,ALLOW_BROWSER_COMPAT=false, and durableLEDGER_STATE_FILEstorage. - Call
set_verifier(<verifier pubkey>)afterinitialize_config. - Keep the legacy memo relay disabled; the player wallet submits the signed RunRecord transaction.
- For Mainnet, move the verifier key to HSM/KMS-backed signing and replace the single-host JSON state file with an operationally durable database/replicated store.
Top-3 leaderboard (matches on-chain by construction)
submit_verified_run also feeds the verified score into a global Leaderboard PDA
(["leaderboard"], top-3, sorted descending) via lb_insert. Because it uses the same signed
score that is recorded, the displayed leaderboard can never disagree with the on-chain record.
Each entry stores { score, player (wallet), name[16] }. The front-end reads and renders it in a
top-left panel (ChainClient.fetchLeaderboard / refreshLeaderboard in
src/chain/chain_client.js; showLeaderboard in chain_config.js). The name is a cosmetic,
unsigned display field; the wallet is the authoritative (signed) identifier.
Remaining anti-cheat boundary
The schema-2 replay verifier currently proves package/hash/checkpoint consistency; it does not independently execute Phaser physics. The event ledger prevents raw score injection and catches duplicate/impossible accounting, but a custom client can manufacture plausible timed events. Do not enable real-value Mainnet mining until an independent audit approves either a deterministic authoritative simulation, a server-hosted gameplay authority, or an equivalent anti-bot design.