Spec 06 — ProofVerifier Program
Owner: anchor-engineer + zk-circuit-engineer Depends on: 05 Blocks: 07 (TaskMarket verifies proofs at settlement) References: backend PDF §2.1 (ProofVerifier listed in upgrade table §2.6, 7-day timelock for verifier-key rotation), §2.4 (Groth16 via Light Protocol, ~400K CUs pre-SIMD-0334 / ~200K post), §5.1 (Account Validation, Upgrade Safety), §5.2 (trusted setup), §6.1 (batch verification — M2+ optimization, hook only in M1)
Goal
On-chain Groth16 verifier on bn254. Loads a verifying key from a governance-controlled PDA, accepts (proof, public_inputs), runs the pairing check via Light Protocol's bn254 primitives, and returns a verdict that TaskMarket (spec 07) treats as authoritative.
M1 ships: single-proof verify, VK governance rotation with 7-day timelock, public-input ordering locked to spec 05, CU-budgeted to fit in one tx, batch verify stubbed (handler + interface only).
State
VerifierConfig PDA — singleton
- Seeds:
[b"verifier_config"] - Fields:
authority: Pubkey— governanceactive_vk: Pubkey— pointer to currently activeVerifierKeyPDApending_vk: Option<Pubkey>— rotation staging slotpending_activates_at: i64— now + 7 days at propose timepaused: boolbump: u8
VerifierKey PDA — per-VK (multiple may exist; at most one active)
- Seeds:
[b"vk", vk_id.as_ref()]wherevk_id: [u8; 32]— governance-chosen label, typically a hash of the VK bytes - Fields:
vk_id: [u8; 32]alpha_g1: [u8; 64]beta_g2: [u8; 128]gamma_g2: [u8; 128]delta_g2: [u8; 128]ic: Vec<[u8; 64]>—ic.len() == num_public_inputs + 1; for spec 05 = 6num_public_inputs: u8— pinned to 5 at initial setup; rotations may change it but public-input order MUST stay documentedcircuit_label: [u8; 32]— e.g. "task_completion_v1"is_production: bool— false for test-SRS VKs; TaskMarket rejects non-production in mainnet mode (seeGlobalModeflag below)registered_at: i64registered_by: Pubkeybump: u8
On-chain size: fixed-width for num_public_inputs <= 16 (M1 cap; reviewer may raise). Expected ~1.4 KB per VerifierKey account.
GlobalMode PDA (reused from a future GovernanceProgram; M1 stubs a local copy)
- Seeds:
[b"mode"] - Fields:
is_mainnet: bool,bump: u8. Devnet builds setis_mainnet = falseso test-SRS VKs are accepted.
Scaffold-vs-spec deltas (reconciled against programs/proof_verifier/src/{state.rs, guard.rs})
Spec §State above captures pre-landing intent. Scaffold evolved across 645ce1a (initial + 7d VK timelock), 61e4d80 (batch RLC state-machine → BatchState), c759a7b (reentrancy-guard primitives → ReentrancyGuard + AllowedCallers), 41d18ff (F-2026-04 closed via load_caller_guard), cd5b594 (reset_guard helper extract). Deltas:
VerifierConfigdrift (1 absent):pending_authority: Option<Pubkey>(state.rs:19) — two-step auth staging slot, same pattern as treasury / capability / dispute scaffolds (cycles 175-177). Spec §Instructionstransfer_authority+accept_authorityat line 96 implies the slot; §State doesn't enumerate it.VerifierKeyaligned. 12 fields 1-to-1. (Separate queued drift at line 35num_public_inputs == 5prose: stale post-7c2143c/ cycle-117 INBOX finding →9post-F-2026-02 circuit extension; tracked as am-2 §Events-refresh carry-over, not patched here.)GlobalModealigned. 2 fields 1-to-1.BatchStatePDA — absent from spec entirely (11 fields).state.rs:52-69:cranker: Pubkey,vk_key: Pubkey,batch_id: [u8;16],count: u8,max_proofs: u8,acc_alpha: [u8;64],acc_vk_x: [u8;64],acc_c: [u8;64],random_state: [u8;32],neg_a_scaled: Vec<[u8;64]>cap 10,b_points: Vec<[u8;128]>cap 10,bump. Landed61e4d80replacingbatch_verify_stubwith RLC state machine (open_batch → add_batch_proof × N → finalize_batch / abort_batch per BACKLOG row 24). Spec §Instructions line 93 still enumeratesbatch_verify_stubas a single ix — stale post-61e4d80. Held for §Instructions-refresh cycle (not patched here).ReentrancyGuardPDA — absent from spec (5 fields).guard.rs:48-56:active: bool,entered_by: Pubkey,entered_at_slot: u64,reset_proposed_at: i64,bump. Seeds[SEED_GUARD]. Landedc759a7b.AllowedCallersPDA — absent from spec (2 fields).guard.rs:58-64:programs: Vec<Pubkey>cap 8,bump. Seeds[SEED_ALLOWED_CALLERS]. Landedc759a7b. Guard-state-vocabulary matrix row: proof_verifier =2-PDA (ReentrancyGuard + AllowedCallers)— confirms cycle-177's 2-PDA cohort prediction for a 3rd program after treasury + dispute. Post-cycle §State-sweep arc state: 4-of-5 M1-in-scope landings (capabilityN/A+ treasury2-PDA+ dispute2-PDA+ proof_verifier2-PDA); task_market §State remaining.- Absent module-level constants (9):
state.rs:3-6—MAX_PUBLIC_INPUTS = 16,MAX_IC_LEN = 17,MAX_BATCH_SIZE = 10,VK_ROTATION_TIMELOCK_SECS = 7 * 86_400. Spec §VerifierConfig line 22 "now + 7 days at propose time" carries the timelock as prose; scaffold exposes it as a const (reviewer-visible magic-number policy).state.rs:8-13—BN254_FIELD_MODULUS_BE: [u8;32]— load-bearing forscalar_in_fieldpub-input field-bounds check. Spec §register_vkline 59 references "Light Protocol's bn254 deserializers" for curve bounds; Light dep removed post-scaffold — scaffold ships direct modulus constant +sol_alt_bn128_group_opsyscalls instead.guard.rs:14-18—SEED_GUARD,SEED_ALLOWED_CALLERS,MAX_ALLOWED_CALLERS = 8,MAX_CPI_STACK_HEIGHT = 3,ADMIN_RESET_TIMELOCK_SECS = 24h.
- Absent helper fns (7):
state.rs:71-80—scalar_in_field(scalar_be) -> bool— rejects pub inputs ≥ BN254 field order (4 unit tests).guard.rs:20-46—load_caller_guard(caller_guard_ai, expected_caller_program) -> Result<ReentrancyGuard>— F-2026-04 safe load (owner + PDA-derivation + discriminator validation +activeflag deser).guard.rs:66-72—try_enter(guard, caller, slot).guard.rs:74-77—exit(guard).guard.rs:79-84—reset_guard(g)— admin-reset helper extractedcd5b594.guard.rs:86-107—check_callee_preconditions(self_guard, caller_guard_active, caller_program, allowed, stack_height)— 4 require! rows (stack-depth + caller-allowlist + caller-guard-active + self-not-reentered).guard.rs:109-120—assert_reset_timelock(guard, now)— 24h gate for admin reset.
Not-patched sibling drifts (held for separate cycles):
- Spec §Instructions line 93
batch_verify_stubstale post-61e4d80RLC state machine — queued §Instructions-refresh, multi-cycle scope (replaces single ix withopen_batch → add_batch_proof × N → finalize_batch / abort_batchsub-sequence). - Spec §VerifierKey line 35
num_public_inputs == 5stale post-7c2143c/ cycle-117 →9— already queued as am-2 §Events-refresh carry-over (cycle 174). - Spec §
register_vkline 59 "Light Protocol's bn254 deserializers" stale — Light dep removed; scaffold usesscalar_in_field+ directsol_alt_bn128_group_opsyscalls. Queued for §Instructions-refresh. - Spec §
register_vkline 53 signature carries single-tx payload shape — stale post-b5916a6/ cycle-117 INBOX resolution (chunkedinit_vk+append_vk_icper Anchor 0.31 + Solana 1232-byte tx ceiling). Tracked as resolved in INBOX; queued for §Instructions-refresh pairing.
Instructions
init_config(authority)
- One-shot, deployer-signed.
register_vk(vk_id, alpha_g1, beta_g2, gamma_g2, delta_g2, ic, num_public_inputs, circuit_label, is_production)
- Signers:
authority - Validation:
VerifierKeyforvk_iddoes not existnum_public_inputs == ic.len() - 1num_public_inputs <= 16- Points lie on correct curves — delegate to Light Protocol's
bn254deserializers (error on invalid encoding) !config.paused
- Creates
VerifierKey. Does NOT activate it. - Emits:
VkRegistered { vk_id, circuit_label, is_production }
propose_vk_activation(vk_id)
- Signers:
authority - Validation: VK exists, no pending rotation already in flight
- Sets
config.pending_vk = Some(vk_id),pending_activates_at = now + 7 days(§2.6). - Emits:
VkActivationProposed { vk_id, activates_at }
execute_vk_activation()
- Signers: any (permissionless crank) after timelock
- Validation:
pending_vk.is_some(),now >= pending_activates_at - Swaps
active_vk = pending_vk, clears pending. - Emits:
VkActivated { vk_id }
cancel_vk_activation()
- Signers:
authority. Clears pending without activating.
verify_proof(proof_a: [u8; 64], proof_b: [u8; 128], proof_c: [u8; 64], public_inputs: Vec<[u8; 32]>) -> Result<()>
- Signers: any caller (typically TaskMarket via CPI; also usable by relayers)
- Validation:
!config.pausedactive_vkaccount passed in matchesconfig.active_vkpublic_inputs.len() == vk.num_public_inputs- Each public input is a valid bn254 scalar (< field modulus)
- If
mode.is_mainnet, requirevk.is_production == true
- Computation:
- Pairing check
e(A, B) == e(alpha, beta) * e(VK_x, gamma) * e(C, delta)whereVK_x = IC[0] + Σ IC[i+1] * public_input[i] - Delegated to Light Protocol
groth16_verifier::verify(or equivalent bn254 primitive — pinned library version captured inCargo.toml)
- Pairing check
- Result:
Ok(())on valid;Err(ProofInvalid)otherwise. - Does not mutate state (pure verifier). TaskMarket records the outcome in its own state.
batch_verify_stub(proofs: Vec<BatchEntry>, public_inputs: Vec<Vec<[u8; 32]>>)
- M1: returns
NotImplemented. Handler reserved + IDL entry committed so SDK type is stable (§6.1 roadmap).
set_paused, transfer_authority, accept_authority
- Standard governance hooks as in specs 02/03.
Scaffold-vs-spec deltas (reconciled against programs/proof_verifier/src/lib.rs #[program] block, 22 pub fn entries)
Pre-edit §Instructions enumerates 8 explicit handlers plus the 3-call governance umbrella (11 ix). Scaffold ground truth = 22 pub fn, covering 4 families absent from the pre-edit text plus a stub-replacement and two arg-shape drifts. All 11 absent blocks land post-scaffold via the commit anchors cited below. Closes the M1 §Instructions sweep arc (5-of-5 in-scope programs reconciled — capability_registry cycle 172, treasury_standard cycle 163, dispute_arbitration cycle 166, task_market cycle 167, agent_registry cycle 173, proof_verifier this cycle).
- Stub-replacement (1 ix → 4 ix state machine, landed
61e4d80+ stack-overrun fixb16794e): spec line 93batch_verify_stubwas reserved as a singleNotImplementedhandler at M1; scaffold replaces it with a 4-ix random-linear-combination state machine that reduces 4N pairings to N+3 per the §6.1 batch-verify roadmap.open_batch(batch_id: [u8; 16], max_proofs: u8)—batch_verify.rs:44. Permissionless cranker-signed init ofBatchStatePDA (seeds[b"batch", cranker, batch_id], capped atMAX_BATCH_SIZE). Setsbatch.vk_key = config.active_vk(snapshot at open-time) + Fiat-Shamir seed from(batch_id, cranker, slot). Emits no event.add_batch_proof(proof_a, proof_b, proof_c, public_inputs)—batch_verify.rs:99. Cranker-signed (has_one = cranker). Per-proof: validatesvk_key == config.active_vksnapshot, derives next RLC scalar via keccak Fiat-Shamir chain (128-bit truncation), accumulates(α·A, α·VK_x, α·C)intobatch.acc_*G1 points. Idempotent undercount < max_proofs. Emits no event.finalize_batch()—batch_verify.rs:194. Permissionless. Assertsbatch.count >= 1. Single 4-pair pairing check over the accumulated points +(α_sum·alpha, beta)+(VK_x_sum, gamma)+(C_sum, delta). On success closes the PDA (rent-refund to cranker) + emitsBatchVerified { batch_id, count, vk_id }. On failure leaves the PDA open forabort_batch. Note: §State block (lines 14–47) does not declare theBatchStatePDA — surfaced inline below.abort_batch()—batch_verify.rs:256. Cranker-signed escape hatch; closes the PDA without emitting. Used after a single badadd_batch_proofwould have causedfinalize_batchto fail.
- Chunked VK registration (2 ix, landed
b5916a6, supersedes legacy single-txregister_vkfor IC > ~6 G1 points): F-2026-02 circuit rebinding (per7c2143c) extended thetask_completioncircuit from 5 → 9 public inputs, growingICfrom 6 → 10 G1 points. Resulting single-txregister_vkpayload (~1166 bytes) exceeded both the Anchor 0.31 clientBuffer.alloc(1000)scratch buffer and the Solana ~1232-byte transaction-size ceiling. Resolution = chunked PDA-mutation pair; legacyregister_vkretained for backward compat on small VKs.init_vk(vk_id, alpha_g1, beta_g2, gamma_g2, delta_g2, num_public_inputs, circuit_label, is_production)—init_vk.rs:33. Authority-signed. AllocatesVerifierKeyPDA (seeds[b"vk", vk_id]) withic = Vec::new()+registered_at = 0(sentinel for "init phase, not yet finalized";registered_by = authoritypins the chunk-uploader). Validatesnum_public_inputs <= MAX_PUBLIC_INPUTS+!paused. Emits no event (theVkRegisteredemit is deferred to the finalize chunk per §Events line 104 dual-emit callout).append_vk_ic(ic_points: Vec<[u8; 64]>, finalize: bool)—append_vk_ic.rs:28. Authority-signed (registered_by == authorityconstraint pins the same uploader asinit_vk). Pushes IC points intovk.icunder boundsvk.ic.len() < num_public_inputs + 1; rejects if PDA is already finalized (registered_at != 0constraint viaVkAlreadyFinalized). Whenfinalize == true: asserts exactvk.ic.len() == num_public_inputs + 1, setsregistered_at = now, emitsVkRegistered { vk_id, circuit_label, is_production }— the second of the twoVkRegisteredcall sites per §Events line 104.
- Reputation rail (1 ix, F-2026-02 fail-close interim then re-enabled, landed
733cc7ainitial +7c2143crebinding):verify_and_update_reputation(proof_a, proof_b, proof_c, public_inputs, agent_did, capability_bit, sample, task_id)—reputation_cpi.rs:119. Caller signs (typically task_market via CPI; reentrancy-guarded by the cross-program guard DAG —caller_guard+self_guard+allowed_callersPDAs validated viaload_caller_guardper F-2026-04 fix). After Groth16 verification succeeds, extracts(agent_did, capability_bit, sample_hash, task_id)frompublic_inputs[5..=8](the 4-field rebinding per F-2026-02 full-fix outline), recomputessample_hashon-chain viahash_sample(sample)and asserts equality (defense-in-depth against sample tampering), then CPIsagent_registry::update_reputationsigned by the[b"rep_authority"]PDA.proof_key = keccak(proof_a)for replay tracking on the callee. Status note:ProofVerifierError::ReputationBindingNotReady(errors.rs:62) was the F-2026-02 interim fail-close return; the variant remains declared but is no longer the active return path post-7c2143c. Audit-fix-manifest line 18 still tracks the entry asDEFERRED (fail-close in place)— surfaced inline as a manifest-vs-scaffold drift not patched here. Spec §Events line 107 carries the matching pre-edit fail-closed claim and is similarly stale post-7c2143c.
- Guard-admin family (4 ix, #7 scaffolding landed
c759a7b+2f76d3f, helper-extractcd5b594): matches the agent_registry / treasury_standard / dispute_arbitration / task_market guard-admin rollout cohort.init_guard(initial_callers: Vec<Pubkey>)—guard.rs:42. Authority-gated one-shot. InitializesReentrancyGuardPDA (seeds[SEED_GUARD]) +AllowedCallersPDA (seeds[SEED_ALLOWED_CALLERS]).set_allowed_callers(programs: Vec<Pubkey>)—guard.rs:75. Authority-gated list rewrite.propose_guard_reset()—guard.rs:105. Authority-gated; starts admin-reset timelock.admin_reset_guard()—guard.rs:125. Authority-gated post-timelock crank.- Guard-admin-vocabulary matrix row (post-cycle): proof_verifier =
live-runtime-ix + struct-only-events. Per §Events line 110 the program inverts the cohort convention —ReentrancyRejectedis live ×4 (only program in the cohort with live runtime-rejection emits) whileGuardEnteredis struct-only andGuardInitialized/GuardAdminReset/AllowedCallersUpdatedadmin events are absent entirely. Runtime-ix surface (init_guard / set_allowed_callers / propose_guard_reset / admin_reset_guard) is live as authority-gated handlers but admin-event emits are not wired. 5-program guard-vocabulary matrix complete: capability_registryN/A; treasury_standard / dispute_arbitration / task_marketlive-events, runtime-ix varies; agent_registrylive-events + live-runtime-ix; proof_verifierlive-runtime-ix + struct-only-events(sole inverter).
- Arg-shape drift on
init_config(pre-edit signature understates by 1 arg): spec line 50init_config(authority)(1 arg); scaffoldlib.rs:21is 2 args:(authority, is_mainnet). Theis_mainnet: boolpopulates theGlobalModePDA at init-time rather than via a separate setter post-init.init_config.rs:7-30accounts struct co-inits bothVerifierConfigandGlobalModePDAs in the same handler — a single-tx bootstrap rather than the spec's implied two-step (init_configforVerifierConfig+ a separateinit_modeforGlobalMode). Not a behavior change; consolidation matches the agent_registry cycle-173init_globalconsolidation pattern (init-time pubkey/flag population vs spec's implied per-setter rollout). - Arg-shape drift on
propose_vk_activation(pre-edit signature carriesvk_id, scaffold takes ctx-only): spec line 64propose_vk_activation(vk_id); scaffoldlib.rs:56is(ctx)only. Thevk_idis derived from thepending_vk: Account<VerifierKey>passed inProposeVkActivationaccounts struct (vk_activation.rs:29); Anchor's PDA seed validation (seeds = [b"vk", vk.vk_id]) enforces the binding without the redundant ix-arg. Same idiom as the cycle-167 task_marketassigned_agentderivation drift. - Authority two-step naming drift: spec line 96 umbrella
set_paused, transfer_authority, accept_authority— scaffold matches but exports the two-step underinstructions::authority::{transfer_authority_handler, accept_authority_handler}(authority.rs:20+:42). No arg-shape or behavior drift.
State-side drift surfaced (not patched here): VerifierConfig in state.rs carries pending_authority: Option<Pubkey> (the two-step authority transfer slot, populated by transfer_authority and consumed by accept_authority) absent from §State VerifierConfig block (lines 16–24). VerifierKey.registered_at doubles as a 0-sentinel finalize-flag for the chunked-flow init_vk + append_vk_ic lifecycle — semantic load not documented in §State VerifierKey line 38 (which describes it as a wall-clock stamp only). BatchState PDA (seeds [b"batch", cranker, batch_id], fields cranker + vk_key + batch_id + count + max_proofs + acc_alpha + acc_vk_x + acc_c + random_state + bump) is a new account type absent from §State entirely. AllowedCallers + ReentrancyGuard PDAs (seeds [SEED_ALLOWED_CALLERS] + [SEED_GUARD]) are also absent — same omission as the 4 sister M1 in-scope programs' §State blocks. MAX_BATCH_SIZE + REP_PUBLIC_INPUT_COUNT + REP_AUTHORITY_SEED + MAX_PUBLIC_INPUTS are new constants. Held for future §State-sweep cycle.
Errors drift surfaced (not patched here): §Errors block (lines 116–131) lists 14 variants; scaffold errors.rs enumerates more (chunked-flow + reputation + guard + batch families: VkAlreadyFinalized, IcLengthMismatch, TooManyPublicInputs, InvalidBatchSize, BatchFull, BatchEmpty, VkSnapshotMismatch, ReputationBindingNotReady (interim, see reputation rail above), PoseidonError, SampleHashMismatch, CpiDepthExceeded, PairingCheckFailed, etc.). Bundled with the (ad-2) cross-spec §Errors sweep candidate.
§Done-checklist drift surfaced (not patched here): §Done-checklist line 199 batch_verify_stub present in IDL, returns NotImplemented is stale post-61e4d80 — scaffold ships the 4-ix state machine, not the stub. Audit-package-m1.md §3.4 instruction-list wording (batch_verify_stub [OUT OF SCOPE — returns NotImplemented]) carries the matching stale claim and is held for the §3.4-deferral discipline of cycles 127/129. Cross-spec sweep candidate; not patched here per single-section scope.
Audit-package-m1 §3.4 register_vk target-line discipline: audit-package §3.4 enumerates 10 instructions (init_config, init_vk, append_vk_ic, register_vk, propose_vk_activation, execute_vk_activation, cancel_vk_activation, verify_proof, batch_verify_stub, set_paused, authority two-step) — covers the chunked + legacy VK paths but does not enumerate verify_and_update_reputation (per F-2026-02 DEFERRED status, intentional omission from the audit-frozen surface) or the 4 guard-admin handlers (per the §3.4-deferral cycle-127/129 discipline holding the audit-package surface stable across post-freeze additions). No §3.4 edit landed this cycle; cross-cite preserved here for reviewer cross-reading the audit package vs the spec.
Post-edit §Instructions arc state: 6-of-6 §Instructions reconciliations land (cycle 163 task_market / cycle 166 dispute_arbitration / cycle 167 treasury_standard / cycle 172 capability_registry / cycle 173 agent_registry / this cycle proof_verifier). M1 §Instructions sweep arc closed across all 5 M1-in-scope programs + task_market (which spans M1 + M2 caller surfaces). Remaining §Instructions sweep candidates carry forward to M2-only specs: (ag) specs/program-governance.md, (ab) specs/program-fee-collector.md, (ac) specs/program-nxs-staking.md. State-sweep arc + Events-refresh arc + Errors-cross-spec arc remain queued per cycle 173 next-options block.
Events
Emit surface = 11 struct declarations in events.rs, 14 emit! call sites across 7 ix files (source: grep emit! programs/proof_verifier/src/). 10 events live, 1 struct-only.
- Global init —
VerifierInitialized { authority }atinit_config.rs:46. - VK lifecycle —
VkRegistered { vk_id, circuit_label, is_production }emits twice:register_vk.rs:71(legacy single-tx path) andappend_vk_ic.rs:49(chunked-flow finalize per cycle-117init_vk + append_vk_ic × Nsupersession after F-2026-02 circuit extension drove the single-tx payload over the Anchor 0.31 client 1000-byte scratch-buffer ceiling; resolved publicb5916a6).VkActivationProposed { vk_id, activates_at }atvk_activation.rs:45(7-day timelock target per spec §1).VkActivated { vk_id }atvk_activation.rs:89.VkActivationCancelled { vk_id }atvk_activation.rs:116. - Verify-proof — no success event on the happy path (hot path; TaskMarket's
TaskVerified/VerificationFailedper cycle-164 §Events sweep close the settlement-side trace). Reject path emitsReentrancyRejected { program, offending_caller, slot }×2 atverify_proof.rs:75+:92on guard-check failure (caller mismatch / stack-height overflow). Proof_verifier is the only program across the 9-cycle §Events sweep whereReentrancyRejectedis live — every sister program declares it as scaffold-parity placeholder only. - Batch verify —
BatchVerified { batch_id, count, vk_id }atbatch_verify.rs:232insidefinalize_batch_handler.open_batch,add_batch_proof,abort_batchemit nothing — the batch state-machine is observable on-chain viaBatchStatePDA reads + the terminalBatchVerifiedonly. - Reputation CPI (F-2026-02 inert rail) —
ReentrancyRejected×2 atreputation_cpi.rs:146+:163on guard-check failure. Theverify_and_update_reputation_handleritself is fail-closed per F-2026-02 (cycle-73reports/proof-verifier-audit.md§Findings) and returns before state change, but the guard-check reject path still fires. Indexer-side: aReentrancyRejectedkeyed onreputation_cpi.rsmeans a caller tried the inert rail and violated the guard — fail-closed on both axes, no state change. - Pause —
PausedSet { paused }atset_paused.rs:23. - Authority two-step —
AuthorityTransferProposed { pending }atauthority.rs:26.AuthorityTransferAccepted { new_authority }atauthority.rs:55. - Guard runtime (struct-only) —
GuardEntered { program, caller, slot, stack_height }declared atevents.rs:53but no emit site in the crate. Inverse of the cohort convention: all 8 prior-swept programs declareReentrancyRejectedstruct-only +GuardEnteredlive (withGuardEnteredcounts of 7 in task_market, 1 in treasury, 0 in fee_collector / nxs_staking / dispute / governance / capability). Proof_verifier inverts toReentrancyRejectedlive ×4 +GuardEnteredstruct-only. Guard-admin events (GuardInitialized/GuardAdminReset/AllowedCallersUpdated) absent entirely — zero struct declarations, zero emit sites — matching the 2-of-5-live treasury/task-market admin-side convention but on the runtime axis proof_verifier is the lone outlier.
Field-carrying shape: vk_id: [u8; 32] on 5 of 11 (4 VK-lifecycle + BatchVerified). slot: u64 on 2 (GuardEntered struct-only + ReentrancyRejected). No timestamp: i64 field on any event — proof_verifier is the only program across the 9-cycle §Events sweep without a single timestamp-carrying event (cf. treasury 13-of-14, task-market 12-of-21, agent-registry 9-of-9, etc.). Indexer-side, block time for VK-lifecycle + authority-transfer + pause comes from program_events.block_time / slot. No agent_did on any event (protocol-infrastructure program, not per-agent). activates_at: i64 on VkActivationProposed is the sole i64 timestamp-shape field and is a timelock target, not a wall-clock stamp.
Pre-edit note "no event on verify_proof" correct on the happy path; reject path does emit ReentrancyRejected ×2 per the Verify-proof bullet above. Pre-edit enumerated 6 of 11 events; 5 were absent (VerifierInitialized, BatchVerified, VkRegistered dual-emit supersession, ReentrancyRejected live ×4, GuardEntered struct-only).
Errors
UnauthorizedPausedVkAlreadyExistsVkNotFoundVkMismatch— providedVerifierKeyaccount does not matchconfig.active_vkPublicInputCountMismatchPublicInputOutOfFieldProofMalformed— curve deserialization failureProofInvalid— pairing check failsTimelockNotElapsedNoPendingActivationActivationPendingNotProductionVkNotImplemented
Public-input ordering — locked to spec 05
index 0: task_hash
index 1: result_hash
index 2: deadline
index 3: submitted_at
index 4: criteria_root
Any reorder requires a new circuit, new VK, new rotation. circuit_label surfaces the version.
CU budget (M1 default, reviewer may tighten)
| Instruction | Target |
|---|---|
verify_proof (5 public inputs) |
400k CUs (§2.4) |
register_vk |
30k |
propose_vk_activation |
10k |
execute_vk_activation |
8k |
TaskMarket verify+settle budget in §2.1 is ~400k total, so verify_proof lives alone in a dedicated ComputeBudgetInstruction::SetComputeUnitLimit(500_000) paired tx, with settle as a separate instruction in the same tx only if CU headroom allows post-optimization. Reviewer to confirm post-benchmark.
Post-SIMD-0334 target (not M1): ~200k CUs per §2.4.
Invariants
config.active_vkpoints to aVerifierKeywhose account exists and whosevk_idmatches.pending_vkis set ⇔pending_activates_at > 0.execute_vk_activationalways enforcesnow >= pending_activates_at.verify_proofis pure — no account writes.- Public-input count of the active VK never changes without a new circuit label.
- In mainnet mode, a non-production VK can never be activated (enforced at
propose_vk_activation). VerifierKey.ic.len() == num_public_inputs + 1is constructor-enforced and immutable.
Security checks (backend §5.1 + §5.2)
- Account Validation: Anchor seeds + bump on
VerifierConfig,VerifierKey,GlobalMode.verify_proofassertspassed_vk.key() == config.active_vk. Owner = program. Discriminator enforced. - Re-entrancy:
verify_proofperforms no CPI and writes no state. Cannot re-enter anything. - Integer Safety: scalar reduction validated (
< field modulus) before pairing. Library (Light Protocol) handles curve arithmetic internally with checked ops; version pinned. - Authorization: VK registration + rotation gated on
authority. Execution of pending rotation is permissionless but only after timelock. - Upgrade Safety: program upgrade authority = Squads 4-of-7, 7-day timelock (§2.6). In-program VK rotation timelock also 7 days per §2.6 "Verifier key rotations via governance".
- Trusted Setup: VK bytes originate only from spec 05's MPC ceremony for mainnet (
is_production = trueflag). Devnet/test VKs flagged false; mainnet mode rejects them. - Slashing Safety: N/A.
- Oracle Safety: N/A.
- Token Safety: N/A.
- Pause: blocks
verify_proofand VK registration. While paused, TaskMarket's settle path rejects — documented as "verifier halt = task stall", not fund loss; timeouts still fire refunds.
Interactions with spec 07 (TaskMarket)
- TaskMarket CPIs
verify_proofduringfinalize_task. Passes:- Proof a/b/c bytes (64 + 128 + 64)
- 5 public inputs derived from its own
TaskContractstate:task_hash,result_hash,deadline,submitted_at,criteria_root.
- TaskMarket validates the response code and transitions
statusaccordingly. ProofVerifier itself does not know TaskMarket exists — this is a one-way, stateless call. - To support both atomic verify+settle (when CU budget allows) and split verify-then-settle (when it doesn't), TaskMarket spec exposes both paths. M1 ships split.
Done-checklist
- Program compiles with Anchor 1.0 + Light Protocol bn254 primitives pinned
-
register_vk+propose_vk_activation+execute_vk_activationhappy path test - Timelock test: execute rejected before, succeeds after, warp clock
- Mainnet-mode test: activating a
is_production = falseVK rejected -
verify_prooftest vectors from spec 05's test SRS (valid proof) →Ok -
verify_proofnegative tests: flipped bit in proof, tampered public input, wrong VK →ProofInvalid/VkMismatch - Malformed curve point →
ProofMalformed(not a panic) - CU measurement of
verify_prooflogged; within 400k budget -
batch_verify_stubpresent in IDL, returnsNotImplemented - IDL at
target/idl/proof_verifier.json - Public-input ordering doc block mirrored verbatim in spec 05 and spec 07
- Security auditor pass; findings closed