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. |
| 9 | import { type Address, createPublicClient, type Hex, http, keccak256 } from 'viem' |
| 10 | import { TOKEN_ABI } from '../abi' |
| 11 | import { LIMITS } from '../chain' |
| 12 | import { buildSimulation, proposalActors, rpcBody, SIM_REQUEST_BYTES } from '../simulate' |
| 13 | import { utf8ToBase64 } from '../workflow' |
| 14 | |
| 15 | const [rpc, depFile, cfgFile, ...impls] = process.argv.slice(2) |
| 16 | if (!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 | } |
| 20 | const dep = (await Bun.file(depFile).json()) as { airlockAdmin: Address; proxy: Address; holders: Address[] } |
| 21 | const 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 | } |
| 33 | const client = createPublicClient({ transport: http(rpc) }) |
| 34 | const lc = (a: string) => a.toLowerCase() as Address |
| 35 | |
| 36 | const head = await client.getBlockNumber() |
| 37 | const block = head - 2n |
| 38 | const blk = await client.getBlock({ blockNumber: block }) |
| 39 | const 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 | ) |
| 42 | const k = balances.findIndex((b) => b > 0n) |
| 43 | if (k < 0) throw new Error('no holder with a balance at the probe block') |
| 44 | const holder = lc(cfg.holders[k]) |
| 45 | const balance = balances[k] |
| 46 | const amount = balance / 100n > 0n ? balance / 100n : 1n |
| 47 | const fk = balances.findIndex((b, i) => b > 0n && i !== k && lc(cfg.holders[i]) !== holder) |
| 48 | const funder = fk >= 0 ? lc(cfg.holders[fk]) : null |
| 49 | const fundAmount = fk >= 0 ? (balances[fk] < amount ? balances[fk] : amount) : 0n |
| 50 | const { stranger, receiver } = proposalActors(BigInt(cfg.chainId), lc(cfg.airlockAdmin), 0n, blk.hash as Hex) |
| 51 | |
| 52 | const perProvider = 1 + cfg.simRequests |
| 53 | const perRun = perProvider * cfg.rpcs.length |
| 54 | console.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 | ) |
| 57 | console.log(`HTTP calls: ${perProvider}/provider (1 eth_getCode + ${cfg.simRequests} eth_simulateV1), ${perRun}/run of ${LIMITS.httpCalls}\n`) |
| 58 | console.log('impl req calls jsonB base64B (limit 88000 / 120000) selectors new probes fit') |
| 59 | |
| 60 | let over = 0 |
| 61 | for (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 | } |
| 96 | console.log(over ? `\nMEASURE: ${over} request(s) over an HTTP size limit` : '\nMEASURE: every request within the HTTP size limits') |
| 97 | process.exit(over ? 1 : 0) |