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:
- You join a challenge (“walk 0.5 km in 3 hours”)
- Your phone or fitness tracker collects evidence (HealthKit data)
- A LightChain worker — picked by the gateway, not us — runs an LLM over the evidence and your goal
- The worker signs a
JobCompletedevent on chain saying “yes, they hit the goal” (or “no”) - We sign that verdict on chain through our attestor
- 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
AutoProofServicepushes 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:
- Heals missing proof fields once per challenge (
paramsHash,benchmarkHash) - Reconciles evidence (re-pulls from API providers if linked)
- Inserts an
aivm_jobsrow withstatus='queued'keyed on(challenge_id, subject)(idempotent viaON 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:
| Step | Endpoint | Effect |
|---|---|---|
| SIWE auth | chat-api.testnet.lightchain.ai | JWT for gateway calls |
| Select session | /api/sessions/select | Returns assigned worker + ECDH pubkeys |
| ECDH wrap | (local) | Wrap session key for worker + disputer |
| Prepare session | /api/sessions/prepare | 65-byte dispatcher signature |
| Create session | JobRegistry.createSession() | On-chain session anchor |
| Upload blob | /api/blobs | Versioned prompt blob hash |
| Submit job | JobRegistry.submitJob() payable 0.02 LCAI | On-chain job anchor |
| Listen | wss://relay.testnet.lightchain.ai/ws | Encrypted 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)_snapshotAndBookcomputes the snapshot:winnersPool= sum of contributions from participants withwinner = truelosersPool= 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)
autoDistributeWorkerreads on-chain state, classifies wallets into winners/losers, callsautoDistribute(id, winners[], losers[]), then pushes Treasury allowances to recipients
Active contracts (testnet v2, chain ID 8200)
| Contract | Address | Purpose |
|---|---|---|
| ChallengePay | 0xeC651C299E978667fCDeF706Ef5Dd285e56EFd0b | Core lifecycle / stakes / claims |
| ChallengePayLightchainAttestor | 0xb400770550Db25Af86b1c3CC380e92BC777E3360 | Records verdicts, implements verify() |
| Treasury | 0xF8E32344CC311A82f20112484F686b1038122FF3 | Bucketed custody, pull-based claims |
| MultiSigWallet (protocol) | 0xd9e56435290A2e8f93D6F8a0e329478D8E851469 | 2-of-3 protocol multi-sig (admin recovery) |
| EventChallengeRouter | 0x08BA527C65FeD4653E8569fd26C582A72F4157d8 | Multi-outcome event routing |
| ChallengeAchievement | 0xd4949186434C2F2b186A7E20Fcbe58ae1939a630 | Soulbound NFTs |
LightChain dispatcher signing key: 0xd92d9989d6a7A5aEcB4Da59D414Fb0673aDC2519 (LightChain Foundation, single canonical key).
JobRegistry (LightChain v2): 0x531b3a87c5d785441b9cf55b98169f20fd9056a7.
aivm_jobs state machine
| State | Set by | Meaning |
|---|---|---|
queued | dispatcher | Waiting for the worker to claim |
processing | worker (claim) | Worker is running judge → attest → submitProofFor |
done | runner | All three on-chain steps succeeded; participant marked as winner |
rejected | runner | LightChain worker returned FAIL; terminal, no retry |
failed | worker (catch) | Transient error (gateway / RPC / DB); retry next poll |
dead | worker | Exceeded MAX_ATTEMPTS (default 10) — manual triage |
canceled | dispatcher | Challenge reached terminal state before this job ran |
Trust model
| Trust unit | What they do | What’s at stake |
|---|---|---|
| LightChain worker | Runs llama3-8b on the prompt, signs JobCompleted | 5,000 LCAI stake, slashable up to 50% |
| LightChain dispatcher | Signs prepareSession authorisation | Single canonical key (LightChain Foundation) |
| LightChain disputer | Holds disputer encryption key for arbitration | Trusted 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
- Incident #13: per-participant queueing fix and recovery
- Operations runbook — pm2 supervisor, deadline sweeps, env handling
- Architecture — full repo map and webapp surface