Skip to content

feat(solidity): recover historical storage layouts - #2882

Open
banteg wants to merge 3 commits into
argotorg:stagingfrom
banteg:feat/solidity-historical-storage-layouts
Open

feat(solidity): recover historical storage layouts#2882
banteg wants to merge 3 commits into
argotorg:stagingfrom
banteg:feat/solidity-historical-storage-layouts

Conversation

@banteg

@banteg banteg commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Problem

Solidity verification already works for historical contracts, but solc did not expose Standard JSON storageLayout until 0.5.13. As a result, otherwise fully verified Solidity contracts compiled with 0.4.x through 0.5.12 are stored and returned through v2 with a null layout.

This creates two separate gaps:

  1. New historical verifications have enough compiler information to reconstruct a layout, but the normal compilation path currently leaves the artifact absent.
  2. Existing verified rows need a safe backfill mechanism. The current private repair flow has no Solidity equivalent and a broad database update would risk attaching a source-specific layout to the wrong compiled-contract row.

The reconstruction also has to produce the same shape as native solc output: a top-level ordered variable list plus a recursive type table containing array bases, mapping keys and values, and struct members. The existing TypeScript model did not describe that full schema.

Chosen approach

Reconstruct from the original compiler AST

The compiler package now reconstructs the layout from the AST emitted by the exact historical solc version used for verification:

solc range AST requested result
0.4.0–0.4.11 legacyAST standalone reconstruction supported
0.4.12–0.5.12 ast standalone reconstruction supported
0.5.13+ native storageLayout preserved unchanged

The implementation normalizes both AST dialects into one internal model, resolves the exact compilation target, follows Solidity's linearized inheritance order, excludes constants, and applies the historical packing rules. It emits solc-compatible storage entries and recursively interns the required type descriptions for:

  • elementary and fixed-point values
  • enums and contract/library references
  • fixed and dynamic arrays
  • mappings
  • structs and nested struct members
  • internal and external function types
  • legacy data-location and mutability encodings

Historical compiler differences are versioned explicitly. This includes the 0.4.14 nightly fixed-point width transition and Sourcify's -ci. to -nightly. compiler-version normalization.

Extraction is fail-closed. If a source, target, declaration, type, array length, inheritance relationship, or AST shape cannot be resolved exactly, no reconstructed artifact is attached.

Integrate it into normal Solidity compilation

For the safe persistence range, SolidityCompilation requests the appropriate source-level AST in addition to the existing compiler outputs. After a successful compile it invokes the optional historical extractor only when the compiler did not already return storageLayout.

This keeps native solc output authoritative and lets new historical verifications receive the artifact without introducing a server-only special case. Extraction failures are logged but do not turn an otherwise valid contract verification into a failure.

Use a conservative persistence boundary

The standalone extractor supports 0.4.0 through 0.5.12, but automatic persistence and database backfill are restricted to 0.4.7 through 0.5.12.

Before 0.4.7, distinct source programs can produce identical creation and runtime bytecode without metadata binding the source identity. Sourcify deduplicates compiled_contracts by compiler and bytecode identity, so two deployments with different storage layouts can share one compiled-contract row. Writing either reconstructed layout to that shared row would make it incorrect for the other deployment.

From 0.4.7 onward, compiler metadata binds the compilation/source identity into the bytecode, making a source-specific artifact safe under the current row model. Pre-0.4.7 extraction remains available as a standalone capability, but this PR deliberately does not persist it.

Add an exact-identity backfill

A new private stateless method, replace-solidity-storage-layout, recompiles and verifies the submitted contract before updating storage.

The update is accepted only when all of the following hold:

  • language is Solidity
  • compiler version is in the persisted 0.4.7–0.5.12 range
  • the fresh compilation matches the deployed contract
  • chain and address resolve to one unambiguous stored compilation
  • compiler version, language, fully qualified target, settings, additional compiler input, and complete source set match the stored compilation
  • stored storageLayout is missing or JSON null

The SQL update uses jsonb_set to modify only compilation_artifacts.storageLayout. It never replaces the surrounding compilation artifacts and never overwrites an existing non-null layout. Identity mismatches, unsupported versions, malformed recovery results, ambiguous rows, and repeat attempts return a 400-level error.

Complete the TypeScript schema

The Solidity compiler-output types now model recursive layout data including base, key, value, and members, and correctly allow the native null type table used for contracts with no storage variables. Runtime output remains compatible with native solc JSON.

Alternatives considered

Infer layout from bytecode or observed storage accesses

Rejected. Bytecode cannot reliably recover unused variables, source labels, AST IDs, struct boundaries, mapping key/value types, or the distinction between several types with identical runtime widths. Storage access traces would be incomplete and deployment-specific rather than compiler-exact.

Parse historical source with a modern compiler or a separate Solidity parser

Rejected. Historical syntax, name resolution, inheritance, type identifiers, data locations, mutability, and packing behavior changed across these releases. Using the exact original compiler's AST avoids creating a second historical Solidity frontend and keeps the reconstruction tied to the verified compilation.

Implement reconstruction only inside the backfill endpoint

Rejected. That would repair old rows but leave every new pre-0.5.13 verification incomplete. Keeping extraction in the compiler package lets the normal verification path and the repair path share one implementation.

Persist layouts for 0.4.0–0.4.6

Rejected under the current database model. Bytecode identity is not enough to select a source-specific layout safely in this range. Supporting it would require moving layout storage to a per-verification/source-identity record or otherwise changing compiled-row deduplication.

Backfill rows by version/address alone

Rejected. A successful bytecode match is not sufficient to prove that the submitted source/settings are the same compilation already represented by the row. The chosen path recompiles, verifies, compares the complete stored compilation identity, and performs an atomic guarded update.

Replace or normalize native layouts

Rejected. solc 0.5.13+ remains the source of truth, including any version-specific schema details or compiler behavior. Reconstruction runs only when native output is absent.

Continue below Solidity 0.4.x

Out of scope. This PR intentionally starts at 0.4.0 rather than expanding into older compiler archaeology.

Impact on the current Sourcify dataset

The Sourcify v2 dataset snapshot generated at 2026-07-07T02:26:39Z contains:

  • 197,735 compiled-contract rows in the safe persisted range, 0.4.7 through 0.5.12
  • 197,444 of those compiled rows referenced by at least one verification
  • 10,647,807 verification rows linked to those compilations
  • 10,644,234 distinct verified deployments after deduplicating by verified_contracts.deployment_id

The dump contains 40,259,667 distinct verified deployments overall, so 26.44% of the current verified-contract population is in the range addressed by this PR.

compiler family eligible compiled rows used compiled rows distinct verified deployments share of all deployments
0.4.7–0.4.26 147,277 147,090 10,197,247 25.33%
0.5.0–0.5.12 50,458 50,354 446,987 1.11%
Total 197,735 197,444 10,644,234 26.44%

The population is concentrated in three compiler releases: 0.4.23 accounts for 3,653,020 deployments, 0.4.11 for 3,221,404, and 0.4.16 for 2,367,139. Together they represent 86.82% of the affected range.

These counts describe the existing population eligible to gain a layout, not a guarantee that every row will be updated. Reconstruction deliberately fails closed on unresolved historical AST constructs, and the backfill additionally requires an exact stored-compilation identity match.

Validation

  • full monorepo build
  • package lint, formatting, and TypeScript checks
  • 10 historical extractor tests covering native-schema parity, both AST dialects, inheritance, packing, structs, arrays, mappings, function and library types, constants, UTF-8 source offsets, fixed-point version boundaries, CI/nightly normalization, empty layouts, and fail-closed behavior
  • 18 Solidity compilation tests, including recovery through the real solc 0.4.11 legacy-AST path
  • 10 guarded backfill unit tests
  • real-solc matrix across 0.4.7, 0.4.11, 0.4.12, 0.4.26, and 0.5.12
  • live PostgreSQL/RPC integration using official solc 0.5.12: verify, store, null the historical artifact, backfill, expose through v2, reject mismatched source identity, and reject overwrite

@banteg

banteg commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

The key distinction is between “we can calculate the layout” and “we have a safe database row in which to store it.”

Consider two solc 0.4.0 contracts whose state variables are never used:

contract C {
    uint256 a;
    uint256 b;
}
contract C {
    uint128 a;
    uint128 b;
    uint256 c;
}

Their layouts differ:

Variable First contract Second contract
a slot 0, offset 0, 32 bytes slot 0, offset 0, 16 bytes
b slot 1, offset 0, 32 bytes slot 0, offset 16, 16 bytes
c absent slot 1, offset 0, 32 bytes

But because none of these variables are accessed, the executable code can be identical. In early solc versions, there is also no source-sensitive metadata suffix distinguishing the two compilations. Therefore both creation and runtime bytecode can be byte-for-byte identical.

Sourcify’s database effectively sees:

compiler: solc 0.4.0
creation bytecode hash: X
runtime bytecode hash: Y

for both contracts. It consequently reuses one compiled_contracts row:

Verification A ─┐
                ├── Compiled contract row X/Y
Verification B ─┘

The problem appears when adding a source-specific field to that shared row:

Compiled contract row X/Y
└── storageLayout = layout from source A

Verification B now exposes source A’s layout, even though B has a different layout.

Why an exact bytecode match does not help: both sources genuinely compile to that same bytecode. Matching the deployed contract proves that the source is a valid explanation of the bytecode, but it does not prove it is the only valid source—or the source associated with every verification sharing the row.

From 0.4.7 onward, solc adds source-sensitive compiler metadata to the bytecode. That metadata commits to information including source hashes and compilation settings. Changing the source therefore changes the metadata suffix and consequently the bytecode hash:

Source A → executable code + metadata hash A → compiled row A
Source B → executable code + metadata hash B → compiled row B

Even if the executable instructions are identical, Sourcify no longer deduplicates the two compilations into one row. Each row can safely carry its own reconstructed layout.

Standalone pre-0.4.7 extraction is still correct because it operates on a specific source bundle supplied by the caller:

specific source + exact compiler + target → correct layout for that source

What is unsafe is persisting that result into a bytecode-keyed row potentially shared by other source bundles.

To persist pre-0.4.7 layouts safely, Sourcify would need a larger data-model change, such as:

  • Store layouts per verification rather than per compiled-contract row.
  • Include a canonical source/settings identity in compiled-row deduplication.
  • Split existing shared rows by source identity before backfilling.

That is why the PR can reconstruct from 0.4.0 onward, but only stores results automatically from 0.4.7 onward.

@banteg
banteg marked this pull request as ready for review July 11, 2026 20:51
@banteg

banteg commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

also i think this applies to 0.6.0+ contracts that turn off the metadata (metadata.bytecodeHash: "none"). this theoretically allows submitting a different source that compiles to the same bytecode but shifts storage layout.

i think the safest way is probably to show 'verified storage layout' if you could prove the exact source shape with either bzzr0, bzzr1, ipfs metadata.

@marcocastignoli marcocastignoli moved this from Triage to Backlog in Sourcify Public Jul 21, 2026
@banteg

banteg commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Updated this branch to current staging (859adf2a) with merge commit 1ca519c0; history was preserved and nothing was force-pushed.

The only merge conflicts were from the API v1 removal/move. I kept the storage-layout endpoint in apiPrivate, fixed its imports, and migrated the integration test from removed /verify/solc-json to the async /v2/verify/:chain/:address worker flow.

Provenance check:

  • Official solc probes show 0.4.7 output carries bzzr0, while 0.5.12 carries bzzr1/compiler metadata.
  • solc 0.5.12 rejects metadata.bytecodeHash = "none"; support starts at 0.6.0.
  • Therefore the historical 0.4.7-0.5.12 layouts persisted by this PR remain source-bound through the recompiled bytecode/metadata identity. The 0.6+ no-metadata case raised above is a real broader concern for native compiler layouts, but is not introduced or widened by this historical fallback.
  • The backfill still requires source/settings/target/additional-input identity, rejects ambiguous deployments, fills only missing/null layouts, and never overwrites an existing non-null layout.

Validation completed:

  • focused build of compilers-types, bytecode-utils, compilers, lib-sourcify, and server
  • package checks/typechecks for compilers-types, compilers, lib-sourcify, and server
  • full compilers suite: 29 passing
  • SolidityCompilation suite: 18 passing
  • storage-layout backfill unit suite: 10 passing
  • real-solc matrix for 0.4.7, 0.4.11, 0.4.12, 0.4.26, and 0.5.12: expected packed/mapping layout on every version

The database-backed integration test could not run locally because Docker/OrbStack is unavailable in this worktree environment; the migrated test compiles and the focused unit/runtime coverage above is green. GitHub reports the PR mergeable, and both checks attached to the new head are green.

@banteg

banteg commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

CI follow-up: CircleCI received the exact new head as pipeline 11549 / workflow 65244970-4948-49d6-85fb-74c7d0685290, but setup job 69137 has remained not_running since 09:49 UTC with no start time or reported pipeline error. Two other current Sourcify PR workflows are in the same project-level state. The previous head's eight CircleCI contexts were green; the two Socket checks on 1ca519c0 are green. This frozen CircleCI allocation is the only remaining external validation blocker on the new head.

@banteg

banteg commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@marcocastignoli @kuzdogan could you review this when you have a chance? The staging merge, provenance conclusion, validation evidence, and current CircleCI allocation state are documented immediately above. GitHub does not permit the fork author to add formal reviewer requests on this PR.

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

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants