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 / measure-requests.ts
cre/veto/tools/measure-requests.ts97 lines · 4728 bytes
1// Measures the eth_simulateV1 requests the attack simulation builds for one or more implementations, against the
2// CRE HTTP limits (request 120 kb, response 250 kb, 15 HTTP calls per run). It builds the exact same requests the
3// workflow would (simulate.ts buildSimulation), using the same holder / funder / amount selection as airlock.ts,
4// then prints per request: call count, plain JSON bytes, and base64 bytes (what the HTTP capability sends).
5//
6// bun tools/measure-requests.ts <rpc-url> <deployment.json> <config.json> <label=0xImpl> [<label=0xImpl> ...]
7//
8// Nothing is sent on chain: eth_getCode, eth_call (balanceOf) and a block read only.
9import { type Address, createPublicClient, type Hex, http, keccak256 } from 'viem'
10import { TOKEN_ABI } from '../abi'
11import { LIMITS } from '../chain'
12import { buildSimulation, proposalActors, rpcBody, SIM_REQUEST_BYTES } from '../simulate'
13import { utf8ToBase64 } from '../workflow'
14
15const [rpc, depFile, cfgFile, ...impls] = process.argv.slice(2)
16if (!rpc || !depFile || !cfgFile || impls.length === 0) {
17 console.error('usage: bun tools/measure-requests.ts <rpc-url> <deployment.json> <config.json> <label=0xImpl> ...')
18 process.exit(2)
19}
20const dep = (await Bun.file(depFile).json()) as { airlockAdmin: Address; proxy: Address; holders: Address[] }
21const cfg = (await Bun.file(cfgFile).json()) as {
22 chainId: number
23 airlockAdmin: Address
24 proxy: Address
25 holders: Address[]
26 watch: Address[]
27 rpcs: { url: string; label: string }[]
28 callGas: number
29 readGas: number
30 maxCalls: number
31 simRequests: number
32}
33const client = createPublicClient({ transport: http(rpc) })
34const lc = (a: string) => a.toLowerCase() as Address
35
36const head = await client.getBlockNumber()
37const block = head - 2n
38const blk = await client.getBlock({ blockNumber: block })
39const balances = await Promise.all(
40 cfg.holders.map((h) => client.readContract({ address: cfg.proxy, abi: TOKEN_ABI, functionName: 'balanceOf', args: [h as Address], blockNumber: block })),
41)
42const k = balances.findIndex((b) => b > 0n)
43if (k < 0) throw new Error('no holder with a balance at the probe block')
44const holder = lc(cfg.holders[k])
45const balance = balances[k]
46const amount = balance / 100n > 0n ? balance / 100n : 1n
47const fk = balances.findIndex((b, i) => b > 0n && i !== k && lc(cfg.holders[i]) !== holder)
48const funder = fk >= 0 ? lc(cfg.holders[fk]) : null
49const fundAmount = fk >= 0 ? (balances[fk] < amount ? balances[fk] : amount) : 0n
50const { stranger, receiver } = proposalActors(BigInt(cfg.chainId), lc(cfg.airlockAdmin), 0n, blk.hash as Hex)
51
52const perProvider = 1 + cfg.simRequests
53const perRun = perProvider * cfg.rpcs.length
54console.log(
55 `probe block ${block}; holder ${holder} (bal ${balance}); funder ${funder ?? 'none'} (fund ${fundAmount}); maxCalls ${cfg.maxCalls}; simRequests ${cfg.simRequests}; providers ${cfg.rpcs.length}`,
56)
57console.log(`HTTP calls: ${perProvider}/provider (1 eth_getCode + ${cfg.simRequests} eth_simulateV1), ${perRun}/run of ${LIMITS.httpCalls}\n`)
58console.log('impl req calls jsonB base64B (limit 88000 / 120000) selectors new probes fit')
59
60let over = 0
61for (const spec of impls) {
62 const eq = spec.indexOf('=')
63 const label = eq > 0 ? spec.slice(0, eq) : spec
64 const impl = lc(eq > 0 ? spec.slice(eq + 1) : spec)
65 const code = (await client.getBytecode({ address: impl as Address, blockNumber: block })) ?? '0x'
66 const { layout, requests } = buildSimulation({
67 proxy: lc(cfg.proxy),
68 newImpl: impl as Address,
69 newCode: code as Hex,
70 holder,
71 amount,
72 funder,
73 fundAmount,
74 holders: cfg.holders.map(lc),
75 watch: cfg.watch.map(lc),
76 stranger,
77 receiver,
78 block,
79 callGas: cfg.callGas,
80 readGas: cfg.readGas,
81 maxCalls: cfg.maxCalls,
82 maxRequests: cfg.simRequests,
83 })
84 requests.forEach((params, i) => {
85 const body = rpcBody('eth_simulateV1', params)
86 const b64 = utf8ToBase64(body).length
87 const flagJson = body.length > SIM_REQUEST_BYTES ? ' !JSON>88k' : ''
88 const flagB64 = b64 > LIMITS.httpRequestBytes ? ' !REQ>120k' : ''
89 if (flagJson || flagB64) over++
90 console.log(
91 `${label.padEnd(20)} ${String(i + 1).padStart(3)} ${String(layout.chunks[i].calls.length).padStart(5)} ${String(body.length).padStart(6)} ${String(b64).padStart(7)}${flagJson}${flagB64}` +
92 (i === 0 ? ` ${String(layout.selectors.length).padStart(9)} ${String(layout.newSelectors.length).padStart(4)} ${String(layout.probes).padStart(6)} ${layout.truncated ? 'TRUNC' : 'all'}` : ''),
93 )
94 })
95}
96console.log(over ? `\nMEASURE: ${over} request(s) over an HTTP size limit` : '\nMEASURE: every request within the HTTP size limits')
97process.exit(over ? 1 : 0)