Skip to content

Parameterized deploy-verification abstract: derive expectations from creation code, check against source and every chain #27

Description

@thedavidmeister

Deploy-pin verification is hand-written per repo, enumerated per version and per chain, and it does not check the thing that matters. This proposes one parameterized abstract here that every deploy repo inherits.

What is unverified today

Take rain.factory.deploy: three released versions, five supported networks — fifteen facts about what is live on chain. Exactly five are checked, and only for whichever version the current alias points at. Nothing verifies that 0_1_3 is still deployed on Flare, or that any historical version reached a network added after its release.

Those are not immutable facts. They can go false with nobody touching the repo — a release that reached four of five chains, a chain added later that older versions never got, a deployment that never landed.

Meanwhile the per-version tagged tests do etchZoltuFactory + deployZoltu into the local test EVM. That is a local reproduction, not a chain read: no createSelectFork appears in them at all.

Sort the assertions by what they are anchored to

  • Internal to the stored set — keccak256(RUNTIME_CODE) == BYTECODE_HASH, deployZoltu(CREATION_CODE) == DEPLOYED_ADDRESS, deployed.codehash == BYTECODE_HASH. Real derivations that catch an inconsistently-generated set. None can catch a snapshot of the WRONG contract, because a consistent snapshot of the wrong thing satisfies all of them.
  • Anchored to source — CREATION_CODE == type(X).creationCode. The only check that catches snapshotting the wrong contract. Applies to the candidate only; a historical tag is meant to diverge from current source.
  • Anchored to chain — the fork checks. The only thing that catches "it never deployed" or "it is not there any more".

Creation code is the only parameter

LibRainDeploy.zoltuAddress is a pure function of the creation code and identical across chains, and a local deployZoltu yields the runtime code and its hash. So from CREATION_CODE alone the expectation set is derivable, and everything else in the pointers file becomes a checked output rather than a test input.

The existing tagged test already derives this way — it just imports four symbols per version and aliases them symmetrically, so the structure reads as four parameters when only one is. The constant names are fixed and the path is src/generated/<tag>/<Contract>.pointers.sol, so the version string is the whole parameterization.

Ask: an abstract here, generated concretes there

In rain.deploy: an abstract test contract parameterized over (version, creationCode) plus the stored triple to check against. For each version it derives the expected address and codehash from the creation code, then asserts:

  1. the derived values match the stored DEPLOYED_ADDRESS / BYTECODE_HASH / RUNTIME_CODE — pure, offline
  2. for the candidate only, CREATION_CODE == type(X).creationCode — pure, offline
  3. across LibRainDeploy.supportedNetworks(), the derived address carries code with the derived codehash on every chain — forked

Keep 1 and 2 separable from 3, so an RPC outage cannot fail a release on assertions that need no network.

In each deploy repo: a GENERATED concrete supplying the array of versions and their creation codes, inheriting the abstract. No per-version functions, no per-chain functions, nothing hand-written that grows. LibRainDeploy.supportedNetworks() already returns the chains, so the fork dimension is generic.

At release: cutting a release regenerates the concrete so the newly frozen snapshot is in the enumeration the moment it exists, and the whole matrix runs BEFORE publish. A version that did not deploy cleanly to every chain never gets a tag. Regenerating for existing versions is the point, not overhead — the matrix grows in both dimensions, and a network added to supportedNetworks() leaves every historical version with an unverified cell that only a regenerated enumeration picks up.

Chain-independent runtime is a requirement, not a caveat

A constructor reading block.chainid or similar yields a different codehash per chain, which a single BYTECODE_HASH cannot express. Contracts are written to be compatible with predictable and verifiable deployment, not the reverse — deploying through Zoltu buys address predictability, and a constructor that reads chain state spends it. So a per-chain codehash mismatch is a DEFECT and the check fails hard; there is no per-chain codehash concept to add.

This is also statically checkable and belongs as a lint alongside the pragma convention record_verdict already enforces.

Open question for a human

The matrix failing because an OLD version is missing from a chain is a real signal, but not necessarily this release's fault. Whether that blocks the tag or reports separately is a judgement call.

Related: rainlanguage/rainix#303 (release-time verify asserts nothing about the snapshot it publishes), #304 (pointer generation belongs in build, which is what lets the concrete be generated with the constants), #301/#302 (rainix owning cut-release).

Activity

  1. added a commit that references this issue on Aug 13, 2026
    10b14d5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions