AI Verification

AI Verification (LightChain v2 Worker Network)

LightChallenge uses LightChain v2’s worker network for trustless verification of challenge evidence. Workers run real LLM compute (Ollama / llama3-8b), produce signed verdicts, and our on-chain attestor records the verdict so ChallengePay can finalize and pay out.

The previous v1 AIVM path (commit-reveal-PoI ceremony with AIVMInferenceV2 + LCAIValidatorRegistry) was archived in 2026-04-28 in favour of this leaner Option-2 attestor design. The v1 contracts remain deployed on testnet but are unused. See .attic/v1-aivm-path/ in the repo for the archived code.

What is the LightChain v2 worker network?

LightChain v2 (launched 2026-04-27) is a permissionless network where workers stake LCAI to run AI inference jobs. The dispatch is gateway-mediated — clients (us) submit prompts via SIWE-authed gateway, the gateway assigns a worker, the worker emits a JobCompleted event on chain, and the response is delivered through an encrypted relay (ECDH-P-256 + AES-GCM).

LightChallenge is a client of this network — we submit jobs via the gateway, decrypt verdicts via the relay, and bridge them on-chain through our own attestor contract.

For non-technical users

A challenge gets verified like this:

  1. You join a challenge (“walk 0.5 km in 3 hours”)
  2. Your phone or fitness tracker collects evidence (HealthKit data)
  3. A LightChain worker — picked by the gateway, not us — runs an LLM over the evidence and your goal
  4. The worker signs a JobCompleted event on chain saying “yes, they hit the goal” (or “no”)
  5. We sign that verdict on chain through our attestor
  6. The challenge contract reads our attestation and pays out

You don’t need to trust us. The worker’s signed JobCompleted event is public, and our attestor address can be revoked without redeploy if it ever cheats.


Pipeline: evidence to payout

┌─────────────── EVIDENCE COLLECTION ────────────────┐
│                                                      │
│  iOS / Web → POST /api/aivm/intake → public.evidence │
│                          ↓                           │
│  evidenceEvaluator → public.verdicts (pass/fail)     │
│                          ↓                           │
│  challengeDispatcher → public.aivm_jobs              │
│                        (one row per                  │
│                         (challenge_id, participant)) │
│                                                      │
└──────────────────────────────────────────────────────┘

┌─────────────── DISPATCH (per-participant) ─────────┐
│                                                      │
│  challengeWorker.claimNextJobs()                     │
│      ↓                                               │
│  runChallengePayLightchainJob(challengeId, subject)  │
│      │                                               │
│      │ 1. judgeChallengeViaLightchain(...)           │
│      │     ├─ SIWE auth → gateway                    │
│      │     ├─ /api/sessions/select  → worker + ECDH  │
│      │     ├─ /api/sessions/prepare → dispatcher sig │
│      │     ├─ JobRegistry.createSession()            │
│      │     ├─ /api/blobs upload → versioned blob     │
│      │     ├─ JobRegistry.submitJob() (0.02 LCAI fee)│
│      │     ├─ wss://relay → encrypted chunks         │
│      │     └─ AES-GCM decrypt → verdict JSON         │
│      │                                               │
│      │ 2. ChallengePayLightchainAttestor.attest(...) │
│      │                                               │
│      │ 3. ChallengePay.submitProofFor(id, sub, p)    │
│      │                                               │
│      ↓                                               │
│  aivm_jobs.status = 'done'                           │
│                                                      │
└──────────────────────────────────────────────────────┘

┌─────────────── FINALIZATION + PAYOUT ──────────────┐
│                                                      │
│  After proofDeadlineTs:                              │
│                                                      │
│  ChallengePay.finalize(id)                           │
│      ↓                                               │
│  outcome = (winnersPool > 0) ? Success : Fail        │
│      ↓                                               │
│  _snapshotAndBook() routes:                          │
│    • Winners' pool → claimable + per-winner bonus    │
│    • Loser pool fees → protocol multi-sig + creator  │
│    • If no winners: full distributable → multi-sig   │
│      (intentional admin recovery path)               │
│      ↓                                               │
│  autoDistributeWorker pushes Treasury allowances     │
│      ↓                                               │
│  Wallets receive funds                               │
│                                                      │
└──────────────────────────────────────────────────────┘

Single linear function — no commit/reveal, no PoI ceremony, no event-watching indexer to bridge finalization. The on-chain JobCompleted event from the LightChain worker is the public audit trail.


Per-participant queueing

Since migration 050_aivm_jobs_per_participant.sql (2026-04-30), the aivm_jobs table is keyed on (challenge_id, subject) — one row per participant, not per challenge. This is enforced by:

ALTER TABLE public.aivm_jobs
  ADD CONSTRAINT aivm_jobs_challenge_id_subject_key
  UNIQUE (challenge_id, subject);

Why it matters: challenges in our model can have multiple participants, each submitting their own evidence. The on-chain contract (ChallengePay.submitProofFor(id, participant, proof)) supports per-participant verification. Pre-migration, the dispatcher only matched verdicts whose subject equalled challenges.subject (the creator), which silently dropped joiners and caused incident #13.

The rule going forward: any challenge participant who has a passing verdict (verdicts.pass = true) gets their own aivm_jobs row, gets judged independently on the LightChain worker network, and gets submitProofFor called per-participant. Multiple participants on the same challenge run in parallel.


Phase details

1. Evidence collection

Users submit fitness or gaming data:

  • Apple Health — iOS AutoProofService pushes HealthKit data during the proof window
  • Strava / Fitbit — auto-synced server-side via OAuth refresh
  • Garmin — syncs into HealthKit on iOS, then to us
  • Gaming — OpenDota / Riot / FACEIT pulled server-side

Stored in public.evidence.

2. Evidence evaluation

The evidenceEvaluator worker runs the appropriate evaluator (fitness or gaming) against the challenge rules and writes a row to public.verdicts with pass, score, reasons, and evaluator columns. For walking challenges with HealthKit step data, walkingKm falls back to stepsToKm(steps_count) using a 0.762 m/step stride heuristic so the user doesn’t need a GPS-grade Workout to pass.

3. Per-participant dispatch

The challengeDispatcher (poll: 10s) joins verdicts against challenges and produces one candidate per (challenge, passing-verdict participant) pair. For each, it:

  1. Heals missing proof fields once per challenge (paramsHash, benchmarkHash)
  2. Reconciles evidence (re-pulls from API providers if linked)
  3. Inserts an aivm_jobs row with status='queued' keyed on (challenge_id, subject) (idempotent via ON CONFLICT DO NOTHING)

4. Worker claim and run

The challengeWorker (poll: 5s) claims (queued, failed) jobs with FOR UPDATE SKIP LOCKED, sets them to processing, and calls runChallengePayLightchainJob(challengeId, subject).

The runner overrides challenge.subject with the participant being verified before passing the record to judgeChallengeViaLightchain — so the prompt is built around the participant’s evidence, not the creator’s.

5. LightChain judging

judgeChallengeViaLightchain performs the SIWE → gateway → worker → relay round trip:

StepEndpointEffect
SIWE authchat-api.testnet.lightchain.aiJWT for gateway calls
Select session/api/sessions/selectReturns assigned worker + ECDH pubkeys
ECDH wrap(local)Wrap session key for worker + disputer
Prepare session/api/sessions/prepare65-byte dispatcher signature
Create sessionJobRegistry.createSession()On-chain session anchor
Upload blob/api/blobsVersioned prompt blob hash
Submit jobJobRegistry.submitJob() payable 0.02 LCAIOn-chain job anchor
Listenwss://relay.testnet.lightchain.ai/wsEncrypted chunks
Decrypt(local AES-GCM)Verdict JSON: {challengeId, verified}

The worker emits JobCompleted on JobRegistry with the response hash. That event is the public audit trail.

6. On-chain attestation

If verdict.passed:

ChallengePayLightchainAttestor.attest(
  challengeId, subject, jobId, jobCompletedTx,
  responseHash, ciphertextHash, worker, true
);
 
ChallengePay.submitProofFor(challengeId, subject, encodedProof);

submitProofFor calls attestor.verify(), which reads the attestation row by (challengeId, subject) and returns true. The contract sets c.winner[subject] = true, accumulates c.winnersPool, and increments c.winnersCount.

If !verdict.passed: the runner marks aivm_jobs.status = 'rejected' and returns. No on-chain action — the participant simply doesn’t get a winner mark, and on finalize they’re a loser.

7. Finalize and pay out

After proofDeadlineTs:

  • ChallengePay.finalize(id) is called (anyone can call; usually the deadline-sweep worker)
  • _snapshotAndBook computes the snapshot:
    • winnersPool = sum of contributions from participants with winner = true
    • losersPool = total pool minus winners pool
    • Per-winner bonus = (distributable * 1e18 / winnersPool)
    • If winnersPool == 0: full distributable goes to the protocol multi-sig as a Treasury bucket grant (this is the intentional admin recovery path)
  • autoDistributeWorker reads on-chain state, classifies wallets into winners/losers, calls autoDistribute(id, winners[], losers[]), then pushes Treasury allowances to recipients

Active contracts (testnet v2, chain ID 8200)

ContractAddressPurpose
ChallengePay0xeC651C299E978667fCDeF706Ef5Dd285e56EFd0bCore lifecycle / stakes / claims
ChallengePayLightchainAttestor0xb400770550Db25Af86b1c3CC380e92BC777E3360Records verdicts, implements verify()
Treasury0xF8E32344CC311A82f20112484F686b1038122FF3Bucketed custody, pull-based claims
MultiSigWallet (protocol)0xd9e56435290A2e8f93D6F8a0e329478D8E8514692-of-3 protocol multi-sig (admin recovery)
EventChallengeRouter0x08BA527C65FeD4653E8569fd26C582A72F4157d8Multi-outcome event routing
ChallengeAchievement0xd4949186434C2F2b186A7E20Fcbe58ae1939a630Soulbound NFTs

LightChain dispatcher signing key: 0xd92d9989d6a7A5aEcB4Da59D414Fb0673aDC2519 (LightChain Foundation, single canonical key).

JobRegistry (LightChain v2): 0x531b3a87c5d785441b9cf55b98169f20fd9056a7.


aivm_jobs state machine

StateSet byMeaning
queueddispatcherWaiting for the worker to claim
processingworker (claim)Worker is running judge → attest → submitProofFor
donerunnerAll three on-chain steps succeeded; participant marked as winner
rejectedrunnerLightChain worker returned FAIL; terminal, no retry
failedworker (catch)Transient error (gateway / RPC / DB); retry next poll
deadworkerExceeded MAX_ATTEMPTS (default 10) — manual triage
canceleddispatcherChallenge reached terminal state before this job ran

Trust model

Trust unitWhat they doWhat’s at stake
LightChain workerRuns llama3-8b on the prompt, signs JobCompleted5,000 LCAI stake, slashable up to 50%
LightChain dispatcherSigns prepareSession authorisationSingle canonical key (LightChain Foundation)
LightChain disputerHolds disputer encryption key for arbitrationTrusted role with single key
Our attestor (LCAI_WORKER_PK)Bridges the worker’s verdict on-chain via attest(...)One signing key — rotatable via setAttestor without redeploy

There is no validator quorum service in v2 — workers ARE the trust unit. Our v1 LCAIValidatorRegistry + PoI quorum design was an architectural mismatch and was retired in 2026-04-28.

If our attestor cheats: the on-chain JobCompleted chain shows our signed jobId points at a worker output that doesn’t match what we attested. LightChain’s disputer can decrypt the original response and arbitrate. The attestor key can be revoked via attestor.setAttestor(addr, false) without redeploying.


Cost / runway

  • 0.02 LCAI per job (worker fee)
  • Plus a few cents in gas (attestor + ChallengePay txs)
  • Top up the deployer wallet at lightfaucet.ai before runs

Admin recovery path

When a challenge fails for any reason — bug, network outage, evaluator regression — the loser pool isn’t lost. Per contracts/ChallengePay.sol:917-920, if winnersPool == 0 at finalize time, the entire distributable pool goes to the protocol multi-sig as a Treasury bucket grant. The 2-of-3 multi-sig can then submit a transaction redirecting those funds to affected users.

The submitTransaction → confirm × 2 → execute flow is what the reimbursement script for incident #13 uses. The same pattern handles any future incident class without needing a contract upgrade.


See also