feat: deploy script for SnapchainConfigRegistry - #495
Merged
topocount merged 7 commits intoAug 13, 2026
Conversation
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.
There was a problem hiding this comment.
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
DeploySnapchainConfigRegistryplus sharedSnapchainConfigRegistrySeeddata gated byblock.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.
|
Coverage after merging kjs/neyn-13022-c5-deploy-snapchain-config-registry into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
topocount
deleted the
kjs/neyn-13022-c5-deploy-snapchain-config-registry
branch
August 13, 2026 18:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Deploys
SnapchainConfigRegistryand hands ownership to the address that will author config changes. Fifth and last contract-side change in the series after #489, #490, #491, #492.DeploySnapchainConfigRegistryfollowsDeployTierRegistryexactly: the sameSalts/DeploymentParams/Addresses/Contractsstructs, the samerunDeploy/runSetupoverload pairs so the fork test can run withbroadcast = false, and deployment through the ImmutableCreate2Factory so re-running is a no-op.runSetupseeds the validator-set history and both peer lists, then callstransferOwnership. Ownable2Step leaves the target aspendingOwneruntil 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_sepoliaRPC 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_SALTexists as an override if that address turns out to be taken on Sepolia.The CREATE2 factory at
0x0000000000FFe8B47B3e2130213B802212439497is deployed on Sepolia — verified, not assumed — soImmutableCreate2Deployerworks there unchanged.Seed data moves next to the script
The mainnet keys, peer strings, and ten-entry history move out of
SnapchainConfigRegistryTestSuiteintoSnapchainConfigRegistrySeed, 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 aneth_mainnetfork:test_deploymentParams—owner/pendingOwnerafter 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:
ImmutableCreate2Deployeraccumulatesnamesin storage acrossregistercalls, so a secondrunDeployfrom a contract that already deployed one insetUpfinds the first entry's address on the repeat pass, marks itFOUND, anddeploymentChanged()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
.env.prodon purpose.vm.envAddressreverts 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 tobytes32(0)viavm.envOr, matching the existing house pattern; a zero salt carries no caller-lock, so mine the vanity salt before deploying.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.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, andUpgradeL2fork OP/Base mainnet and only an L1 endpoint was supplied locally.forge fmt --checkis 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
eth_sepoliaRPC URL infoundry.toml..env.prodand.env.example.README.mdto includeSnapchain Config Registrydetails.SnapchainConfigRegistrySeedfor seeding validator sets and peers.Snapchain Config Registry.