Skip to content

feat: deploy script for SnapchainConfigRegistry - #495

Merged
topocount merged 7 commits into
mainfrom
kjs/neyn-13022-c5-deploy-snapchain-config-registry
Aug 13, 2026
Merged

feat: deploy script for SnapchainConfigRegistry#495
topocount merged 7 commits into
mainfrom
kjs/neyn-13022-c5-deploy-snapchain-config-registry

Conversation

@topocount

@topocount topocount commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Deploys SnapchainConfigRegistry and hands ownership to the address that will author config changes. Fifth and last contract-side change in the series after #489, #490, #491, #492.

DeploySnapchainConfigRegistry follows DeployTierRegistry exactly: the same Salts / DeploymentParams / Addresses / Contracts structs, the same runDeploy / runSetup overload pairs so the fork test can run with broadcast = false, and deployment through the ImmutableCreate2Factory so re-running is a no-op. runSetup seeds the validator-set history and both peer lists, then calls transferOwnership. Ownable2Step leaves the target as pendingOwner until it accepts in a separate transaction, so the handoff is deliberately not complete when the script exits.

Two instances, two chains

One registry per Snapchain network: Mainnet on Ethereum L1, Testnet on Sepolia. Separate chains rather than two addresses on one chain, so a testnet mistake cannot touch mainnet state and the rehearsal costs nothing real. Adds the eth_sepolia RPC endpoint that needs.

With identical creation code, constructor args, and salt, the two land at the same address on both chains — convenient, but nothing should depend on it, since a redeploy on either chain breaks the coincidence. SNAPCHAIN_CONFIG_REGISTRY_TESTNET_CREATE2_SALT exists as an override if that address turns out to be taken on Sepolia.

The CREATE2 factory at 0x0000000000FFe8B47B3e2130213B802212439497 is deployed on Sepolia — verified, not assumed — so ImmutableCreate2Deployer works there unchanged.

Seed data moves next to the script

The mainnet keys, peer strings, and ten-entry history move out of SnapchainConfigRegistryTestSuite into SnapchainConfigRegistrySeed, inherited by both the deploy script and the suite. This is the follow-up #492 flagged in its own comment: the golden test now pins the literal bytes the deploy writes rather than a parallel copy that can drift. All 55 tests from #492 pass untouched, which is the evidence the move changed no rendered byte.

Seed selection is gated on chain id

Not passed in as a parameter. Snapchain testnet runs a wholly separate validator set — different keys, different heights — so a script that silently fell back to mainnet's history on Sepolia would produce a registry that is wrong in the worst available way: well-formed, renderable, and capable of taking every testnet node down on boot, since a malformed or wrong key is a startup panic rather than a warning.

History is append-only, so that is not a recoverable mistake. The remedy would be a redeploy at a fresh salt plus repointing every node. The chain id is the one input that cannot be fat-fingered at broadcast time, so it is what selects. Sepolia reverts with NoSeedDataForChain(chainId) until the testnet data is transcribed and verified in a follow-up.

Tests

test/Deploy/DeploySnapchainConfigRegistry.t.sol, four tests against an eth_mainnet fork:

  • test_deploymentParamsowner / pendingOwner after the two-step handoff, validatorSetCount() == 10, configVersion() == 12 (ten appends plus two peer setters), entry 0 and entry 9 heights, both peer strings.
  • test_rendersIdenticallyToReferenceInstance — the script's seeding path renders identically to a prank-seeded reference instance. Deliberately not a second copy of the expected document: both instances draw from _mainnetValidatorSets() and test: SnapchainConfigRegistry suite with golden validators.toml parity #492's golden test pins the reference to a literal, so the chain closes — script output equals suite output equals the checked-in bytes — while leaving exactly one copy of that ~5 KB string in the repo.
  • test_ownershipHandoff — the incoming owner accepts, can then append, and the deployer can no longer.
  • test_revertsSeedingUnknownChain — the seed gate fires on Sepolia.

That last one needs its own test contract, which is worth knowing before writing any other multi-deploy test against this base: ImmutableCreate2Deployer accumulates names in storage across register calls, so a second runDeploy from a contract that already deployed one in setUp finds the first entry's address on the repeat pass, marks it FOUND, and deploymentChanged() then reports no change. Setup would skip rather than revert, and the test would pass for the wrong reason. It did, until it was split out.

Before this can be broadcast

  1. Owner address and vanity salt are unset in .env.prod on purpose. vm.envAddress reverts on an unset variable, which is the correct failure until the owner is chosen — a placeholder would deploy a registry owned by the wrong address, and ownership is what governs the validator set. Note the salt defaults to bytes32(0) via vm.envOr, matching the existing house pattern; a zero salt carries no caller-lock, so mine the vanity salt before deploying.
  2. The seeded peer strings are provisional and marked as such in the source. Two of the seven bootstrap entries are 10.0.x.x, routable only inside one operator's VPC, and every node currently runs its own self-omitting list, so there is no canonical pair to transcribe. Publishing an unroutable list in a registry external validators read would be actively misleading, so this needs deciding before the seed goes onchain.
  3. The deployer EOA needs Sepolia ETH.

Verification

571 tests pass under FOUNDRY_PROFILE=ci, including the four above against a live L1 fork. The 5 failures in that run are pre-existing and environmental — AuthKeys, DeployL2, DeployTierRegistry, UpgradeBundler, and UpgradeL2 fork OP/Base mainnet and only an L1 endpoint was supplied locally. forge fmt --check is clean.


PR-Codex overview

This PR introduces the Snapchain Config Registry, which manages validator sets and peer lists for Snapchain networks on Ethereum mainnet and Sepolia testnet, ensuring isolation between networks and preventing configuration errors.

Detailed summary

  • Added eth_sepolia RPC URL in foundry.toml.
  • Introduced Snapchain configuration parameters in .env.prod and .env.example.
  • Updated README.md to include Snapchain Config Registry details.
  • Modified deployment scripts to handle Snapchain configurations.
  • Created SnapchainConfigRegistrySeed for seeding validator sets and peers.
  • Implemented tests for the deployment and functionality of the Snapchain Config Registry.

✨ Ask PR-Codex anything about this PR by commenting with /codex {your question}

Adds `DeploySnapchainConfigRegistry`, modeled on `DeployTierRegistry`: the
same `Salts` / `DeploymentParams` / `Addresses` / `Contracts` structs and
`runDeploy` / `runSetup` overload pairs, deploying through the
ImmutableCreate2Factory so the script stays idempotent. `runSetup` seeds the
validator-set history and peer lists, then hands ownership to the address that
will author config changes from then on.

Two instances are expected, one per Snapchain network: Mainnet on Ethereum L1,
Testnet on Sepolia. Separate chains rather than two addresses on one chain, so
a testnet mistake cannot touch mainnet state. Adds the `eth_sepolia` RPC
endpoint that arrangement needs.

Seed data moves out of the test suite into `SnapchainConfigRegistrySeed`,
inherited by both the deploy script and the suite. This is the follow-up the
test suite flagged in its own comment: the golden test now pins the literal
bytes the deploy writes rather than a parallel copy that can drift.

Seed selection is gated on `block.chainid` rather than passed in. Snapchain
testnet runs a wholly separate validator set, so a script that silently fell
back to mainnet's history on Sepolia would produce a registry that is wrong in
the worst available way -- well-formed, renderable, and capable of panicking
every testnet node on boot, since a malformed key is a startup panic rather
than a warning. History is append-only, so that is not a recoverable mistake:
the remedy is a redeploy at a fresh salt plus repointing every node. Sepolia
therefore reverts with `NoSeedDataForChain` until the testnet data is
transcribed and verified.

The fork test asserts the script-seeded registry renders identically to a
prank-seeded reference instance rather than against a second copy of the
expected document. Both draw from the same seed and the suite's golden test
pins the reference to a literal, so script output equals suite output equals
the checked-in bytes, with one copy of that string in the repo.

The seed-gate case needs its own test contract, because
ImmutableCreate2Deployer accumulates `names` in storage across `register`
calls: a second deploy from a contract that already deployed one in `setUp`
finds the first entry's address on the repeat pass and `deploymentChanged()`
then reports no change, so setup would skip rather than revert.

Owner address and vanity salt are left unset in `.env.prod` on purpose.
`vm.envAddress` reverts on an unset variable, which is the correct failure
until the owner is chosen -- a placeholder would deploy a registry owned by the
wrong address, and ownership is what governs the validator set.
Copilot AI lite review requested due to automatic review settings August 5, 2026 22:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds an onchain deployment pathway for SnapchainConfigRegistry that deterministically deploys via ImmutableCreate2Factory, seeds chain-specific initial state, and initiates a two-step ownership handoff. This rounds out the contract-side work by making the deploy output and the golden test share the same seed source, reducing drift risk.

Changes:

  • Introduces DeploySnapchainConfigRegistry plus shared SnapchainConfigRegistrySeed data gated by block.chainid.
  • Adds fork-based tests validating deployment params, rendered TOML parity vs a reference instance, ownership handoff behavior, and unknown-chain seeding reverts.
  • Updates docs, README, and environment/RPC configuration to include Sepolia support and deployment parameters.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/SnapchainConfigRegistry/SnapchainConfigRegistryTestSuite.sol Refactors the suite to inherit seed data from the shared seed contract for deploy/test parity.
test/Deploy/DeploySnapchainConfigRegistry.t.sol Adds fork tests covering deploy+seed behavior, rendering parity, ownership handoff, and seed gating.
script/DeploySnapchainConfigRegistry.s.sol New deploy script using ImmutableCreate2 deployer and chain-id–selected seed data with two-step ownership transfer.
script/abstract/SnapchainConfigRegistrySeed.sol Centralizes mainnet seed history + peer strings and enforces explicit chain-id gating.
README.md Documents SnapchainConfigRegistry as a top-level contract and adds Sepolia deployment section.
foundry.toml Adds eth_sepolia RPC alias for fork/script usage.
docs/docs.md Adds L1 contract documentation for Snapchain Config Registry, including invariants/assumptions/admin model.
.env.prod Adds (intentionally unset) production env keys for deploy ownership/salts and adds Sepolia RPC var.
.env.example Adds example env keys for SnapchainConfigRegistry ownership/salts and Sepolia RPC var.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

The new script and deploy test carried UNLICENSED, matching the other files
in `script/` and `test/Deploy/`. Relicense both as MIT so they match the
contracts in `src/`, which are MIT throughout.

Leaves the pre-existing `script/*.s.sol` and `test/Deploy/*` files on
UNLICENSED; sweeping those is a separate change.
Two branches of the deploy script had no coverage. Every existing test hits
`deploymentChanged() == true`, so the first-run path was well covered and the
second-run path not at all; and `loadDeploymentParams` / `loadSalt` were never
called by anything.

`test_reRunIsIdempotent` runs the script a second time against an
already-deployed registry and asserts nothing moves. The second run is the
dangerous one: seeding again would append the ten-entry history a second time,
and re-running `transferOwnership` after the owner had accepted would quietly
hand the registry back to the deployer. Deployed from a fresh script instance,
since ImmutableCreate2Deployer accumulates `names` in storage and a real re-run
starts with that empty.

`test_loadsParamsAndSelectsSaltByChain` covers env loading and the Sepolia salt
override, including the fallback that puts both registries at the same address.
The failure mode there is invisible until broadcast: a mistyped variable name
reads as unset, and `vm.envOr` then supplies `bytes32(0)` -- a salt with no
caller-lock, at an address anyone could have front-run. Written as one function
in a deliberate order, because `vm.setEnv` writes the process environment with
no way to unset afterwards, so the no-override case must be asserted first.

Both verified by mutation: forcing setup to always run fails only the
idempotency test, and dropping the Sepolia branch fails only the salt test.
ImmutableCreate2Deployer hardcodes 0age's factory in three places: the
address register() predicts against, the hasBeenDeployed query, and the
safeCreate2 call. Isolate them behind virtual hooks so a script can route
its deployments through a different factory.

No behavior change. The default stays the ImmutableCreate2Factory, which
every contract this repo has deployed went through -- an override moves
the deployment address of everything the overriding script registers, so
it is not a decision to inherit by accident.
…2 proxy

The ImmutableCreate2Factory requires the salt's first 20 bytes to equal
msg.sender, which binds a mined salt to the key that broadcasts. The
canonical deterministic-deployment proxy at 0x4e59b448... checks nothing,
so the whole 32-byte salt is searchable and the address the registry lands
at is a property of the contract alone.

What that gives up is exclusivity: anyone can submit the same salt and init
code first. The deployment itself is harmless -- same creation code and same
constructor argument, so the contract that lands is ours, owned by us, at
their expense -- but runSetup gated on deploymentChanged(), which reports
whether *this run* deployed it, and would have logged "no changes" over a
registry left permanently unseeded. Gate on configVersion() instead, which
asks the registry what it holds rather than what this process did, and also
closes the same hole for a run interrupted between deploy and setup.

Covered by a test that deploys the registry from a stranger through the
proxy and asserts the script seeds it anyway. Also corrects the deployment
shape doc, which still described two registries on Ethereum L1 rather than
mainnet plus Sepolia.
Puts the registry at 0x00000000fc51aD6eb74EAE89ba4b01b1776fBA85, matching
the 0x00000000fc prefix every other contract in this repo carries. Mined
against the canonical proxy, so it has no deployer prefix -- but it is still
bound to DEPLOYER, which is the registry's constructor argument, and to this
exact build of the creation code.

Leaves the testnet override commented out rather than set to zeros. loadSalt
reads it with vm.envOr, so unset falls back to the shared salt, which is what
puts both registries at the same address; zeros would not have meant "no
override" but "deploy Sepolia at the salt-zero address".
Sepolia reverted with NoSeedDataForChain because testnet's validator set is
wholly separate from mainnet's and transcribing it was deferred. Fill it in:
thirteen entries and seven keys, from the VALIDATOR_SETS string in
snapchain-deployer/.stack/deploy.yml, which every testnet pod there carries
byte-identically.

Two of the seven keys are retired -- one ran from genesis to 24_811_937 and
its replacement left at 28_681_000 -- and are kept because read nodes syncing
from genesis verify old commit signatures against the historical entries.

Verified against the live cluster rather than the file alone. The last thirty
commits carry the five current keys (juno, iris, vega, merry, gloin) at shard
heights 42_738_208 / 43_002_880 / 43_012_625, all far past the final entry at
37_197_000, so the transcribed history is complete and not merely current.
The certificates show four of the five signers: quorum is 4 of 5 and gloin is
network-distant, so its precommit lands after quorum forms -- its own heights
track the cluster.

Peers come from tau, the read node, because validators each omit themselves
and tau omits nothing. The unresolved question about publishing 10.0.x.x VPC
addresses in a registry external validators read applies here as it does to
mainnet, and is noted in the same terms.

The seed gate test moves to OP mainnet, which is now the nearest plausible
chain with no data. Adds a test asserting no mainnet key reaches a testnet
registry -- append-only history makes that unrecoverable short of a redeploy
at a fresh salt.
@github-actions

Copy link
Copy Markdown

Coverage after merging kjs/neyn-13022-c5-deploy-snapchain-config-registry into main will be

100.00%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   Bundler.sol100%100%100%100%
   BundlerV1.sol100%100%100%100%
   FnameResolver.sol100%100%100%100%
   IdGateway.sol100%100%100%100%
   IdRegistry.sol100%100%100%100%
   KeyGateway.sol100%100%100%100%
   KeyRegistry.sol100%100%100%100%
   RecoveryProxy.sol100%100%100%100%
   SnapchainConfigRegistry.sol100%100%100%100%
   StorageRegistry.sol100%100%100%100%
   TierRegistry.sol100%100%100%100%
src/abstract
   EIP712.sol100%100%100%100%
   Guardians.sol100%100%100%100%
   Migration.sol100%100%100%100%
   Nonces.sol100%100%100%100%
   Signatures.sol100%100%100%100%
src/validators
   SignedKeyRequestValidator.sol100%100%100%100%

@topocount
topocount merged commit 58d6b28 into main Aug 13, 2026
4 checks passed
@topocount
topocount deleted the kjs/neyn-13022-c5-deploy-snapchain-config-registry branch August 13, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants