Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / cre / veto / tools / check-evidence.ts
cre/veto/tools/check-evidence.ts73 lines · 3508 bytes
1// Recomputes every AirlockReceiver verdict's evidenceHash from chain data alone: AirlockAdmin.getProposal(id) plus
2// the action and rule in the receiver's Verdict event. Prints one line per verdict; exits 1 on any mismatch.
3//
4// bun tools/check-evidence.ts <rpc-url> <deployment.json>
5//
6// The deployment JSON is the one script/DeployEth.s.sol writes (chainId, startBlock, airlockAdmin, receiver).
7// The Verdict logs are read from startBlock in windows of 10,000 blocks, each tried up to 3 times. Some public RPCs
8// refuse old logs (publicnode answers some requests with 4444 "pruned history unavailable"); on live Sepolia pass one
9// that keeps the history, e.g. https://sepolia.gateway.tenderly.co.
10import { type Address, createPublicClient, type Hex, http } from 'viem'
11import { ACTION, AIRLOCK_ADMIN_ABI, RECEIVER_ABI } from '../abi'
12import { canonicalEvidence, evidenceHashOf, proposalDigest } from '../report'
13import { RULE_IDS, type RuleId, ruleHash } from '../rules'
14
15const [rpc, file] = process.argv.slice(2)
16if (!rpc || !file) {
17 console.error('usage: bun tools/check-evidence.ts <rpc-url> <deployment.json>')
18 process.exit(2)
19}
20const d = (await Bun.file(file).json()) as { chainId: number; startBlock: number; airlockAdmin: Address; receiver: Address }
21const client = createPublicClient({ transport: http(rpc) })
22const byHash = new Map(RULE_IDS.map((r) => [ruleHash(r), r]))
23
24const WINDOW = 10_000n
25const TRIES = 3
26const verdictLogs = (fromBlock: bigint, toBlock: bigint) =>
27 client.getContractEvents({ address: d.receiver, abi: RECEIVER_ABI, eventName: 'Verdict', fromBlock, toBlock })
28const logs: Awaited<ReturnType<typeof verdictLogs>> = []
29const head = await client.getBlockNumber()
30for (let lo = BigInt(d.startBlock); lo <= head; lo += WINDOW) {
31 const hi = lo + WINDOW - 1n < head ? lo + WINDOW - 1n : head
32 for (let attempt = 1; ; attempt++) {
33 try {
34 logs.push(...(await verdictLogs(lo, hi)))
35 break
36 } catch (e) {
37 if (attempt >= TRIES) {
38 console.error(`blocks ${lo}-${hi}: the RPC refused the Verdict logs ${TRIES} times (pass an RPC that keeps the history)`)
39 throw e
40 }
41 await Bun.sleep(2000)
42 }
43 }
44}
45let bad = 0
46for (const log of logs) {
47 const { proposalId, action, ruleHash: rh, evidenceHash } = log.args as { proposalId: bigint; action: number; ruleHash: Hex; evidenceHash: Hex }
48 const g = await client.readContract({ address: d.airlockAdmin, abi: AIRLOCK_ADMIN_ABI, functionName: 'getProposal', args: [proposalId] })
49 const proposal = {
50 id: proposalId,
51 kind: Number(g.kind),
52 proposer: g.proposer,
53 target: g.target,
54 value: g.value,
55 codehash: g.codehash,
56 dataHash: g.dataHash,
57 proposedBlock: BigInt(g.proposedBlock),
58 readyBlock: BigInt(g.readyBlock),
59 }
60 const rule: RuleId | null = action === ACTION.Clear ? null : (byHash.get(rh) ?? null)
61 const evidence = canonicalEvidence({
62 chainId: BigInt(d.chainId),
63 airlockAdmin: d.airlockAdmin,
64 proposal,
65 proposalDigest: proposalDigest(proposal),
66 verdict: { verdict: action === ACTION.Clear ? 'pass' : 'veto', rule },
67 })
68 const ok = evidenceHashOf(evidence) === evidenceHash.toLowerCase()
69 if (!ok) bad++
70 console.log(`${ok ? 'ok ' : 'BAD '} #${proposalId} ${action === ACTION.Clear ? 'Clear' : `Veto ${rule ?? rh}`} evidence ${evidenceHash}${ok ? ' recomputed from getProposal' : ` != ${evidenceHashOf(evidence)}`}`)
71}
72console.log(`${logs.length} verdict(s), ${bad} mismatch(es)`)
73process.exit(bad ? 1 : 0)