Skip to content

feat: add zksync EraVM source verification and Abstract chain - #2788

Open
coffee-the-dev wants to merge 54 commits into
argotorg:stagingfrom
coffee-the-dev:feat/zksync
Open

coffee-the-dev wants to merge 54 commits into
argotorg:stagingfrom
coffee-the-dev:feat/zksync

Conversation

@coffee-the-dev

Copy link
Copy Markdown

Summary

Adds ZKsync EraVM/zksolc source verification support to Sourcify, extending the standard JSON verification API surface.

Zksolc verification is selected by passing zksolcVersion to the normal /v2/verify/{chainId}/{address} JSON-input flow The underlying compilerVersion remains 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

  • Adds a local zksolc compiler adapter exported from @ethereum-sourcify/compilers.
  • Downloads and caches zksolc binaries from the current Matter Labs release repository, with legacy pre-1.5 binary support from zksolc-bin.
  • Downloads and caches era-solc binaries from the Matter Labs era-solidity releases.
  • Invokes zksolc through standard JSON using --standard-json and --solc.
  • Handles pre-1.5 zksolc CLI differences/nuances:
    • maps settings.enableEraVMExtensions / settings.isSystem to --system-mode
    • maps settings.forceEVMLA / settings.forceEvmla to --force-evmla
    • avoids requesting unsupported evm output for legacy zksolc versions
  • Supports exact upstream Solidity compiler versions like v0.8.26+commit.8a97fa7a by resolving Sourcify's normal native solc binaries and passing them to zksolc.
  • Adds zksolc compiler tests with broader parity coverage against solc compiler, including version normalization, binary URL patterns, legacy repositories, pre/post-1.5 argument differences, and upstream solc selection.

packages/lib-sourcify

  • Adds ZkSolcCompilation for EraVM Solidity compilations.
  • Adds IZkSolcCompiler and includes zksolc in the shared compilation type surface.
  • Keeps the language as Solidity but marks the compilation target VM as eravm.
  • Normalizes and expands compiler-version candidates for zksolc:
    • exact era-solc versions like 0.8.26-1.0.1
    • solc release versions like 0.8.26
    • commit-bearing solc versions like v0.8.26+commit.8a97fa7a
  • For commit-bearing solc versions, tries the exact upstream solc first, then falls back through compatible era-solc editions.
  • Enforces known valid era-solc edition combinations:
    • supports editions 1.0.2, 1.0.1, and 1.0.0
    • excludes 1.0.2 for zksolc versions before 1.5
    • excludes unavailable Solidity/era-solc combinations
  • Retries zksolc verification inside the normal verification flow when a candidate compiles but does not match bytecode.
  • Adjusts output selection so required zksolc outputs are requested without dropping user-provided output selections.
  • Treats zksolc byte-for-byte matches as perfect matches even though EraVM auxdata does not follow normal EVM CBOR metadata assumptions.
  • Disables strict CBOR validation for zksolc bytecode matching, while keeping existing Solidity/Vyper/Fe behavior intact.
  • Exports zksolc compilation metadata in VerificationExport:
    • compiler: "zksolc"
    • compilerVersion: zksolc version
    • zksolc.solcCompilerVersion: final underlying solc/era-solc version used
  • Adds a zksolc compilation test matrix covering:
    • zksolc 1.5.x with newer solc/era-solc behavior
    • pre-1.5 zksolc combinations such as 1.4.1 + 0.8.4-1.0.1
    • older zksolc combinations such as 1.3.17 + 0.7.6-1.0.1
    • invalid era-solc/zksolc combinations
    • compile retry and match retry behavior
    • metadata object parsing
    • perfect vs partial match behavior

services/server

  • Add zksolc verification on the existing JSON-input verification endpoint.
  • Adds optional zksolcVersion to the request body.
  • Adds ZkSolcJsonInput as a Solidity standard JSON superset for zksolc-specific settings.
  • Detects zksolc verification from zksolcVersion and zksolc-specific settings.
  • Rejects zksolc requests unless:
    • the input language is Solidity
    • zksolcVersion is present when zksolc-specific settings are used
    • the target chain has zksolc support enabled
  • Adds ZkSolcLocal and passes zksolc/era-solc/solc repository paths through CLI and worker initialization.
  • Adds config entries for zksolcRepo and eraSolcRepo.
  • Updates API docs to describe zksolc verification via Solidity standard JSON and to show an EraVM example request.
  • Updates persisted verification output mapping so zksolc verified contracts store:
    • compiler = "zksolc"
    • version = <zksolcVersion>
    • additional_input.era_solc_version = <final underlying solc/era-solc version>
  • Adds tests for service validation, worker threading, API request behavior, and database column mapping.

Chain configuration

  • Adds chain-level zksolc capability metadata:
    • zksolc.supported: true | false
  • Threads this field through SourcifyChain and generated chain config loading.
  • Enables Abstract Mainnet in the default local chain config:
    • chain id 2741
    • RPC https://api.mainnet.abs.xyz
    • zksolc.supported: true
  • Non-zksolc chains reject zksolc verification attempts before worker compilation.

Database

  • Adds a Sourcify-owned migration allowing additional_input.era_solc_version on compiled_contracts.
  • Updates the Sourcify database schema snapshot accordingly.
  • Does not update the upstream services/database/database-specs submodule.

Verification behavior

For zksolc requests, supports both explicit era-solc compiler versions and normal Solidity compiler versions:

  • If compilerVersion is an era-solc version, Sourcify uses that exact era-solc binary.
  • If compilerVersion is a commit-bearing Solidity release, for example v0.8.26+commit.8a97fa7a, Sourcify first tries the exact upstream solc binary through zksolc.
  • If the exact upstream solc candidate fails to compile or fails bytecode matching, Sourcify falls back through compatible era-solc candidates until one matches or all candidates are exhausted.
  • If compilerVersion is 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.ts
  • packages/lib-sourcify/test/Compilation/ZkSolcCompilation.spec.ts
  • packages/lib-sourcify/test/SourcifyChain.spec.ts
  • services/server/test/integration/apiv2/verification/verify.json.spec.ts
  • services/server/test/unit/VerificationService.spec.ts
  • services/server/test/unit/utils/database-util.spec.ts
  • services/server/test/unit/verificationWorker.spec.ts

Manual 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)

@coffee-the-dev
coffee-the-dev marked this pull request as ready for review May 13, 2026 13:09
@coffee-the-dev

Copy link
Copy Markdown
Author

Happy to break this up into smaller PRs by area (compiler then lib then api etc) if that is easier for review/merging

@kuzdogan kuzdogan self-assigned this May 19, 2026
kuzdogan and others added 2 commits May 19, 2026 19:16
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 <[email protected]>
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 <[email protected]>
kuzdogan and others added 3 commits May 20, 2026 09:45
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
kuzdogan and others added 3 commits June 26, 2026 15:13
zksolc verification was gated both by server config (compiler repos) and by a
per-chain `zksolc.supported` flag. Drop the per-chain dependency so zksolc is
purely a server capability: zksolc bytecode only matches EraVM chains, so a
request on a non-EraVM chain simply fails to match instead of needing an
explicit allowlist.

- remove the chain `zksolc.supported` check from validateZkSolcRequest
- remove the `zksolc` field from SourcifyChainExtension/SourcifyChain and its
  chain construction/serialization
- update tests: the request is now rejected only when zksolc is disabled on the
  server (isZkSolcEnabled), not per chain

Co-Authored-By: Claude Opus 4.8 <[email protected]>
… code

zksolc request validation moved to the API middleware (validateZkSolcRequest),
which throws the request-level InvalidParametersError. The verification-level
"invalid_parameter" code left in VerificationErrorCode/getVerificationErrorMessage
is no longer produced by any code path, so remove it; invalid parameters are
reported with the existing request-level invalid_parameter error code.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…mpilation compiler identity

Each Compilation maps 1:1 to a compiler, so the compiler name belongs on the
compilation rather than being spread from an unknown-shape metadata object or
inferred from the language (which is ambiguous: Solidity -> solc or zksolc).

- remove compilationExportMetadata and the CompilationExportMetadata type
- add abstract compilerName getter; SolidityCompilation -> solc,
  VyperCompilation -> vyper, FeCompilation -> fe, ZkSolcCompilation -> zksolc
  (YulCompilation inherits solc)
- PreRunCompilation reconstructs any language, so it derives compilerName from
  the language (or zksolc when the stored version is a zksolc toolchain string)
- add resolvedCompilerVersion getter: defaults to compilerVersion, overridden
  by zksolc to the combined zksolc:<v>;solc:<v> string (compilerVersion stays a
  plain semver for the Solidity heuristics in Verification)
- Verification.export() sets compiler/compilerVersion explicitly
- compiler is now required on VerificationExport; drop the unused zksolc export
  field (solcCompilerVersion is already encoded in the combined version)
- move getCompilerNameFromLanguage into lib-sourcify; remove it from the server

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@coffee-the-dev

Copy link
Copy Markdown
Author

Most important question from my side: Why should we have retries? This deviates from our current paradigm that the user should know which is the correct compiler version.

The key problem is that the user doesn't really control this at all, and is reliant on their tooling which seems to be broken all over the place. They can at most set solc version + zksolc version in configs, but the actual semantic "edition" is not controllable or even known by the developer/user at compile time.

@kuzdogan kuzdogan moved this from Sprint - Needs Review to Sprint - Up Next in Sourcify Public Jul 2, 2026
@kuzdogan kuzdogan moved this from Sprint - Up Next to Sprint - In Progress in Sourcify Public Jul 6, 2026
kuzdogan and others added 12 commits July 7, 2026 10:58
Split the interleaved zksolc logic out of the shared bytecode.ts into a
clean 3-file DAG:

- cbor.ts:  generic CBOR-tail primitives + AuxdataStyle enum (no EraVM
            knowledge). Holds splitCborAuxdata and the extracted
            decodeSolidityCborObject that EraVM reuses.
- eravm.ts: all EraVM-specific logic — splitEraVmAuxdata, the new
            decodeEraVmAuxdata (padding strip), and ERA_VM_* constants.
            Depends only on cbor.ts.
- bytecode.ts: public facade — decode/splitAuxdata dispatch and the
            Solidity/Vyper branches. Its only EraVM awareness is two thin
            dispatch lines; it re-exports the CBOR public API so the
            package's exported surface is unchanged.

Move the four EraVM test cases + fixtures into a dedicated eravm.spec.ts.
Update the stale bytecode.ts reference in ZKSOLC.md to eravm.ts.

No behavior change: all 22 bytecode-utils tests pass and lib-sourcify
consumers need no changes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…p version retries

- Add ZkSolcVerification with EraVM creation matching via the versioned bytecode
  hash (sha256 of the recompiled runtime bytecode, referenced in the
  ContractDeployer create/create2 calldata). Reuses the canonical matchBytecodes
  + constructor-args transform; creationMatch inherits the runtime match type.
- Add eraBytecodeHash() to bytecode-utils (0x0100 | lenWords | sha256[-28:]).
- Gate the four solc-only steps in Verification (source-id <0.3.6, extra-file and
  IR-ordering bugs, exact runtime-length) on the compiler (isSolidityViaSolc)
  instead of the language, so zksolc (language 'Solidity', targets EraVM) is
  excluded while Solidity and Yul stay unchanged.
- Remove the version-candidate retry/iteration from the verification flow
  (Verification retry wrapper, AbstractCompilation.useNextCompilerVersionCandidate,
  ZkSolcCompilation candidate iteration + compile retry). ZkSolcCompilation now
  takes the concrete {zksolc, solc} versions; candidate resolution will be
  reinstated at a higher (Etherscan) level.
- Instantiate ZkSolcVerification on-site in the worker (compilerName === 'zksolc').

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ompilation

It was defined in AbstractCompilation but only PreRunCompilation reconstructs
a compiler name from a stored language; single-compiler compilations know
their compiler directly. Colocate it with its only consumer. Still exported
via the PreRunCompilation barrel, so the public API is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…inline

The runCompiler override point wrapped only the main compile path and was a
thin passthrough in every solc/vyper/fe compilation. Restore the base
compileAndReturnCompilationTarget to call compiler.compile directly (as before
the zksolc work) and let ZkSolcCompilation, which already fully overrides
compileAndReturnCompilationTarget, invoke its two-version compiler.compile
internally. Leaves the other compilations untouched.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ata structurally

Rework EraVM creation matching so it no longer borrows the Solidity
[creationCode][args] prefix model, which doesn't fit EraVM:

- Gate on the creation tx targeting the ContractDeployer system contract
  (0x…8006), so only direct deploys are matched. The check lives entirely in
  ZkSolcVerification; the base class only gains a generic protected creationTxTo
  field kept off the already-fetched creation tx.
- Replace normalizeDeployerCalldata (positional slice + concat into a fake
  creation bytecode) with decodeContractDeployerCalldata, a single ABI decode
  against the selector's param types that yields the versioned bytecode hash and
  the constructor args directly.
- Match by hash equality (decoded bytecodeHash vs eraBytecodeHash of the
  recompiled runtime bytecode), not a byte-prefix comparison.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
zksolc support was removed from SourcifyChain / SourcifyChainInstance (zksolc
is derived from the compilerVersion input, not the chain object), but this test
still referenced sourcifyChain.zksolc and no longer compiled — a tsc error that
was failing the whole SourcifyChain.spec.ts suite.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Collapse the compilerVersion/resolvedCompilerVersion duality: ZkSolcCompilation
and PreRunCompilation now keep the combined `zksolc:<v>;solc:<v>` string as
compilerVersion (overriding the base cleanCompilerVersion, which would strip the
prefix) and parse the zksolc/solc versions internally. Remove the
resolvedCompilerVersion getter from AbstractCompilation, ZkSolcCompilation, and
PreRunCompilation; export() now reads compilerVersion. This also fixes the
similarity re-verify path, which passed the plain zksolc semver (misdetected as
plain Solidity) instead of the composite.

Drop the makeZkSolcCompilation test helper and pass the composite string
directly at all call sites, matching production and the PreRun test helper.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add end-to-end verification tests for native zkSync EraVM (zksolc) contracts,
replaying 14 already-verified Abstract mainnet contracts against the public RPC
(ZkSolcVerification.e2e.spec.ts + committed source fixtures). Coverage spans the
keccak (<=1.5.12) and CBOR (>=1.5.13) metadata eras, legacy zksolc down to 1.3.8,
explicit and resolved era-solc editions, constructor args, and perfect/partial
runtime plus perfect/null creation matches. The suite self-skips when the RPC is
unreachable or ZKSYNC_E2E=false.

Also move the unit-level EraVM matching tests out of ZkSolcCompilation.spec.ts
into ZkSolcVerification.spec.ts, and extract the shared zksolc mock helpers into
test/utils/zksolcTestHelpers.ts. Add a real zksolc IZkSolcCompiler to test/utils.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…storage

Mark stored bytecode with the VM it targets so EraVM (zksolc) contracts are
handled distinctly from EVM ones:

- lib-sourcify: add a `Vm` type and a `compilation.vm` getter ('evm' by
  default, 'eravm' for ZkSolcCompilation); expose it on VerificationExport.
- db: add a `vm` column to the `code` table (default 'evm'); `insertCode`
  only writes it for non-EVM bytecode so the shared insert stays compatible
  with the Verifier Alliance schema (which has no `vm` column).
- Skip EraVM contracts when storing to the Verifier Alliance database — they
  are stored only in the Sourcify database.
- EtherscanVerify: submit EraVM contracts with the zksync fields
  (compilermode=zksync, zksolcVersion, solc `compilerversion`); keep skipping
  EraVM for Blockscout (separate API) and Routescan (unverified support).

Adds unit + integration coverage for the code `vm` tagging, the Alliance skip,
and the Etherscan zksync payload.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Keep EraVM (zksolc) contracts out of all external explorer verify backends for
now (Etherscan/Blockscout/Routescan); they are stored only in the Sourcify DB.
Etherscan does support EraVM verification (compilermode=zksync + zksolcVersion)
and this can be enabled later — see PR notes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
… shape

The verifyFromJsonInput/similarity deep-equal fixture was missing `compiler`
and (after this PR) `vm` from the expected `compilation` object, failing all
7 assertVerificationExport cases. Add both keys.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ed chain

Adds an end-to-end zksolc/EraVM verification that runs the real worker
(real zksolc + era-solc compilation) against a mocked Abstract chain, then
asserts the full /v2/contract response.

- MockRpcServer: a local JSON-RPC server replaying captured Abstract mainnet
  responses, so the verification worker (separate thread, can't be nock'd)
  reads deterministic on-chain data with no live RPC.
- Fixture: DealersMulticall on Abstract (2741) — CBOR metadata, constructor
  args, perfect runtime + perfect creation match. input.json + captured
  eth_getCode/getTransactionByHash/getTransactionReceipt.
- Asserts match statuses, compilation identity (compiler=zksolc, combined
  version), runtime/creation transformations (constructorArguments), deployment
  info, and that stored code is tagged vm='eravm'.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@kuzdogan

Copy link
Copy Markdown
Member

I'm getting a bit overwhelmed by it so I think I need someone else to look at this.

I think it's nicely isolated from the normal verification logic. I'm just not too comfortable with the testing. I've mostly vibecoded it to import various cases from Etherscan so I'm not sure to what extent we are able to cover cases. Maybe we don't have to have a perfect e2e solution and we can keep fixing things along the way.

I've left the "compiler iteration" completely outside the scope. As is, it would not be possible to import from Etherscan as the solc version will be incomplete. We either offload this to people importing from Etherscan or maybe have some iteration on the server side.

Most of the added files and code are tests so don't get surprised by 92 files added.

@manuelwedler manuelwedler left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit cautious on this feature, so maybe I added a bit more nitpicky comments. But in general the new design choices look good and I think we can get it merged soon.

): Promise<void> {
const releaseTag = stripTagVersionV ? stripLeadingV(version) : version;
const encodedURIFilename = encodeURIComponent(fileName);
const githubCompilerURI = `${host}${releaseTag}/${encodedURIFilename}`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: the attacker-controlled compilerVersion reaches this URL and the on-disk path with no validation.

For zksolc the only gate on compilerVersion is the new OpenAPI pattern ^zksolc:.+;solc:.+$ (apiv2.yaml). validateCompilerVersion in middlewares.ts is not wired into any route, and parseZkSolcCompilerVersion splits on ([^;]+);solc:(.+). So both halves are arbitrary strings, slashes and .. included, and they flow straight into path.join() and into this template.

Chain for the zksolc half (the era-solc half is identical via getEraSolcExecutable):

  1. normalizeZkSolcVersion only prepends v, so ../../../../owner/repo/releases/download/tag survives.
  2. getZkSolcFileName builds zksolc-<platform>-v<version>, and path.join(zksolcRepoPath, fileName) resolves the .. segments — the write target escapes the compiler repo directory.
  3. Here releaseTag is inserted raw (only fileName is encodeURIComponent-ed), so fetch() normalizes the .. segments and the request leaves matter-labs/era-compiler-solidity for an arbitrary owner/repo on github.com. I confirmed this resolves: https://github.com/matter-labs/era-compiler-solidity/releases/download/ + ../../../../attacker/repo/releases/download/v1/evil becomes https://github.com/attacker/repo/releases/download/v1/evil, and GitHub serves it (302 to the asset CDN).
  4. On HTTP 200 fetchAndSaveCompiler does mkdirSync(recursive) + writeFileSync(..., { mode: 0o755 }) at the traversed path, and validateCompilerPath then runs spawnSync(compilerPath, ['--version']) on it.

So a single unauthenticated POST /v2/verify/:chainId/:address can make the worker write an executable outside the compiler repo directory and execute it. Reachable on master/staging, where zksolcRepo/eraSolcRepo are configured and isZkSolcEnabled is therefore true.

The pre-existing solc path is permissive too (^v?\d+\.\d+\.\d+.*$ allows a trailing /..), but it is pinned to binaries.soliditylang.org, which only serves official binaries. The new zksolc path is what makes the owner/repo attacker-selectable.

Suggested fix: validate both halves against a strict charset before they are used, ideally in validateZkSolcRequest so it is rejected with a 400 rather than deep in the worker — e.g. zksolc ^v?\d+\.\d+\.\d+$ plus the vm-1.5.0-a167aa3 special case, and solc ^(zkVM-)?v?\d+\.\d+\.\d+(-1\.0\.\d+|\+commit\.[0-9a-fA-F]+)?$. Tightening the ZkSolcVersion pattern in apiv2.yaml alone would help, but a defensive check at the path.join/URL boundary is worth having regardless.

);
}

export function getZkSolcCompilerVersionCandidates(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getZkSolcCompilerVersionCandidates is never called outside its own module and the tests — the candidate-expansion/fallback feature it implements no longer exists.

$ grep -rn "getZkSolcCompilerVersionCandidates" packages services --include=*.ts | grep -v test
packages/lib-sourcify/src/Compilation/ZkSolcCompilation.ts:189

compile() now does a single compiler.compile(zksolcVersion, solcCompilerVersion, jsonInput), and the retry machinery (useNextCompilerVersionCandidate, retryWithNextCompilerVersionCandidate, resetVerificationAttempt) is gone. That removal is good and resolves my earlier threads. What is left over is the dead function, its supporting helpers (ERA_SOLC_EDITIONS, isSupportedEraSolcVersion, isEraSolcEditionAvailable, isEraSolcEditionCompatibleWithZkSolc, MAX_ERA_SOLC_1_0_0_SOLIDITY_VERSION), and the whole ZkSolcCompilerVersionCandidates describe block, which now asserts behaviour production never runs.

The docs still describe the removed behaviour, so they read as a feature list that the code does not implement:

  • PR description: "If the exact upstream solc candidate fails to compile or fails bytecode matching, Sourcify falls back through compatible era-solc candidates until one matches or all candidates are exhausted."
  • ZKSOLC.md: "If the solc half is missing the -1.0.0 edition suffix, the actual era edition is found by iterating over the candidates" and "ZkSolcCompilation tries upstream solc first (when given a commit-bearing version), then falls back through era-solc candidates."
  • zksync-fixtures/README.md: "resolved from a commit-form version by candidate expansion."

Concretely, zksolc:1.5.10;solc:0.8.26 now routes to the upstream solc downloader and never tries 0.8.26-1.0.2/1/0.

Either drop the dead code and correct the three docs, or re-wire the candidate expansion if dropping it was not intended. Worth being explicit about which, since it changes what a caller has to supply.

// ZKSYNC_E2E=false) so offline runs / default CI stay green. It downloads
// zksolc + era-solc binaries on first run.

const ENABLED = process.env.ZKSYNC_E2E !== 'false';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This defaults the EraVM e2e suite on, so a normal npm test in lib-sourcify reaches the public internet. The file matches the test/**/*.spec.ts glob in the test script, so CI (test-lib-sourcify) picks it up and, per fixture, downloads zksolc + era-solc binaries from GitHub and reads on-chain state from https://api.mainnet.abs.xyz.

The preflight getBytecode skip keeps a fully offline run green, but it does not help the cases that actually hurt: a reachable-but-slow or rate-limited Abstract RPC, a reorg-independent RPC behaviour change, or a GitHub outage mid-suite, any of which turn into red builds unrelated to the change under test. It also makes the runtime of the default suite depend on network conditions.

Prefer opt-in: process.env.ZKSYNC_E2E === 'true', and enable it explicitly in whichever CI job should own it. The server-side counterpart in verify.zksolc.e2e.spec.ts already got this right with MockRpcServer replaying captured responses — same treatment here would make the suite deterministic, though the compiler downloads would remain.

type: object
nullable: true
description: Additional compiler input fields from the top level of the standard JSON input that are not part of `settings`. Currently contains Vyper's `storage_layout_overrides` when used.
description: Additional compiler input fields from the top level of the standard JSON input that are not part of `settings`, plus compiler plumbing metadata needed to reproduce non-standard compilations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the combined zksolc:<v>;solc:<v> version string carries the toolchain, nothing zksolc-specific is written to additional_input any more — getAdditionalInputFromVerification only ever sets storage_layout_overrides. So "plus compiler plumbing metadata needed to reproduce non-standard compilations" describes a field that no longer exists, and the properties block below still lists only storage_layout_overrides.

Reads like a leftover from the era_solc_version design. Suggest restoring the original sentence.

* @returns The decoded object
*/
export const decodeEraVmAuxdata = (auxdata: string): SolidityDecodedObject => {
return decodeSolidityCborObject(auxdata.replace(/^(?:00)+/, ''));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This strip resolves the "not decodable" half of my earlier padding comment, but only inside this library. The stored cborAuxdata.value still carries the leading zero padding, so a plain CBOR decode of what the API and the database hand out throws (Data read, but end of buffer not reached), with or without the trailing length bytes. For the 1.5.15 fixture that is 41 bytes of padding in front of the map.

The decision is explained well in ZKSOLC.md, but that is a reviewer-facing doc in the compilers package. None of the surfaces a consumer actually reads mention it:

  • apiv2.yaml: every cborAuxdata description reads as if the value decodes like the Solidity one.
  • packages/bytecode-utils/README.md: does not mention AuxdataStyle.ZKSYNC at all, nor that decode() handles the padding for you.

Please document it there — that for EraVM contracts the value is [zero padding][cbor][length], and that consumers must strip leading zero bytes before decoding (or use decode(bytecode, AuxdataStyle.ZKSYNC) from bytecode-utils).

// pass
}

let creationLinkReferencesFallback: LinkReferences | undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional, since it changes more than this PR: this ZKSYNC-only fallback could go away if export() used the compilation getters (creationLinkReferences / runtimeLinkReferences) for every language, which is what the matching logic already does.

Why it is not a plain swap today: the server stores the exported value with || null (database-util.ts lines 794 and 815). Solidity always emits linkReferences: {} because the default output selection asks for it, so nothing changes there. Vyper and Fe have no such field, so they are stored as null today, and the getters would turn that into {}. Same meaning, different stored value.

Clean version: use the getters here for all languages, and let the server map an empty object to null, so stored rows stay as they are. That removes the special case and the two try/catch blocks. Also note the creation half of this fallback is a no-op: the getter reads the same evm.bytecode.linkReferences field and only turns undefined into {}.

Fine to leave for a follow-up if you prefer to keep this PR focused.

export const solc = selectedSolidityCompiler;

// zksolc (zksync EraVM) is optional
const zksolcRepoPath = config.has("zksolcRepo")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer undefined over "" as the "disabled" state, and carry it through the types. The empty string is a sentinel the type system cannot see: VerificationServiceOptions declares both paths as plain string, so "" travels on as if it were a path.

Where it matters: verificationWorker.ts line 71 does not check at all. It always builds ZkSolcLocal from the worker data, with "" when zksolc is disabled, and path.join("", fileName) then resolves against the working directory. The API middleware keeps zksolc jobs out when disabled, so nothing reaches it today, but the worker itself has no guard, unlike this file and ServerFixture.ts.

Suggestion:

  • zksolcRepoPath?: string / eraSolcRepoPath?: string in VerificationServiceOptions and VerificationService; isZkSolcEnabled keeps working unchanged.
  • In the worker, mirror this file: build ZkSolcLocal only when both paths are set, else leave zksolc undefined. createCompilationFromJsonInput already accepts zksolc?: IZkSolcCompiler and throws when a zksolc version arrives without a compiler.
  • The initWorker early-return condition includes zksolc; with undefined as a legal state it needs a separate initialized flag, or should check only solc, vyper and fe.

import http from "http";
import type { AddressInfo } from "net";

/**

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a note here on why this exists next to LocalChainFixture, so nobody tries to fold it into the Hardhat fixture or picks it for a test where the Hardhat node is the right tool. Something like:

Only needed for EraVM (zksolc) tests. Hardhat runs EVM bytecode only: it cannot hold EraVM code, and it cannot produce a creation tx to the ContractDeployer with a receipt that carries the contract address, so LocalChainFixture cannot serve these tests. There is no local EraVM node in the repo (anvil-zksync is archived), so this replays captured responses instead. EVM tests should keep using LocalChainFixture.

Two limits worth stating in the same block: the replay is keyed by method only and ignores request params (any address gets the same eth_getCode result), and it cannot return JSON-RPC errors. Fine for the single-contract e2e it serves; both would need to change before it is used more widely.

if (!sourcifyChain) {
chai.assert.fail(`No chain for chainId ${testCase.chainId} configured`);
console.log(
`Skipping ${testCase.type} test: chainId ${testCase.chainId} is not configured`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this hunk and restore chai.assert.fail(...).

This is a leftover: commit 7ae73b90 changed the assertion into a skip at the same time as it added the chain 40 test with the same pattern. That test was removed in 9e0a7138, but this change stayed. It is unrelated to zksolc.

It is also dead code today. The loop covers chains 1, 43114, 100009 and 56, and all four are in the map this spec builds in beforeEach, so the branch can never run. What it changes is the failure mode: if someone adds a test case for a chain that is missing from the map, the test now passes silently instead of failing.

const res = await chai
.request(serverFixture.server.app)
.get(
`/v2/contract/${CHAIN_ID}/${ADDRESS}?fields=creationBytecode,runtimeBytecode,deployment,compilation,abi`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two related asks for this test, since it is a storage-level verification case rather than an endpoint test:

1. Assert everything, with the existing helpers. This checks a hand-picked subset of fields. The verification-cases framework already has assertDatabase (every row and column a verification writes) and assertApiV2Lookup (the lookup with fields=all, every field compared with a case file). Neither depends on Hardhat: they take a VerificationTestCase JSON (onchain / input / output / verification), the chain id, the deployer address, and a DeploymentInfo (address, tx hash, block number, tx index). All of that is already in rpc-responses.json; only the case file needs to be captured once from this run. MockRpcServer stays as the chain.

This matters beyond coverage: a case file's output.metadata would have exposed the zksolc metadata wrapper shape, and output.creationCodeArtifacts + creationTransformations would have exposed the constructor offset that does not fit the stored recompiled creation code (see my comments on zksolcTestHelpers.ts and ZKSOLC.md). It also closes my earlier thread on verify.json.spec.ts.

2. Move it next to the other cases. verification-cases/ already holds a second spec for a different deployment mode (factories.spec.ts), and testdata/ already has per-language folders (fe, vyper). Suggest verification-cases/eravm.spec.ts, with the case file and rpc-responses.json under testdata/eravm/. That also drops the .e2e.spec.ts suffix, which only the two zksolc files in this PR use.

@manuelwedler manuelwedler removed their assignment Sep 14, 2026
@manuelwedler manuelwedler moved this from Sprint - Needs Review to Sprint - Up Next in Sourcify Public Sep 14, 2026
@kuzdogan kuzdogan moved this from Sprint - Up Next to Sprint - Blocked in Sourcify Public Sep 14, 2026
@kuzdogan

Copy link
Copy Markdown
Member

Moving the task to Blocked, I'm hoping to have a look at the review comments this sprint

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Sprint - Blocked

Development

Successfully merging this pull request may close these issues.

4 participants