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:
- the derived values match the stored
DEPLOYED_ADDRESS / BYTECODE_HASH / RUNTIME_CODE — pure, offline
- for the candidate only,
CREATION_CODE == type(X).creationCode — pure, offline
- 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).
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 that0_1_3is 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+deployZoltuinto the local test EVM. That is a local reproduction, not a chain read: nocreateSelectForkappears in them at all.Sort the assertions by what they are anchored to
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.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.Creation code is the only parameter
LibRainDeploy.zoltuAddressis a pure function of the creation code and identical across chains, and a localdeployZoltuyields 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:DEPLOYED_ADDRESS/BYTECODE_HASH/RUNTIME_CODE— pure, offlineCREATION_CODE == type(X).creationCode— pure, offlineLibRainDeploy.supportedNetworks(), the derived address carries code with the derived codehash on every chain — forkedKeep 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.chainidor similar yields a different codehash per chain, which a singleBYTECODE_HASHcannot 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_verdictalready 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 owningcut-release).