feat: add zksync EraVM source verification and Abstract chain#2788
feat: add zksync EraVM source verification and Abstract chain#2788coffeexcoin wants to merge 26 commits into
Conversation
|
Happy to break this up into smaller PRs by area (compiler then lib then api etc) if that is easier for review/merging |
The function always returns false for `vm-1.5.0-a167aa3` regardless of the `target` argument. That only happens to produce correct behavior because every caller compares against 1.5.0; for any other target (e.g. 1.0.0), the helper would mis-classify that pre-release as below the threshold. Hardcoding 1.5.0 in the name and the body makes the contract honest: this is specifically an "is zksolc ≥ 1.5?" check, and the vm- guard is the correct answer to that specific question. Updates the inline explanatory comment to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Explain that the -gnu suffix on the Windows platform string is the upstream MinGW filename (not a libc choice), and that the Linux candidate list carries both glibc and musl filenames for fallback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Document the zksolc / era-solc / upstream-solc compiler model: their naming and roles, the zksolc 1.5.0 CLI/output-selection split, the modern vs legacy release repos, and the Linux gnu/musl libc handling. Add a pointer comment at the top of zksolcCompiler.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers logic that previously ran only behind network-dependent integration tests or not at all: the pre/post-1.5 standard-JSON CLI argument mapping, the isZkSolcVersionAtLeastV15 edge cases (including the unparseable-version fail-open), the primary/legacy download fallback in getZkSolcExecutable, and the era-solc vs upstream-solc routing in getZkSolcBaseSolcExecutable. Network seams (fetchWithBackoff, getSolcExecutable) are stubbed via module-namespace reassignment so the tests stay offline and fast. getZkSolcStandardJsonArgs and getZkSolcBaseSolcExecutable are exported to make them testable, consistent with the file's existing pattern of exporting internal helpers. zksolcCompiler.ts coverage: lines 76% -> 87%, functions 90% -> 100%. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a ZKSOLC.md section describing how Sourcify spawns zksolc and how zksolc in turn spawns a solc backend via --solc, plus the zkSync compiler-toolchain diagram. Because zksolc spawns the backend as a native child process, the Emscripten soljson build can never be used for EraVM verification. That made solJsonRepoPath dead weight: it was threaded through useZkSolcCompiler, getZkSolcBaseSolcExecutable and getUpstreamSolcExecutable but never used -- yet still load-bearing as a guard condition, so omitting it silently disabled upstream-solc resolution. Remove it from those functions, from ZkSolcLocal, and from both ZkSolcLocal call sites; the guard now checks only solcRepoPath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| normalizedVersion: string, | ||
| ): string[] { | ||
| const primary = getZkSolcFileName(platform, normalizedVersion); | ||
| if (platform.endsWith('-gnu')) { |
There was a problem hiding this comment.
| if (platform.endsWith('-gnu')) { | |
| if (platform.startsWith('linux-')) { |
Nitpick. This looks like the correct check as windows does not have upstream -musl binary but the windows platform is windows-amd64-gnu so the windows case would also fall into the prev. statement.
| */ | ||
| export class ZkSolcCompilation extends AbstractCompilation { | ||
| public language: CompilationLanguage = 'Solidity'; | ||
| public readonly targetVM = 'eravm'; |
| readonly auxdataStyle: AuxdataStyle.ZKSYNC = AuxdataStyle.ZKSYNC; | ||
|
|
||
| public readonly zksolcVersion: string; | ||
| public readonly requestedSolcCompilerVersion: string; |
There was a problem hiding this comment.
I've gathered some notes and questions:
Notes for the next reviewer
1. The new runCompiler wrapper — design sign-off
This PR adds a protected abstract runCompiler() to AbstractCompilation, and
each compilation subclass (Solidity, Vyper, Fe, PreRun, ZkSolc) now
supplies its own implementation. It replaces the single hardcoded
this.compiler.compile(version, jsonInput, forceEmscripten) call in the base
class, which no longer fits zksolc's two-version (zksolcVersion +
solcVersion) signature.
Are we OK with this as the long-term shape? It's a clean template-method split
and it also lets us drop the old as any cast on jsonInput. The tradeoff is
that every future compilation type must now implement runCompiler. No
objection from me — flagging it for an explicit decision.
Changes I've made
1. Removed the unused solJsonRepoPath parameter
I dropped solJsonRepoPath from the zksolc compiler chain — useZkSolcCompiler,
getZkSolcBaseSolcExecutable, getUpstreamSolcExecutable, and ZkSolcLocal
plus its two call sites.
Reason: it was misleading. zksolc resolves its solc backend as a native
binary that it spawns via --solc — the Emscripten soljson JS build can
never be used here. The parameter was never read; worse, it was load-bearing as
a guard condition (&& solJsonRepoPath), so omitting it silently disabled
upstream-solc resolution. The guard now checks only solcRepoPath.
2. Added zksolc compiler unit tests
I added deterministic, offline unit tests to zksolcCompiler.spec.ts to raise
coverage of the new adapter: pre/post-1.5 CLI-arg mapping, isZkSolcVersionAtLeastV15
edge cases, download fallback, and era-solc vs. upstream-solc routing. Coverage
of zksolcCompiler.ts went from ~76%/60%/90% to ~87%/70%/100%
(lines/branches/functions).
Todos
1. Set the zksolc field in sourcifyeth/sourcify-chains
This PR adds a zksolc?: { supported: boolean } field to SourcifyChain and
SourcifyChainExtension. The per-chain extension data lives in the external
sourcifyeth/sourcify-chains repo — so this field still needs to be populated
there for the chains it applies to (the zkSync EraVM / zk-stack chains, e.g.
Abstract). Without it, those chains won't be flagged for zksolc verification.
TODO: open a follow-up in sourcifyeth/sourcify-chains to add zksolc to the
applicable chain entries.
2. Check the Etherscan import for EraVM contracts
Verify the "import from Etherscan" verification path works seamlessly for
zksync / EraVM contracts — i.e. that the Etherscan-API response for a
zksolc-compiled contract is parsed correctly (zksolc version, era-solc version,
settings) and the import produces a valid verification.
Questions to @coffeexcoin
1. era-solc forks vs. upstream solc — is era-solc actually required?
I don't fully follow when zksolc uses the era-solc forks (e.g. 0.8.30-1.0.2)
versus plain upstream solc binaries. From getZkSolcCompilerVersionCandidates,
the candidate list is built as:
- submitted version is already era-solc (
0.8.26-1.0.1) → that exact edition only - submitted version is commit-bearing (
v0.8.26+commit.…) → upstream solc
first, era-solc editions as fallback - submitted version is a plain release (
0.8.26) → era-solc editions only
Since upstream solc works as a zksolc backend and is even tried first for
commit-bearing versions: what is the actual purpose of era-solc vs. just
using the native upstream solc binary? What does the Matter Labs fork change
that plain solc can't provide?
2. We skip CBOR decoding for the EraVM trailer — but it looks CBOR-encoded
The PR treats the EraVM bytecode trailer as a non-CBOR, bare 32-byte hash:
bytecode-utilssplitAuxdatareturns[bytecode]forAuxdataStyle.ZKSYNC
— i.e. "no auxdata to split out";VerificationpassesvalidateCbor: falsetoextractAuxdataTransformation;ZkSolcCompilation.generateEraVmBytecodeHashPositionmarks only the final
32 bytes as the auxdata region.
But the on-chain bytecode of a real Abstract contract says otherwise. The
deployed bytecode of
https://abscan.org/address/0x9cc639c9855cf9e959c47a7bdf1c3c14468a270f ends with:
a2 64 "ipfs" 5822 1220<32-byte multihash>
64 "solc" 7824 "zksolc:1.5.15;solc:0.8.24;llvm:1.0.1"
0055
That is a standard Solidity-style CBOR auxdata map followed by a 2-byte
length (0x0055 = 85 → ~87-byte trailer) — exactly the EVM solc layout, just
with a combined zksolc;solc;llvm version string. It is not a bare 32-byte
hash.
Clarification needed:
- Where is the EraVM "bytecode hash" appended, and where is it used? Is it
distinct from theipfshash inside the CBOR map above? - Which zksolc versions emit a CBOR auxdata vs. a bare bytecode hash, and what
does themetadata.bytecodeHash/hashTypesetting control across versions?
The CBOR above can be decoded with the Sourcify playground:
https://playground.sourcify.dev/?bytecode=0xa2646970667358221220c8a1d50ed58fe61338ad26340368b3b993ac100ede6d05bd18ade2d125d114bf64736f6c6378247a6b736f6c633a312e352e31353b736f6c633a302e382e32343b6c6c766d3a312e302e310055
3. Runtime vs. creation bytecode for EraVM — need more context
ZkSolcCompilation assigns creationBytecode and runtimeBytecode to the
same value (evm.bytecode.object), and generateCborAuxdataPositions
always sets creationBytecodeCborAuxdata = {} while runtime gets the hash
position. I'd like more context on the intent here.
The generic Verification flow still runs the creation path: when
creatorTxHash is set it fetches an "onchain creation bytecode" and compares it
to compilation.creationBytecode. But on zkSync that doesn't line up with the
EVM model. Example — this Abstract contract:
https://abscan.org/address/0x9cc639c9855cf9e959c47a7bdf1c3c14468a270f#code
Its "Contract Creation Code" is not EVM init code. It is calldata to the
ContractDeployer system contract: a create selector (9c4d535b), a salt,
the EraVM versioned bytecode hash (0100…), then ABI-encoded constructor args
(the trailing zero-padded address). The contract bytecode itself is not in the
tx — it is referenced by hash and shipped in the tx's factory_deps. That's
why the explorer's "creation code" looks like a block of ABI-encoded words.
Questions:
- Is there a concept of creation-bytecode matching for EraVM at all? If so,
should we take the runtime code as the creation code (as the PR does), or do
we need theContractDeployercalldata, as in the abscan example above? - Depending on that answer, what should
creationBytecodeCborAuxdatabe?
4. What artifacts should an EraVM verification store?
zksolc seems to emit only abi, metadata, and evm.bytecode.object, so most
compiled_contracts artifact fields end up null/{} (no deployedBytecode,
no source maps; creation cborAuxdata = {}, runtime = the 32-byte position;
immutableReferences = {}).
Is that the full set? Does zksolc ever emit linkReferences, immutable
references, or anything else? What are the complete outputs we actually get from
a zksolc compilation? We should have test cases that both document and assert
this expected artifact set — the way #2692 did for Fe.
Posted with Claude Code
|
Another point we should consider is if and how we'll handle VerA for this case. |
For current zksolc versions, upstream solc is not a valid backend anymore. Since zksolc 1.5.8, using upstream solc is explicitly prohibited by zksolc The reason upstream solc still appears in the candidate logic is historical/API compatibility. Older zksolc versions did allow upstream solc, and explorer/Etherscan-style APIs may submit the upstream long version, e.g. So |
Good catch. EraVM runtime bytecode can have two different metadata trailer shapes: Updated with 1ad9de5
Pushed a fix is to make ZKsync auxdata detection format-sensitive instead of always treating the final 32 bytes as the auxdata region. The updated logic now:
The I also added tests for:
|
EraVM does not have EVM-style initcode matching in the normal sense. as ZKSync stores a single copy of the contract bytecode and the chain references that single copy of bytecode for all matching contracts using the stored bytecode hash. For native EraVM contracts, zksolc’s emitted bytecode is the bytecode preimage that gets published as a factory dependency and then installed by hash. The deployment transaction does not carry
The actual contract bytecode is provided separately through This is also how the Matter Labs tooling models it:
So in this PR, For verification, the meaningful bytecode match for EraVM is the deployed/runtime bytecode, or equivalently the factory-dep bytecode identified by the versioned bytecode hash. The ContractDeployer calldata is useful for deployment metadata, especially constructor args and bytecode-hash correlation, but it is not “creation bytecode” in the EVM initcode sense. Given the current PR scope, I think Longer term, for first-class EraVM deployment matching, it should be a ZKsync-specific path:
But that would be separate from EVM creation-bytecode matching. For this PR, runtime bytecode matching plus runtime auxdata handling is a minimal compatibility surface, and creation auxdata should stay empty. |
For EraVM, It is not fully accurate to say that the only possible outputs are I updated this to make the generic artifact handling explicit:
So the expected stored artifact set is now:
I also added tests covering the version-gated output selection, generic artifact export/storage, link-reference fallback, and the three Abstract EraVM sample contracts from the PR body. |
Summary
Adds ZKsync EraVM/zksolc source verification support to Sourcify, extending the standard JSON verification API surface.
Zksolc verification is selected by passing
zksolcVersionto the normal/v2/verify/{chainId}/{address}JSON-input flow The underlyingcompilerVersionremains the solc or era-solc version requested by the user, and Sourcify records the final era-solc/solc version used for the verified compilation.Changes by module
packages/compilers@ethereum-sourcify/compilers.zksolc-bin.--standard-jsonand--solc.settings.enableEraVMExtensions/settings.isSystemto--system-modesettings.forceEVMLA/settings.forceEvmlato--force-evmlaevmoutput for legacy zksolc versionsv0.8.26+commit.8a97fa7aby resolving Sourcify's normal native solc binaries and passing them to zksolc.packages/lib-sourcifyZkSolcCompilationfor EraVM Solidity compilations.IZkSolcCompilerand includes zksolc in the shared compilation type surface.Soliditybut marks the compilation target VM aseravm.0.8.26-1.0.10.8.26v0.8.26+commit.8a97fa7a1.0.2,1.0.1, and1.0.01.0.2for zksolc versions before 1.5VerificationExport:compiler: "zksolc"compilerVersion: zksolc versionzksolc.solcCompilerVersion: final underlying solc/era-solc version used1.5.xwith newer solc/era-solc behavior1.4.1 + 0.8.4-1.0.11.3.17 + 0.7.6-1.0.1services/serverzksolcVersionto the request body.ZkSolcJsonInputas a Solidity standard JSON superset for zksolc-specific settings.zksolcVersionand zksolc-specific settings.zksolcVersionis present when zksolc-specific settings are usedZkSolcLocaland passes zksolc/era-solc/solc repository paths through CLI and worker initialization.zksolcRepoanderaSolcRepo.compiler = "zksolc"version = <zksolcVersion>additional_input.era_solc_version = <final underlying solc/era-solc version>Chain configuration
zksolc.supported: true | falseSourcifyChainand generated chain config loading.2741https://api.mainnet.abs.xyzzksolc.supported: trueDatabase
additional_input.era_solc_versiononcompiled_contracts.services/database/database-specssubmodule.Verification behavior
For zksolc requests, supports both explicit era-solc compiler versions and normal Solidity compiler versions:
compilerVersionis an era-solc version, Sourcify uses that exact era-solc binary.compilerVersionis a commit-bearing Solidity release, for examplev0.8.26+commit.8a97fa7a, Sourcify first tries the exact upstream solc binary through zksolc.compilerVersionis a plain Solidity release, Sourcify expands it directly to compatible era-solc candidates.This is intended to support explorer-style submissions where users only know the Solidity compiler version while still allowing exact era-solc requests when that is known; this supports existing tooling and legacy verifications/compile artifacts that may not bear the exact solc fork edition (v1.0.x)
Testing
Automated coverage added or expanded:
packages/compilers/test/zksolcCompiler.spec.tspackages/lib-sourcify/test/Compilation/ZkSolcCompilation.spec.tspackages/lib-sourcify/test/SourcifyChain.spec.tsservices/server/test/integration/apiv2/verification/verify.json.spec.tsservices/server/test/unit/VerificationService.spec.tsservices/server/test/unit/utils/database-util.spec.tsservices/server/test/unit/verificationWorker.spec.tsManual verification performed against Abstract Mainnet contracts from abscan/Etherscan-compatible source metadata, including exact upstream solc fallback behavior for commit-bearing compiler versions.
Candidate contracts tested include:
https://abscan.org/address/0xbc176ac2373614f9858a118917d83b139bcb3f8c#code - zksolc 1.5.7, solc v0.8.26+commit.8a97fa7a (resolves to v0.8.26-1.0.1)
https://abscan.org/address/0x4f7589c619d59443db52489dd375de63e03e671d#code - zksolc v1.3.19, solc v0.6.12+commit.27d51765 (resolves to direct v0.6.12+commit.27d51765)
https://abscan.org/address/0x0929d81a73a83b73e5de2ba63a15ce2a18addbe2#code - zksolc v1.5.15, solc v0.8.26+commit.8a97fa7a (resolves to v0.8.26-1.0.2)