forked from blockscout/blockscout
-
Notifications
You must be signed in to change notification settings - Fork 0
Verify Mainnet Account Abstraction sources #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
75107c7
Document Mainnet AA source verification design
JOY 26ae490
Plan Mainnet AA source verification
JOY c732e10
Build immutable Mainnet AA verification inputs
JOY 62ac961
Gate Mainnet AA compiler bytecode
JOY 3c018ff
Verify Mainnet AA sources exactly
JOY 839d9c1
Wire Mainnet AA source verification
JOY 060d7e1
Test Mainnet AA source verification UI
JOY afed4e6
Fix Mainnet AA deployment provenance
JOY f9576b9
Harden Mainnet AA verification gates
JOY File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
373 changes: 373 additions & 0 deletions
373
.github/scripts/extract-mainnet-aa-verification-inputs.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,373 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { mkdir, readFile, writeFile } from 'node:fs/promises'; | ||
| import { dirname, isAbsolute, join, posix, relative, resolve } from 'node:path'; | ||
|
|
||
| const CHAIN_ID = 7979; | ||
| const ENTRY_POINT_ADDRESS = '0x0000000071727De22E5E9d8BAf0edAc6f37da032'; | ||
| const KERNEL_ADDRESS = '0xd6CEDDe84be40893d153Be9d467CD6aD37875b28'; | ||
| const KERNEL_FACTORY_ADDRESS = '0x2577507b78c2008Ff367261CB6285d44ba5eF2E9'; | ||
| const ECDSA_VALIDATOR_ADDRESS = '0x845ADb2C711129d4f3966735eD98a9F09fC4cE57'; | ||
| const FACTORY_STAKER_ADDRESS = '0xd703aaE79538628d27099B8c4f621bE4CCd142d5'; | ||
|
|
||
| const OUTPUT_SELECTION = [ | ||
| 'abi', | ||
| 'evm.deployedBytecode.object', | ||
| 'evm.deployedBytecode.immutableReferences', | ||
| 'evm.deployedBytecode.linkReferences', | ||
| 'evm.methodIdentifiers', | ||
| ]; | ||
|
|
||
| const targets = [ | ||
| { | ||
| key: 'entry-point', | ||
| address: ENTRY_POINT_ADDRESS, | ||
| contractName: 'EntryPoint', | ||
| sourcePath: 'contracts/core/EntryPoint.sol', | ||
| diskSourcePath: 'contracts/core/EntryPoint.sol', | ||
| standardInputFile: 'entry-point.standard-input.json', | ||
| compilerOutputFile: 'entry-point.compiler-output.json', | ||
| compilerPackage: 'solc-0.8.23', | ||
| compilerVersion: 'v0.8.23+commit.f704f362', | ||
| evmVersion: 'paris', | ||
| optimizer: { enabled: true, runs: 1_000_000 }, | ||
| viaIR: true, | ||
| metadata: { bytecodeHash: 'ipfs' }, | ||
| licenseType: 'gnu_gpl_v3', | ||
| spdxLicense: 'GPL-3.0', | ||
| constructorArgs: '', | ||
| expectedCodeSha256: '4dcad467095cd9af58006b270475ac7591c6946bca08552f6789727097b51eae', | ||
| rpcChecks: [], | ||
| verificationMatch: 'full', | ||
| sourceFamily: 'account-abstraction', | ||
| }, | ||
| { | ||
| key: 'kernel', | ||
| address: KERNEL_ADDRESS, | ||
| contractName: 'Kernel', | ||
| sourcePath: 'src/Kernel.sol', | ||
| diskSourcePath: 'src/Kernel.sol', | ||
| standardInputFile: 'kernel.standard-input.json', | ||
| compilerOutputFile: 'kernel.compiler-output.json', | ||
| compilerPackage: 'solc-0.8.28', | ||
| compilerVersion: 'v0.8.28+commit.7893614a', | ||
| evmVersion: 'prague', | ||
| optimizer: { enabled: true, runs: 200 }, | ||
| viaIR: true, | ||
| metadata: { appendCBOR: false, bytecodeHash: 'none' }, | ||
| licenseType: 'mit', | ||
| spdxLicense: 'MIT', | ||
| constructorArgs: '0000000000000000000000000000000071727de22e5e9d8baf0edac6f37da032', | ||
| expectedCodeSha256: 'd13e7ff2bc90271659100c83f49ee6250555bbf26ed35c2315f243c6849a2127', | ||
| rpcChecks: [{ signature: 'entrypoint()', expectedAddress: ENTRY_POINT_ADDRESS }], | ||
| verificationMatch: 'partial', | ||
| sourceFamily: 'kernel', | ||
| soladySourcePrefix: 'lib/solady/src', | ||
| }, | ||
| { | ||
| key: 'kernel-factory', | ||
| address: KERNEL_FACTORY_ADDRESS, | ||
| contractName: 'KernelFactory', | ||
| sourcePath: 'dependencies/kernel-v3.3/src/factory/KernelFactory.sol', | ||
| diskSourcePath: 'src/factory/KernelFactory.sol', | ||
| standardInputFile: 'kernel-factory.standard-input.json', | ||
| compilerOutputFile: 'kernel-factory.compiler-output.json', | ||
| compilerPackage: 'solc-0.8.28', | ||
| compilerVersion: 'v0.8.28+commit.7893614a', | ||
| evmVersion: 'prague', | ||
| optimizer: { enabled: true, runs: 200 }, | ||
| viaIR: true, | ||
| metadata: { appendCBOR: false, bytecodeHash: 'none' }, | ||
| licenseType: 'mit', | ||
| spdxLicense: 'MIT', | ||
| constructorArgs: '000000000000000000000000d6cedde84be40893d153be9d467cd6ad37875b28', | ||
| expectedCodeSha256: '56443d7d18bfd62d5d69b04fc8207e439bf904166335dd7159e0eeef1cba2367', | ||
| rpcChecks: [{ signature: 'implementation()', expectedAddress: KERNEL_ADDRESS }], | ||
| verificationMatch: 'partial', | ||
| sourceFamily: 'kernel', | ||
| soladySourcePrefix: 'dependencies/solady-0.1.26/src', | ||
| }, | ||
| { | ||
| key: 'ecdsa-validator', | ||
| address: ECDSA_VALIDATOR_ADDRESS, | ||
| contractName: 'ECDSAValidator', | ||
| sourcePath: 'src/validator/ECDSAValidator.sol', | ||
| diskSourcePath: 'src/validator/ECDSAValidator.sol', | ||
| standardInputFile: 'ecdsa-validator.standard-input.json', | ||
| compilerOutputFile: 'ecdsa-validator.compiler-output.json', | ||
| compilerPackage: 'solc-0.8.25', | ||
| compilerVersion: 'v0.8.25+commit.b61c2a91', | ||
| evmVersion: 'paris', | ||
| optimizer: { enabled: true, runs: 200 }, | ||
| viaIR: true, | ||
| metadata: { appendCBOR: false, bytecodeHash: 'none' }, | ||
| licenseType: 'mit', | ||
| spdxLicense: 'MIT', | ||
| constructorArgs: '', | ||
| expectedCodeSha256: 'be711f07f49e57bf56c512b6f32f7c77d9ec1881c4051ed33a45cfad8c7a8b8e', | ||
| rpcChecks: [], | ||
| verificationMatch: 'partial', | ||
| sourceFamily: 'kernel-ecdsa', | ||
| soladySourcePrefix: 'lib/solady/src', | ||
| }, | ||
| { | ||
| key: 'factory-staker', | ||
| address: FACTORY_STAKER_ADDRESS, | ||
| contractName: 'FactoryStaker', | ||
| sourcePath: 'src/factory/FactoryStaker.sol', | ||
| diskSourcePath: 'src/factory/FactoryStaker.sol', | ||
| standardInputFile: 'factory-staker.standard-input.json', | ||
| compilerOutputFile: 'factory-staker.compiler-output.json', | ||
| compilerPackage: 'solc-0.8.24', | ||
| compilerVersion: 'v0.8.24+commit.e11b9ed9', | ||
| evmVersion: 'paris', | ||
| optimizer: { enabled: true, runs: 200 }, | ||
| viaIR: false, | ||
| metadata: { appendCBOR: false, bytecodeHash: 'none' }, | ||
| licenseType: 'mit', | ||
| spdxLicense: 'MIT', | ||
| constructorArgs: '', | ||
| expectedCodeSha256: 'f91091bf1260892a4d0b834494489fea55be2f2f968ad6b1abc1410531f2a2a1', | ||
| rpcChecks: [], | ||
| verificationMatch: 'partial', | ||
| sourceFamily: 'kernel-ecdsa', | ||
| soladySourcePrefix: 'lib/solady/src', | ||
| }, | ||
| ]; | ||
|
|
||
| function fail(message) { | ||
| throw new Error(message); | ||
| } | ||
|
|
||
| async function readJson(path) { | ||
| return JSON.parse(await readFile(path, 'utf8')); | ||
| } | ||
|
|
||
| function isInside(root, candidate) { | ||
| const child = relative(root, candidate); | ||
| return child === '' || (!child.startsWith('..') && !isAbsolute(child)); | ||
| } | ||
|
|
||
| function validatePrimarySource(content, target) { | ||
| if (!content.includes(`SPDX-License-Identifier: ${target.spdxLicense}`)) { | ||
| fail(`${target.contractName} source must declare SPDX license ${target.spdxLicense}`); | ||
| } | ||
| if (!new RegExp(`\\b(?:abstract\\s+)?contract\\s+${target.contractName}\\b`).test(content)) { | ||
| fail(`${target.contractName} is missing from ${target.sourcePath}`); | ||
| } | ||
| } | ||
|
|
||
| function validateEntryPointBuildInfo(buildInfo, target) { | ||
| const expectedLongVersion = target.compilerVersion.slice(1); | ||
| if (buildInfo.solcLongVersion !== expectedLongVersion) { | ||
| fail(`${target.contractName} compiler must be ${expectedLongVersion}`); | ||
| } | ||
| const input = buildInfo.input; | ||
| const settings = input?.settings; | ||
| if (input?.language !== 'Solidity') { | ||
| fail(`${target.contractName} build language must be Solidity`); | ||
| } | ||
| if (settings?.evmVersion !== target.evmVersion) { | ||
| fail(`${target.contractName} EVM version must be ${target.evmVersion}`); | ||
| } | ||
| if ( | ||
| settings?.optimizer?.enabled !== target.optimizer.enabled || | ||
| settings?.optimizer?.runs !== target.optimizer.runs | ||
| ) { | ||
| fail(`${target.contractName} optimizer settings are not canonical`); | ||
| } | ||
| if (settings?.viaIR !== true) { | ||
| fail(`${target.contractName} build must use viaIR`); | ||
| } | ||
| if ((settings?.metadata?.bytecodeHash ?? 'ipfs') !== 'ipfs') { | ||
| fail(`${target.contractName} metadata bytecode hash must be ipfs`); | ||
| } | ||
| const primarySource = input?.sources?.[target.sourcePath]?.content; | ||
| if (typeof primarySource !== 'string') { | ||
| fail(`${target.contractName} standard input is missing ${target.sourcePath}`); | ||
| } | ||
| validatePrimarySource(primarySource, target); | ||
| if (buildInfo.output?.contracts?.[target.sourcePath]?.[target.contractName] == null) { | ||
| fail(`${target.contractName} is missing from compiler output`); | ||
| } | ||
| return structuredClone(input); | ||
| } | ||
|
|
||
| function importedPaths(content) { | ||
| const paths = []; | ||
| const expressions = [ | ||
| /import\s*["']([^"']+)["']\s*;/g, | ||
| /import\s+[^;]*?\s+from\s*["']([^"']+)["']\s*;/g, | ||
| ]; | ||
| for (const expression of expressions) { | ||
| for (const match of content.matchAll(expression)) { | ||
| paths.push(match[1]); | ||
| } | ||
| } | ||
| return paths; | ||
| } | ||
|
|
||
| function resolveImport({ importerUnit, importerDiskPath, importPath, kernelCheckout, target }) { | ||
| if (importPath.startsWith('.')) { | ||
| const sourceUnit = posix.normalize(posix.join(posix.dirname(importerUnit), importPath)); | ||
| if (sourceUnit === '..' || sourceUnit.startsWith('../')) { | ||
| fail(`Import escapes source-unit root: ${importPath}`); | ||
| } | ||
| return { | ||
| sourceUnit, | ||
| diskPath: resolve(dirname(importerDiskPath), importPath), | ||
| }; | ||
| } | ||
|
|
||
| if (importPath.startsWith('solady/')) { | ||
| const suffix = importPath.slice('solady/'.length); | ||
| return { | ||
| sourceUnit: posix.join(target.soladySourcePrefix, suffix), | ||
| diskPath: resolve(kernelCheckout, 'lib', 'solady', 'src', ...suffix.split('/')), | ||
| }; | ||
| } | ||
|
|
||
| if (importPath.startsWith('ExcessivelySafeCall/')) { | ||
| const suffix = importPath.slice('ExcessivelySafeCall/'.length); | ||
| return { | ||
| sourceUnit: posix.join('lib/ExcessivelySafeCall/src', suffix), | ||
| diskPath: resolve(kernelCheckout, 'lib', 'ExcessivelySafeCall', 'src', ...suffix.split('/')), | ||
| }; | ||
| } | ||
|
|
||
| fail(`Unsupported Kernel import: ${importPath}`); | ||
| } | ||
|
|
||
| async function collectKernelSources({ kernelCheckout, target }) { | ||
| const root = resolve(kernelCheckout); | ||
| const pending = [{ | ||
| sourceUnit: target.sourcePath, | ||
| diskPath: resolve(root, ...target.diskSourcePath.split('/')), | ||
| }]; | ||
| const sources = {}; | ||
|
|
||
| while (pending.length > 0) { | ||
| const current = pending.pop(); | ||
| if (sources[current.sourceUnit] != null) { | ||
| continue; | ||
| } | ||
| if (!isInside(root, current.diskPath)) { | ||
| fail(`Kernel source escapes checkout: ${current.diskPath}`); | ||
| } | ||
|
|
||
| let content; | ||
| try { | ||
| content = await readFile(current.diskPath, 'utf8'); | ||
| } catch { | ||
| fail(`Missing Kernel source ${current.sourceUnit} at ${current.diskPath}`); | ||
| } | ||
| sources[current.sourceUnit] = { content }; | ||
|
|
||
| for (const importPath of importedPaths(content)) { | ||
| pending.push(resolveImport({ | ||
| importerUnit: current.sourceUnit, | ||
| importerDiskPath: current.diskPath, | ||
| importPath, | ||
| kernelCheckout: root, | ||
| target, | ||
| })); | ||
| } | ||
| } | ||
|
|
||
| return sources; | ||
| } | ||
|
|
||
| function canonicalOutputSelection() { | ||
| return { '*': { '*': OUTPUT_SELECTION } }; | ||
| } | ||
|
|
||
| async function entryPointInput(aaCheckout, target) { | ||
| const debugPath = resolve( | ||
| aaCheckout, | ||
| 'artifacts', | ||
| 'contracts', | ||
| 'core', | ||
| 'EntryPoint.sol', | ||
| 'EntryPoint.dbg.json', | ||
| ); | ||
| const debugArtifact = await readJson(debugPath); | ||
| if (typeof debugArtifact.buildInfo !== 'string' || debugArtifact.buildInfo.length === 0) { | ||
| fail('EntryPoint debug artifact does not reference build-info'); | ||
| } | ||
| const buildInfo = await readJson(resolve(dirname(debugPath), debugArtifact.buildInfo)); | ||
| return validateEntryPointBuildInfo(buildInfo, target); | ||
| } | ||
|
|
||
| async function kernelInput(kernelCheckout, target) { | ||
| const sources = await collectKernelSources({ kernelCheckout, target }); | ||
| const primarySource = sources[target.sourcePath]?.content; | ||
| if (typeof primarySource !== 'string') { | ||
| fail(`${target.contractName} standard input is missing ${target.sourcePath}`); | ||
| } | ||
| validatePrimarySource(primarySource, target); | ||
|
|
||
| const settings = { | ||
| evmVersion: target.evmVersion, | ||
| optimizer: target.optimizer, | ||
| metadata: target.metadata, | ||
| remappings: [ | ||
| `solady/=${target.soladySourcePrefix}/`, | ||
| 'ExcessivelySafeCall/=lib/ExcessivelySafeCall/src/', | ||
| ], | ||
| outputSelection: canonicalOutputSelection(), | ||
| }; | ||
| if (target.viaIR) { | ||
| settings.viaIR = true; | ||
| } | ||
| return { language: 'Solidity', sources, settings }; | ||
| } | ||
|
|
||
| function publicTarget(target) { | ||
| const { diskSourcePath: _diskSourcePath, sourceFamily: _sourceFamily, soladySourcePrefix: _soladySourcePrefix, ...manifestTarget } = target; | ||
| return manifestTarget; | ||
| } | ||
|
|
||
| async function main() { | ||
| const [, , aaArgument, kernelArgument, ecdsaKernelArgument, outputArgument] = process.argv; | ||
| if (!aaArgument || !kernelArgument || !ecdsaKernelArgument || !outputArgument) { | ||
| fail( | ||
| 'usage: extract-mainnet-aa-verification-inputs.mjs <account-abstraction-checkout> <kernel-checkout> <ecdsa-kernel-checkout> <output-directory>', | ||
| ); | ||
| } | ||
|
|
||
| const aaCheckout = resolve(aaArgument); | ||
| const kernelCheckout = resolve(kernelArgument); | ||
| const ecdsaKernelCheckout = resolve(ecdsaKernelArgument); | ||
| const outputDirectory = resolve(outputArgument); | ||
| await mkdir(outputDirectory, { recursive: true }); | ||
|
|
||
| for (const target of targets) { | ||
| const input = target.sourceFamily === 'account-abstraction' | ||
| ? await entryPointInput(aaCheckout, target) | ||
| : await kernelInput( | ||
| target.sourceFamily === 'kernel-ecdsa' ? ecdsaKernelCheckout : kernelCheckout, | ||
| target, | ||
| ); | ||
| await writeFile( | ||
| join(outputDirectory, target.standardInputFile), | ||
| `${JSON.stringify(input, null, 2)}\n`, | ||
| 'utf8', | ||
| ); | ||
| } | ||
|
|
||
| const manifest = { | ||
| version: 2, | ||
| chainId: CHAIN_ID, | ||
| contracts: targets.map(publicTarget), | ||
| }; | ||
| await writeFile( | ||
| join(outputDirectory, 'manifest.json'), | ||
| `${JSON.stringify(manifest, null, 2)}\n`, | ||
| 'utf8', | ||
| ); | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error(error instanceof Error ? error.message : String(error)); | ||
| process.exitCode = 1; | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented-out imports (e.g.,
// import "./Foo.sol";or inside/* ... */blocks) are currently matched by the regular expressions inimportedPaths. This can cause the script to attempt to resolve non-existent or irrelevant files, leading to unexpected build crashes.To make the import extraction robust, strip single-line and multi-line comments from the Solidity source content before running the regex matches.