Conversation
Brings the numeric-namespace change onto the upgrade-tooling branch and adds the PR-scoped upgrade scripts, pinned pre-upgrade snapshots, and fork tests that swap the registry, PoP controller, PoP rules, and reverse resolver proxies in place and redeploy the lens, so every address is kept. The fork suite issues a lite username against live state to prove it lands as a subname beneath its numeric container.
Brings the generic SubnodeUtils, the unified lite-node derivation, the registry write returning the subnode, and the lens store-row and cold-path fixes onto the in-place upgrade branch, so the swapped implementations carry them.
Brings the clarified `persist` documentation and the registry store-write tests onto the in-place upgrade branch so it stays in step with the feature branch.
## Description Upgrades the deployed proxies in place so the numeric-namespace change reaches live state while every address is kept. It swaps the registry, PoP controller, PoP rules, and reverse resolver implementations behind their existing proxies and redeploys the immutable lens, repointing the `popLens` key. Each proxy swap resolves its target from the on-disk manifest and diffs the new storage layout against a pinned pre-upgrade snapshot, failing closed if a slot moves, shrinks, or changes type. Fork tests run each script against live Paseo Asset Hub state through the ETH-RPC adapter. One test per upgrade proves the swap keeps the address, the owner, and the state; an end-to-end test issues a lite username and checks it lands as a subname beneath its numeric container, owned in the registry rather than held as a token. Every upgrade script, pre-upgrade snapshot, and fork test in this change is scoped to this pull request and removed before merge, so the default branch keeps no upgrade scaffolding and the fork suite runs zero tests between upgrades. This branch is based on the upgrade-tooling branch and carries the numeric-namespace contract change so the swapped implementations contain it. Migration note: lite usernames issued before this upgrade were recorded as atomic labels under the top level. The upgraded code addresses a lite name at the container-then-stem node, so a pre-upgrade lite name is not read by the new path and its subname node can be issued afresh. That overwrite is accepted. ## Type - [ ] Bug fix - [x] Feature - [x] Breaking change - [ ] Documentation - [ ] Chore - [ ] Refactor - [ ] Security ## Scope - [x] Registration - [x] Resolver - [ ] Store - [x] Proof of Personhood - [x] Deployment scripts - [x] Tests ## Related Issues - #287 ## Fixes ## Checklist ### Code - [x] Follows project style - [x] `forge build` passes - [x] `forge test` passes - [x] No new compiler warnings ### Testing - [x] New tests added for changed behavior - [ ] Fuzz tests added where applicable - [ ] Invariant tests verified ### Security - [x] No new `selfdestruct` or `delegatecall` - [x] Access control reviewed - [x] No storage layout conflicts (for upgradeable contracts) ### Documentation - [x] NatSpec updated on changed interfaces - [ ] README updated if needed ### Breaking Changes - [ ] No breaking changes - [x] Breaking changes documented below **Breaking changes:** - Lite usernames issued before this upgrade are addressed at a different node afterwards and are not migrated. Their subname node can be issued afresh, so a pre-upgrade lite name is not preserved. ## How to test Fork tests need the local ETH-RPC adapter on `paseo_local`. ```bash bun run test:fork ``` ## Notes Upgrade scripts run through `scripts/deploy/upgrade.sh`, for example `SCRIPT=UpgradeRegistry ACCOUNT_NAME=<keystore> RPC_URL=<network> ./scripts/deploy/upgrade.sh`.
Brings the dev branch up to tag v0.8.0 (3c8a9e9), so the upgrade artefacts sit on top of the released code instead of a fork point 43 commits behind it. This is step 1 of the Paseo in-place upgrade: #301 (writeNewLabel), #304 (protocol version and codehash declarations), #298 (UUPS StoreFactory) and #305 (deferred store-write gate) all arrive before any upgrade script is written against them. The textual merge was conflict-free, but it left one semantic conflict: DotnsRegistryOld calls StoreUtils.writeLabel, which #301 removed. The snapshot has to keep reproducing the implementation deployed on chain, so it cannot move to writeNewLabel. Resolved by adding contracts/utils/StoreUtilsOld.sol, a verbatim PR-scoped snapshot of the library as deployed, and pointing the Old registry at it. It is deleted along with every other *Old.sol before the upgrade branch merges to master. Outside *Old.sol, the contract diff against master is the four storage-layout fixes this branch carries and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Description Merges master at v0.8.0 (3c8a9e9) into the upgrade branch ## Type - [ ] Bug fix - [ ] Feature - [ ] Breaking change - [ ] Documentation - [x] Chore - [ ] Refactor - [ ] Security ## Scope - [ ] Registration - [ ] Resolver - [x] Store - [ ] Proof of Personhood - [x] Deployment scripts - [ ] Tests ## Related Issues ## Fixes ## Checklist ### Code - [x] Follows project style - [x] `forge build` passes - [x] `forge test` passes - [x] No new compiler warnings ### Testing - [ ] New tests added for changed behavior - [ ] Fuzz tests added where applicable - [ ] Invariant tests verified ### Security - [x] No new `selfdestruct` or `delegatecall` - [x] Access control reviewed - [x] No storage layout conflicts (for upgradeable contracts) ### Documentation - [x] NatSpec updated on changed interfaces - [x] README updated if needed ### Breaking Changes - [x] No breaking changes - [ ] Breaking changes documented below **Breaking changes:** ## How to test ```bash forge test --mt <testName> ``` ## Notes
The `*Old.sol` snapshots on this branch described implementations that were replaced in place months ago. All four built to different bytecode from what Paseo Asset Hub Next is running, and nothing noticed: the OpenZeppelin layout diff compares the new implementation against whatever the snapshot happens to be, so a stale reference produces an honest diff of the wrong pair, and a change that lives in calldata leaves no trace in a layout at all. Regenerate all of them from the build that is actually deployed (c852004), and prove it. `scripts/shell/verify-snapshots.sh` builds each snapshot and compares runtime bytecode against the implementation behind the proxy, masking only `UUPSUpgradeable.__self` and the trailing CBOR metadata. All twelve upgraded proxies match byte for byte. `fork-tests.sh` runs it before the suite, so the check cannot be skipped by anyone running the tests the normal way. The snapshot set is larger than the twelve contracts because master has moved 26 contract files since the deployed build. A snapshot that imports the current tree stops reproducing the deployed bytecode, and one that imports a snapshot hands a renamed type to a signature expecting the current one. The set is therefore closed over both: everything reachable that changed, plus everything that reaches one of those. Sixteen unchanged interfaces and libraries stay shared, unrenamed. CI now runs on pull requests into `spha/**`. It previously triggered on master alone, so a PR into this branch got lint and nothing else, which is how the stale snapshots reached review in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the gap it caught The storage-layout diff only ran inside an upgrade script, which meant it only ran when someone brought up the ETH-RPC adapter and ran the fork suite. Lift it into a plain unit test, one case per proxy, so every push answers the question the upgrade actually turns on: would `upgradeProxy` accept this layout change. It found one on its first run. `DotnsProtocolRegistry` appends `_expectedCodehash` and `_protocolVersion` for the declarations added in #304, but left `__gap` at 50, so the gap starts two slots later than on the deployed proxy and the contract's footprint grows. The validator rejects it, and any field appended after the gap in a later release would land on a slot the live proxy does not expect. Sized to 48, which keeps the 54-slot footprint. This is a fifth storage-layout correction of the same family as the four already on this branch, and like those it stays here per the branch policy. The fix moves no bytecode: a gap reserves slots and emits no code, so the implementation still builds byte-identical to the v0.8.0 tag. The release-drift finding is unchanged, and `DotnsRegistrarController` remains the only proxy whose deployed code will differ from the tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ation Thirteen scripts and thirteen fork tests, paired one to one. Twelve swap a proxy in place; the thirteenth migrates the store factory, which cannot be swapped because what is deployed is a plain contract from an earlier release and the current one puts the factory behind a proxy at a different address. Each fork test reads live state through the deployed implementation, runs the script's own internal path rather than reimplementing the upgrade, and reads the same state back. Each asserts the implementation address actually changed, without which a swap that silently did nothing would satisfy every state-preservation assertion. The assertions are per contract: the registry drives the new deferral gate against the live controller set, which a unit test cannot reach; the registrar controller reads back the protocol registry pointer that the retained slot exists to keep in place; the escrow asserts its redeem window is both unchanged and non-zero, that being the field an upgrade has dropped before. `StoreFactoryMigrator` closes the migration without a release. It is the shipped factory with four fields widened and a one-shot owner-only import, swapped into the proxy for a single transaction and swapped back out, so the deployment ends on the shipped implementation with the bindings in place. Both directions are layout-diffed. The holder list it needs cannot be read from the factory: the mapping has no enumeration, and the list it does keep holds store addresses, which do not know which user they belong to. Only `LabelStoreDeployed` carries the pairing, so `scripts/shell/store-holders.sh` replays it and reconciles the result against the factory's own count. It refuses to print a list it cannot account for, which matters because the public gateway answers log queries with an empty result rather than an error: a naive replay against it looks like it worked and is empty. The count also moves, from 57 to 58 while this was being written, so the list is read immediately before broadcasting and never reused. `DeclareRelease` extends `WireDeployments` rather than restating its key list, so a release that adds a key cannot be declared on a fresh deploy and silently skipped on every upgrade. Docs carry the parts that are not obvious from the code. CONTRIBUTING now says the artefacts stay on a branch that never merges, and that a snapshot is of the deployed code rather than of the previous release. DEPLOYMENTS and a folder README record why this network's chain id does not identify it, why its gateway's empty log results are a trap, and why `verify --tag` will report one key as drift permanently. The README no longer claims a lite name's node cannot collide with a subname, which stopped being true when lite names became subnames. The note stayed out of the manifest deliberately: the deploy pipeline parses every key there as an address, so a text field fails it on first read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four things an AI review caught, the first of which was mine and the worst. The migration was built on a false premise. I claimed the deployed factory had no enumeration and that stores did not know their user, and wrote a log-replay helper, a count-reconciliation guard and two fork tests around that. Both claims are false: `getLabelStores(0, n)` answers on the live factory, and `IDotnsStore.owner()` on a live store round-trips back through `getLabelStore`. `importStores` now takes only the old factory and reads the count, the list and each store's owner on chain, checking every pairing back through the mapping before it writes. `store-holders.sh` is deleted. That also closes a race the old shape carried. The set was read off chain and passed in, so a store created between the read and the import was silently left unbound, and its holder would be handed an empty store on their next registration. The count on this network moved while this branch was being written. Reading inside the transaction removes the window, and a test covers a binding created after the decision to migrate. The script could not have been broadcast. It read the destination from the manifest key `StoreFactory`, which still names the factory being migrated from, and nothing deployed the replacement: the CREATE3 address for it is empty on chain. Meanwhile the fork test quietly deployed the proxy itself, so the two told different stories and the test passed. `MigrateStoreFactory` now deploys the replacement through the pipeline's own helper, which also adopts one a previous run left behind, and records it and its beacons in the manifest. The success condition was never asserted. After a real migration every mint resolves `STORE_FACTORY` through the protocol registry and calls `ensureLabelStore`; nothing exercised that path. The fork test now upgrades the protocol registry first, because the rewire declares a codehash that entrypoint does not have yet, runs all four legs, and asserts a live holder resolves to the store they already had with no extra store deployed. Two smaller ones. `verify-snapshots.sh` ran only from `fork-tests.sh`, and CI calls `forge test` directly, so the check this branch leans on was absent from the one place reviewers see; it now runs in the fork job. `UserStore` added `_protocolRegistry` without shrinking its gap, the same defect already fixed on the protocol registry; it is harmless at zero user stores and would not stay harmless through a beacon rotation. `docs/PASEO-V080-RUNBOOK.md` writes down the broadcast order. Nothing in the scripts enforces it and they are independently runnable by design, so the three dependencies that fix the order live there: registry first because every `version()` reads through it, the migration before the declaration because it rewires a key the declaration records, and the declaration last because it claims something about the whole deployment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d factory Three findings from the second review, all correct. The fork test could not have run the deploy leg it claims to exercise. `_deployReplacement` reaches `_create3Factory()`, which falls back to reading `DotnsProtocolRegistry` out of the manifest, and the manifest is only populated by `initDeployment`, which `run` calls and the harness does not. The test would have failed at CREATE3 resolution before reaching its assertion, and the unit tests cannot see that. `_deployReplacement` now adopts the CREATE3 factory from the protocol registry it was already handed, so the leg depends on no ambient state and production and the test run the same path. The script erased the only durable pointer to the factory it migrates away from. The deploy reuses the `StoreFactory` label and the replacement mints its own beacons, so after a successful run all three manifest entries described the new deployment, while the imported stores stayed on the old beacons. Those beacons answer to the old factory, and it is the only contract that can ever rotate the implementation behind those stores. The outgoing addresses are now written first, as `StoreFactoryLegacy`, `LabelStoreBeaconLegacy` and `UserStoreBeaconLegacy`, and the runbook says which one a later rotation goes through. The resume claim was too strong. Adoption covers a run that died between the deploy and the import, and nothing else: once the import has landed a second run reverts on the first user it tries to bind, and if the proxy is still on the migrator the adopt is refused, because the deployer requires the occupant to delegate to the implementation that run deployed. The NatSpec said the step was resumable; it now says what is actually true, and the runbook marks step 13 as the one step to inspect and continue rather than re-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Description Brings Paseo Asset Hub Next to the current release in place: 12 proxies swapped, store factory migrated, release declared last. Only two shipped contracts change (`DotnsProtocolRegistry`, new `StoreFactoryMigrator`); the rest is upgrade tooling, snapshots and tests. Two things were broken and passing every check: - **All four `Old.sol` snapshots were stale**, describing implementations replaced in place months ago, so every layout diff compared the wrong pair. Nothing can catch that: the diff compares whatever it is handed, and a calldata-only change leaves no trace in a layout. Regenerated from `c8520046` (the build actually deployed) and proved by `scripts/shell/verify-snapshots.sh`, which compares each snapshot's bytecode against the implementation behind the proxy. All 12 match; `fork-tests.sh` runs it before the suite. - **`DotnsProtocolRegistry` could not be upgraded onto the live proxy.** #304 appended two fields but left `__gap` at 50, so the gap starts two slots late and the validator rejects it. Now 48. Found by the new layout suite, which moves the diff into a plain unit test so it runs on every push. No bytecode change. The store factory cannot be swapped: what is deployed is a plain contract, and the release puts the factory behind a proxy elsewhere. Its bindings are proxy storage with no export, so re-pointing `STORE_FACTORY` at a fresh factory gives every existing holder a second empty store and drops their labels from enumeration. `StoreFactoryMigrator` avoids that without a release: the shipped factory with four fields widened plus a one-shot owner-only import, swapped in for one transaction and back out, both directions layout-diffed. ## Type - [ ] Bug fix - [ ] Feature - [ ] Breaking change - [ ] Documentation - [x] Chore - [ ] Refactor - [ ] Security ## Scope - [ ] Registration - [ ] Resolver - [ ] Store - [ ] Proof of Personhood - [x] Deployment scripts - [ ] Tests ## Related Issues Follows #308 (master merged into this branch). Supersedes #306. ## Fixes Fixes deployment upgrade to v0.8.0 on Paseo ## Checklist ### Code - [ ] Follows project style - [ ] `forge build` passes - [ ] `forge test` passes - [ ] No new compiler warnings ### Testing - [ ] New tests added for changed behavior - [ ] Fuzz tests added where applicable - [ ] Invariant tests verified ### Security - [ ] No new `selfdestruct` or `delegatecall` - [ ] Access control reviewed - [ ] No storage layout conflicts (for upgradeable contracts) ### Documentation - [ ] NatSpec updated on changed interfaces - [ ] README updated if needed ### Breaking Changes - [ ] No breaking changes - [ ] Breaking changes documented below **Breaking changes:** ## How to test ```bash forge test --match-path 'test/unit/upgrade/LayoutCompatibility.t.sol' forge test --match-path 'test/unit/store/StoreFactoryMigrator.t.sol' ``` ## Notes **Permanent drift on `DotnsRegistrarController`.** Its retained slot changes emitted slot constants, so the deployed code will differ from the v0.8.0 tag forever. A tag build reads `protocolRegistry` as zero and bricks the contract, so the branch build is the only deployable one. `verify --tag` reports that one key as drift by design; a second one is a real finding. Recorded in DEPLOYMENTS.md and `deployments/paseo-assethub/README.md`. **Holder list for the migration** cannot be read from the factory: no enumeration, and the list it keeps holds stores, which do not know their user. `scripts/shell/store-holders.sh` replays `LabelStoreDeployed` and reconciles against the factory's count, refusing a list it cannot account for. The public gateway returns empty for `eth_getLogs` instead of erroring, so a naive replay looks like it worked. The count moved 57 to 58 mid-work, so read it immediately before broadcasting. **Manifest deliberately untouched.** `initDeployment` parses every key as an address, so a note field breaks the pipeline; it went in a sibling README.
Prepares the rename of `spha/registrar-upgrade`. GitHub retargets open pull requests and leaves a redirect, but it does not touch file contents, so these four references would have gone stale silently. The CI trigger is the one that matters. It matched `spha/**`, and a renamed branch stops matching, which puts pull requests into it back to running lint and nothing else. That is the gap that let a storage snapshot describing a replaced implementation reach review in the first place. Now matched as `dev/**`, by prefix rather than by name, so the next long-lived branch is covered the day it is created instead of the day someone remembers this file. The other three are prose: CONTRIBUTING on why the artefacts stay on a branch that never merges, and DEPLOYMENTS and the manifest folder README on why this network's deployed code matches no release tag. Land this before the rename, not after. Nothing here touches `contracts/**`, `test/**` or `**.sol`, so the test workflow's path filter skips this commit under either branch name and no window opens either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
) Follows the rename of `spha/registrar-upgrade` to `dev/testnet-upgrades`. GitHub retargets open pull requests and leaves a redirect, but it does not touch file contents, so four references would have gone stale silently. The CI trigger is the one that matters. It matched `spha/**`, and the renamed branch stops matching, which puts pull requests into it back to running lint and nothing else. That is the gap that let a storage snapshot describing a replaced implementation reach review in the first place. Now matched as `dev/**`, by prefix rather than by name, so the next long-lived branch is covered the day it is created. The other three are prose: CONTRIBUTING on why the artefacts stay on a branch that never merges, and DEPLOYMENTS plus the manifest folder README on why this networks deployed code matches no release tag. Nothing here touches `contracts/**`, `test/**` or `**.sol`, so the test workflow path filter skips this commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…uite `DeclareRelease` would have aborted on Paseo Asset Hub Next, at step 14, after every swap and the store migration had already been broadcast. The registry's self-reference key resolves to the zero address there, and `_verifyDeployment` asserts it matches the manifest. Sixteen of the seventeen keys are wired and agree; that one is a hole a fresh deploy never has, left by a network wired before the key existed. `_wireMissingKeys` sets keys that are unset and leaves every other key exactly as it is. A key pointing somewhere unexpected still fails verification, because that is drift, and repairing it here would make the check that follows tautological and hide the thing it exists to surface. The fork suite has now been run, for the first time. All 15 tests pass against live state, including the three for the store migration, which could not have run at all before the CREATE3 resolution fix in the previous commit: they would have died resolving the factory out of a manifest the harness never loaded. They went unrun because they needed Docker. The adapter only translates for the same node the hosted endpoint already fronts, so `PASEO_FORK_RPC` now overrides the fork endpoint and the whole suite runs in under a minute with no container. The default stays on the local alias, so CI and anyone mid-deployment are not silently pointed at a public gateway. `fork-tests.sh` and the runbook carry the invocation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI Summary
Deploy ContractsDeployed addresses vs the expected setExpected is the committed expected-address set; actual is this CI deployment of the same pipeline.
Labelsdependencies, smartcontracts, other, scope: registration, scope: resolver, type: test, type: docs, scope: store, scope: pop |
|
UpgradeResolver: success. Run. Broadcast record and manifest attached as artifacts. Re-add |
|
UpgradeReverseResolver: success. Run. Broadcast record and manifest attached as artifacts. Re-add |
|
UpgradeContentResolver: success. Run. Broadcast record and manifest attached as artifacts. Re-add |
|
UpgradePopResolver: success. Run. Broadcast record and manifest attached as artifacts. Re-add |
|
MigrateStoreFactory: failure. Run. Broadcast record and manifest attached as artifacts. Re-add |
The first MigrateStoreFactory broadcast proved a single transaction importing all 63 live bindings does not fit in a block: estimation died mid-loop around the 44th store at any gas limit, the multi-dimensional weight exhaustion surfacing as OpenZeppelin's FailedCall. Nothing off-chain models that ceiling, so the import is now paged. - importStores(oldFactory, offset, limit): idempotent per binding, a conflicting rebind still reverts, the final page asserts the count - the script broadcasts pages of DOTNS_IMPORT_CHUNK (default 15, roughly a third of the measured capacity) - runbook step 13 maps each death state to label retry or manual page replay; a fork test drills the mid-import recovery against live Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
MigrateStoreFactory: success. Run. Broadcast record and manifest attached as artifacts. Re-add |
|
DeclareRelease: success. Run. Broadcast record and manifest attached as artifacts. Re-add |
|
All 15 runbook steps executed and chain-verified: ownership rotated to a fresh key, 12 proxies swapped to v0.8.0, the store factory migrated with all 63 bindings carried, and the release declared. |
Description
Review only. Never merged. Do not close either: this PR is also the broadcast console.
dev/testnet-upgradesis a permanent branch (see CONTRIBUTING): master flows into it, never back. This PR exists to review the upgrade in one diff, to get a full CI run over it, and to drive the broadcast: adding arun:<Script>label here triggers one gated runbook step, which pauses on thepaseo-upgradeenvironment until a reviewer approves it, then reports back as a comment and removes its label.docs/PASEO-V080-RUNBOOK.mdon the head branch is the order and the operating manual. The PR stays open until the upgrade is declared and verified; close it after that.70 files, 20 commits, but only six shipped contracts change, all storage layout, no behaviour:
__whiteListSlot, soprotocolRegistrykeeps its slotWithout these the live proxies fail the layout diff, and the registrar controller would read
protocolRegistryas zero and brick.Everything else is upgrade material: 23 storage snapshots of the deployed code (
c8520046, not any tag), 13 upgrade and migration scripts, 13 fork tests,StoreFactoryMigrator, the runbook.Type
Scope
Related Issues
Supersedes #281, closed as permanent-by-design. Branch content merged via #310 and #311.
Fixes
Every pre-existing
Old.solsnapshot described an implementation replaced in place months ago, so each layout diff compared the wrong pair while staying green. The store-factory migration could not have been broadcast at all.Checklist
Code
forge buildpassesforge testpassesTesting
Security
selfdestructordelegatecallDocumentation
Breaking Changes
Breaking changes:
How to test
Notes
15 fork tests pass against live Paseo, plus 13 layout checks and 6 migrator unit tests.
verify-snapshots.shproves every snapshot reproduces the deployed bytecode, which is the check that would have caught the stale ones.DotnsRegistrarController's deployed code will differ from the v0.8.0 tag permanently, soverify --tagreports that one key as drift by design; a second key is a real finding. See DEPLOYMENTS.md anddeployments/paseo-assethub/README.md.Merging this would put 23
Old.solfiles on master, which CONTRIBUTING tells reviewers to refuse. That rule is correct and this PR is the documented exception: a review vehicle, not a merge candidate.