E2E Pipeline Testing
End-to-end testing of the LightChallenge pipeline — from challenge creation through LightChain v2 worker judgment to on-chain payout.
Test scripts
| Script | Purpose |
|---|---|
scripts/demoLightchainJudge.ts | Smallest. Submits a single prompt to a LightChain v2 worker, decrypts the response. Verifies gateway + relay + ECDH wiring. |
scripts/demoChallengeLeanE2E.ts | Mid. Adds the on-chain attest + ChallengePayLightchainAttestor.verify() step. No ChallengePay interaction. |
scripts/demoChallengeFullLifecycle.ts | Full. Creates a real challenge, joins, submits evidence, drives the full runner (runChallengePayLightchainJob), finalizes, and pays out via autoDistribute. |
Run:
npx tsx scripts/demoChallengeFullLifecycle.tsRequires webapp/.env.local with: PRIVATE_KEY, DATABASE_URL, LCAI_WORKER_PK, CHALLENGEPAY_LIGHTCHAIN_ATTESTOR_ADDRESS, NEXT_PUBLIC_CHALLENGEPAY_ADDR. A funded wallet on chain 8200 with ~0.15 LCAI is enough for one run.
What the full lifecycle test covers
| Phase | Layer | What happens | Real or simulated |
|---|---|---|---|
| 1 | On-chain | createChallenge(CreateParams) on ChallengePay | Real |
| 2 | On-chain | joinChallengeNative(id) for a participant wallet | Real |
| 3 | DB | Insert evidence row matching what POST /api/aivm/intake would write | Real |
| 4 | DB | evidenceEvaluator writes a verdicts row with pass=true | Real (same code path) |
| 5 | DB | challengeDispatcher writes an aivm_jobs row keyed on (challenge_id, subject) | Real |
| 6 | LightChain | judgeChallengeViaLightchain(...): SIWE auth → gateway → assigned worker → ECDH-encrypted relay → AES-GCM verdict decrypt | Real (talks to LightChain testnet v2) |
| 7 | On-chain | ChallengePayLightchainAttestor.attest(...) records the verdict | Real |
| 8 | On-chain | ChallengePay.submitProofFor(id, subject, proof) calls attestor.verify() and marks the participant as winner | Real |
| 9 | Time | Wait until proofDeadlineTs (script uses a 60s window for testing) | — |
| 10 | On-chain | ChallengePay.finalize(id) snapshots winners + books fees | Real |
| 11 | On-chain | autoDistribute(id, winners[], losers[]) pushes Treasury allowances | Real |
| 12 | DB | claimsIndexer records the WinnerClaimed / LoserClaimed events | Real |
Nothing is simulated — every step talks to chain 8200, the LightChain v2 gateway, and a real Postgres database. Total runtime: ~90s, mostly waiting for proofDeadlineTs.
Phase details
Phase 1: Create challenge
ChallengePay.createChallenge({
kind: 0, // Solo
currency: 0, // Native LCAI
stakeAmount: parseEther("0.001"),
startTs: now + 60,
duration: 60, // 1 min for the test
proofDeadlineTs: startTs + duration + 60, // 60s grace
verifier: CHALLENGEPAY_LIGHTCHAIN_ATTESTOR_ADDRESS,
...
})The creator’s stake is deposited into the Treasury bucket. c.payoutsDone is false; status = Active.
Phase 6: LightChain v2 judging
judgeChallengeViaLightchain performs the SIWE → gateway → relay round trip and returns:
{
passed: boolean,
rawResponse: string, // decrypted verdict JSON
jobId: bigint,
worker: `0x${string}`,
responseHash: `0x${string}`,
ciphertextHash: `0x${string}`,
txs: { jobCompleted: `0x${string}` }, // on-chain audit trail
}The on-chain JobCompleted event from JobRegistry is the public audit trail — anyone can verify our attestation against the worker’s signed event.
Phase 7: Attest
ChallengePayLightchainAttestor.attest(
challengeId, subject, jobId, jobCompletedTx,
responseHash, ciphertextHash, worker, true
);One tx. Idempotent — re-attesting the same (challengeId, subject) reverts with AlreadyAttested.
Phase 8: Submit proof
ChallengePay.submitProofFor(challengeId, subject, abiEncodedProof)
→ calls attestor.verify(challengeId, subject, proof)
→ reads attestation row → returns true
→ c.winner[subject] = true
→ c.winnersPool += contrib
→ c.winnersCount += 1If verify() returns false (no attestation, attestation says passed=false, or proof bytes don’t match the attestation’s hashes), submitProofFor does NOT mark the participant as winner.
Phase 10-11: Finalize + auto-distribute
After proofDeadlineTs passes:
finalize(id)setsc.status = Finalized,c.outcome = Success(sincewinnersPool > 0)_snapshotAndBookbooks the per-winner bonus, protocol fee, creator feeautoDistribute(id, [subject], [])pushes the Treasury allowance to the participant’s wallet
If winnersPool == 0 (no one passed): outcome = Fail, the entire distributable goes to the protocol multi-sig. See incident #13 for the recovery pattern.
What is real
| Component | Status |
|---|---|
| LightChain testnet v2 (chain ID 8200) | Real chain |
ChallengePay contract (0xeC65…Fd0b) | Real deployment |
ChallengePayLightchainAttestor (0xb400…3360) | Real deployment |
Treasury (0xF8E3…22FF3) | Real deployment |
LightChain v2 gateway (chat-api.testnet.lightchain.ai) | Real (LightChain Foundation) |
| LightChain v2 worker | Real (staked node, llama3-8b) |
LightChain v2 relay (relay.testnet.lightchain.ai/ws) | Real |
| PostgreSQL database (Neon) | Real |
| Evidence evaluator | Real code path |
There’s no simulation layer in v2 — workers run real LLM compute and emit signed JobCompleted events on chain. The only thing the script “fakes” is the iOS / webapp UX — it inserts evidence directly into the DB instead of going through POST /api/aivm/intake.
Smaller demos
For unit-level testing without spending a full 0.15 LCAI per run:
# Test gateway + relay only (no on-chain writes)
npx tsx scripts/demoLightchainJudge.ts
# Test gateway + relay + attestor (one on-chain attest tx)
npx tsx scripts/demoChallengeLeanE2E.tsBoth write to the testnet but skip ChallengePay challenge creation, so they’re cheaper.
Contract test suite
npx hardhat testCovers ChallengePay, ChallengePayLightchainAttestor, Treasury, MultiSigWallet, EventChallengeRouter, ChallengeAchievement, both fitness + gaming evaluators, and the lifecycle resolver (test/lifecycle.test.ts — pins the incident #13 UI fix).