diff --git a/.env.example b/.env.example index c9240b3e5..3c8525b06 100644 --- a/.env.example +++ b/.env.example @@ -55,7 +55,7 @@ DOTNS_TLD=dot DEPLOYMENT_NETWORK= # Optional. Address of a pre-deployed CREATE3 factory to reuse. When set, -# DeployCore adopts it instead of minting a new one, so every DotNS address is +# DeployCore adopts it instead of minting a new one, so every dotNS address is # pinned to that factory and stays the same across chain resets. `bun run # deploy:all` sets this automatically from the factory step; set it by hand only # when running `bun run deploy` (the pipeline on its own). diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6b635390f..1de1318fe 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @waytwotall @sphamjoli +* @paritytech/dotns diff --git a/.github/abi-contracts.txt b/.github/abi-contracts.txt index c7cc76914..8511dde30 100644 --- a/.github/abi-contracts.txt +++ b/.github/abi-contracts.txt @@ -44,6 +44,7 @@ IPopRules IDotnsProtocolRegistry IDotnsNameEscrow IDotnsPopController +IDotnsPopControllerLegacy IDotnsPopResolver IDotnsPopLens IDotnsController diff --git a/.github/labels.yml b/.github/labels.yml index b959e161e..a08724bc0 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -120,7 +120,7 @@ - name: "dotns-sdk" color: "5319e7" - description: "DotNS SDK (UI, CLI, libraries)" + description: "dotNS SDK (UI, CLI, libraries)" - name: "smartcontracts" color: "e3a21a" diff --git a/.github/workflows/contract-coverage.yml b/.github/workflows/contract-coverage.yml index f1842a6d2..324206d44 100644 --- a/.github/workflows/contract-coverage.yml +++ b/.github/workflows/contract-coverage.yml @@ -60,6 +60,40 @@ jobs: - run: bun install + # `cache/` and `out/` are forge's own incremental-compile state: it hashes each + # source file and only recompiles what changed since the hash it last saw. A + # restored cache from an unrelated commit is always safe to build on top of. + # Forge just recompiles whatever the content hashes say is dirty, so a partial + # match via `restore-keys` still saves most of a cold `via_ir` build. This has + # to sit after `bun install`: its postinstall (`setup.bash`) force-reclones + # `lib/`'s pinned dependencies and reapplies the OZ patches on top, so hashing + # `lib/` any earlier would key the cache off content these steps are about to + # replace. + # + # Forge also drops persisted fuzz/invariant/test failure state under + # `cache/fuzz`, `cache/invariant`, and `cache/test-failures`, and replays it + # on the next run before exploring anything new (see foundry.toml's + # `[profile.ci.invariant]` comment). That state is cleared below, both before + # this job's `forge coverage` run and again right before the job ends, so it + # never rides along in what gets restored or saved here. + # + # This job keys its cache under `foundry-build-coverage-`, with `coverage` + # ahead of the OS on purpose. `forge coverage --ir-minimum` compiles under + # different settings from the test job and leaves different content in + # `cache/`/`out/`, so the two must never restore each other's builds. The + # test job restores anything starting with `foundry-build-${{ runner.os }}-` + # and GitHub picks the newest match, so any key that starts with that + # prefix (even `foundry-build-${{ runner.os }}-coverage-...`) would get + # picked up by it. Putting `coverage` first keeps it out of that prefix. + - uses: actions/cache@v4 + with: + path: | + cache + out + key: foundry-build-coverage-${{ runner.os }}-${{ hashFiles('foundry.toml', 'remappings.txt', 'contracts/**/*.sol', 'test/**/*.sol', 'scripts/**/*.sol', 'lib/**/*.sol') }} + restore-keys: | + foundry-build-coverage-${{ runner.os }}- + - name: Build contracts env: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" @@ -78,10 +112,17 @@ jobs: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" run: | set +e + + # A restored cache can carry a fuzz/invariant/test failure persisted by + # an earlier, unrelated run. Forge replays those before running anything + # new, so a stale one would fail this job even after the real bug behind + # it was fixed. Clear it before running coverage. + rm -rf cache/fuzz cache/invariant cache/test-failures + forge coverage --ir-minimum --report summary \ --no-match-coverage "test/|lib/" \ 2>&1 | tee coverage.log - CODE=$? + CODE=${PIPESTATUS[0]} python3 scripts/reports/coverage-report.py coverage.log exit $CODE @@ -231,6 +272,14 @@ jobs: await github.rest.issues.createComment({ owner, repo, issue_number, body }); } + # Last step in the job on purpose: `actions/cache`'s save only runs once + # every step above has finished, and it caches whatever is on disk at that + # point, not just what "Run coverage" left behind. Clearing here too means + # nothing this job did can leave a persisted failure in the saved cache. + - name: Clear persisted Foundry failure state before cache save + if: always() + run: rm -rf cache/fuzz cache/invariant cache/test-failures + cleanup: runs-on: ubuntu-latest if: github.event.action == 'closed' diff --git a/.github/workflows/deploy-contracts.yml b/.github/workflows/deploy-contracts.yml index 0165adef7..07588396b 100644 --- a/.github/workflows/deploy-contracts.yml +++ b/.github/workflows/deploy-contracts.yml @@ -282,7 +282,7 @@ jobs: fi # This CI deploy reproduces the expected address set. The canonical factory - # was deployed above, and every DotNS address is a pure function of that + # was deployed above, and every dotNS address is a pure function of that # factory plus a fixed salt, so the freshly deployed manifest must equal the # committed expected set. Assert that, print the expected-vs-actual table, then # rerun the pipeline to prove the deploy is resumable: a rerun adopts every diff --git a/.github/workflows/format-lint-product.yml b/.github/workflows/format-lint-product.yml index 60c8e1ae5..da489ef93 100644 --- a/.github/workflows/format-lint-product.yml +++ b/.github/workflows/format-lint-product.yml @@ -9,6 +9,12 @@ permissions: pull-requests: write issues: write +# Keyed by PR number so a new push cancels this PR's still-running lint job instead of +# queuing behind it; every other content-triggered workflow in this repo does the same. +concurrency: + group: format-lint-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: check: runs-on: ubuntu-latest diff --git a/.github/workflows/gas-report.yml b/.github/workflows/gas-report.yml index bf47a9b8d..1d23c298a 100644 --- a/.github/workflows/gas-report.yml +++ b/.github/workflows/gas-report.yml @@ -118,10 +118,31 @@ jobs: - run: bun install - - name: Start revive-eth-rpc (paseo_local fork target) - run: | - docker compose up -d --build - bash scripts/shell/wait-for-eth-rpc.sh + # `cache/` and `out/` are forge's own incremental-compile state: it hashes each + # source file and only recompiles what changed since the hash it last saw. A + # restored cache from an unrelated commit is always safe to build on top of. + # Forge just recompiles whatever the content hashes say is dirty, so a partial + # match via `restore-keys` still saves most of a cold `via_ir` build. This has + # to sit after `bun install`, not just after the master shard's patch step + # above: `bun install`'s postinstall (`setup.bash`) force-reclones `lib/`'s + # pinned dependencies for both shards and reapplies the OZ patches on top, + # so hashing `lib/` any earlier would key the cache off content that step + # is about to replace. + # + # Forge also drops persisted fuzz/invariant/test failure state under + # `cache/fuzz`, `cache/invariant`, and `cache/test-failures`, and replays it + # on the next run before exploring anything new (see foundry.toml's + # `[profile.ci.invariant]` comment). That state is cleared below, both before + # this job's `forge test` run and again right before the job ends, so it + # never rides along in what gets restored or saved here. + - uses: actions/cache@v4 + with: + path: | + cache + out + key: foundry-build-${{ runner.os }}-${{ matrix.target.id }}-${{ hashFiles('foundry.toml', 'remappings.txt', 'contracts/**/*.sol', 'test/**/*.sol', 'scripts/**/*.sol', 'lib/**/*.sol') }} + restore-keys: | + foundry-build-${{ runner.os }}-${{ matrix.target.id }}- - name: Generate gas report env: @@ -136,6 +157,13 @@ jobs: # while preserving the solc compilation cache under `cache/`. rm -rf out/build-info forge build + + # A restored cache can carry a fuzz/invariant/test failure persisted by + # an earlier, unrelated run. Forge replays those before running anything + # new, so a stale one would show up in this gas report even after the + # real bug behind it was fixed. Clear it before testing. + rm -rf cache/fuzz cache/invariant cache/test-failures + # Skip invariant tests in the gas report: invariant runs dominate # the wall-clock for ~20% of the gas signal (most invariants exercise # the same paths the unit/fuzz suites already cost). Keep the @@ -146,10 +174,6 @@ jobs: --no-match-path 'test/fork/**' \ 2>&1 | tee gas-report.txt - - name: Tear down revive-eth-rpc - if: always() - run: docker compose down --volumes --remove-orphans - - name: Stage shard output if: always() run: | @@ -163,6 +187,14 @@ jobs: path: shard retention-days: 7 + # Last step in the job on purpose: `actions/cache`'s save only runs once + # every step above has finished, and it caches whatever is on disk at that + # point, not just what "Generate gas report" left behind. Clearing here too + # means nothing this job did can leave a persisted failure in the saved cache. + - name: Clear persisted Foundry failure state before cache save + if: always() + run: rm -rf cache/fuzz cache/invariant cache/test-failures + report: name: Gas Report Diff needs: generate diff --git a/.github/workflows/genesis-extractor-test.yml b/.github/workflows/genesis-extractor-test.yml index d5c795dda..63221e331 100644 --- a/.github/workflows/genesis-extractor-test.yml +++ b/.github/workflows/genesis-extractor-test.yml @@ -13,6 +13,13 @@ on: permissions: contents: read +# Keyed by PR number so a new push cancels this PR's still-running extractor check +# instead of queuing behind it; every content-triggered workflow in this repo does +# the same. +concurrency: + group: genesis-extractor-test-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: genesis-extractor-test: runs-on: ubuntu-latest diff --git a/.github/workflows/issue-add-to-project.yml b/.github/workflows/issue-add-to-project.yml index dfbac4605..23e3e0833 100644 --- a/.github/workflows/issue-add-to-project.yml +++ b/.github/workflows/issue-add-to-project.yml @@ -1,4 +1,4 @@ -name: Add Issues to DotNS Project +name: Add Issues to dotNS Project on: issues: diff --git a/.github/workflows/paseo-upgrade-step.yml b/.github/workflows/paseo-upgrade-step.yml index 00d800e84..bc360d666 100644 --- a/.github/workflows/paseo-upgrade-step.yml +++ b/.github/workflows/paseo-upgrade-step.yml @@ -13,16 +13,13 @@ name: Paseo Upgrade Step # The gate is the `testnet-upgrades` environment: every run pauses until a required reviewer # approves it, whoever added the label. The label is the request; the approval is the decision. # Labels are also how retries work: the job removes its label when it finishes, so re-adding it -# re-runs the step. The rotation script skips what already moved and the twelve swaps are -# idempotent; step 13 is the exception, and its runbook section says to inspect, not re-add. +# re-runs the step. Re-running one that already landed is harmless: a single-proxy upgrade swaps +# in the same code again, and the other steps skip what is already on chain. # -# Key handling. Step 0 signs with the repository-level `DOTNS_ADMIN_KEY`, because its value -# cannot be read by anyone, so it cannot be re-entered as an environment secret. That exposure -# is time-boxed: step 0's whole purpose is to make that key powerless. The fresh key the -# operator generates IS entered as an environment secret under the same name, which shadows the -# repository one for every later step, and the repository copy is then deleted. Runner logs on -# this public repository are world-readable, so nothing here may ever echo key material; the -# password below only encrypts a keystore that lives for one run. +# Key handling. Every step signs with `DOTNS_ADMIN_KEY` from the `testnet-upgrades` +# environment, the owner of every proxy and of the protocol registry. Runner logs on this public +# repository are world-readable, so nothing here may ever echo key material; the password below +# only encrypts a keystore that lives for one run. on: pull_request: @@ -57,17 +54,20 @@ jobs: run: | set -euo pipefail SCRIPT="${LABEL#run:}" + # Each step names the contracts it upgrades, whose snapshots are checked against the + # chain right before it broadcasts. Steps that upgrade no proxy name none. case "$SCRIPT" in - RotateOwnership|UpgradeProtocolRegistry|UpgradeRegistry|UpgradeRegistrar|\ - UpgradeRegistrarController|UpgradePopController|UpgradePopRules|\ - UpgradeNameEscrow|UpgradeNameWhitelist|UpgradeResolver|UpgradeReverseResolver|\ - UpgradeContentResolver|UpgradePopResolver|MigrateStoreFactory|DeclareRelease) ;; + UpgradeRegistry) SUBJECTS="DotnsRegistry" ;; + UpgradePopRules) SUBJECTS="PopRules" ;; + UpgradePopResolverAndController) SUBJECTS="DotnsPopResolver DotnsPopController" ;; + RedeployPopLens|DeclareRelease) SUBJECTS="" ;; *) - echo "::error::'$SCRIPT' is not a runbook step; see docs/PASEO-V080-RUNBOOK.md" + echo "::error::'$SCRIPT' is not a runbook step; see docs/PASEO-V100-RUNBOOK.md" exit 1 ;; esac echo "script=$SCRIPT" >> "$GITHUB_OUTPUT" + echo "subjects=$SUBJECTS" >> "$GITHUB_OUTPUT" # The head commit, not the merge ref. A pull_request checkout defaults to the PR merged # into its base, and master merged into this branch is not the code that was reviewed or @@ -93,32 +93,28 @@ jobs: docker compose up --build -d eth-rpc scripts/shell/wait-for-eth-rpc.sh http://127.0.0.1:8545 - # Only before anything has been broadcast: the moment step 1 lands, the snapshots stop - # matching the chain by design, so this gate is pinned to step 0 instead of pretending to - # be a mid-flight health check. + # Scoped to the contracts this step upgrades. Every proxy an earlier step upgraded has + # moved past its snapshot by design; the ones this step is about to swap must still match, + # and a mismatch here means the code changed underneath the campaign. - name: Verify snapshots against the deployed bytecode - if: steps.step.outputs.script == 'RotateOwnership' + if: steps.step.outputs.subjects != '' env: RPC_URL: http://127.0.0.1:8545 + SNAPSHOT_SUBJECTS: ${{ steps.step.outputs.subjects }} run: scripts/shell/verify-snapshots.sh - name: Broadcast ${{ steps.step.outputs.script }} env: SCRIPT: ${{ steps.step.outputs.script }} RPC_URL: http://127.0.0.1:8545 - # `_account.sh` imports this into a run-local keystore when none exists. Environment - # secrets shadow repository ones for jobs that declare the environment, which is what - # switches every post-rotation step onto the fresh key without touching this file. + # `_account.sh` imports this into a run-local keystore when none exists. PRIVATE_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} # Encrypts a keystore that exists for the lifetime of this runner. Masked because no # log line should print even throwaway credentials in cleartext. ACCOUNT_PASSWORD: testnet-upgrades-run-${{ github.run_id }} - # Step parameters, configured as environment variables on `testnet-upgrades`: - # DOTNS_NEW_OWNER for step 0, DOTNS_RELEASE_TAG for step 14, and optionally - # DOTNS_OLD_STORE_FACTORY as the step 13 cross-check. - DOTNS_NEW_OWNER: ${{ vars.DOTNS_NEW_OWNER }} + # Step parameter, configured as an environment variable on `testnet-upgrades`: the + # release tag `RedeployPopLens` scopes its salt to and `DeclareRelease` declares. DOTNS_RELEASE_TAG: ${{ vars.DOTNS_RELEASE_TAG }} - DOTNS_OLD_STORE_FACTORY: ${{ vars.DOTNS_OLD_STORE_FACTORY }} run: | set -euo pipefail echo "::add-mask::$ACCOUNT_PASSWORD" @@ -126,8 +122,8 @@ jobs: # The broadcast record and the manifest are the two things a step changes on disk. The # manifest cannot be pushed back from here: the branch requires signed commits, which a - # runner cannot produce, so step 13's manifest change is committed by the operator from - # the artifact. Everything else leaves the workspace unchanged. + # runner cannot produce, so the manifest `RedeployPopLens` writes is committed by the + # operator from the artifact. Everything else leaves the workspace unchanged. - name: Keep the broadcast record if: always() uses: actions/upload-artifact@v4 @@ -151,5 +147,5 @@ jobs: run: | set -euo pipefail gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" \ - --body "**${SCRIPT}**: ${RESULT}. [Run](${RUN_URL}). Broadcast record and manifest attached as artifacts. Re-add \`${LABEL}\` to retry; for MigrateStoreFactory, read the runbook before retrying anything." + --body "**${SCRIPT}**: ${RESULT}. [Run](${RUN_URL}). Broadcast record and manifest attached as artifacts. Re-add \`${LABEL}\` to retry." gh pr edit "$PR" --repo "$GITHUB_REPOSITORY" --remove-label "$LABEL" diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 9632d30b2..d6275abd4 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -9,6 +9,13 @@ permissions: pull-requests: write issues: write +# Keyed by PR number so a new push (or label change) cancels this PR's still-running +# title/label check instead of queuing behind it; every content-triggered workflow in +# this repo does the same. +concurrency: + group: pr-title-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: title: name: PR Title diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 67cdf371a..51d7caf82 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -1,46 +1,62 @@ name: Publish Beta Package +# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, the +# `guard` job refuses a tag whose commit is not on master, and the gated job pauses on the +# `releases` environment for a reviewer. A release therefore ships reviewed master code and +# takes two distinct human actions: cutting the tag, and approving the run it starts. on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+-*" - workflow_dispatch: - inputs: - version: - description: "Pre-release version, e.g. v0.5.5-rc1. The tag is created from the selected branch." - required: true - type: string permissions: contents: write -# Two runs for the same version would race to attach assets to the same draft. On a -# dispatch `github.ref_name` is the branch, so key on the requested version instead. +# Two runs for the same version would race to attach assets to the same draft. concurrency: - group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }} + group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: false jobs: + # Runs first, with no environment and read-only access: a tag that does not point at a + # commit on master stops here, before a reviewer is asked to approve and before any + # secret is read. master only takes pull requests, which need an approving review, so + # this keeps every release on reviewed code. + guard: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require the tagged commit to be on master + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then + echo "::error::$GITHUB_REF_NAME points at $GITHUB_SHA, which is not on master. Tag a commit on master." + exit 1 + fi + echo "$GITHUB_REF_NAME points at $GITHUB_SHA, which is on master." + beta-release: + needs: guard runs-on: ubuntu-latest - # Baked into the genesis registry, so it decides which networks the artifact suits. - # Job-level: five steps name the file derived from it and must not disagree. + # Gated: this job reads the genesis owner key, which lives only on the `releases` + # environment, so every run waits for a reviewer's approval there. + environment: releases + # One genesis per TLD, since the TLD is baked into the registry: `.testnet` for previewnet, + # `.paseo` for Paseo Asset Hub Next V2. Job-level: several steps name the files it yields. env: - DOTNS_TLD: testnet + DOTNS_TLDS: testnet paseo steps: - # On workflow_dispatch the tag does not exist yet; the release step creates it - # from the branch this run was started on. Read through an env var rather than - # interpolating the input into the script. + # The run is started by pushing the tag, so the tag already exists and is the release's + # identity. Only the tag push triggers this workflow: the `release tags` ruleset keeps + # the workflow's token from creating a `v*` tag, so a run without one could not publish. - name: Resolve release tag - env: - GH_TOKEN: ${{ github.token }} - INPUT_VERSION: ${{ inputs.version }} run: | - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - TAG="$INPUT_VERSION" - else - TAG="$GITHUB_REF_NAME" - fi + TAG="$GITHUB_REF_NAME" # The suffix is restricted to characters GitHub keeps verbatim in an asset # name; a space, for instance, is rewritten to a dot and would fail the # asset check after a full build. Segmented like run.sh's semver check @@ -50,16 +66,8 @@ jobs: echo "::error::Pre-release version must look like v1.2.3-rc1, got '$TAG'." exit 1 fi - # A release created for an existing tag is cut at that tag's commit: GitHub - # ignores target_commitish when the tag is already there. The ABIs would come - # from this branch while the release pointed somewhere else. - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] \ - && gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" >/dev/null 2>&1; then - echo "::error::Tag $TAG already exists; use a different version." - exit 1 - fi echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" - echo "Releasing $TAG from $GITHUB_REF_NAME." + echo "Releasing $TAG." - name: Reject a pre-published release env: @@ -87,6 +95,30 @@ jobs: - uses: ./.github/actions/setup-foundry + # The environment pairs the key (secret) with its address (variable). Deriving one from + # the other before the build turns a mistyped key into a failed run instead of a genesis + # owned by an address nobody holds. The address is public; the key is never printed. + - name: Check the owner key against its declared address + env: + DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} + DOTNS_ADMIN_ADDRESS: ${{ vars.DOTNS_ADMIN_ADDRESS }} + run: | + set -euo pipefail + if [ -z "$DOTNS_ADMIN_KEY" ]; then + echo "::error::DOTNS_ADMIN_KEY is not configured on the releases environment" + exit 1 + fi + if [ -z "$DOTNS_ADMIN_ADDRESS" ]; then + echo "::error::DOTNS_ADMIN_ADDRESS is not configured on the releases environment" + exit 1 + fi + DERIVED="$(cast wallet address --private-key "$DOTNS_ADMIN_KEY")" + if [ "$(printf '%s' "$DERIVED" | tr 'A-Z' 'a-z')" != "$(printf '%s' "$DOTNS_ADMIN_ADDRESS" | tr 'A-Z' 'a-z')" ]; then + echo "::error::DOTNS_ADMIN_KEY derives to $DERIVED, the environment declares $DOTNS_ADMIN_ADDRESS" + exit 1 + fi + echo "Owner key derives to the declared address $DERIVED" + - uses: oven-sh/setup-bun@v2 with: bun-version: "1.2.6" @@ -125,8 +157,12 @@ jobs: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" FACTORY_DEPLOYER_KEY: ${{ secrets.FACTORY_DEPLOYER_KEY }} DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} - DOTNS_ADMIN_MNEMONIC: ${{ secrets.DOTNS_ADMIN_MNEMONIC }} - run: bash scripts/genesis/build-genesis.sh release + run: | + for tld in $DOTNS_TLDS; do + DOTNS_TLD="$tld" bash scripts/genesis/build-genesis.sh release + done + # The address set every genesis above carries, which the builder asserted. + cp deployments/expected.json release/dotns-genesis-addresses.json - name: Extract ABIs run: | @@ -152,6 +188,11 @@ jobs: find release/abis -maxdepth 1 -type f -printf '%f\n' | sort > release/expected-assets.txt echo "Extracted $(wc -l < release/expected-assets.txt) ABIs" + # Record which published ABIs each contract inherits from, read from the same build. + # The ABI diff step uses it to list a change once, under the interface that declares + # it, even when the names do not match. Kept out of release/, since it is not an asset. + bun scripts/js/release-metadata.mjs bases --out "$RUNNER_TEMP/abi-bases.json" + # No addresses: a pre-release is cut to be deployed, so the recorded addresses still # belong to the previous deployment of different code. Publishing them under this tag # would break the promise that a release's addresses and ABIs come from the same release. @@ -165,7 +206,7 @@ jobs: TAG="$RELEASE_TAG" cd release zip -r "../dotns-abis-${TAG}.zip" abis/ release-manifest.json codehashes.json \ - "dotns-genesis-${DOTNS_TLD}.json" + dotns-genesis-*.json - name: Generate release body env: @@ -175,7 +216,7 @@ jobs: ASSET_BASE="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$TAG" cat > release-body.md << 'ENDOFBODY' - ## DotNS ABI Package (Pre-release) + ## dotNS ABI Package (Pre-release) > **This is a pre-release.** ABIs may change before the stable release. @@ -199,7 +240,7 @@ jobs: ### Usage ```ts - import StoreAbi from "./abis/Store.json"; + import LabelStoreAbi from "./abis/LabelStore.json"; import RegistrarAbi from "./abis/DotnsRegistrar.json"; ``` ENDOFBODY @@ -222,7 +263,12 @@ jobs: echo "not the previous live deployment's." echo "" echo "- **Release contents:** [release-manifest.json]($ASSET_BASE/release-manifest.json)" - echo "- [dotns-genesis-${DOTNS_TLD}.json]($ASSET_BASE/dotns-genesis-${DOTNS_TLD}.json) — pallet-revive genesis, \`.${DOTNS_TLD}\` TLD, for ${DOTNS_TLD} networks only." + for tld in $DOTNS_TLDS; do + echo "- [dotns-genesis-${tld}.json]($ASSET_BASE/dotns-genesis-${tld}.json) - pallet-revive genesis, \`.${tld}\` TLD, for .${tld} networks only." + done + echo "- [dotns-genesis-addresses.json]($ASSET_BASE/dotns-genesis-addresses.json) - the addresses those genesis files carry." + echo "" + echo "Previewnet uses \`.testnet\`, Paseo Asset Hub Next V2 uses \`.paseo\`." } >> release-body.md - name: ABI diff since previous release @@ -241,22 +287,21 @@ jobs: prev_args=(--previous prev-abis/abis --previous-tag "$PREV") fi bun scripts/js/release-metadata.mjs abidiff --current release/abis \ - "${prev_args[@]}" --json release/abi-diff.json >> release-body.md + "${prev_args[@]}" --bases "$RUNNER_TEMP/abi-bases.json" \ + --json release/abi-diff.json >> release-body.md - name: Create draft pre-release with artifacts uses: softprops/action-gh-release@v2 with: - # Explicit because on workflow_dispatch there is no tag to infer; the action - # creates it at this run's commit. + # The tag that started this run. tag_name: ${{ env.RELEASE_TAG }} - target_commitish: ${{ github.sha }} files: | dotns-abis-*.zip release/abis/*.json release/release-manifest.json release/codehashes.json release/abi-diff.json - release/dotns-genesis-${{ env.DOTNS_TLD }}.json + release/dotns-genesis-*.json body_path: release-body.md draft: true prerelease: true @@ -275,7 +320,8 @@ jobs: echo "release-manifest.json" echo "codehashes.json" echo "abi-diff.json" - echo "dotns-genesis-${DOTNS_TLD}.json" + echo "dotns-genesis-addresses.json" + for tld in $DOTNS_TLDS; do echo "dotns-genesis-${tld}.json"; done } | sort > "$RUNNER_TEMP/wanted-assets.txt" if ! diff -u "$RUNNER_TEMP/wanted-assets.txt" "$RUNNER_TEMP/actual-assets.txt"; then echo "::error::Draft pre-release $TAG does not match the expected asset set; delete the draft and re-run." diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 3ae5748f4..b55a8f8d4 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,60 +1,68 @@ name: Publish Release Package +# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, the +# `guard` job refuses a tag whose commit is not on master, and the gated job pauses on the +# `releases` environment for a reviewer. A release therefore ships reviewed master code and +# takes two distinct human actions: cutting the tag, and approving the run it starts. on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" - workflow_dispatch: - inputs: - version: - description: "Version to release, e.g. v0.5.5. The tag is created from the selected branch." - required: true - type: string permissions: contents: write -# Two runs for the same version would race to attach assets to the same draft. On a -# dispatch `github.ref_name` is the branch, so key on the requested version instead. +# Two runs for the same version would race to attach assets to the same draft. concurrency: - group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }} + group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: false jobs: + # Runs first, with no environment and read-only access: a tag that does not point at a + # commit on master stops here, before a reviewer is asked to approve and before any + # secret is read. master only takes pull requests, which need an approving review, so + # this keeps every release on reviewed code. + guard: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require the tagged commit to be on master + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then + echo "::error::$GITHUB_REF_NAME points at $GITHUB_SHA, which is not on master. Tag a commit on master." + exit 1 + fi + echo "$GITHUB_REF_NAME points at $GITHUB_SHA, which is on master." + release: + needs: guard runs-on: ubuntu-latest - # Baked into the genesis registry, so it decides which networks the artifact suits. - # Job-level: five steps name the file derived from it and must not disagree. + # Gated: this job reads the genesis owner key, which lives only on the `releases` + # environment, so every run waits for a reviewer's approval there. + environment: releases + # One genesis per TLD, since the TLD is baked into the registry: `.testnet` for previewnet, + # `.paseo` for Paseo Asset Hub Next V2. Job-level: several steps name the files it yields. env: - DOTNS_TLD: testnet + DOTNS_TLDS: testnet paseo steps: - # On workflow_dispatch the tag does not exist yet; the release step creates it - # from the branch this run was started on. Read through an env var rather than - # interpolating the input into the script. + # The run is started by pushing the tag, so the tag already exists and is the release's + # identity. Only the tag push triggers this workflow: the `release tags` ruleset keeps + # the workflow's token from creating a `v*` tag, so a run without one could not publish. - name: Resolve release tag - env: - GH_TOKEN: ${{ github.token }} - INPUT_VERSION: ${{ inputs.version }} run: | - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - TAG="$INPUT_VERSION" - else - TAG="$GITHUB_REF_NAME" - fi + TAG="$GITHUB_REF_NAME" if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "::error::Version must look like v1.2.3, got '$TAG'." exit 1 fi - # A release created for an existing tag is cut at that tag's commit: GitHub - # ignores target_commitish when the tag is already there. The ABIs would come - # from this branch while the release pointed somewhere else. - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] \ - && gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" >/dev/null 2>&1; then - echo "::error::Tag $TAG already exists; use a different version." - exit 1 - fi echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" - echo "Releasing $TAG from $GITHUB_REF_NAME." + echo "Releasing $TAG." - name: Reject a pre-published release env: @@ -82,6 +90,30 @@ jobs: - uses: ./.github/actions/setup-foundry + # The environment pairs the key (secret) with its address (variable). Deriving one from + # the other before the build turns a mistyped key into a failed run instead of a genesis + # owned by an address nobody holds. The address is public; the key is never printed. + - name: Check the owner key against its declared address + env: + DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} + DOTNS_ADMIN_ADDRESS: ${{ vars.DOTNS_ADMIN_ADDRESS }} + run: | + set -euo pipefail + if [ -z "$DOTNS_ADMIN_KEY" ]; then + echo "::error::DOTNS_ADMIN_KEY is not configured on the releases environment" + exit 1 + fi + if [ -z "$DOTNS_ADMIN_ADDRESS" ]; then + echo "::error::DOTNS_ADMIN_ADDRESS is not configured on the releases environment" + exit 1 + fi + DERIVED="$(cast wallet address --private-key "$DOTNS_ADMIN_KEY")" + if [ "$(printf '%s' "$DERIVED" | tr 'A-Z' 'a-z')" != "$(printf '%s' "$DOTNS_ADMIN_ADDRESS" | tr 'A-Z' 'a-z')" ]; then + echo "::error::DOTNS_ADMIN_KEY derives to $DERIVED, the environment declares $DOTNS_ADMIN_ADDRESS" + exit 1 + fi + echo "Owner key derives to the declared address $DERIVED" + - uses: oven-sh/setup-bun@v2 with: bun-version: "1.2.6" @@ -108,21 +140,25 @@ jobs: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" run: forge test -vv - # pallet-revive genesis, so a chain can carry DotNS from block zero rather than + # pallet-revive genesis, so a chain can carry dotNS from block zero rather than # deploying it afterwards. Built here rather than downstream: this repo owns the # contracts, the deploy scripts and the CREATE3 factory key that fixes every address, # and the artifact then ships from the same commit as the ABIs beside it. # - # The owner key is taken from DOTNS_ADMIN_KEY if set, otherwise derived from - # DOTNS_ADMIN_MNEMONIC. It ends up owning the registry, resolvers, registrar, store factory - # and beacons in the genesis storage, so the script refuses to run without one. + # The owner key is DOTNS_ADMIN_KEY, checked against its declared address above. It + # ends up owning the registry, resolvers, registrar, store factory and beacons in the + # genesis storage, so the script refuses to run without it. - name: Build pallet-revive genesis env: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" FACTORY_DEPLOYER_KEY: ${{ secrets.FACTORY_DEPLOYER_KEY }} DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} - DOTNS_ADMIN_MNEMONIC: ${{ secrets.DOTNS_ADMIN_MNEMONIC }} - run: bash scripts/genesis/build-genesis.sh release + run: | + for tld in $DOTNS_TLDS; do + DOTNS_TLD="$tld" bash scripts/genesis/build-genesis.sh release + done + # The address set every genesis above carries, which the builder asserted. + cp deployments/expected.json release/dotns-genesis-addresses.json - name: Extract ABIs run: | @@ -148,6 +184,11 @@ jobs: find release/abis -maxdepth 1 -type f -printf '%f\n' | sort > release/expected-assets.txt echo "Extracted $(wc -l < release/expected-assets.txt) ABIs" + # Record which published ABIs each contract inherits from, read from the same build. + # The ABI diff step uses it to list a change once, under the interface that declares + # it, even when the names do not match. Kept out of release/, since it is not an asset. + bun scripts/js/release-metadata.mjs bases --out "$RUNNER_TEMP/abi-bases.json" + # Addresses come from the committed deployment manifests, the same files the # deployment pipeline in dotns-releases measures a live deploy against. A consumer # that only has the release needs them to reach any contract at all. @@ -159,7 +200,7 @@ jobs: TAG="$RELEASE_TAG" cd release zip -r "../dotns-abis-${TAG}.zip" abis/ deployments.json release-manifest.json \ - codehashes.json "dotns-genesis-${DOTNS_TLD}.json" + codehashes.json dotns-genesis-*.json - name: Generate release body env: @@ -169,7 +210,7 @@ jobs: ASSET_BASE="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$TAG" cat > release-body.md << 'ENDOFBODY' - ## DotNS ABI Package + ## dotNS ABI Package **Contracts** | Contract | ABI | @@ -191,7 +232,7 @@ jobs: ### Usage ```ts - import StoreAbi from "./abis/Store.json"; + import LabelStoreAbi from "./abis/LabelStore.json"; import RegistrarAbi from "./abis/DotnsRegistrar.json"; ``` ENDOFBODY @@ -212,6 +253,11 @@ jobs: echo "|---------|---------:|" jq -r '.networks | to_entries[] | "| \(.key) | \(.value.chainId) |"' release/deployments.json echo "" + echo "A network can still run an earlier release until it is upgraded to this one. Its protocol" + echo "registry's \`protocolVersion()\` returns the release it runs as bare semver (\`1.0.0\`, no \`v\`)," + echo "or an empty string when the network has never declared one. \`deployments:verify --tag\`" + echo "checks the chain against a release ([Verifying a release]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/blob/$TAG/RELEASE_ARTIFACTS.md#verifying-a-release))." + echo "" echo "- **Addresses:** [deployments.json]($ASSET_BASE/deployments.json)" echo "- **Release contents:** [release-manifest.json]($ASSET_BASE/release-manifest.json)" echo "- **Code identity:** [codehashes.json]($ASSET_BASE/codehashes.json) (stripped-metadata build hashes of this release's contracts; input to upgrade checks)" @@ -221,11 +267,16 @@ jobs: echo "" echo "### Genesis" echo "" - echo "Pallet-revive genesis state, for a chain that should carry DotNS from block zero" + echo "Pallet-revive genesis state, for a chain that should carry dotNS from block zero" echo "rather than deploying it afterwards." echo "" - echo "- [dotns-genesis-${DOTNS_TLD}.json]($ASSET_BASE/dotns-genesis-${DOTNS_TLD}.json) — registry initialised with the **\`.${DOTNS_TLD}\` TLD**, so it suits **${DOTNS_TLD} networks only** (this is what previewnet uses). Not for a production chain." - echo "- A chain booted from this genesis carries the fresh-deploy address set: \`deployments/expected.json\` in the repository at this tag. [deployments.json]($ASSET_BASE/deployments.json) records the live networks instead, and the two can differ where a live network has moved a contract." + for tld in $DOTNS_TLDS; do + echo "- [dotns-genesis-${tld}.json]($ASSET_BASE/dotns-genesis-${tld}.json) - registry initialised with the **\`.${tld}\` TLD**, so it suits **.${tld} networks only**. Not for a production chain." + done + echo "" + echo "Previewnet uses \`.testnet\`, Paseo Asset Hub Next V2 uses \`.paseo\`." + echo "" + echo "- [dotns-genesis-addresses.json]($ASSET_BASE/dotns-genesis-addresses.json) - the address set a chain booted from any of these genesis files carries. [deployments.json]($ASSET_BASE/deployments.json) records the live networks instead, and the two can differ where a live network has moved a contract." } >> release-body.md # `gh release view` with no tag resolves through /releases/latest, which skips @@ -257,15 +308,14 @@ jobs: prev_args=(--previous prev-abis/abis --previous-tag "$PREV") fi bun scripts/js/release-metadata.mjs abidiff --current release/abis \ - "${prev_args[@]}" --json release/abi-diff.json >> release-body.md + "${prev_args[@]}" --bases "$RUNNER_TEMP/abi-bases.json" \ + --json release/abi-diff.json >> release-body.md - name: Create draft release with artifacts uses: softprops/action-gh-release@v2 with: - # Explicit because on workflow_dispatch there is no tag to infer; the action - # creates it at this run's commit. + # The tag that started this run. tag_name: ${{ env.RELEASE_TAG }} - target_commitish: ${{ github.sha }} files: | dotns-abis-*.zip release/abis/*.json @@ -273,7 +323,7 @@ jobs: release/release-manifest.json release/codehashes.json release/abi-diff.json - release/dotns-genesis-${{ env.DOTNS_TLD }}.json + release/dotns-genesis-*.json body_path: release-body.md draft: true prerelease: false @@ -293,7 +343,8 @@ jobs: echo "release-manifest.json" echo "codehashes.json" echo "abi-diff.json" - echo "dotns-genesis-${DOTNS_TLD}.json" + echo "dotns-genesis-addresses.json" + for tld in $DOTNS_TLDS; do echo "dotns-genesis-${tld}.json"; done } | sort > "$RUNNER_TEMP/wanted-assets.txt" if ! diff -u "$RUNNER_TEMP/wanted-assets.txt" "$RUNNER_TEMP/actual-assets.txt"; then echo "::error::Draft release $TAG does not match the expected asset set; delete the draft and re-run." diff --git a/.github/workflows/push_checking.yml b/.github/workflows/push_checking.yml index eec0cc96d..6becb7a18 100644 --- a/.github/workflows/push_checking.yml +++ b/.github/workflows/push_checking.yml @@ -84,6 +84,31 @@ jobs: - run: bun install + # `cache/` and `out/` are forge's own incremental-compile state: it hashes each + # source file and only recompiles what changed since the hash it last saw. A + # restored cache from an unrelated commit is always safe to build on top of. + # Forge just recompiles whatever the content hashes say is dirty, so a partial + # match via `restore-keys` still saves most of a cold `via_ir` build. This has + # to sit after `bun install`: its postinstall (`setup.bash`) force-reclones + # `lib/`'s pinned dependencies and reapplies the OZ patches on top, so hashing + # `lib/` any earlier would key the cache off content these steps are about to + # replace. + # + # Forge also drops persisted fuzz/invariant/test failure state under + # `cache/fuzz`, `cache/invariant`, and `cache/test-failures`, and replays it + # on the next run before exploring anything new (see foundry.toml's + # `[profile.ci.invariant]` comment). That state is cleared below, both before + # this job's `forge test` runs and again right before the job ends, so it + # never rides along in what gets restored or saved here. + - uses: actions/cache@v4 + with: + path: | + cache + out + key: foundry-build-${{ runner.os }}-${{ hashFiles('foundry.toml', 'remappings.txt', 'contracts/**/*.sol', 'test/**/*.sol', 'scripts/**/*.sol', 'lib/**/*.sol') }} + restore-keys: | + foundry-build-${{ runner.os }}- + - name: Run tests id: run env: @@ -101,6 +126,13 @@ jobs: # build-info from another solc invocation would otherwise survive. # Preserving `cache/` keeps incremental compile fast. rm -rf out/build-info + + # A restored cache can carry a fuzz/invariant/test failure persisted by + # an earlier, unrelated run. Forge replays those before running anything + # new, so a stale one would fail this job even after the real bug behind + # it was fixed. Clear it before building or testing. + rm -rf cache/fuzz cache/invariant cache/test-failures + forge build --sizes > build.log 2>&1 BUILD=$? @@ -146,6 +178,14 @@ jobs: - if: steps.run.outputs.result && contains(steps.run.outputs.result, 'Failed') run: exit 1 + # Last step in the job on purpose: `actions/cache`'s save only runs once + # every step above has finished, and it caches whatever is on disk at that + # point, not just what "Run tests" left behind. Clearing here too means + # nothing this job did can leave a persisted failure in the saved cache. + - name: Clear persisted Foundry failure state before cache save + if: always() + run: rm -rf cache/fuzz cache/invariant cache/test-failures + # Fork tests validate the PR-scoped upgrade scripts against live Paseo Asset Hub # state through the ETH-RPC adapter. They exist only during an upgrade PR, so a # cheap detect job decides whether the heavy fork runner is provisioned at all: @@ -170,7 +210,7 @@ jobs: needs: detect-fork if: github.event.action != 'closed' && needs.detect-fork.outputs.has_fork == 'true' # The fork job builds the revive ETH-RPC adapter image and runs the upgrade - # suite, so it is kept off the shared test runner and onto the Parity XL runner. + # suite in a job of its own, separate from the matrix test runners. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-metadata.yml b/.github/workflows/release-metadata.yml index d0ff5577d..af29be386 100644 --- a/.github/workflows/release-metadata.yml +++ b/.github/workflows/release-metadata.yml @@ -7,6 +7,13 @@ name: Release Metadata # resolved against sources. on: pull_request: + # Includes labeled and unlabeled so adding `deployment-record` re-runs the manifest guard + # below. Listing types at all replaces the default set, so the defaults are repeated here. + types: [opened, edited, synchronize, reopened, labeled, unlabeled, ready_for_review] + # A pull request touching none of the paths below never starts this workflow, so no job in + # it reports a conclusion on that pull request. Harmless while these are optional checks, + # and the thing to settle before marking any of them required: a required check that never + # reports blocks the merge. paths: - "deployments/**" - ".github/abi-contracts.txt" @@ -16,6 +23,13 @@ on: permissions: contents: read +# Keyed by PR number so a new push cancels this PR's still-running manifest check +# instead of queuing behind it; every content-triggered workflow in this repo does +# the same. +concurrency: + group: release-metadata-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: validate: runs-on: ubuntu-latest @@ -29,3 +43,50 @@ jobs: - name: Validate deployment manifests and the contract list run: bun scripts/js/release-metadata.mjs validate + + # Live network manifests record what is deployed on a real chain and ship verbatim into the + # deployments.json release asset, so they are written by a deploy and never by a code change + # (CONTRIBUTING.md). The CREATE3 parity gates compare deployments/expected.json rather than + # these files, so this job is where that rule is enforced. A pull request that records a real + # deploy carries the `deployment-record` label and is exempt. + # Runs on every pull request rather than being skipped for a labelled one: a skipped job + # reports no conclusion, so making this a required check would leave a correctly labelled + # deploy pull request unable to merge. The label is read inside the step instead. + live-manifests-unchanged: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Reject edits to a live network manifest + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + IS_DEPLOY_RECORD: ${{ contains(github.event.pull_request.labels.*.name, 'deployment-record') }} + run: | + changed=$(git diff --name-only "$BASE_SHA"...HEAD -- 'deployments/*/*.json') + # A manifest identical to master's was carried in by merging master into a long-lived + # branch, which records nothing new. On a pull request into master it cannot match. + edited="" + for f in $changed; do + if git diff --quiet origin/master HEAD -- "$f" 2>/dev/null; then + echo "Carried from master unchanged: $f" + else + edited="$edited $f" + fi + done + changed="${edited# }" + if [ -z "$changed" ]; then + echo "No live network manifest was edited." + exit 0 + fi + if [ "$IS_DEPLOY_RECORD" = "true" ]; then + echo "Live network manifest edited under the 'deployment-record' label:" + printf ' %s\n' $changed + exit 0 + fi + echo "::error::A code pull request must not edit a live network manifest." + printf ' %s\n' $changed + echo "Update deployments/expected.json instead; the live file is written by a deploy." + echo "If this pull request records a real deploy, add the 'deployment-record' label." + exit 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60d6abfb7..f1a3b3060 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to DotNS +# Contributing to dotNS -These guidelines apply to the DotNS repository ("dotns"). Contributions are welcome via issues, pull requests, reviews, and testing feedback. Protocol behaviour is documented in [README.md](./README.md) and network addresses in `deployments//.json`; this file is the contributor mechanics. +These guidelines apply to the dotNS repository ("dotns"). Contributions are welcome via issues, pull requests, reviews, and testing feedback. Protocol behaviour is documented in [README.md](./README.md) and network addresses in `deployments//.json`; this file is the contributor mechanics. ## Types of contributing @@ -74,7 +74,7 @@ Before opening a pull request: ## Feature design -Treat the chain as the database. Assume no servers and no indexers. If a feature needs an offchain service to be usable, it is not a DotNS feature. +Treat the chain as the database. Assume no servers and no indexers. If a feature needs an offchain service to be usable, it is not a dotNS feature. This has a practical implication: every feature must come with an explicit query path. A client should be able to start from a small set of known contracts and find everything it needs with a bounded number of calls. Every getter is `external view`; controllers and resolvers check authorisation on writes, never on reads; governance key rotation does not break existing read paths because consumers re-resolve their siblings through the protocol registry on every call. @@ -118,12 +118,12 @@ Example query paths. Each row starts from a small set of known contracts; every | Lookup | Path | | --- | --- | -| Lite labelhash => full-person node | Protocol registry => PoP resolver => `fullClaim(liteLabelhash)` | -| Full-person node => lite labelhash | Protocol registry => PoP resolver => `liteLink(fullNode)` | +| Device-name labelhash => personhood-name node | Protocol registry => PoP resolver => `personhoodNodeOf(deviceLabelhash)` | +| Personhood-name node => device-name labelhash | Protocol registry => PoP resolver => `deviceLabelhashOf(personhoodNode)` | | Node => chat key | Protocol registry => PoP resolver => `chatKey(node)` | | Node or tokenId => registered label | Protocol registry => registrar => `labelOf(uint256(node))` | -| Base stem => gateway-reservation state | Protocol registry => PoP controller => `isReservedForClaim(baseLabel)` | -| Base stem => cross-flow reservation state | Protocol registry => PopRules => `isBaseNameReserved(baseLabel)` | +| Personhood name => gateway-reservation state | Protocol registry => PoP controller => `isReservedForClaim(label)` | +| Base name => cross-flow reservation state | Protocol registry => PopRules => `isBaseNameReserved(baseName)` | | Node => ERC721 owner | Protocol registry => registrar => `ownerOf(uint256(node))` | | Subnode => forward-registry owner | Protocol registry => registry => `owner(subnode)` | | Node => forward address record | Protocol registry => forward resolver => address record | @@ -135,7 +135,7 @@ Any new contract address that other contracts need to read must be looked up thr If you are adding a new contract category, add a `bytes32` key for it in `DotnsConstants.sol`, wire it up in `WireDeployments.s.sol` (including its entry in `_registryEntries`, so its code identity is declared and verified with the rest), and list the contract and its interface in `.github/abi-contracts.txt` so their ABIs ship in the release artifact. Read it the same way every existing contract does. Give the contract the standard `version()` mirror — a `view` that returns `protocolRegistry.protocolVersion()`, copied from any existing contract — and never a hardcoded version constant: what a network runs is declared once, on the protocol registry, every contract reports that one value, and per-contract identity is the declared codehash the deploy pipeline writes, not a self-report compiled into the bytecode. -A change that moves or adds an address (a new salt, a new contract, a contract restructured behind a proxy) must update `deployments/expected.json` in the same PR — that diff is where review sees the move — and must NOT touch any `deployments//.json`. Those are records of live networks, updated only by a real deploy on that network; editing one from a code PR publishes an address nothing is deployed at. The expected set diverging from a network's manifest is normal and means a redeploy or migration is owed on that network — see "Network manifests and the expected set" in `DEPLOYMENTS.md`. +A change that moves or adds an address (a new salt, a new contract, a contract restructured behind a proxy) must update `deployments/expected.json` in the same PR — that diff is where review sees the move — and must NOT touch any `deployments//.json`. Those are records of live networks, updated only by a real deploy on that network; editing one from a code PR publishes an address nothing is deployed at. The expected set diverging from a network's manifest is normal and means a redeploy or migration is owed on that network — see "Network manifests and the expected set" in `DEPLOYMENTS.md`. CI enforces this: `release-metadata.yml` fails a pull request that edits a live manifest unless the pull request carries the `deployment-record` label, which is how a real deploy records its addresses. Bad — the registrar address is frozen at construction, so rotating it needs an upgrade: @@ -191,7 +191,7 @@ Do not cache the new address in storage on the existing contract during the upgr ## Static analysis and security tooling -DotNS uses automated checks (including static analysis) on pull requests. +dotNS uses automated checks (including static analysis) on pull requests. Important caveats: @@ -315,11 +315,11 @@ Operational pitfalls of the local ETH-RPC adapter, each learned the hard way: 2. Delete the paired fork test under `test/fork/`. 3. Delete every `*Old.sol` and `I*Old.sol` referenced only by the upgrade script. 4. Delete temporary forge artefacts: `broadcast/') lines.append("") lines.append("") @@ -55,7 +55,7 @@ def generate_html(findings): lines.append('
') lines.append('
') - lines.append('

DotNS 4naly3er Report

') + lines.append('

dotNS 4naly3er Report

') lines.append(f'

Generated {timestamp} | {total} findings

') lines.append("
") diff --git a/scripts/reports/coverage-report.py b/scripts/reports/coverage-report.py index 8b3684817..2f77d5cbd 100644 --- a/scripts/reports/coverage-report.py +++ b/scripts/reports/coverage-report.py @@ -89,7 +89,7 @@ def generate_html(files, total_line_coverage): lines = [] lines.append('') lines.append('') - lines.append("DotNS Coverage Report") + lines.append("dotNS Coverage Report") lines.append('') lines.append("") lines.append("") @@ -97,7 +97,7 @@ def generate_html(files, total_line_coverage): lines.append('
') lines.append('
') - lines.append('

DotNS Coverage Report

') + lines.append('

dotNS Coverage Report

') lines.append(f'

Generated {timestamp} | {len(files)} files

') lines.append("
") diff --git a/scripts/reports/gas-report.py b/scripts/reports/gas-report.py index 94492b556..cd6882da3 100644 --- a/scripts/reports/gas-report.py +++ b/scripts/reports/gas-report.py @@ -135,7 +135,7 @@ def generate_html(master_contracts, current_contracts): lines = [] lines.append('') lines.append('') - lines.append("DotNS Gas Report") + lines.append("dotNS Gas Report") lines.append('') lines.append("") lines.append("") @@ -143,7 +143,7 @@ def generate_html(master_contracts, current_contracts): lines.append('
') lines.append('
') - lines.append('

DotNS Gas Report

') + lines.append('

dotNS Gas Report

') lines.append(f'

Generated {timestamp} | {len(all_contract_names)} contracts

') lines.append("
") diff --git a/scripts/reports/slither-report.py b/scripts/reports/slither-report.py index 2deac7e28..034310fb1 100644 --- a/scripts/reports/slither-report.py +++ b/scripts/reports/slither-report.py @@ -48,7 +48,7 @@ def generate_html(findings): lines = [] lines.append('') lines.append('') - lines.append("DotNS Slither Report") + lines.append("dotNS Slither Report") lines.append('') lines.append("") lines.append("") @@ -56,7 +56,7 @@ def generate_html(findings): lines.append('
') lines.append('
') - lines.append('

DotNS Slither Report

') + lines.append('

dotNS Slither Report

') lines.append(f'

Generated {timestamp} | {total} findings

') lines.append("
") diff --git a/scripts/reports/test-report.py b/scripts/reports/test-report.py index 81205b7bd..f7846e108 100644 --- a/scripts/reports/test-report.py +++ b/scripts/reports/test-report.py @@ -85,7 +85,7 @@ def generate_html(suites): lines = [] lines.append('') lines.append('') - lines.append("DotNS Test Report") + lines.append("dotNS Test Report") lines.append('') lines.append("") lines.append("") @@ -93,7 +93,7 @@ def generate_html(suites): lines.append('
') lines.append('
') - lines.append('

DotNS Test Report

') + lines.append('

dotNS Test Report

') lines.append(f'

Generated {timestamp} | {len(suites)} suites

') lines.append("
") diff --git a/scripts/shell/fork-tests.sh b/scripts/shell/fork-tests.sh index eef4b01df..d27695afa 100755 --- a/scripts/shell/fork-tests.sh +++ b/scripts/shell/fork-tests.sh @@ -39,8 +39,9 @@ if adapter_is_up; then else echo "fork-tests: starting the eth-rpc adapter (docker compose up --build -d eth-rpc)" docker compose up --build -d eth-rpc - scripts/shell/wait-for-eth-rpc.sh "$RPC_URL" fi +# Answering `eth_chainId` comes before the block index is usable, so a reused adapter waits too. +scripts/shell/wait-for-eth-rpc.sh "$RPC_URL" # The OpenZeppelin upgrade validator reads Foundry build-info, and a stale incremental # build trips it with "Found multiple contracts with name ...". Start from a clean diff --git a/scripts/shell/verify-snapshots.sh b/scripts/shell/verify-snapshots.sh index 608da79c0..1fc1285ed 100755 --- a/scripts/shell/verify-snapshots.sh +++ b/scripts/shell/verify-snapshots.sh @@ -21,6 +21,13 @@ set -euo pipefail # scripts/shell/verify-snapshots.sh # against RPC_URL # RPC_URL=https://eth-rpc-paseo-next.polkadot.io ... # DOTNS_NETWORK=paseo-assethub scripts/shell/verify-snapshots.sh +# SNAPSHOT_SUBJECTS="DotnsPopResolver DotnsPopController" ... # only these contracts +# +# SNAPSHOT_SUBJECTS limits the bytecode comparison to the named contracts. Mid-campaign, every +# proxy already upgraded has moved past its snapshot by design, so the gate before a step checks +# only the contracts that step upgrades. A named contract also passes when it already runs this +# build's implementation, which is what a step that died part way leaves behind; anything else is +# still a failure. Every snapshot is still built. ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT" @@ -50,11 +57,12 @@ echo "verify-snapshots: $manifest against $RPC_URL (chain $chain_id)" forge build >/dev/null -RPC_URL="$RPC_URL" MANIFEST="$manifest" python3 - "${snapshots[@]}" <<'PY' +RPC_URL="$RPC_URL" MANIFEST="$manifest" SNAPSHOT_SUBJECTS="${SNAPSHOT_SUBJECTS:-}" python3 - "${snapshots[@]}" <<'PY' import json, os, sys, time, urllib.request RPC = os.environ["RPC_URL"] manifest = json.load(open(os.environ["MANIFEST"])) +only = set(os.environ["SNAPSHOT_SUBJECTS"].split()) # EIP-1967 implementation slot. SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" @@ -99,7 +107,21 @@ def deployed(name): return addr, bytes.fromhex((rpc("eth_getCode", [addr, "latest"]) or "0x")[2:]) -failures, checked, skipped = [], 0, [] +def matches(art, live): + """Whether `live` is the artefact's runtime code, immutables and metadata masked.""" + built = bytearray(bytes.fromhex(art["deployedBytecode"]["object"][2:])) + live = bytearray(live) + if len(built) != len(live): + return False + for refs in art["deployedBytecode"].get("immutableReferences", {}).values(): + for r in refs: + start, length = r["start"], r["length"] + built[start:start + length] = b"\0" * length + live[start:start + length] = b"\0" * length + return strip_metadata(built) == strip_metadata(live) + + +failures, checked, skipped, upgraded = [], 0, [], 0 for path in sys.argv[1:]: snapshot = os.path.basename(path)[:-4] # DotnsRegistryOld subject = snapshot[:-3] # DotnsRegistry @@ -108,6 +130,9 @@ for path in sys.argv[1:]: # their own; they are pulled in so the contract snapshots compile. skipped.append(snapshot) continue + if only and subject not in only: + skipped.append(snapshot) + continue artefact = f"out/{snapshot}.sol/{snapshot}.json" if not os.path.exists(artefact): @@ -117,28 +142,32 @@ for path in sys.argv[1:]: art = json.load(open(artefact)) built = bytearray(bytes.fromhex(art["deployedBytecode"]["object"][2:])) impl, live = deployed(subject) - live = bytearray(live) checked += 1 + if matches(art, live): + print(f" ok {snapshot} == {subject} at {impl}") + continue + + # Scoped to a step: a subject that already runs this build was upgraded by an earlier run of + # the same step, which is the state a retry has to start from. + current = f"out/{subject}.sol/{subject}.json" + if only and os.path.exists(current) and matches(json.load(open(current)), live): + print(f" ok {subject} at {impl} already runs this build") + upgraded += 1 + continue + if len(built) != len(live): failures.append( f"{snapshot}: {len(built)} bytes built against {len(live)} deployed at {impl}" ) - continue - - for refs in art["deployedBytecode"].get("immutableReferences", {}).values(): - for r in refs: - start, length = r["start"], r["length"] - built[start:start + length] = b"\0" * length - live[start:start + length] = b"\0" * length - - if strip_metadata(built) != strip_metadata(live): - failures.append(f"{snapshot}: same length but different code from {impl}") else: - print(f" ok {snapshot} == {subject} at {impl}") + failures.append(f"{snapshot}: same length but different code from {impl}") + +if only - {os.path.basename(p)[:-7] for p in sys.argv[1:]}: + failures.append(f"SNAPSHOT_SUBJECTS names contracts with no snapshot: {sorted(only - {os.path.basename(p)[:-7] for p in sys.argv[1:]})}") if skipped: - print(f" -- {len(skipped)} snapshot(s) with no deployed counterpart: {', '.join(skipped)}") + print(f" -- {len(skipped)} snapshot(s) not compared: {', '.join(skipped)}") if failures: print("\nverify-snapshots: FAILED", file=sys.stderr) @@ -152,5 +181,8 @@ if failures: ) sys.exit(1) -print(f"\nverify-snapshots: {checked} snapshot(s) reproduce the deployed bytecode") +summary = f"{checked - upgraded} snapshot(s) reproduce the deployed bytecode" +if upgraded: + summary += f", {upgraded} contract(s) already run this build" +print(f"\nverify-snapshots: {summary}") PY diff --git a/test/base/BaseDotns.t.sol b/test/base/BaseDotns.t.sol index 2d771ca59..ad7454b20 100644 --- a/test/base/BaseDotns.t.sol +++ b/test/base/BaseDotns.t.sol @@ -40,8 +40,8 @@ import {IPersonhood} from "../../contracts/external/personhood/IPersonhood.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; /// @title BaseDotns -/// @notice Common Foundry test base for deploying a DotNS stack behind UUPS proxies. -/// @dev Deploys and wires the core DotNS contracts used by test suites: +/// @notice Common Foundry test base for deploying a dotNS stack behind UUPS proxies. +/// @dev Deploys and wires the core dotNS contracts used by test suites: /// - StoreFactory: per-user Store instances used for immutable registration writes /// - DotnsRegistrar: ERC721-backed registrar used to allocate label ownership /// - DotnsRegistry: forward registry used to set subnode ownership under .dot @@ -78,7 +78,7 @@ abstract contract BaseDotns is Test { /// @notice Deployed cost-model registry resolved by PopRules under `COST_MODEL`. DotnsCostModelRegistry public costModelRegistry; - /// @notice Deployed DotNS registrar instance. + /// @notice Deployed dotNS registrar instance. DotnsRegistrar public dotnsRegistrar; /// @notice Deployed registrar controller instance. @@ -96,30 +96,42 @@ abstract contract BaseDotns is Test { /// @notice Deployed reverse resolver instance. DotnsReverseResolver public dotnsReverseResolver; - /// @notice Deployed PoP resolver instance (chat keys + lite links). + /// @notice Deployed PoP resolver instance (chat keys + device-name links). DotnsPopResolver public dotnsPopResolver; - /// @notice Deployed PoP controller instance (gateway-driven lite/full issuance). + /// @notice Deployed PoP controller instance (gateway-driven device-name and personhood-name + /// issuance). DotnsPopController public dotnsPopController; /// @notice Deployed PoP lens instance (read-only view over PoP identity data). DotnsPopLens public dotnsPopLens; - /// @notice Selector for the typed reserveLiteName entrypoint. - bytes4 internal constant SELECTOR_RESERVE_LITE_TYPED = - bytes4(keccak256("reserveLiteName((string,address,bytes))")); + /// @notice Selector for the typed issueDeviceName entrypoint. + bytes4 internal constant SELECTOR_ISSUE_DEVICE_NAME = + bytes4(keccak256("issueDeviceName((string,address,bytes))")); - /// @notice Selector for the typed reserveBaseName entrypoint. - bytes4 internal constant SELECTOR_RESERVE_BASE_TYPED = - bytes4(keccak256("reserveBaseName(((string,address,bytes),string))")); + /// @notice Selector for the typed issueDeviceNameWithReservation entrypoint. + bytes4 internal constant SELECTOR_ISSUE_DEVICE_NAME_WITH_RESERVATION = + bytes4(keccak256("issueDeviceNameWithReservation(((string,address,bytes),string))")); - /// @notice Selector for the typed reserveBaseNameOnly entrypoint. - bytes4 internal constant SELECTOR_RESERVE_BASE_ONLY_TYPED = - bytes4(keccak256("reserveBaseNameOnly((address,string))")); + /// @notice Selector for the typed reservePersonhoodName entrypoint. + bytes4 internal constant SELECTOR_RESERVE_PERSONHOOD_NAME = + bytes4(keccak256("reservePersonhoodName((address,string))")); - /// @notice Selector for the typed registerBaseName entrypoint. - bytes4 internal constant SELECTOR_REGISTER_BASE_TYPED = - bytes4(keccak256("registerBaseName((string,address,(uint8,string,bytes)))")); + /// @notice Selector for the typed issuePersonhoodName entrypoint. + bytes4 internal constant SELECTOR_ISSUE_PERSONHOOD_NAME = + bytes4(keccak256("issuePersonhoodName((string,address,(uint8,string,bytes)))")); + + /// @notice Selector of `reserveLiteName((string,address,bytes))`. + /// @dev A literal: the legacy selectors never change while the legacy interface exists, so a + /// signature edit fails the pin in the legacy suite. + bytes4 internal constant SELECTOR_LEGACY_RESERVE_LITE_NAME = 0x220015c0; + + /// @notice Selector of `reserveBaseName(((string,address,bytes),string))`. + bytes4 internal constant SELECTOR_LEGACY_RESERVE_BASE_NAME = 0xd89f1bce; + + /// @notice Selector of `registerBaseName((string,address,(uint8,string,bytes)))`. + bytes4 internal constant SELECTOR_LEGACY_REGISTER_BASE_NAME = 0x7c4b376b; /// @notice Default reservation duration used by the PoP controller. uint64 public constant DEFAULT_RESERVATION_DURATION = 7 days; @@ -157,24 +169,32 @@ abstract contract BaseDotns is Test { bytes32 public constant ZERO_HASH = bytes32(0); // Classification-valid labels for the PoP path (post PopRules enforcement). - // A stem of 7 behind a two-digit suffix classifies as PopLite. These carry the gateway's + // A stem of 7 behind a two-digit suffix classifies as Devicehood. These carry the gateway's // separator, which reaches the chain only through the PoP path: the public path rejects a // separator, so a dotted label is always gateway-issued. - /// @notice PoP lite classification label fixture A. - string internal constant LITE_LABEL_A = "michael.01"; - /// @notice PoP lite classification label fixture B. - string internal constant LITE_LABEL_B = "matthew.02"; - /// @notice PoP lite classification label fixture C. - string internal constant LITE_LABEL_C = "william.03"; - /// @notice PoP lite classification label fixture D. - string internal constant LITE_LABEL_D = "richard.04"; - - // baselength 8 with no trailing digits classifies as PopFull. - /// @notice PoP full classification label fixture A. + /// @notice PoP device-name classification label fixture A. + string internal constant DEVICE_LABEL_A = "michael.01"; + /// @notice PoP device-name classification label fixture B. + string internal constant DEVICE_LABEL_B = "matthew.02"; + /// @notice PoP device-name classification label fixture C. + string internal constant DEVICE_LABEL_C = "william.03"; + /// @notice PoP device-name classification label fixture D. + string internal constant DEVICE_LABEL_D = "richard.04"; + + // Base length 8 with no trailing digits classifies as Personhood. + /// @notice Personhood-name fixture A: a single name, as a person would choose it. + string internal constant PERSONHOOD_LABEL_A = "victoria"; + /// @notice Personhood-name fixture B. + string internal constant PERSONHOOD_LABEL_B = "isabella"; + /// @notice Personhood-name fixture C. + string internal constant PERSONHOOD_LABEL_C = "caroline"; + + // Ordinary labels for suites outside PoP: 8 letters, no digits and no device suffix. + /// @notice Plain label fixture A, for suites that register or grant a label outside PoP. string internal constant BASE_LABEL_A = "alicebob"; - /// @notice PoP full classification label fixture B. + /// @notice Plain label fixture B. string internal constant BASE_LABEL_B = "wonderla"; - /// @notice PoP full classification label fixture C. + /// @notice Plain label fixture C. string internal constant BASE_LABEL_C = "carolboy"; // Measured whole at 11, which is 9 or more, so these classify as NoStatus. @@ -355,7 +375,7 @@ abstract contract BaseDotns is Test { vm.stopPrank(); vm.warp(block.timestamp + 365 days); // Default every account to `None` (NoStatus) on the personhood - // precompile. Per-account `_grantPopFull`/`_grantPopLite` calls install + // precompile. Per-account `_grantPersonhood`/`_grantDevicehood` calls install // more specific mocks that override this selector-only stub. vm.mockCall( DotnsConstants.PERSONHOOD, @@ -425,14 +445,14 @@ abstract contract BaseDotns is Test { node = LabelUtils.namehashUnder(_tldNode(), LabelUtils.labelhashMemory(label)); } - /// @notice Computes the hierarchical subnode for a lite label `.`. - /// @dev A lite username is `stem` beneath the numeric container `suffix.tld`, so its node is + /// @notice Computes the hierarchical subnode for a device name `.`. + /// @dev A device name is `stem` beneath the numeric container `suffix.tld`, so its node is /// `namehash(namehash(tldNode, keccak(suffix)), keccak(stem))`, not a hash of the whole - /// label. Mirrors @custom:function DotnsPopController._liteSubnode. - /// @param liteLabel Lite label, e.g. `michael.01`. + /// label. Mirrors @custom:function DotnsPopController._deviceSubnode. + /// @param deviceLabel Device name, e.g. `michael.01`. /// @return node The subnode identifier. - function _liteNodeOf(string memory liteLabel) internal pure returns (bytes32 node) { - (string memory stem, string memory suffix) = StringUtils.splitLiteLabel(liteLabel); + function _deviceNodeOf(string memory deviceLabel) internal pure returns (bytes32 node) { + (string memory stem, string memory suffix) = StringUtils.splitDeviceLabel(deviceLabel); bytes32 parentNode = LabelUtils.namehashUnder(_tldNode(), LabelUtils.labelhashMemory(suffix)); node = LabelUtils.namehashUnder(parentNode, LabelUtils.labelhashMemory(stem)); @@ -452,13 +472,14 @@ abstract contract BaseDotns is Test { /// @notice Mocks the personhood precompile so it returns `tier` for `who` /// under the dotns context. /// @dev Single source of truth for tier mocking. Tier numbering matches - /// @custom:contract IPersonhood: 0=None, 1=Lite, 2=Full. `contextAlias` is non-zero + /// @custom:contract IPersonhood, which names them 0=None, 1=Lite, 2=Full; + /// 1 is `Devicehood` and 2 is `Personhood` here. `contextAlias` is non-zero /// whenever `tier != 0` so callers that read it (cross-context /// identity tests) still see a deterministic value. function _setUserPopStatus(address who, IPopRules.PopStatus tier) internal { uint8 status; - if (tier == IPopRules.PopStatus.PopFull) status = 2; - else if (tier == IPopRules.PopStatus.PopLite) status = 1; + if (tier == IPopRules.PopStatus.Personhood) status = 2; + else if (tier == IPopRules.PopStatus.Devicehood) status = 1; // PopStatus.Reserved is a label classification, never a user tier; map // anything else to None. bytes32 contextAlias = status == 0 ? bytes32(0) : keccak256(abi.encode(who, status)); @@ -471,14 +492,14 @@ abstract contract BaseDotns is Test { ); } - /// @notice Grants PopFull status to `who` via the personhood precompile mock. - function _grantPopFull(address who) internal { - _setUserPopStatus(who, IPopRules.PopStatus.PopFull); + /// @notice Grants Personhood status to `who` via the personhood precompile mock. + function _grantPersonhood(address who) internal { + _setUserPopStatus(who, IPopRules.PopStatus.Personhood); } - /// @notice Grants PopLite status to `who` via the personhood precompile mock. - function _grantPopLite(address who) internal { - _setUserPopStatus(who, IPopRules.PopStatus.PopLite); + /// @notice Grants Devicehood status to `who` via the personhood precompile mock. + function _grantDevicehood(address who) internal { + _setUserPopStatus(who, IPopRules.PopStatus.Devicehood); } /// @notice Resets `who` back to `None` on the personhood precompile mock. @@ -522,22 +543,22 @@ abstract contract BaseDotns is Test { /// against the resolver and store hold. Chat keys are persisted eagerly on the PoP /// resolver at reserve time regardless of settlement. Tests that want to observe /// cold-path semantics (label stashed, no store deployed) must call - /// @custom:function _rootReserveBaseName or @custom:function _rootReserveLiteName - /// directly. + /// @custom:function _rootIssueDeviceNameWithReservation or + /// @custom:function _rootIssueDeviceName directly. function _reservePop( address user, - string memory liteLabel, + string memory deviceLabel, bytes memory chatKey, - string memory reservedBaseLabel + string memory reservedLabel ) internal { - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: liteLabel, user: user, chatKey: chatKey + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: deviceLabel, user: user, chatKey: chatKey }), - reservedBaseLabel: reservedBaseLabel + reservedLabel: reservedLabel }) ); if (dotnsPopController.pendingClaimCountOf(user) != 0) { @@ -546,28 +567,59 @@ abstract contract BaseDotns is Test { } } - /// @notice Dispatches the typed `reserveLiteName` call under a mocked Root origin. - function _rootReserveLiteName(IDotnsPopController.LiteRegistration memory params) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_LITE_TYPED, params)); + /// @notice Dispatches the typed `issueDeviceName` call under a mocked Root origin. + function _rootIssueDeviceName(IDotnsPopController.DeviceNameIssuance memory params) internal { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_ISSUE_DEVICE_NAME, params)); } /// @notice Dispatches the typed `reserveBaseName` call under a mocked Root origin. - function _rootReserveBaseName(IDotnsPopController.BaseReservation memory params) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_BASE_TYPED, params)); + function _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation memory params + ) + internal + { + _dispatchFromRoot( + abi.encodeWithSelector(SELECTOR_ISSUE_DEVICE_NAME_WITH_RESERVATION, params) + ); } - /// @notice Dispatches the typed `reserveBaseNameOnly` call under a mocked Root origin. - function _rootReserveBaseNameOnly(IDotnsPopController.BaseNameReservation memory params) + /// @notice Dispatches the typed `reservePersonhoodName` call under a mocked Root origin. + function _rootReservePersonhoodName(IDotnsPopController.PersonhoodNameReservation memory params) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_BASE_ONLY_TYPED, params)); + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_PERSONHOOD_NAME, params)); } - /// @notice Dispatches the typed `registerBaseName` call under a mocked Root origin. + /// @notice Dispatches the typed `issuePersonhoodName` call under a mocked Root origin. /// @dev Dispatches the label as written. The gateway sends what People Chain holds, so a - /// helper that reshaped it would hide whether a fixture is a valid lite label. - function _rootRegisterBaseName(IDotnsPopController.FullRegistration memory params) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_REGISTER_BASE_TYPED, params)); + /// helper that reshaped it would hide whether a fixture is a valid device name. + function _rootIssuePersonhoodName(IDotnsPopController.PersonhoodNameIssuance memory params) + internal + { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_ISSUE_PERSONHOOD_NAME, params)); + } + + /// @notice Dispatches the legacy `reserveLiteName` call under a mocked Root origin. + function _rootLegacyReserveLiteName(IDotnsPopController.DeviceNameIssuance memory params) + internal + { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_LEGACY_RESERVE_LITE_NAME, params)); + } + + /// @notice Dispatches the legacy `reserveBaseName` call under a mocked Root origin. + function _rootLegacyReserveBaseName( + IDotnsPopController.DeviceNameIssuanceWithReservation memory params + ) + internal + { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_LEGACY_RESERVE_BASE_NAME, params)); + } + + /// @notice Dispatches the legacy `registerBaseName` call under a mocked Root origin. + function _rootLegacyRegisterBaseName(IDotnsPopController.PersonhoodNameIssuance memory params) + internal + { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_LEGACY_REGISTER_BASE_NAME, params)); } /// @notice Calls `payload` on the PoP controller under a mocked Root origin. @@ -591,25 +643,25 @@ abstract contract BaseDotns is Test { return data; } - /// @notice Constructs a `Link` that inherits the chat key from a prior lite label. - function _linkWithLite(string memory liteLabel) + /// @notice Constructs a `Link` that inherits the chat key from a prior device name. + function _linkWithDeviceName(string memory deviceLabel) internal pure returns (IDotnsPopController.Link memory) { return IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.LiteUsername, liteLabel: liteLabel, chatKey: "" + kind: IDotnsPopController.LinkKind.DeviceName, deviceLabel: deviceLabel, chatKey: "" }); } - /// @notice Constructs a `Link` carrying a fresh chat key (no lite inheritance). + /// @notice Constructs a `Link` carrying a fresh chat key (no device-name inheritance). function _linkFresh(bytes memory chatKey) internal pure returns (IDotnsPopController.Link memory) { return IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.None, liteLabel: "", chatKey: chatKey + kind: IDotnsPopController.LinkKind.None, deviceLabel: "", chatKey: chatKey }); } @@ -717,7 +769,7 @@ abstract contract BaseDotns is Test { /// @notice Registers `label` for `labelOwner` under the requested PoP status and /// returns its node. - /// @dev For NoStatus, no status is set on the oracle. For PopLite/PopFull, status is set + /// @dev For NoStatus, no status is set on the oracle. For Devicehood/Personhood, status is set /// for `(labelOwner, label)` before commit-reveal. /// @param label The label to register (without the `.dot` suffix). /// @param labelOwner The address that will own the registered label. diff --git a/test/fork/BasePopFork.t.sol b/test/fork/BasePopFork.t.sol new file mode 100644 index 000000000..2f55d3796 --- /dev/null +++ b/test/fork/BasePopFork.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; +import {IDotnsPopController} from "../../contracts/registrars/IDotnsPopController.sol"; +import {IDotnsPopControllerLegacy} from "../../contracts/registrars/IDotnsPopControllerLegacy.sol"; +import {IDotnsProtocolRegistry} from "../../contracts/registry/IDotnsProtocolRegistry.sol"; +import {IPersonhood} from "../../contracts/external/personhood/IPersonhood.sol"; +import {DotnsConstants} from "../../contracts/utils/DotnsConstants.sol"; +import {LabelUtils} from "../../contracts/utils/LabelUtils.sol"; + +/// @title BasePopFork +/// @notice Shared PoP issuance flow for the fork tests that swap the PoP resolver, controller and +/// lens: issues a device name and a personhood name linked to it through the legacy +/// entrypoints the gateway pallet calls. +/// @dev Paseo's PoP resolver holds no links yet, so there is no live link to read back across the +/// swap. Writing one through the upgraded pair and reading it back in both directions is +/// what proves the pair works together. The labels are chosen to be unregistered on Paseo; +/// `_requireUnregistered` turns a collision into a clear failure. +/// @custom:security-contact admin@parity.io +abstract contract BasePopFork is BaseUpgradeFork { + /// @notice Device name issued by the flow, `stem.NN`. + string internal constant DEVICE_LABEL = "zqxforkp.07"; + + /// @notice Personhood name issued by the flow, linked to `DEVICE_LABEL`. + string internal constant PERSONHOOD_LABEL = "zqxforkp"; + + /// @notice Account the flow issues both names to. + address internal popUser; + + function setUp() public virtual override { + super.setUp(); + popUser = makeAddr("popUser"); + } + + /// @notice Issues `DEVICE_LABEL`, then `PERSONHOOD_LABEL` linked to it, under a Root origin. + /// @dev Goes through `IDotnsPopControllerLegacy`, the selectors the frozen gateway pallet + /// dispatches, so the test covers the path live traffic takes. + /// @param controller DotnsPopController proxy. + /// @return personhoodNode Node of the issued personhood name. + function _issueLinkedPair(address controller) internal returns (bytes32 personhoodNode) { + _mockRootOrigin(); + _mockPersonhood(popUser, 2); + + personhoodNode = _nodeOf(PERSONHOOD_LABEL); + _requireUnregistered(personhoodNode); + + IDotnsPopControllerLegacy(controller) + .reserveLiteName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: popUser, chatKey: _chatKey() + }) + ); + IDotnsPopControllerLegacy(controller) + .registerBaseName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL, + user: popUser, + link: IDotnsPopController.Link({ + kind: IDotnsPopController.LinkKind.DeviceName, + deviceLabel: DEVICE_LABEL, + chatKey: "" + }) + }) + ); + } + + /// @notice Node of a name directly under the TLD. + function _nodeOf(string memory label) internal view returns (bytes32 node) { + bytes32 tldNode = IDotnsProtocolRegistry(_live("DotnsProtocolRegistry")).tldNode(); + node = LabelUtils.namehashUnder(tldNode, LabelUtils.labelhashMemory(label)); + } + + /// @notice A valid 65-byte uncompressed chat key. + function _chatKey() internal pure returns (bytes memory key) { + key = new bytes(65); + key[0] = 0x04; + for (uint256 i = 1; i < 65; ++i) { + key[i] = 0x2a; + } + } + + /// @notice Makes the personhood precompile report `status` for `who` in the dotNS context. + /// @dev The precompile is not part of forked state, so every read of it is mocked. + function _mockPersonhood(address who, uint8 status) internal { + vm.mockCall( + DotnsConstants.PERSONHOOD, + abi.encodeWithSelector( + IPersonhood.personhoodStatus.selector, who, DotnsConstants.PERSONHOOD_CONTEXT + ), + abi.encode( + IPersonhood.PersonhoodInfo({ + status: status, contextAlias: keccak256(abi.encode(who, status)) + }) + ) + ); + } + + /// @notice Reverts unless `node` is unowned on the fork. + function _requireUnregistered(bytes32 node) internal view { + (bool ok, bytes memory data) = + _live("DotnsRegistry").staticcall(abi.encodeWithSignature("owner(bytes32)", node)); + require( + ok && abi.decode(data, (address)) == address(0), + "fork: the PoP test label is already registered on this network" + ); + } +} diff --git a/test/fork/MigrateStoreFactory.t.sol b/test/fork/MigrateStoreFactory.t.sol deleted file mode 100644 index 2796cd00d..000000000 --- a/test/fork/MigrateStoreFactory.t.sol +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {MigrateStoreFactory} from "../../scripts/deploy/MigrateStoreFactory.s.sol"; -import {UpgradeProtocolRegistryHarness} from "./UpgradeProtocolRegistry.t.sol"; -import {StoreFactory} from "../../contracts/store/StoreFactory.sol"; -import {StoreFactoryMigrator} from "../../contracts/store/StoreFactoryMigrator.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {IStoreFactory} from "../../contracts/store/IStoreFactory.sol"; -import {IDotnsStore} from "../../contracts/store/IDotnsStore.sol"; -import {StoreUtils} from "../../contracts/utils/StoreUtils.sol"; -import {DotnsConstants} from "../../contracts/utils/DotnsConstants.sol"; -import {IDotnsProtocolRegistry} from "../../contracts/registry/IDotnsProtocolRegistry.sol"; - -/// @title MigrateStoreFactoryHarness -/// @notice Exposes the migration script's legs so the test drives the production path. -/// @dev The script's `run` reads the network folder and writes the manifest, neither of which a -/// fork should do. The legs underneath take their addresses directly, so the test runs the -/// same code in the same order without touching the working tree. -contract MigrateStoreFactoryHarness is MigrateStoreFactory { - /// @notice Deploys the replacement proxy, as the script's first step does. - function deployReplacement( - address owner, - address protocolRegistry - ) - external - returns (address replacement) - { - replacement = _deployReplacement(owner, protocolRegistry); - } - - /// @notice Imports the bindings from `oldFactory` into `replacement`. - function importInto(address owner, address replacement, address oldFactory) external { - _importBindings(owner, replacement, oldFactory); - } - - /// @notice Returns the proxy to the shipped implementation. - function restore(address owner, address replacement) external { - _restoreShippedImplementation(owner, replacement); - } - - /// @notice Points the `storeFactory` key at the replacement and declares its codehash. - function rewire(address owner, address replacement, address protocolRegistry) external { - _rewireKey(owner, replacement, protocolRegistry); - } -} - -/// @title MigrateStoreFactoryForkTest -/// @notice Pairs one-to-one with `scripts/deploy/MigrateStoreFactory.s.sol`. Runs the migration -/// against live state and checks the condition that actually matters: after the rewire, -/// the path every registration takes returns the store a holder already has. -/// @dev The only part of the upgrade that moves user state between contracts instead of swapping -/// code underneath it, so it is where a mistake is least reversible. Holders are read from -/// the chain, never invented: a fixture would show the import copies a fixture, and what -/// needs showing is that it copies what the network holds. -/// -/// The protocol registry is upgraded first, because the rewire declares a codehash and that -/// entrypoint does not exist on the implementation the network starts on. That is also the -/// production ordering, so running it here keeps the two honest. -/// @custom:security-contact admin@parity.io -contract MigrateStoreFactoryForkTest is BaseUpgradeFork { - /// @notice The factory the network runs today, which the manifest still names. - IStoreFactory internal oldFactory; - - /// @notice The live protocol registry, rewired to the replacement by the migration. - IDotnsProtocolRegistry internal protocolRegistry; - - /// @notice Owner of the deployment, impersonated to authorise every step. - address internal factoryOwner; - - /// @notice Drives the script's own migration path. - MigrateStoreFactoryHarness internal migrator; - - function setUp() public override { - super.setUp(); - - oldFactory = IStoreFactory(_live("StoreFactory")); - protocolRegistry = IDotnsProtocolRegistry(_live("DotnsProtocolRegistry")); - factoryOwner = _ownerOf(address(oldFactory)); - migrator = new MigrateStoreFactoryHarness(); - - // `_rewireKey` declares a codehash, which the deployed registry cannot do until it is - // swapped. Production runs the registry first for the same reason. - UpgradeProtocolRegistryHarness registryUpgrade = new UpgradeProtocolRegistryHarness(); - registryUpgrade.upgrade(_ownerOf(address(protocolRegistry)), address(protocolRegistry)); - } - - /// @notice After the migration, a lookup through the registry returns the holder's own store. - /// @dev The success condition for the whole exercise, and the one an earlier version of this - /// test never reached. Every mint and transfer resolves `STORE_FACTORY` through the - /// protocol registry and calls `ensureLabelStore`, which deploys a store when the caller - /// has none. Had the bindings not come across, this call would quietly deploy a second, - /// empty store for a user who already has one, and their labels would stop being - /// enumerable against their address. - function test_after_rewire_a_live_holder_resolves_to_their_existing_store() public { - uint256 total = oldFactory.getLabelStoreCount(); - assertTrue(total != 0, "fork precondition: the network has stores to migrate"); - - address[] memory stores = oldFactory.getLabelStores(0, total); - address holderStore = stores[0]; - address holder = IDotnsStore(holderStore).owner(); - assertEq(oldFactory.getLabelStore(holder), holderStore, "fork precondition: binding agrees"); - - address replacement = migrator.deployReplacement(factoryOwner, address(protocolRegistry)); - migrator.importInto(factoryOwner, replacement, address(oldFactory)); - migrator.restore(factoryOwner, replacement); - migrator.rewire(factoryOwner, replacement, address(protocolRegistry)); - - assertEq( - protocolRegistry.get(DotnsConstants.STORE_FACTORY), - replacement, - "the key resolves to the replacement" - ); - - // The production path, reached the way registration reaches it. - IStoreFactory resolved = IStoreFactory(protocolRegistry.get(DotnsConstants.STORE_FACTORY)); - vm.prank(factoryOwner); - address ensured = StoreUtils.ensureLabelStore(resolved, holder); - - assertEq(ensured, holderStore, "the holder resolves to the store they already had"); - assertEq(resolved.getLabelStoreCount(), total, "no extra store was deployed for anyone"); - } - - /// @notice Every holder the old factory reports comes across, not only the first. - /// @dev The count is the cheap end-to-end check. A partial import would satisfy any - /// single-holder assertion while leaving the rest to be handed empty stores later. - function test_every_live_holder_is_carried() public { - uint256 total = oldFactory.getLabelStoreCount(); - address[] memory stores = oldFactory.getLabelStores(0, total); - - address replacement = migrator.deployReplacement(factoryOwner, address(protocolRegistry)); - migrator.importInto(factoryOwner, replacement, address(oldFactory)); - migrator.restore(factoryOwner, replacement); - - StoreFactory migrated = StoreFactory(replacement); - assertEq(migrated.getLabelStoreCount(), total, "every binding landed"); - - for (uint256 i; i < total; ++i) { - address holder = IDotnsStore(stores[i]).owner(); - assertEq(migrated.getLabelStore(holder), stores[i], "holder keeps their existing store"); - } - } - - /// @notice A death between import pages is finished by replaying the pages. - /// @dev The import is paged because the whole list does not fit in one pallet-revive block, - /// and pages are separate transactions, so the run can die with a prefix imported and - /// the proxy parked on the migrator. This drills the recovery the runbook prescribes - /// against live state: replay the import from the top, and the pages that already landed - /// skip themselves. The count assertions are the proof nothing was carried twice. - function test_a_death_between_pages_is_finished_by_replaying_them() public { - uint256 total = oldFactory.getLabelStoreCount(); - assertTrue(total > 1, "fork precondition: enough stores for more than one page"); - - address replacement = migrator.deployReplacement(factoryOwner, address(protocolRegistry)); - - // Park the proxy exactly where a mid-run death leaves it: on the migrator, holding a - // prefix of the bindings. - uint256 firstPage = total / 2; - StoreFactoryMigrator parked = new StoreFactoryMigrator(); - vm.prank(factoryOwner); - UUPSUpgradeable(replacement) - .upgradeToAndCall( - address(parked), - abi.encodeCall( - StoreFactoryMigrator.importStores, (address(oldFactory), 0, firstPage) - ) - ); - assertEq( - IStoreFactory(replacement).getLabelStoreCount(), - firstPage, - "the interrupted state holds exactly the prefix" - ); - - // The continuation: the import leg again, whole. Its first pages overlap what landed. - migrator.importInto(factoryOwner, replacement, address(oldFactory)); - migrator.restore(factoryOwner, replacement); - - StoreFactory migrated = StoreFactory(replacement); - assertEq(migrated.getLabelStoreCount(), total, "the replay finished the import"); - - address[] memory stores = oldFactory.getLabelStores(0, total); - for (uint256 i; i < total; ++i) { - address holder = IDotnsStore(stores[i]).owner(); - assertEq( - migrated.getLabelStore(holder), stores[i], "no holder was rebound or duplicated" - ); - } - } - - /// @notice A user the old factory never held is still unbound afterwards. - /// @dev The import writes bindings directly, so it is worth showing it writes only what the - /// old factory holds. A spurious binding is worse than a missing one: it consumes the - /// user's single permanent slot, and the factory then refuses them a real store forever. - function test_import_binds_nobody_the_old_factory_did_not_hold() public { - address stranger = makeAddr("stranger"); - assertEq( - oldFactory.getLabelStore(stranger), address(0), "fork precondition: stranger is unbound" - ); - - address replacement = migrator.deployReplacement(factoryOwner, address(protocolRegistry)); - migrator.importInto(factoryOwner, replacement, address(oldFactory)); - migrator.restore(factoryOwner, replacement); - - assertEq( - StoreFactory(replacement).getLabelStore(stranger), - address(0), - "a user the old factory never held stays unbound" - ); - } -} diff --git a/test/fork/PaseoV100Campaign.t.sol b/test/fork/PaseoV100Campaign.t.sol new file mode 100644 index 000000000..c72201046 --- /dev/null +++ b/test/fork/PaseoV100Campaign.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +import {BasePopFork} from "./BasePopFork.t.sol"; +import {UpgradeRegistryHarness} from "./UpgradeRegistry.t.sol"; +import {UpgradePopRulesHarness} from "./UpgradePopRules.t.sol"; +import {UpgradePopResolverAndControllerHarness} from "./UpgradePopResolverAndController.t.sol"; +import {RedeployPopLensHarness} from "./RedeployPopLens.t.sol"; +import {DeclareRelease} from "../../scripts/deploy/DeclareRelease.s.sol"; +import {IDotnsProtocolRegistry} from "../../contracts/registry/IDotnsProtocolRegistry.sol"; +import {IDotnsPopLens} from "../../contracts/registrars/IDotnsPopLens.sol"; + +/// @title DeclareReleaseHarness +/// @notice Runs `DeclareRelease.declare` against the manifest with the lens the campaign deployed. +/// @dev On chain, the operator commits the manifest `RedeployPopLens` wrote before `DeclareRelease` +/// runs. A fork test must not write the manifest, so the harness substitutes the new lens in +/// the loaded addresses and runs the same steps in the same order. +contract DeclareReleaseHarness is DeclareRelease { + /// @notice The body of `declare`, with `popLens` replaced by `lens`. + function declareWithLens(address owner, address lens, string memory releaseTag) external { + initDeployment("paseo-assethub", vm.toString(block.chainid)); + Addresses memory addr = _loadAddresses(); + addr.popLens = lens; + + _requireKeysMatchManifest(addr); + _wireMissingKeys(owner, addr); + _declareCodeIdentity(owner, addr); + _verifyDeployment(addr, owner); + _declareProtocolVersion(owner, addr, releaseTag); + } +} + +/// @title PaseoV100CampaignForkTest +/// @notice The whole v0.8.0 to v1.0.0 campaign on one fork, in broadcast order: registry, rules, +/// the PoP resolver and controller pair, the lens, then the release declaration. +/// @dev The rehearsal for the label-driven run. Each step's own fork test proves the step; this +/// proves the sequence, including that the release declaration verifies once every swap has +/// landed and that the network is consistent between the steps. +/// @custom:security-contact admin@parity.io +contract PaseoV100CampaignForkTest is BasePopFork { + /// @notice Bare semver the campaign declares. + string internal constant RELEASE_TAG = "1.0.0"; + + /// @notice Owner of every proxy and of the protocol registry. + address internal owner; + + /// @notice The protocol registry. + IDotnsProtocolRegistry internal registry; + + function setUp() public override { + super.setUp(); + registry = IDotnsProtocolRegistry(_live("DotnsProtocolRegistry")); + owner = _ownerOf(address(registry)); + } + + /// @notice All five steps in order end on a verified 1.0.0 declaration. + function test_campaign_ends_on_a_verified_release() public { + assertEq(registry.protocolVersion(), "0.8.0", "starts on 0.8.0"); + + new UpgradeRegistryHarness().upgrade(owner, _live("DotnsRegistry")); + new UpgradePopRulesHarness().upgrade(owner, _live("PopRules")); + new UpgradePopResolverAndControllerHarness() + .upgrade(owner, _live("DotnsPopResolver"), _live("DotnsPopController")); + address lens = new RedeployPopLensHarness() + .redeploy(owner, address(registry), _live("DotnsPopLens"), RELEASE_TAG); + + new DeclareReleaseHarness().declareWithLens(owner, lens, RELEASE_TAG); + + assertEq(registry.protocolVersion(), RELEASE_TAG, "declares 1.0.0"); + + bytes32 personhoodNode = _issueLinkedPair(_live("DotnsPopController")); + assertEq( + IDotnsPopLens(lens).nameDetail(DEVICE_LABEL).personhoodNode, + personhoodNode, + "issuance and lens work end to end" + ); + } + + /// @notice A declaration run against a manifest that still names the outgoing lens stops + /// before it writes a codehash. + function test_declaration_refuses_a_stale_manifest() public { + address outgoing = _live("DotnsPopLens"); + new UpgradePopResolverAndControllerHarness() + .upgrade(owner, _live("DotnsPopResolver"), _live("DotnsPopController")); + new RedeployPopLensHarness().redeploy(owner, address(registry), outgoing, RELEASE_TAG); + + DeclareReleaseHarness declarer = new DeclareReleaseHarness(); + vm.expectRevert(bytes("DeclareRelease: key popLens points away from the manifest")); + declarer.declareWithLens(owner, outgoing, RELEASE_TAG); + } + + /// @notice After the first two steps alone, the v0.8.0 PoP controller still issues against the + /// upgraded registry and rules, so the pause between labels leaves a working network. + function test_v080_controller_still_issues_after_registry_and_rules() public { + new UpgradeRegistryHarness().upgrade(owner, _live("DotnsRegistry")); + new UpgradePopRulesHarness().upgrade(owner, _live("PopRules")); + + bytes32 personhoodNode = _issueLinkedPair(_live("DotnsPopController")); + (bool ok, bytes memory data) = _live("DotnsRegistry") + .staticcall(abi.encodeWithSignature("owner(bytes32)", personhoodNode)); + assertTrue(ok, "registry readable"); + assertEq(abi.decode(data, (address)), popUser, "issued through the v0.8.0 controller"); + } +} diff --git a/test/fork/RedeployPopLens.t.sol b/test/fork/RedeployPopLens.t.sol new file mode 100644 index 000000000..874ea09d1 --- /dev/null +++ b/test/fork/RedeployPopLens.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +import {BasePopFork} from "./BasePopFork.t.sol"; +import {UpgradePopResolverAndControllerHarness} from "./UpgradePopResolverAndController.t.sol"; +import {RedeployPopLens} from "../../scripts/deploy/RedeployPopLens.s.sol"; +import {IDotnsPopLens} from "../../contracts/registrars/IDotnsPopLens.sol"; +import {IDotnsPopResolver} from "../../contracts/resolvers/IDotnsPopResolver.sol"; +import {IDotnsProtocolRegistry} from "../../contracts/registry/IDotnsProtocolRegistry.sol"; +import {DotnsConstants} from "../../contracts/utils/DotnsConstants.sol"; + +/// @title RedeployPopLensHarness +/// @notice Exposes the script's internal path so the test drives the code the production run +/// executes. +/// @dev The script's `run` reads the network folder and writes the manifest, neither of which a +/// fork test should touch, so the harness enters below both. +contract RedeployPopLensHarness is RedeployPopLens { + /// @notice Deploys and wires the lens under `owner` through the script's own internal. + function redeploy( + address owner, + address protocolRegistry, + address outgoing, + string memory releaseTag + ) + external + returns (address lens) + { + lens = _redeployPopLens(owner, protocolRegistry, outgoing, releaseTag); + } +} + +/// @title RedeployPopLensForkTest +/// @notice Pairs one-to-one with `scripts/deploy/RedeployPopLens.s.sol`. Runs after the PoP +/// resolver and controller swap, as on chain, then redeploys the lens and proves the key, +/// the declaration and the reads it serves. +/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. +/// @custom:security-contact admin@parity.io +contract RedeployPopLensForkTest is BasePopFork { + /// @notice Release tag the salt is scoped to. + string internal constant RELEASE_TAG = "1.0.0"; + + /// @notice The protocol registry holding the `popLens` key. + IDotnsProtocolRegistry internal registry; + + /// @notice Lens the manifest records today. + address internal outgoing; + + /// @notice Protocol registry owner, impersonated to authorise the rewire. + address internal registryOwner; + + /// @notice Drives the script's redeploy path. + RedeployPopLensHarness internal redeployer; + + function setUp() public override { + super.setUp(); + registry = IDotnsProtocolRegistry(_live("DotnsProtocolRegistry")); + outgoing = _live("DotnsPopLens"); + registryOwner = _ownerOf(address(registry)); + redeployer = new RedeployPopLensHarness(); + + UpgradePopResolverAndControllerHarness pair = new UpgradePopResolverAndControllerHarness(); + address controller = _live("DotnsPopController"); + pair.upgrade(_ownerOf(controller), _live("DotnsPopResolver"), controller); + } + + /// @notice The key moves to the new lens with a matching declaration, and the lens serves the + /// link the upgraded pair writes. + function test_redeploy_rewires_declares_and_serves_links() public { + assertEq(registry.get(DotnsConstants.POP_LENS), outgoing, "key starts on the outgoing lens"); + + address lens = redeployer.redeploy(registryOwner, address(registry), outgoing, RELEASE_TAG); + + assertTrue(lens != outgoing, "a new lens"); + assertEq(registry.get(DotnsConstants.POP_LENS), lens, "key points at the new lens"); + assertEq( + registry.expectedCodehash(DotnsConstants.POP_LENS), + lens.codehash, + "declaration matches the executing code" + ); + assertEq(IDotnsPopLens(lens).protocolRegistry(), address(registry), "bound to the registry"); + + bytes32 personhoodNode = _issueLinkedPair(_live("DotnsPopController")); + IDotnsPopLens.NameDetail memory detail = IDotnsPopLens(lens).nameDetail(PERSONHOOD_LABEL); + assertEq(detail.node, personhoodNode, "node"); + assertEq(detail.owner, popUser, "owner"); + assertEq( + detail.deviceLabelhash, + IDotnsPopResolver(_live("DotnsPopResolver")).deviceLabelhashOf(personhoodNode), + "device link read through the lens" + ); + + IDotnsPopLens.NameDetail memory device = IDotnsPopLens(lens).nameDetail(DEVICE_LABEL); + assertEq(device.personhoodNode, personhoodNode, "personhood link read through the lens"); + } + + /// @notice A second run adopts the lens the first one deployed and broadcasts nothing new. + function test_rerun_adopts_the_deployed_lens() public { + address first = redeployer.redeploy(registryOwner, address(registry), outgoing, RELEASE_TAG); + address second = + redeployer.redeploy(registryOwner, address(registry), outgoing, RELEASE_TAG); + + assertEq(second, first, "same lens"); + assertEq(registry.get(DotnsConstants.POP_LENS), first, "key unchanged"); + } + + /// @notice A key pointing somewhere unexpected stops the run before it broadcasts. + function test_refuses_a_key_the_manifest_does_not_describe() public { + vm.expectRevert( + bytes( + "RedeployPopLens: popLens key matches neither the manifest nor this release's lens" + ) + ); + redeployer.redeploy(registryOwner, address(registry), makeAddr("elsewhere"), RELEASE_TAG); + } +} diff --git a/test/fork/RotateOwnership.t.sol b/test/fork/RotateOwnership.t.sol deleted file mode 100644 index a55f25cf9..000000000 --- a/test/fork/RotateOwnership.t.sol +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {RotateOwnership, IOwnable} from "../../scripts/deploy/RotateOwnership.s.sol"; -import {UpgradeRegistryHarness} from "./UpgradeRegistry.t.sol"; - -/// @title RotateOwnershipHarness -/// @notice Exposes the rotation script's internal path so the test drives the code the -/// production run executes, inventory checks included. -contract RotateOwnershipHarness is RotateOwnership { - /// @notice Rotates everything from `current` to `newOwner` through the script's own internal. - function rotate(address current, address newOwner) external { - _rotateEverything(current, newOwner); - } - - /// @notice Runs the sweep leg through the script's own internal. - function sweep(address current, address newOwner) external { - _sweep(current, newOwner); - } -} - -/// @title RotateOwnershipForkTest -/// @notice Pairs one-to-one with `scripts/deploy/RotateOwnership.s.sol`. Rotates the live -/// deployment's ownership on a fork and proves the property the step exists for: the old -/// key loses the ability to act, the new key gains it, and no address moves. -/// @dev Step 0 of the runbook, so this runs against the pre-upgrade chain state on purpose: -/// rotation is broadcast before any implementation is swapped. -/// @custom:security-contact admin@parity.io -contract RotateOwnershipForkTest is BaseUpgradeFork { - /// @notice The contracts the script must rotate, resolved from the manifest in setUp. - /// @dev Kept as labels so a mismatch against the script's own list fails by name. - string[14] internal labels = [ - "DotnsProtocolRegistry", - "DotnsRegistry", - "DotnsRegistrar", - "DotnsRegistrarController", - "DotnsPopController", - "PopRules", - "DotnsNameEscrow", - "DotnsNameWhitelist", - "DotnsResolver", - "DotnsReverseResolver", - "DotnsContentResolver", - "DotnsPopResolver", - "DotnsCostModelRegistry", - "StoreFactory" - ]; - - /// @notice The deployment key, read from the chain, not assumed. - address internal currentOwner; - - /// @notice The fresh key ownership moves to. - address internal freshOwner; - - /// @notice Drives the script's own rotation path. - RotateOwnershipHarness internal rotator; - - function setUp() public override { - super.setUp(); - currentOwner = _ownerOf(_live("DotnsRegistry")); - freshOwner = makeAddr("fresh-owner"); - rotator = new RotateOwnershipHarness(); - } - - /// @notice Every inventoried contract answers to the new key afterwards, at its old address. - /// @dev The address assertions look redundant and are not: "rotation moves no addresses" is - /// the claim that makes rotation preferable to redeployment, so the test states it - /// against the manifest instead of leaving it as prose. - function test_rotation_moves_every_owner_and_no_address() public { - address[] memory targets = new address[](labels.length); - for (uint256 i; i < labels.length; ++i) { - targets[i] = _live(labels[i]); - assertEq( - IOwnable(targets[i]).owner(), - currentOwner, - string.concat("fork precondition: ", labels[i], " answers to the deployment key") - ); - } - - rotator.rotate(currentOwner, freshOwner); - - for (uint256 i; i < labels.length; ++i) { - assertEq( - IOwnable(targets[i]).owner(), - freshOwner, - string.concat(labels[i], " answers to the fresh key") - ); - assertEq( - _live(labels[i]), - targets[i], - string.concat(labels[i], " is still at the address the manifest names") - ); - } - } - - /// @notice After rotation the old key cannot upgrade, and the new key can. - /// @dev The lockout is the security property the step exists for, and the new key working is - /// what makes the runbook's steps 1 to 14 possible afterwards. Both are shown through - /// the registry upgrade script's own path, so what is proven is the exact pair of facts - /// the deployment relies on next. - function test_old_key_is_locked_out_and_new_key_can_upgrade() public { - address registry = _live("DotnsRegistry"); - rotator.rotate(currentOwner, freshOwner); - - UpgradeRegistryHarness upgrader = new UpgradeRegistryHarness(); - - vm.expectRevert(bytes("UpgradeRegistry: broadcaster is not the proxy owner")); - upgrader.upgrade(currentOwner, registry); - - upgrader.upgrade(freshOwner, registry); - assertTrue( - _implementationOf(registry) != address(0), "the fresh key performed a real upgrade" - ); - } - - /// @notice The sweep funds the new owner from the old account, leaving only the gas buffer. - /// @dev The sweep is the new owner's funding: the old key cannot be extracted for a manual - /// transfer, so this leg is the one signer its balance has. Asserted against the live - /// balance, whatever it is on the day, because the amount is not the property; the - /// transfer of nearly all of it is. - function test_sweep_funds_the_new_owner_and_leaves_only_the_buffer() public { - uint256 before = currentOwner.balance; - assertTrue( - before > 2 ether, "fork precondition: the old account holds more than the buffer" - ); - - rotator.rotate(currentOwner, freshOwner); - rotator.sweep(currentOwner, freshOwner); - - assertEq( - freshOwner.balance, before - 2 ether, "the new owner received everything but the buffer" - ); - assertEq(currentOwner.balance, 2 ether, "the old account keeps exactly the buffer"); - } - - /// @notice A second run from the old key completes as a no-op instead of failing. - /// @dev Partial-failure recovery: an interrupted rotation is finished by running again, and - /// contracts that already moved are skipped. A full re-run therefore has nothing to do, - /// and proving it does nothing loudly is what makes re-running safe to recommend. - function test_rerun_after_completion_is_a_noop() public { - rotator.rotate(currentOwner, freshOwner); - rotator.rotate(currentOwner, freshOwner); - - for (uint256 i; i < labels.length; ++i) { - assertEq( - IOwnable(_live(labels[i])).owner(), - freshOwner, - string.concat(labels[i], " unchanged by the re-run") - ); - } - } -} diff --git a/test/fork/UpgradeContentResolver.t.sol b/test/fork/UpgradeContentResolver.t.sol deleted file mode 100644 index 9335d0ace..000000000 --- a/test/fork/UpgradeContentResolver.t.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradeContentResolver} from "../../scripts/deploy/UpgradeContentResolver.s.sol"; -import {DotnsContentResolver} from "../../contracts/resolvers/DotnsContentResolver.sol"; - -/// @title UpgradeContentResolverHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradeContentResolverHarness is UpgradeContentResolver { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradeContentResolver(owner, proxy); - } -} - -/// @title UpgradeContentResolverForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradeContentResolver.s.sol`. Upgrades the -/// deployed DotnsContentResolver and proves the content and text records it serves stay readable -/// through the new implementation. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradeContentResolverForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradeContentResolverHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsContentResolver"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradeContentResolverHarness(); - } - - /// @notice The protocol registry pointer survives, so every record stays resolvable. - /// @dev This swap carries no behavioural change: `version()` reads the protocol registry's - /// declaration instead of a hardcoded string. The pointer is what every record lookup - /// goes through, so it is both the state most worth asserting and the one a layout - /// mistake would take out first, turning every read into a call to the zero address. - function test_upgrade_keeps_records_resolvable() public { - DotnsContentResolver resolver = DotnsContentResolver(proxy); - - address registryBefore = address(resolver.protocolRegistry()); - address implementationBefore = _implementationOf(proxy); - - assertEq( - registryBefore, _live("DotnsProtocolRegistry"), "fork precondition: pointer is correct" - ); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertEq( - address(resolver.protocolRegistry()), registryBefore, "protocol registry preserved" - ); - } -} diff --git a/test/fork/UpgradeNameEscrow.t.sol b/test/fork/UpgradeNameEscrow.t.sol deleted file mode 100644 index 82069d7ee..000000000 --- a/test/fork/UpgradeNameEscrow.t.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradeNameEscrow} from "../../scripts/deploy/UpgradeNameEscrow.s.sol"; -import {IDotnsNameEscrow} from "../../contracts/escrow/IDotnsNameEscrow.sol"; - -/// @title UpgradeNameEscrowHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradeNameEscrowHarness is UpgradeNameEscrow { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradeNameEscrow(owner, proxy); - } -} - -/// @title UpgradeNameEscrowForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradeNameEscrow.s.sol`. Upgrades the deployed -/// escrow and proves the balances it custodies and the windows governing them survive. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradeNameEscrowForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradeNameEscrowHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsNameEscrow"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradeNameEscrowHarness(); - } - - /// @notice Custodied balances and the configured windows survive the swap. - /// @dev The escrow holds real deposits, so this is the one upgrade where a layout mistake - /// loses money rather than state. `redeemWindow` is asserted non-zero as well as - /// unchanged: a zero there leaves every release reverting, and `DEPLOYMENTS.md` records - /// it as the field an upgrade has historically dropped. - function test_upgrade_preserves_balances_and_windows() public { - IDotnsNameEscrow escrow = IDotnsNameEscrow(proxy); - - uint256 feesBefore = escrow.protocolFees(); - uint256 releasedBefore = escrow.releasedTokenCount(); - uint256 cooldownBefore = escrow.cooldown(); - uint256 redeemBefore = escrow.redeemWindow(); - address implementationBefore = _implementationOf(proxy); - - assertTrue(redeemBefore != 0, "fork precondition: redeem window is seeded"); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertEq(escrow.protocolFees(), feesBefore, "protocol fees preserved"); - assertEq(escrow.releasedTokenCount(), releasedBefore, "released token count preserved"); - assertEq(escrow.cooldown(), cooldownBefore, "cooldown preserved"); - assertEq(escrow.redeemWindow(), redeemBefore, "redeem window preserved and still non-zero"); - } -} diff --git a/test/fork/UpgradeNameWhitelist.t.sol b/test/fork/UpgradeNameWhitelist.t.sol deleted file mode 100644 index e4a5a1f52..000000000 --- a/test/fork/UpgradeNameWhitelist.t.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradeNameWhitelist} from "../../scripts/deploy/UpgradeNameWhitelist.s.sol"; -import {IDotnsNameWhitelist} from "../../contracts/whitelist/IDotnsNameWhitelist.sol"; - -/// @title UpgradeNameWhitelistHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradeNameWhitelistHarness is UpgradeNameWhitelist { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradeNameWhitelist(owner, proxy); - } -} - -/// @title UpgradeNameWhitelistForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradeNameWhitelist.s.sol`. Upgrades the deployed -/// whitelist and proves its caps and allocations survive. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradeNameWhitelistForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradeNameWhitelistHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsNameWhitelist"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradeNameWhitelistHarness(); - } - - /// @notice Caps survive the swap. - /// @dev This implementation changes only `initialize`, which does not run on an upgrade, so - /// the swap should be observationally inert. Asserting that explicitly is the point: it - /// is the contract where an unexpected difference would be most surprising, and the - /// cheapest place to notice one. - function test_upgrade_is_observationally_inert() public { - IDotnsNameWhitelist whitelist = IDotnsNameWhitelist(proxy); - - uint16 claimantsBefore = whitelist.maxClaimants(); - uint16 grantBatchBefore = whitelist.maxGrantBatch(); - uint256 reasonBytesBefore = whitelist.maxReasonBytes(); - address implementationBefore = _implementationOf(proxy); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertEq(whitelist.maxClaimants(), claimantsBefore, "claimant cap preserved"); - assertEq(whitelist.maxGrantBatch(), grantBatchBefore, "grant batch cap preserved"); - assertEq(whitelist.maxReasonBytes(), reasonBytesBefore, "reason byte cap preserved"); - } -} diff --git a/test/fork/UpgradePopController.t.sol b/test/fork/UpgradePopController.t.sol deleted file mode 100644 index 56a70be9b..000000000 --- a/test/fork/UpgradePopController.t.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradePopController} from "../../scripts/deploy/UpgradePopController.s.sol"; -import {IDotnsPopController} from "../../contracts/registrars/IDotnsPopController.sol"; - -/// @title UpgradePopControllerHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradePopControllerHarness is UpgradePopController { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradePopController(owner, proxy); - } -} - -/// @title UpgradePopControllerForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradePopController.s.sol`. Upgrades the deployed -/// PoP controller and proves the reservation queues and issuance records survive. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradePopControllerForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradePopControllerHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsPopController"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradePopControllerHarness(); - } - - /// @notice Reservation and claim state survive the swap. - /// @dev The controller holds the queues real users are waiting in, so these counts are the - /// state worth asserting. They are read rather than seeded: a fixture would prove the - /// upgrade preserves a fixture, which is not the question. - function test_upgrade_preserves_reservation_and_claim_state() public { - IDotnsPopController controller = IDotnsPopController(proxy); - - uint256 pendingUsersBefore = controller.pendingClaimUserCount(); - uint64 durationBefore = controller.reservationDuration(); - address implementationBefore = _implementationOf(proxy); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertEq( - controller.pendingClaimUserCount(), pendingUsersBefore, "pending claim users preserved" - ); - assertEq(controller.reservationDuration(), durationBefore, "reservation window preserved"); - } -} diff --git a/test/fork/UpgradePopResolver.t.sol b/test/fork/UpgradePopResolver.t.sol deleted file mode 100644 index 547783fe4..000000000 --- a/test/fork/UpgradePopResolver.t.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradePopResolver} from "../../scripts/deploy/UpgradePopResolver.s.sol"; -import {DotnsPopResolver} from "../../contracts/resolvers/DotnsPopResolver.sol"; - -/// @title UpgradePopResolverHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradePopResolverHarness is UpgradePopResolver { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradePopResolver(owner, proxy); - } -} - -/// @title UpgradePopResolverForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradePopResolver.s.sol`. Upgrades the deployed -/// DotnsPopResolver and proves the chat keys and lite links it serves stay readable through the new -/// implementation. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradePopResolverForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradePopResolverHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsPopResolver"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradePopResolverHarness(); - } - - /// @notice The protocol registry pointer survives, so every record stays resolvable. - /// @dev This swap carries no behavioural change: `version()` reads the protocol registry's - /// declaration instead of a hardcoded string. The pointer is what every record lookup - /// goes through, so it is both the state most worth asserting and the one a layout - /// mistake would take out first, turning every read into a call to the zero address. - function test_upgrade_keeps_records_resolvable() public { - DotnsPopResolver resolver = DotnsPopResolver(proxy); - - address registryBefore = address(resolver.protocolRegistry()); - address implementationBefore = _implementationOf(proxy); - - assertEq( - registryBefore, _live("DotnsProtocolRegistry"), "fork precondition: pointer is correct" - ); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertEq( - address(resolver.protocolRegistry()), registryBefore, "protocol registry preserved" - ); - } -} diff --git a/test/fork/UpgradePopResolverAndController.t.sol b/test/fork/UpgradePopResolverAndController.t.sol new file mode 100644 index 000000000..f6662f4de --- /dev/null +++ b/test/fork/UpgradePopResolverAndController.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import {BasePopFork} from "./BasePopFork.t.sol"; +import { + UpgradePopResolverAndController +} from "../../scripts/deploy/UpgradePopResolverAndController.s.sol"; +import {IDotnsPopController} from "../../contracts/registrars/IDotnsPopController.sol"; +import {IDotnsPopResolver} from "../../contracts/resolvers/IDotnsPopResolver.sol"; +import {IDotnsRegistry} from "../../contracts/registry/IDotnsRegistry.sol"; + +/// @title UpgradePopResolverAndControllerHarness +/// @notice Exposes the upgrade script's internal path so the test drives the code the production +/// run executes, including both fail-closed storage-layout diffs. +contract UpgradePopResolverAndControllerHarness is UpgradePopResolverAndController { + /// @notice Upgrades both proxies under `owner` through the script's own internal. + function upgrade(address owner, address resolver, address controller) external { + _upgradeResolverAndController(owner, resolver, controller); + } + + /// @notice Upgrades the resolver alone, leaving the state a run that died between its two + /// transactions leaves behind. + function upgradeResolverOnly(address owner, address resolver) external { + vm.startBroadcast(owner); + Upgrades.upgradeProxy(resolver, RESOLVER_ARTEFACT, "", _resolverOptions()); + vm.stopBroadcast(); + } +} + +/// @title UpgradePopResolverAndControllerForkTest +/// @notice Pairs one-to-one with `scripts/deploy/UpgradePopResolverAndController.s.sol`. Upgrades +/// the deployed PoP resolver and controller and proves they work as a pair: reservation +/// state survives, and a linked personhood issuance through the gateway's legacy +/// entrypoints writes the link in both directions. +/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite +/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode +/// before the first test runs. +/// @custom:security-contact admin@parity.io +contract UpgradePopResolverAndControllerForkTest is BasePopFork { + /// @notice The deployed PoP resolver proxy. + address internal resolver; + + /// @notice The deployed PoP controller proxy. + address internal controller; + + /// @notice Owner of both proxies, impersonated to authorise the upgrades. + address internal proxyOwner; + + /// @notice Drives the script's upgrade path against the live proxies. + UpgradePopResolverAndControllerHarness internal upgrader; + + function setUp() public override { + super.setUp(); + resolver = _live("DotnsPopResolver"); + controller = _live("DotnsPopController"); + proxyOwner = _ownerOf(controller); + assertEq(_ownerOf(resolver), proxyOwner, "one owner for both proxies"); + upgrader = new UpgradePopResolverAndControllerHarness(); + } + + /// @notice Both implementations change, controller state survives, and a linked issuance + /// round-trips through the new resolver. + function test_upgrade_swaps_both_and_links_round_trip() public { + uint256 pendingUsersBefore = IDotnsPopController(controller).pendingClaimUserCount(); + uint64 durationBefore = IDotnsPopController(controller).reservationDuration(); + address resolverImplementationBefore = _implementationOf(resolver); + address controllerImplementationBefore = _implementationOf(controller); + + upgrader.upgrade(proxyOwner, resolver, controller); + + assertTrue( + _implementationOf(resolver) != resolverImplementationBefore, + "the resolver implementation actually changed" + ); + assertTrue( + _implementationOf(controller) != controllerImplementationBefore, + "the controller implementation actually changed" + ); + assertEq( + IDotnsPopController(controller).pendingClaimUserCount(), + pendingUsersBefore, + "pending claim users preserved" + ); + assertEq( + IDotnsPopController(controller).reservationDuration(), + durationBefore, + "reservation window preserved" + ); + + _assertLinkedIssuance(); + } + + /// @notice A run that died between its two transactions is finished by running it again. + function test_rerun_completes_a_resolver_only_upgrade() public { + upgrader.upgradeResolverOnly(proxyOwner, resolver); + address resolverImplementation = _implementationOf(resolver); + address controllerImplementationBefore = _implementationOf(controller); + + upgrader.upgrade(proxyOwner, resolver, controller); + + assertEq( + _implementationOf(resolver), + resolverImplementation, + "the already-upgraded resolver is left alone" + ); + assertTrue( + _implementationOf(controller) != controllerImplementationBefore, + "the controller is upgraded" + ); + _assertLinkedIssuance(); + } + + /// @notice A second run after a complete one broadcasts nothing. + function test_rerun_after_completion_is_a_no_op() public { + upgrader.upgrade(proxyOwner, resolver, controller); + address resolverImplementation = _implementationOf(resolver); + address controllerImplementation = _implementationOf(controller); + + upgrader.upgrade(proxyOwner, resolver, controller); + + assertEq(_implementationOf(resolver), resolverImplementation, "resolver untouched"); + assertEq(_implementationOf(controller), controllerImplementation, "controller untouched"); + } + + /// @notice Issues the linked pair and checks ownership and both link directions. + function _assertLinkedIssuance() internal { + bytes32 personhoodNode = _issueLinkedPair(controller); + + IDotnsRegistry registry = IDotnsRegistry(_live("DotnsRegistry")); + assertEq(registry.owner(personhoodNode), popUser, "personhood name issued to the user"); + + IDotnsPopResolver popResolver = IDotnsPopResolver(resolver); + bytes32 deviceLabelhash = popResolver.deviceLabelhashOf(personhoodNode); + assertTrue(deviceLabelhash != bytes32(0), "device link written"); + assertEq( + popResolver.personhoodNodeOf(deviceLabelhash), + personhoodNode, + "personhood link written in lockstep" + ); + assertEq(popResolver.chatKey(personhoodNode), _chatKey(), "chat key inherited"); + } +} diff --git a/test/fork/UpgradePopRules.t.sol b/test/fork/UpgradePopRules.t.sol index eb8d56842..f75bc5572 100644 --- a/test/fork/UpgradePopRules.t.sol +++ b/test/fork/UpgradePopRules.t.sol @@ -43,9 +43,9 @@ contract UpgradePopRulesForkTest is BaseUpgradeFork { } /// @notice Pricing answers the same before and after. - /// @dev The swap carries no behavioural change, so pricing returning something different - /// would mean the upgrade changed something it was not supposed to. Asserting equality - /// across the swap is the cheapest way to catch that. + /// @dev The swap renames functions and messages and leaves pricing alone, so pricing returning + /// something different would mean the upgrade changed something it was not supposed to. + /// Asserting equality across the swap is the cheapest way to catch that. function test_upgrade_leaves_pricing_unchanged() public { IPopRules rules = IPopRules(proxy); diff --git a/test/fork/UpgradeProtocolRegistry.t.sol b/test/fork/UpgradeProtocolRegistry.t.sol deleted file mode 100644 index 05dfa710e..000000000 --- a/test/fork/UpgradeProtocolRegistry.t.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradeProtocolRegistry} from "../../scripts/deploy/UpgradeProtocolRegistry.s.sol"; -import {IDotnsProtocolRegistry} from "../../contracts/registry/IDotnsProtocolRegistry.sol"; -import {DotnsConstants} from "../../contracts/utils/DotnsConstants.sol"; - -/// @title UpgradeProtocolRegistryHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradeProtocolRegistryHarness is UpgradeProtocolRegistry { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradeProtocolRegistry(owner, proxy); - } -} - -/// @title UpgradeProtocolRegistryForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradeProtocolRegistry.s.sol`. Upgrades the -/// deployed protocol registry and proves every key still resolves to the same address, then that -/// the declaration surface the release adds is live. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradeProtocolRegistryForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradeProtocolRegistryHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsProtocolRegistry"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradeProtocolRegistryHarness(); - } - - /// @notice Key resolution and the TLD survive the swap, and the declarations become callable. - /// @dev The protocol registry is upgraded first in the production order because every other - /// contract's `version()` reads through it. If this swap lost a key, every later swap - /// would be pointed at the zero address, so the resolution assertions here are what make - /// the rest of the sequence safe to run. - function test_upgrade_preserves_every_key_and_adds_the_declarations() public { - IDotnsProtocolRegistry registry = IDotnsProtocolRegistry(proxy); - - bytes32[6] memory keys = [ - DotnsConstants.REGISTRY, - DotnsConstants.REGISTRAR, - DotnsConstants.CONTROLLER, - DotnsConstants.POP_CONTROLLER, - DotnsConstants.RESOLVER, - DotnsConstants.STORE_FACTORY - ]; - - address[6] memory before; - for (uint256 i; i < keys.length; ++i) { - before[i] = registry.get(keys[i]); - } - string memory tldBefore = registry.tld(); - bytes32 tldNodeBefore = registry.tldNode(); - address implementationBefore = _implementationOf(proxy); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - for (uint256 i; i < keys.length; ++i) { - assertEq(registry.get(keys[i]), before[i], "key still resolves to the same address"); - } - assertEq(registry.tld(), tldBefore, "TLD preserved"); - assertEq(registry.tldNode(), tldNodeBefore, "TLD node preserved"); - - // Undeclared until `DeclareRelease` runs, which is deliberate: the release is declared - // once, after every swap has verified, so an abandoned upgrade cannot leave a false claim. - assertEq(registry.protocolVersion(), "", "version starts undeclared after the swap"); - } -} diff --git a/test/fork/UpgradeRegistrar.t.sol b/test/fork/UpgradeRegistrar.t.sol deleted file mode 100644 index 1c63ca03c..000000000 --- a/test/fork/UpgradeRegistrar.t.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradeRegistrar} from "../../scripts/deploy/UpgradeRegistrar.s.sol"; -import {IDotnsRegistrar} from "../../contracts/registrars/IDotnsRegistrar.sol"; -import {IDotnsController} from "../../contracts/registrars/IDotnsController.sol"; - -/// @title UpgradeRegistrarHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradeRegistrarHarness is UpgradeRegistrar { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradeRegistrar(owner, proxy); - } -} - -/// @title UpgradeRegistrarForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradeRegistrar.s.sol`. Upgrades the deployed -/// registrar and proves the live controller authorisations and token state survive. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradeRegistrarForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradeRegistrarHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsRegistrar"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradeRegistrarHarness(); - } - - /// @notice Controller authorisations survive the swap. - /// @dev These are what the registry's new deferral gate reads, and what every minting path - /// goes through. Losing them would leave the network unable to issue a name, so this is - /// the registrar's P0 state rather than any individual token. - function test_upgrade_preserves_controller_authorisations() public { - IDotnsRegistrar registrar = IDotnsRegistrar(proxy); - - IDotnsController popController = IDotnsController(_live("DotnsPopController")); - IDotnsController registrarController = IDotnsController(_live("DotnsRegistrarController")); - - bool popBefore = registrar.controllers(popController); - bool controllerBefore = registrar.controllers(registrarController); - address implementationBefore = _implementationOf(proxy); - - assertTrue(popBefore, "fork precondition: the PoP controller is authorised"); - assertTrue(controllerBefore, "fork precondition: the registrar controller is authorised"); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertTrue(registrar.controllers(popController), "PoP controller still authorised"); - assertTrue( - registrar.controllers(registrarController), "registrar controller still authorised" - ); - } -} diff --git a/test/fork/UpgradeRegistrarController.t.sol b/test/fork/UpgradeRegistrarController.t.sol deleted file mode 100644 index fd0193922..000000000 --- a/test/fork/UpgradeRegistrarController.t.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradeRegistrarController} from "../../scripts/deploy/UpgradeRegistrarController.s.sol"; -import {IDotnsRegistrarController} from "../../contracts/registrars/IDotnsRegistrarController.sol"; -import {DotnsRegistrarController} from "../../contracts/registrars/DotnsRegistrarController.sol"; - -/// @title UpgradeRegistrarControllerHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradeRegistrarControllerHarness is UpgradeRegistrarController { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradeRegistrarController(owner, proxy); - } -} - -/// @title UpgradeRegistrarControllerForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradeRegistrarController.s.sol`. Upgrades the -/// deployed registrar controller and proves the storage-layout fix keeps its protocol registry -/// pointer readable. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradeRegistrarControllerForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradeRegistrarControllerHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsRegistrarController"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradeRegistrarControllerHarness(); - } - - /// @notice The protocol registry pointer survives, which is the whole point of the retained - /// slot. - /// @dev This proxy is the reason the branch carries a `__whiteListSlot` placeholder. The - /// release removed the `whiteList` field, which moved `protocolRegistry` up one slot; an - /// implementation built without the placeholder reads it as the zero address and every - /// lookup through it fails. Reading it back non-zero after the swap is what shows the - /// placeholder did its job against real storage rather than against a fresh deployment. - function test_upgrade_keeps_the_protocol_registry_pointer_readable() public { - DotnsRegistrarController controller = DotnsRegistrarController(proxy); - - address registryBefore = address(controller.protocolRegistry()); - address implementationBefore = _implementationOf(proxy); - - assertTrue(registryBefore != address(0), "fork precondition: pointer is set"); - assertEq( - registryBefore, _live("DotnsProtocolRegistry"), "fork precondition: pointer is correct" - ); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertEq( - address(controller.protocolRegistry()), - registryBefore, - "protocol registry pointer still on the slot the live proxy uses" - ); - } -} diff --git a/test/fork/UpgradeRegistry.t.sol b/test/fork/UpgradeRegistry.t.sol index b11a0ad48..5cd5f1e5f 100644 --- a/test/fork/UpgradeRegistry.t.sol +++ b/test/fork/UpgradeRegistry.t.sol @@ -21,8 +21,8 @@ contract UpgradeRegistryHarness is UpgradeRegistry { /// @title UpgradeRegistryForkTest /// @notice Pairs one-to-one with `scripts/deploy/UpgradeRegistry.s.sol`. Upgrades the deployed -/// registry and proves records survive, then that the gate the release adds actually closes on the -/// live controller set. +/// registry and proves records survive, then that the deferral gate still closes on the live +/// controller set. /// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite /// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode /// before the first test runs. @@ -48,11 +48,11 @@ contract UpgradeRegistryForkTest is BaseUpgradeFork { tldNode = IDotnsProtocolRegistry(_live("DotnsProtocolRegistry")).tldNode(); } - /// @notice The TLD record survives the swap and the new deferral gate rejects a plain caller. - /// @dev The gate is the reason this upgrade exists. It reads `registrar.controllers` from live - /// state on every call, so a unit test cannot show it closing against the real controller - /// set; this can. The caller here owns nothing, so it fails the ownership modifier too, - /// which is why the assertion is only that a plain caller cannot defer. + /// @notice The TLD record survives the swap and the deferral gate rejects a plain caller. + /// @dev The gate reads `registrar.controllers` from live state on every call, so a unit test + /// cannot show it closing against the real controller set; this can. The caller here owns + /// nothing, so it fails the ownership modifier too, which is why the assertion is only + /// that a plain caller cannot defer. function test_upgrade_preserves_records_and_closes_the_deferral_gate() public { IDotnsRegistry registry = IDotnsRegistry(proxy); diff --git a/test/fork/UpgradeResolver.t.sol b/test/fork/UpgradeResolver.t.sol deleted file mode 100644 index bd8f12953..000000000 --- a/test/fork/UpgradeResolver.t.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradeResolver} from "../../scripts/deploy/UpgradeResolver.s.sol"; -import {DotnsResolver} from "../../contracts/resolvers/DotnsResolver.sol"; - -/// @title UpgradeResolverHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradeResolverHarness is UpgradeResolver { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradeResolver(owner, proxy); - } -} - -/// @title UpgradeResolverForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradeResolver.s.sol`. Upgrades the deployed -/// DotnsResolver and proves the forward address records it serves stay readable through the new -/// implementation. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradeResolverForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradeResolverHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsResolver"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradeResolverHarness(); - } - - /// @notice The protocol registry pointer survives, so every record stays resolvable. - /// @dev This swap carries no behavioural change: `version()` reads the protocol registry's - /// declaration instead of a hardcoded string. The pointer is what every record lookup - /// goes through, so it is both the state most worth asserting and the one a layout - /// mistake would take out first, turning every read into a call to the zero address. - function test_upgrade_keeps_records_resolvable() public { - DotnsResolver resolver = DotnsResolver(proxy); - - address registryBefore = address(resolver.protocolRegistry()); - address implementationBefore = _implementationOf(proxy); - - assertEq( - registryBefore, _live("DotnsProtocolRegistry"), "fork precondition: pointer is correct" - ); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertEq( - address(resolver.protocolRegistry()), registryBefore, "protocol registry preserved" - ); - } -} diff --git a/test/fork/UpgradeReverseResolver.t.sol b/test/fork/UpgradeReverseResolver.t.sol deleted file mode 100644 index 88aa84f07..000000000 --- a/test/fork/UpgradeReverseResolver.t.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseUpgradeFork} from "./BaseUpgradeFork.t.sol"; -import {UpgradeReverseResolver} from "../../scripts/deploy/UpgradeReverseResolver.s.sol"; -import {DotnsReverseResolver} from "../../contracts/resolvers/DotnsReverseResolver.sol"; - -/// @title UpgradeReverseResolverHarness -/// @notice Exposes the upgrade script's internal path so the test drives the code the production -/// run executes, including the fail-closed storage-layout diff. -/// @dev Forwarding to the script internal rather than re-implementing the upgrade is what keeps -/// the test honest: a test that called `Upgrades.upgradeProxy` itself would keep passing -/// after the script stopped doing the same thing. -contract UpgradeReverseResolverHarness is UpgradeReverseResolver { - /// @notice Upgrades `proxy` under `owner` through the script's own internal. - function upgrade(address owner, address proxy) external { - _upgradeReverseResolver(owner, proxy); - } -} - -/// @title UpgradeReverseResolverForkTest -/// @notice Pairs one-to-one with `scripts/deploy/UpgradeReverseResolver.s.sol`. Upgrades the -/// deployed DotnsReverseResolver and proves the reverse name records it serves stay readable -/// through the new implementation. -/// @dev Requires the local ETH-RPC adapter on `paseo_local`; see `DEPLOYMENTS.md`. Run the suite -/// with `bun run test:fork`, which also checks every snapshot against the deployed bytecode -/// before the first test runs. -/// @custom:security-contact admin@parity.io -contract UpgradeReverseResolverForkTest is BaseUpgradeFork { - /// @notice The deployed proxy under upgrade. - address internal proxy; - - /// @notice Proxy owner, impersonated to authorise the upgrade. - address internal proxyOwner; - - /// @notice Drives the script's upgrade path against the live proxy. - UpgradeReverseResolverHarness internal upgrader; - - function setUp() public override { - super.setUp(); - proxy = _live("DotnsReverseResolver"); - proxyOwner = _ownerOf(proxy); - upgrader = new UpgradeReverseResolverHarness(); - } - - /// @notice The protocol registry pointer survives, so every record stays resolvable. - /// @dev This swap carries no behavioural change: `version()` reads the protocol registry's - /// declaration instead of a hardcoded string. The pointer is what every record lookup - /// goes through, so it is both the state most worth asserting and the one a layout - /// mistake would take out first, turning every read into a call to the zero address. - function test_upgrade_keeps_records_resolvable() public { - DotnsReverseResolver resolver = DotnsReverseResolver(proxy); - - address registryBefore = address(resolver.protocolRegistry()); - address implementationBefore = _implementationOf(proxy); - - assertEq( - registryBefore, _live("DotnsProtocolRegistry"), "fork precondition: pointer is correct" - ); - - upgrader.upgrade(proxyOwner, proxy); - - assertTrue( - _implementationOf(proxy) != implementationBefore, "the implementation actually changed" - ); - assertEq( - address(resolver.protocolRegistry()), registryBefore, "protocol registry preserved" - ); - } -} diff --git a/test/fuzz/pop/PopFuzz.t.sol b/test/fuzz/pop/PopFuzz.t.sol index 6d6a83256..92d8d0ab3 100644 --- a/test/fuzz/pop/PopFuzz.t.sol +++ b/test/fuzz/pop/PopFuzz.t.sol @@ -8,34 +8,34 @@ import {IPopRules} from "../../../contracts/pop/IPopRules.sol"; /// @notice Property-based tests for @custom:contract PopRules classification, reservation and /// pricing. contract PopRulesFuzzTest is BaseDotns { - function testFuzz_popfull_user_can_access_poplite(uint256 seed, uint256 length) public { + function testFuzz_personhood_user_can_access_devicehood(uint256 seed, uint256 length) public { length = bound(length, 6, 8); - // PopLite is the gateway's separated form, so the stem carries the separator; `length` - // bounds the stem, which is what sets the band. + // Devicehood is the gateway's separated form, so the label carries the separator; + // `length` bounds the stem, which is what sets the band. string memory nameLabel = string(abi.encodePacked(_makeAlpha(seed, length), ".01")); - _grantPopFull(ed); + _grantPersonhood(ed); IPopRules.PriceWithMeta memory priceMetadata = popRules.priceWithCheck(nameLabel, ed); - assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.PopLite)); - assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.Devicehood)); + assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.Personhood)); } - function testFuzz_popfull_user_can_access_nostatus(uint256 seed, uint256 length) public { + function testFuzz_personhood_user_can_access_nostatus(uint256 seed, uint256 length) public { length = bound(length, 9, 14); // Measured whole, so the suffix only lengthens it and the band stays NoStatus. string memory nameLabel = string(abi.encodePacked(_makeAlpha(seed, length), "01")); - _grantPopFull(ed); + _grantPersonhood(ed); IPopRules.PriceWithMeta memory priceMetadata = popRules.priceWithCheck(nameLabel, ed); assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.NoStatus)); - assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.Personhood)); } - function testFuzz_nostatus_user_cannot_access_popfull(uint256 seed) public { + function testFuzz_nostatus_user_cannot_access_personhood(uint256 seed) public { string memory nameLabel = _makeAlpha(seed, 8); _grantNoStatus(ed); @@ -103,7 +103,7 @@ contract PopRulesFuzzTest is BaseDotns { assertEq(priceMetadata.price, popRules.price(nameLabel)); } - function testFuzz_expired_reservation_rolls_forward_to_next_lite_registrant(uint256 seed) + function testFuzz_expired_reservation_rolls_forward_to_next_device_registrant(uint256 seed) public { string memory baseName = _makeAlpha(seed, 6); @@ -135,7 +135,7 @@ contract PopRulesFuzzTest is BaseDotns { baseName[0] = bytes1(uint8(baseName[0]) - 32); string memory mixedCaseName = string(abi.encodePacked(string(baseName), "01")); - _grantPopLite(ed); + _grantDevicehood(ed); vm.expectPartialRevert(IPopRules.PopError.selector); popRules.priceWithCheck(mixedCaseName, ed); diff --git a/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol b/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol index 996939001..c2f967ea9 100644 --- a/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol +++ b/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol @@ -26,20 +26,22 @@ contract DotnsPopControllerFuzz is BaseDotns { return StringUtils.uintToString(value); } - function testFuzz_reserveBaseName_accepts_any_two_digit_suffix(uint8 suffix) public { + function testFuzz_issueDeviceNameWithReservation_accepts_any_two_digit_suffix(uint8 suffix) + public + { suffix = uint8(bound(uint256(suffix), 0, 99)); string memory label = string.concat("joseph", ".", _twoDigitDecimal(uint256(suffix))); - // Lite-tier label requires ed to hold PopLite (or PopFull) status so the + // Device-name label requires ed to hold Devicehood (or Personhood) status so the // `priceWithCheck` guard inside the controller accepts the reservation. - _grantPopLite(ed); + _grantDevicehood(ed); _reservePop(ed, label, "", ""); - assertEq(dotnsRegistry.owner(_liteNodeOf(label)), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf(label)), ed); } - function testFuzz_reserveBaseName_persists_chat_key_exact_bytes( + function testFuzz_issueDeviceNameWithReservation_persists_chat_key_exact_bytes( uint8 suffix, bytes1 keySeed, bool useKey @@ -49,12 +51,12 @@ contract DotnsPopControllerFuzz is BaseDotns { suffix = uint8(bound(uint256(suffix), 0, 99)); string memory label = string.concat("joseph", ".", _twoDigitDecimal(uint256(suffix))); - _grantPopLite(ed); + _grantDevicehood(ed); bytes memory chatKey = useKey ? _validChatKey(keySeed) : bytes(""); _reservePop(ed, label, chatKey, ""); - bytes32 node = _liteNodeOf(label); + bytes32 node = _deviceNodeOf(label); if (chatKey.length == 0) { assertEq(dotnsPopResolver.chatKey(node).length, 0); } else { @@ -62,24 +64,24 @@ contract DotnsPopControllerFuzz is BaseDotns { } } - /// @dev The reservation covers the stem, and the stem is what a public registrant contends - /// for: a digit-suffixed spelling is measured as written and so is an unrelated name - /// the reservation never sees. + /// @dev The reservation covers the base name, and the base name is what a public registrant + /// contends for: a digit-suffixed spelling is measured as written and so is an unrelated + /// name the reservation never sees. function testFuzz_public_register_respects_popRules_reservation(bool reserveFirst) public { - string memory stem = "longnamebob"; + string memory baseName = "longnamebob"; if (reserveFirst) { // The reserved base label `longnamebob` classifies as NoStatus (11 characters); the - // lite label has to be PopLite-eligible. Tiago needs PopFull status so both + // device name has to be Devicehood-eligible. Tiago needs Personhood status so both // `priceWithCheck` calls inside `reserveBaseName` succeed. - _grantPopFull(tiago); - _reservePop(tiago, LITE_LABEL_A, "", stem); + _grantPersonhood(tiago); + _reservePop(tiago, DEVICE_LABEL_A, "", baseName); } - bytes32 secret = keccak256(abi.encodePacked(stem, ed, block.timestamp)); + bytes32 secret = keccak256(abi.encodePacked(baseName, ed, block.timestamp)); IDotnsRegistrarController.Registration memory registration = IDotnsRegistrarController.Registration({ - label: stem, + label: baseName, owner: ed, secret: secret, reserved: true, @@ -95,16 +97,17 @@ contract DotnsPopControllerFuzz is BaseDotns { if (reserveFirst) { vm.expectRevert( abi.encodeWithSelector( - IPopRules.PopError.selector, "Base name reserved for original Lite registrant" + IPopRules.PopError.selector, + "Reserved for a device-name holder's personhood claim" ) ); vm.prank(ed); dotnsRegistrarController.register{value: 1 ether}(registration); } else { - uint256 cost = popRules.priceWithCheck(stem, ed).price; + uint256 cost = popRules.priceWithCheck(baseName, ed).price; vm.prank(ed); dotnsRegistrarController.register{value: cost}(registration); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(stem))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(baseName))), ed); } } @@ -124,15 +127,15 @@ contract DotnsPopControllerFuzz is BaseDotns { vm.prank(owner); dotnsPopController.setReservationDuration(duration); - // Reserving `BASE_LABEL_A` (PopFull classification) alongside the - // lite leg requires ed to hold PopFull status so both `priceWithCheck` + // Reserving `PERSONHOOD_LABEL_A` (Personhood classification) alongside the + // the issuance requires ed to hold Personhood status so both `priceWithCheck` // calls succeed. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, "", BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, "", PERSONHOOD_LABEL_A); vm.warp(block.timestamp + uint256(elapsed)); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); if (uint256(elapsed) <= uint256(duration)) { assertTrue(reserved); assertEq(holder, ed); @@ -152,9 +155,9 @@ contract DotnsPopControllerFuzz is BaseDotns { string memory label = string.concat("joseph", ".", _twoDigitDecimal(uint256(suffix))); bytes memory chatKey = _validChatKey(keySeed); - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({liteLabel: label, user: ed, chatKey: chatKey}) + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({label: label, user: ed, chatKey: chatKey}) ); IDotnsPopController.PendingClaim[] memory pending = @@ -164,7 +167,7 @@ contract DotnsPopControllerFuzz is BaseDotns { assertEq(storeFactory.getLabelStore(ed), address(0)); // Chat key is persisted eagerly on the resolver at reserve time, even though // the LabelStore write is deferred to settlement on the cold path. - bytes32 node = _liteNodeOf(label); + bytes32 node = _deviceNodeOf(label); assertEq(dotnsPopResolver.chatKey(node), chatKey); } @@ -178,15 +181,15 @@ contract DotnsPopControllerFuzz is BaseDotns { string memory label = string.concat("joseph", ".", _twoDigitDecimal(uint256(suffix))); bytes memory chatKey = _validChatKey(keySeed); - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({liteLabel: label, user: ed, chatKey: chatKey}) + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({label: label, user: ed, chatKey: chatKey}) ); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); - bytes32 node = _liteNodeOf(label); + bytes32 node = _deviceNodeOf(label); address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq(ILabelStore(store).getLabel(node), string.concat(label, protocolRegistry.tld())); @@ -210,10 +213,10 @@ contract DotnsPopControllerFuzz is BaseDotns { vm.prank(owner); dotnsPopController.setReservationDuration(duration); - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x77) + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x77) }) ); @@ -221,18 +224,18 @@ contract DotnsPopControllerFuzz is BaseDotns { vm.warp(uint256(mintedAt) + uint256(elapsed)); // Stores always settle: settlement writes the label into the store whether or not the - // reservation deadline has passed, so age never strands a claim. + // claim is older than `reservationDuration`, so age never strands a claim. vm.prank(ed); (uint256 settledCount, bool moreRemaining) = dotnsPopController.settlePendingClaims(ed, type(uint256).max); assertEq(settledCount, 1); assertFalse(moreRemaining); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(node), string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(node), string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } diff --git a/test/fuzz/registrar/DotnsRegistrarControllerFuzz.t.sol b/test/fuzz/registrar/DotnsRegistrarControllerFuzz.t.sol index e43eb3d50..227bf4c52 100644 --- a/test/fuzz/registrar/DotnsRegistrarControllerFuzz.t.sol +++ b/test/fuzz/registrar/DotnsRegistrarControllerFuzz.t.sol @@ -57,9 +57,9 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { function testFuzz_register_refunds_overpayment_inline(uint256 extra, uint256 salt) public { address registrant = tiago; - string memory nameLabel = _labelPopFullPriced(bound(salt, 0, 64)); + string memory nameLabel = _labelPersonhoodPriced(bound(salt, 0, 64)); - _grantPopFull(registrant); + _grantPersonhood(registrant); IDotnsRegistrarController.Registration memory registration = _commitFor(nameLabel, registrant, false); @@ -104,8 +104,8 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { address payer = leonardo; string memory nameLabel = _labelPopfull(bound(salt, 0, 64)); - _grantPopFull(nameOwner); - _grantPopFull(payer); + _grantPersonhood(nameOwner); + _grantPersonhood(payer); IDotnsRegistrarController.Registration memory registration = _commitFor(nameLabel, nameOwner, true, payer); @@ -151,7 +151,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { address recipient = leonardo; string memory nameLabel = _labelPopfull(bound(salt, 0, 64)); - _grantPopFull(sender); + _grantPersonhood(sender); IDotnsRegistrarController.Registration memory registration = _commitFor(nameLabel, sender, true); @@ -185,7 +185,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { uint256 primarySalt = bound(salt, 0, 63); string memory primaryName = _labelPopfull(primarySalt); - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); IDotnsRegistrarController.Registration memory primaryRegistration = _commitFor(primaryName, nameOwner, true); @@ -198,7 +198,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { string memory giftedName = _labelNoStatusPriced(primarySalt + 1); - _grantPopFull(payer); + _grantPersonhood(payer); IDotnsRegistrarController.Registration memory giftedRegistration = _commitFor(giftedName, nameOwner, true, payer); @@ -299,7 +299,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { address recipient = leonardo; string memory nameLabel = _labelPopfull(bound(salt, 0, 64)); - _grantPopFull(sender); + _grantPersonhood(sender); IDotnsRegistrarController.Registration memory registration = _commitFor(nameLabel, sender, true); @@ -366,7 +366,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { vm.warp(block.timestamp + dotnsRegistrarController.minCommitmentAge() + 1); } - /// @notice Generate a label that classifies as PopFull (baselength 8, no trailing digits). + /// @notice Generate a label that classifies as Personhood (baselength 8, no trailing digits). function _labelPopfull(uint256 salt) internal pure returns (string memory label) { return string(abi.encodePacked("popful", _uintToAlphaFixed(salt, 2))); } @@ -377,10 +377,10 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { return string(abi.encodePacked("nostatu", _uintToAlphaFixed(salt, 2), "01")); } - /// @notice Generate an 8-character PopFull-tier label, measured whole, that prices above - /// the floor so the amount is non-zero. A public fixture cannot be PopLite: that is + /// @notice Generate an 8-character Personhood-tier label, measured whole, that prices above + /// the floor so the amount is non-zero. A public fixture cannot be Devicehood: that is /// the gateway's separated form and this path rejects a separator. - function _labelPopFullPriced(uint256 salt) internal pure returns (string memory label) { + function _labelPersonhoodPriced(uint256 salt) internal pure returns (string memory label) { return string(abi.encodePacked("free", _uintToAlphaFixed(salt, 2), "01")); } diff --git a/test/intergration/BasicDotns.reverts.t.sol b/test/intergration/BasicDotns.reverts.t.sol index e436bbf77..6a18e88ca 100644 --- a/test/intergration/BasicDotns.reverts.t.sol +++ b/test/intergration/BasicDotns.reverts.t.sol @@ -11,18 +11,18 @@ import {IDotnsRegistrarController} from "../../contracts/registrars/IDotnsRegist /// @notice Integration coverage for revert paths protecting registration, subnames, /// and resolver writes. contract BasicDotnsIntegrationReverts is BaseDotns { - /// @notice PopFull-classified label fixture exercised by the revert cases. + /// @notice Personhood-classified label fixture exercised by the revert cases. string internal constant NAME_POPFULL = "waytalls"; /// @notice Sample CIDv1 content hash used as a record value. bytes internal constant CID_A = hex"e30101701220aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - function test_revert_poplite_cannot_register_popfull_required() public { + function test_revert_devicehood_cannot_register_personhood_required() public { string memory nameLabel = NAME_POPFULL; address registrant = ed; - _grantPopLite(registrant); + _grantDevicehood(registrant); vm.startPrank(registrant); bytes32 secret = keccak256(abi.encodePacked(nameLabel, registrant, block.timestamp)); @@ -41,11 +41,7 @@ contract BasicDotnsIntegrationReverts is BaseDotns { vm.warp(block.timestamp + dotnsRegistrarController.minCommitmentAge() + 1); - vm.expectRevert( - abi.encodeWithSelector( - IPopRules.PopError.selector, "Requires Full personhood verification" - ) - ); + vm.expectRevert(abi.encodeWithSelector(IPopRules.PopError.selector, "Requires personhood")); dotnsRegistrarController.register(registration); vm.stopPrank(); } @@ -54,7 +50,7 @@ contract BasicDotnsIntegrationReverts is BaseDotns { address parentOwner = ed; address attacker = tiago; - _grantPopFull(parentOwner); + _grantPersonhood(parentOwner); _commitAndRegister(NAME_POPFULL, parentOwner, true); bytes32 parentNode = _namehash(dotNode, keccak256(bytes(NAME_POPFULL))); @@ -76,7 +72,7 @@ contract BasicDotnsIntegrationReverts is BaseDotns { function test_parent_can_reassign_existing_subdomain() public { address parentOwner = ed; - _grantPopFull(parentOwner); + _grantPersonhood(parentOwner); _commitAndRegister(NAME_POPFULL, parentOwner, true); bytes32 parentNode = _namehash(dotNode, keccak256(bytes(NAME_POPFULL))); @@ -104,7 +100,7 @@ contract BasicDotnsIntegrationReverts is BaseDotns { address parentOwner = ed; address attacker = tiago; - _grantPopFull(parentOwner); + _grantPersonhood(parentOwner); _commitAndRegister(NAME_POPFULL, parentOwner, true); bytes32 node = _namehash(dotNode, keccak256(bytes(NAME_POPFULL))); diff --git a/test/intergration/BasicDotns.t.sol b/test/intergration/BasicDotns.t.sol index ac068afd4..31fefc7e3 100644 --- a/test/intergration/BasicDotns.t.sol +++ b/test/intergration/BasicDotns.t.sol @@ -10,9 +10,9 @@ import {ILabelStore} from "../../contracts/store/ILabelStore.sol"; /// @notice End-to-end happy-path integration coverage for registration, /// records, subnames, and ERC721 transfers across all PoP tiers. contract BasicDotnsIntegration is BaseDotns { - /// @notice PopFull-classified label fixture used by the integration flow. + /// @notice Personhood-classified label fixture used by the integration flow. string internal constant NAME_POPFULL = "waytalls"; - /// @notice PopLite-classified label fixture used by the integration flow. + /// @notice Devicehood-classified label fixture used by the integration flow. string internal constant NAME_POPLITE = "way2tall01"; /// @notice NoStatus-classified label fixture used by the integration flow. string internal constant NAME_NOSTATUS = "kitesurfing01"; @@ -37,8 +37,8 @@ contract BasicDotnsIntegration is BaseDotns { string transferRecipientSub; } - function test_popfull_end_to_end() public { - _grantPopFull(ed); + function test_personhood_end_to_end() public { + _grantPersonhood(ed); _flowEndToEnd( FlowParams({ @@ -55,8 +55,8 @@ contract BasicDotnsIntegration is BaseDotns { ); } - function test_poplite_end_to_end() public { - _grantPopLite(leonardo); + function test_devicehood_end_to_end() public { + _grantDevicehood(leonardo); _flowEndToEnd( FlowParams({ @@ -222,7 +222,7 @@ contract BasicDotnsIntegration is BaseDotns { _commitAndRegister(NAME_NOSTATUS, victim, false); assertEq(dotnsReverseResolver.nameOf(victim), ""); - _grantPopFull(victim); + _grantPersonhood(victim); string memory victimPrimary = "victimname01"; _commitAndRegister(victimPrimary, victim, true); diff --git a/test/intergration/PopLifecycleFlow.t.sol b/test/intergration/PopLifecycleFlow.t.sol index 8c6daf07f..ec436751f 100644 --- a/test/intergration/PopLifecycleFlow.t.sol +++ b/test/intergration/PopLifecycleFlow.t.sol @@ -11,15 +11,13 @@ import {ILabelStore} from "../../contracts/store/ILabelStore.sol"; import {LabelUtils} from "../../contracts/utils/LabelUtils.sol"; /// @title PopLifecycleFlow -/// @notice Integration coverage for a PoP-gateway-minted full username across +/// @notice Integration coverage for a PoP-gateway-minted personhood name across /// its full on-chain lifecycle: reservation, claim, record writes, /// subname issuance, transfer, and cross-contract lookup paths a -/// downstream consumer walks starting from the lite username string. +/// downstream consumer walks starting from the device name string. contract PopLifecycleFlow is BaseDotns { - /// @notice Lite label fixture. Baselength 7 with 2 trailing digits classifies as PopLite. - string internal constant LITE_LABEL = "michael.01"; - /// @notice Full label fixture. Baselength 9 with no trailing digits classifies as PopFull. - string internal constant FULL_LABEL = "alicefull"; + /// @notice Device name fixture. Baselength 7 with 2 trailing digits classifies as Devicehood. + string internal constant DEVICE_LABEL = "michael.01"; /// @notice Subname label used for the subnode portion of the flow. string internal constant SUB_LABEL = "app"; /// @notice 65-byte canonical chat-key payload (secp256k1 uncompressed shape). @@ -32,94 +30,102 @@ contract PopLifecycleFlow is BaseDotns { bytes internal constant CONTENT_HASH_B = hex"e30101701220bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - function test_recover_full_username_from_lite_label() public { - _mintLiteThenClaimFull(ed); + function test_recover_personhood_name_from_device_name() public { + _issueDeviceThenClaimPersonhood(ed); - bytes32 liteLabelhash = LabelUtils.labelhashMemory(LITE_LABEL); - bytes32 fullNode = dotnsPopResolver.fullClaim(liteLabelhash); - assertTrue(fullNode != bytes32(0), "lite label has no full claim"); + bytes32 deviceLabelhash = LabelUtils.labelhashMemory(DEVICE_LABEL); + bytes32 personhoodNode = dotnsPopResolver.personhoodNodeOf(deviceLabelhash); + assertTrue(personhoodNode != bytes32(0), "device name has no personhood claim"); - address owner = IERC721(address(dotnsRegistrar)).ownerOf(uint256(fullNode)); + address owner = IERC721(address(dotnsRegistrar)).ownerOf(uint256(personhoodNode)); assertEq(owner, ed); - assertEq(dotnsRegistrar.labelOf(uint256(fullNode)), FULL_LABEL); - assertEq(dotnsPopResolver.chatKey(fullNode), CHAT_KEY); + assertEq(dotnsRegistrar.labelOf(uint256(personhoodNode)), PERSONHOOD_LABEL_A); + assertEq(dotnsPopResolver.chatKey(personhoodNode), CHAT_KEY); // Same label mirrored in the owner's Store under the canonical store key. ILabelStore ownerStore = ILabelStore(storeFactory.getLabelStore(owner)); - assertEq(ownerStore.getLabel(fullNode), string.concat(FULL_LABEL, protocolRegistry.tld())); + assertEq( + ownerStore.getLabel(personhoodNode), + string.concat(PERSONHOOD_LABEL_A, protocolRegistry.tld()) + ); } - function test_pop_full_name_is_soulbound_but_fully_usable() public { - _mintLiteThenClaimFull(ed); + function test_personhood_name_is_soulbound_but_fully_usable() public { + _issueDeviceThenClaimPersonhood(ed); - bytes32 fullNode = _nodeOf(FULL_LABEL); - uint256 fullTokenId = uint256(fullNode); - bytes32 liteLabelhash = LabelUtils.labelhashMemory(LITE_LABEL); + bytes32 personhoodNode = _nodeOf(PERSONHOOD_LABEL_A); + uint256 personhoodTokenId = uint256(personhoodNode); + bytes32 deviceLabelhash = LabelUtils.labelhashMemory(DEVICE_LABEL); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(fullTokenId), ed); - assertEq(dotnsRegistry.owner(fullNode), ed); - assertEq(dotnsRegistrar.labelOf(fullTokenId), FULL_LABEL); - assertEq(dotnsPopResolver.chatKey(fullNode), CHAT_KEY); - assertEq(dotnsPopResolver.liteLink(fullNode), liteLabelhash); - assertEq(dotnsPopResolver.fullClaim(liteLabelhash), fullNode); - assertTrue(dotnsRegistrar.isSoulbound(fullTokenId)); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(personhoodTokenId), ed); + assertEq(dotnsRegistry.owner(personhoodNode), ed); + assertEq(dotnsRegistrar.labelOf(personhoodTokenId), PERSONHOOD_LABEL_A); + assertEq(dotnsPopResolver.chatKey(personhoodNode), CHAT_KEY); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodNode), deviceLabelhash); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceLabelhash), personhoodNode); + assertTrue(dotnsRegistrar.isSoulbound(personhoodTokenId)); // The name is fully usable by its owner: records and subnames work. vm.prank(ed); - dotnsContentResolver.setContenthash(fullNode, CONTENT_HASH_A); - assertEq(dotnsContentResolver.contenthash(fullNode), CONTENT_HASH_A); + dotnsContentResolver.setContenthash(personhoodNode, CONTENT_HASH_A); + assertEq(dotnsContentResolver.contenthash(personhoodNode), CONTENT_HASH_A); - bytes32 subnode = _setSubnode(ed, fullNode, SUB_LABEL, FULL_LABEL, leonardo); + bytes32 subnode = _setSubnode(ed, personhoodNode, SUB_LABEL, PERSONHOOD_LABEL_A, leonardo); assertEq(dotnsRegistry.owner(subnode), leonardo); // It is soulbound: quoting a transfer and attempting one both revert, and ownership // does not move. - vm.expectRevert(abi.encodeWithSelector(IDotnsRegistrar.NameSoulbound.selector, fullTokenId)); - dotnsRegistrar.quoteTransferFee(fullTokenId, tiago); + vm.expectRevert( + abi.encodeWithSelector(IDotnsRegistrar.NameSoulbound.selector, personhoodTokenId) + ); + dotnsRegistrar.quoteTransferFee(personhoodTokenId, tiago); - vm.expectRevert(abi.encodeWithSelector(IDotnsRegistrar.NameSoulbound.selector, fullTokenId)); + vm.expectRevert( + abi.encodeWithSelector(IDotnsRegistrar.NameSoulbound.selector, personhoodTokenId) + ); vm.prank(ed); - dotnsRegistrar.transferFrom(ed, tiago, fullTokenId); + dotnsRegistrar.transferFrom(ed, tiago, personhoodTokenId); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(fullTokenId), ed); - assertEq(dotnsRegistry.owner(fullNode), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(personhoodTokenId), ed); + assertEq(dotnsRegistry.owner(personhoodNode), ed); // PoP-layer records and the owner's continued control are untouched by the blocked move. - assertEq(dotnsPopResolver.chatKey(fullNode), CHAT_KEY); - assertEq(dotnsPopResolver.fullClaim(liteLabelhash), fullNode); - assertEq(dotnsContentResolver.contenthash(fullNode), CONTENT_HASH_A); + assertEq(dotnsPopResolver.chatKey(personhoodNode), CHAT_KEY); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceLabelhash), personhoodNode); + assertEq(dotnsContentResolver.contenthash(personhoodNode), CONTENT_HASH_A); assertEq(dotnsRegistry.owner(subnode), leonardo); vm.prank(ed); - dotnsContentResolver.setContenthash(fullNode, CONTENT_HASH_B); - assertEq(dotnsContentResolver.contenthash(fullNode), CONTENT_HASH_B); + dotnsContentResolver.setContenthash(personhoodNode, CONTENT_HASH_B); + assertEq(dotnsContentResolver.contenthash(personhoodNode), CONTENT_HASH_B); - bytes32 reassignedSubnode = _setSubnode(ed, fullNode, SUB_LABEL, FULL_LABEL, tiago); + bytes32 reassignedSubnode = + _setSubnode(ed, personhoodNode, SUB_LABEL, PERSONHOOD_LABEL_A, tiago); assertEq(reassignedSubnode, subnode); assertEq(dotnsRegistry.owner(subnode), tiago); - // The lite name is a subname owned in the registry, not a transferable token. - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL)), ed); - assertFalse(dotnsRegistrar.exists(uint256(_liteNodeOf(LITE_LABEL)))); + // The device name is a subname owned in the registry, not a transferable token. + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL)), ed); + assertFalse(dotnsRegistrar.exists(uint256(_deviceNodeOf(DEVICE_LABEL)))); } function test_cold_gateway_reserve_then_user_settles_pending_claim() public { - _grantPopFull(ed); + _grantPersonhood(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); - bytes32 liteNode = _liteNodeOf(LITE_LABEL); - assertEq(dotnsRegistry.owner(liteNode), ed); - assertEq(dotnsRegistry.owner(liteNode), ed); + bytes32 deviceNode = _deviceNodeOf(DEVICE_LABEL); + assertEq(dotnsRegistry.owner(deviceNode), ed); + assertEq(dotnsRegistry.owner(deviceNode), ed); assertEq(storeFactory.getLabelStore(ed), address(0)); // Chat key is persisted eagerly on the resolver at reserve time; only the // LabelStore write is deferred for cold-path users. - assertEq(dotnsPopResolver.chatKey(liteNode), CHAT_KEY); + assertEq(dotnsPopResolver.chatKey(deviceNode), CHAT_KEY); IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); - assertEq(pending[0].label, LITE_LABEL); + assertEq(pending[0].label, DEVICE_LABEL); assertGt(pending[0].mintedAt, 0); vm.prank(ed); @@ -128,18 +134,19 @@ contract PopLifecycleFlow is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(liteNode), string.concat(LITE_LABEL, protocolRegistry.tld()) + ILabelStore(store).getLabel(deviceNode), + string.concat(DEVICE_LABEL, protocolRegistry.tld()) ); - assertEq(dotnsPopResolver.chatKey(liteNode), CHAT_KEY); + assertEq(dotnsPopResolver.chatKey(deviceNode), CHAT_KEY); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); } function test_reserve_settle_reserve_cycle_for_same_user() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); uint64 firstMintedAt = dotnsPopController.pendingClaims(ed, 0, 1)[0].mintedAt; @@ -154,67 +161,67 @@ contract PopLifecycleFlow is BaseDotns { address store = storeFactory.getLabelStore(ed); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL)), - string.concat(LITE_LABEL, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL)), + string.concat(DEVICE_LABEL, protocolRegistry.tld()) ); string memory secondLabel = "michael.02"; bytes memory secondKey = hex"04beefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafe"; - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: secondLabel, user: ed, chatKey: secondKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: secondLabel, user: ed, chatKey: secondKey }) ); // The user is warm now, so the second reservation writes straight into the store. assertEq( - ILabelStore(store).getLabel(_liteNodeOf(secondLabel)), + ILabelStore(store).getLabel(_deviceNodeOf(secondLabel)), string.concat(secondLabel, protocolRegistry.tld()) ); - assertEq(dotnsPopResolver.chatKey(_liteNodeOf(secondLabel)), secondKey); + assertEq(dotnsPopResolver.chatKey(_deviceNodeOf(secondLabel)), secondKey); assertGt(firstMintedAt, 0); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); } function test_gateway_name_with_live_pending_claim_is_soulbound_and_settles_for_owner() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); - // The lite name is a subname owned in the registry, not a transferable ERC-721 token, so it - // cannot be moved out of the beneficiary's wallet before settlement: there is no token to - // transfer and the owner holds no reassignment primitive. This is the path the issue + // The device name is a subname owned in the registry, not a transferable ERC-721 token, so + // it cannot be moved out of the beneficiary's wallet before settlement: there is no token + // to transfer and the owner holds no reassignment primitive. This is the path the issue // closes: a pre-claim transfer previously escaped tier pricing entirely. - bytes32 liteNode = _liteNodeOf(LITE_LABEL); - assertEq(dotnsRegistry.owner(liteNode), ed); - assertFalse(dotnsRegistrar.exists(uint256(liteNode))); + bytes32 deviceNode = _deviceNodeOf(DEVICE_LABEL); + assertEq(dotnsRegistry.owner(deviceNode), ed); + assertFalse(dotnsRegistrar.exists(uint256(deviceNode))); // The pending claim is keyed by the original user and still settles into their store. IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); - assertEq(pending[0].label, LITE_LABEL); + assertEq(pending[0].label, DEVICE_LABEL); assertGt(pending[0].mintedAt, 0); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); address edStore = storeFactory.getLabelStore(ed); assertTrue(edStore != address(0)); - bytes32 node = _liteNodeOf(LITE_LABEL); + bytes32 node = _deviceNodeOf(DEVICE_LABEL); assertEq( - ILabelStore(edStore).getLabel(node), string.concat(LITE_LABEL, protocolRegistry.tld()) + ILabelStore(edStore).getLabel(node), string.concat(DEVICE_LABEL, protocolRegistry.tld()) ); } function test_lapsed_pending_claim_settles_and_deploys_store() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); @@ -222,54 +229,55 @@ contract PopLifecycleFlow is BaseDotns { // Permissionless settlement from a stranger address: age never drops the claim, so the // store is deployed for the beneficiary and the label is written and readable. - bytes32 liteNode = _liteNodeOf(LITE_LABEL); + bytes32 deviceNode = _deviceNodeOf(DEVICE_LABEL); vm.prank(makeAddr("settler")); dotnsPopController.settlePendingClaims(ed, type(uint256).max); address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(liteNode), string.concat(LITE_LABEL, protocolRegistry.tld()) + ILabelStore(store).getLabel(deviceNode), + string.concat(DEVICE_LABEL, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); } - function test_lite_via_gateway_then_full_via_public_after_upgrade() public { - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + function test_device_name_via_gateway_then_personhood_name_via_public_after_upgrade() public { + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); - bytes32 liteNode = _liteNodeOf(LITE_LABEL); - assertEq(dotnsRegistry.owner(liteNode), ed); + bytes32 deviceNode = _deviceNodeOf(DEVICE_LABEL); + assertEq(dotnsRegistry.owner(deviceNode), ed); - _grantPopFull(ed); + _grantPersonhood(ed); string memory popfullLabel = "alicedef"; _commitAndRegister(popfullLabel, ed, false); - bytes32 fullNode = _nodeOf(popfullLabel); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(fullNode)), ed); - assertEq(dotnsRegistry.owner(liteNode), ed); + bytes32 personhoodNode = _nodeOf(popfullLabel); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(personhoodNode)), ed); + assertEq(dotnsRegistry.owner(deviceNode), ed); } - /// @notice Mints the lite label for `user` then claims the full label against it. - /// @dev Grants PopFull up front so both the lite reservation leg and the - /// reservedBaseLabel leg pass `priceWithCheck`. - function _mintLiteThenClaimFull(address user) internal { - // The reservedBaseLabel leg of reserveBaseName now runs - // priceWithCheck on FULL_LABEL too, and FULL_LABEL classifies as - // PopFull. Granting PopFull up front satisfies classification/tier - // on both the lite leg (PopLite classification, admitted by the - // PopFull superset) and the full-person claim. - _grantPopFull(user); - _reservePop(user, LITE_LABEL, CHAT_KEY, FULL_LABEL); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: FULL_LABEL, user: user, link: _linkWithLite(LITE_LABEL) + /// @notice Mints the device name for `user` then claims the personhood label against it. + /// @dev Grants Personhood up front so both the device-name reservation leg and the + /// reservedLabel leg pass `priceWithCheck`. + function _issueDeviceThenClaimPersonhood(address user) internal { + // The reservedLabel leg of reserveBaseName now runs + // priceWithCheck on PERSONHOOD_LABEL_A too, and PERSONHOOD_LABEL_A classifies as + // Personhood. Granting Personhood up front satisfies classification/tier + // on both the device-name issuance (Devicehood classification, admitted by the + // Personhood superset) and the personhood claim. + _grantPersonhood(user); + _reservePop(user, DEVICE_LABEL, CHAT_KEY, PERSONHOOD_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: user, link: _linkWithDeviceName(DEVICE_LABEL) }) ); } diff --git a/test/intergration/StoreIntegration.t.sol b/test/intergration/StoreIntegration.t.sol index 3150dd517..ece1f0d33 100644 --- a/test/intergration/StoreIntegration.t.sol +++ b/test/intergration/StoreIntegration.t.sol @@ -51,7 +51,7 @@ contract StoreIntegrationTest is BaseDotns { } function test_pop_controller_registration_writes_label_store() public { - _grantPopFull(ed); + _grantPersonhood(ed); string memory base = BASE_LABEL_A; // DotnsPopResolver.setChatKey requires exactly 65 bytes (uncompressed ECDSA pubkey). @@ -61,8 +61,10 @@ contract StoreIntegrationTest is BaseDotns { chatKey[i] = bytes1(uint8(i + 1)); } - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: base, user: ed, link: _linkFresh(chatKey)}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: base, user: ed, link: _linkFresh(chatKey) + }) ); vm.prank(ed); diff --git a/test/invariant/escrow/DotnsNameEscrowInvariant.t.sol b/test/invariant/escrow/DotnsNameEscrowInvariant.t.sol index e81a2bd1b..99752fcef 100644 --- a/test/invariant/escrow/DotnsNameEscrowInvariant.t.sol +++ b/test/invariant/escrow/DotnsNameEscrowInvariant.t.sol @@ -5,7 +5,7 @@ import {BaseDotns} from "../../base/BaseDotns.t.sol"; import {IDotnsNameEscrow} from "../../../contracts/escrow/IDotnsNameEscrow.sol"; import {EscrowHandler} from "./EscrowHandler.t.sol"; -/// @title Dotns Name Escrow Invariant Suite +/// @title dotNS Name Escrow Invariant Suite /// @notice Asserts solvency, custody, and recipient-locking properties of the name escrow /// across randomised registration, release, withdraw, claim, and transfer flows. contract DotnsNameEscrowInvariantTest is BaseDotns { diff --git a/test/invariant/escrow/EscrowHandler.t.sol b/test/invariant/escrow/EscrowHandler.t.sol index 752da2722..3fd67404b 100644 --- a/test/invariant/escrow/EscrowHandler.t.sol +++ b/test/invariant/escrow/EscrowHandler.t.sol @@ -162,7 +162,7 @@ contract EscrowHandler is Test { /// randomly aligns or splits their PoP statuses, and dispatches the controller /// `register()` call from the payer. Routes to the deposit, depositProtocolFee, /// or skip branch depending on the resulting tier prices. Revert-safe: if the - /// computed price is zero on both sides (PoPLite/PoPFull no-cost path) the call + /// computed price is zero on both sides (Devicehood/Personhood no-cost path) the call /// still completes but ghost state is only updated where state actually changed. /// @param ownerSeed Seed selecting the registrant. /// @param payerSeed Seed selecting the payer. @@ -174,21 +174,21 @@ contract EscrowHandler is Test { address ownerAddr = _pickDifferentActor(payer, payerSeed); if (ownerAddr == address(0)) return; - // Tier-status assignment: 0 = both NoStatus, 1 = elevate payer to PopFull, - // 2 = elevate owner to PopLite, 3 = elevate both (no fee differential). + // Tier-status assignment: 0 = both NoStatus, 1 = elevate payer to Personhood, + // 2 = elevate owner to Devicehood, 3 = elevate both (no fee differential). uint256 mode = bound(statusSeed, 0, 3); if (mode == 0) { _mockPersonhoodTier(payer, IPopRules.PopStatus.NoStatus); _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.NoStatus); } else if (mode == 1) { - _mockPersonhoodTier(payer, IPopRules.PopStatus.PopFull); + _mockPersonhoodTier(payer, IPopRules.PopStatus.Personhood); _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.NoStatus); } else if (mode == 2) { _mockPersonhoodTier(payer, IPopRules.PopStatus.NoStatus); - _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.PopLite); + _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.Devicehood); } else if (mode == 3) { - _mockPersonhoodTier(payer, IPopRules.PopStatus.PopFull); - _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.PopFull); + _mockPersonhoodTier(payer, IPopRules.PopStatus.Personhood); + _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.Personhood); } string memory label = _generateUniqueLabel(); @@ -450,7 +450,7 @@ contract EscrowHandler is Test { } /// @notice Sets a random PoP status for an actor mid-run. - /// @dev Bound: 0 = NoStatus, 1 = PopLite, 2 = PopFull. Allows the fuzzer to flip + /// @dev Bound: 0 = NoStatus, 1 = Devicehood, 2 = Personhood. Allows the fuzzer to flip /// tiers between handler calls so subsequent registrations and transfers exercise /// every routing branch. /// @param actorSeed Seed selecting the actor whose status flips. @@ -469,8 +469,8 @@ contract EscrowHandler is Test { /// @param tier Verification tier to report for `account`. function _mockPersonhoodTier(address account, IPopRules.PopStatus tier) internal { uint8 statusByte; - if (tier == IPopRules.PopStatus.PopFull) statusByte = 2; - else if (tier == IPopRules.PopStatus.PopLite) statusByte = 1; + if (tier == IPopRules.PopStatus.Personhood) statusByte = 2; + else if (tier == IPopRules.PopStatus.Devicehood) statusByte = 1; bytes32 contextAlias = statusByte == 0 ? bytes32(0) : keccak256(abi.encode(account, statusByte)); diff --git a/test/invariant/registrar/DotnsPopControllerInvariant.t.sol b/test/invariant/registrar/DotnsPopControllerInvariant.t.sol index 1bb895c38..8251b98bf 100644 --- a/test/invariant/registrar/DotnsPopControllerInvariant.t.sol +++ b/test/invariant/registrar/DotnsPopControllerInvariant.t.sol @@ -63,7 +63,7 @@ contract DotnsPopControllerInvariant is BaseDotns { /// warp, relink and the two settlement paths) fill no list of their own and are left /// to the campaign. function _seedCoverage() internal { - // A reserve, then a claim on the same base label, so a lite name and a full-person name + // A reserve, then a claim on the same base label, so a device name and a personhood name // both exist and the actor's store is settled. handler.reserve(0, 0, true); handler.claim(0, 0); @@ -71,10 +71,10 @@ contract DotnsPopControllerInvariant is BaseDotns { // An odd base index takes a label of the public path's own, which the gateway cannot // have claimed; the transfer then picks the public token that registration minted. handler.publicRegister(1, 1); - handler.attemptTransfer(handler.mintedLiteTokenCount(), 0); + handler.attemptTransfer(handler.mintedDeviceTokenCount(), 0); // Sub-labels come from a seed and most are not valid DNS labels, so walk seeds until - // one is accepted under a full-person parent. + // one is accepted under a personhood parent. for (uint256 seed; seed < 64 && handler.subnodeCreatedCount() == 0; ++seed) { handler.createSubname(seed, seed, seed); } @@ -91,7 +91,7 @@ contract DotnsPopControllerInvariant is BaseDotns { } /// @notice A user-created subname never collides with a name the gateway issued. - /// @dev A gateway full-person name is a second-level node and a gateway lite name is a subname + /// @dev A gateway personhood name is a second-level node and a gateway device name is a subname /// of its numeric container; a subname a user builds under a name they own must land on /// neither, or a user could reach a gateway-issued node. function invariant_subnames_never_reach_a_gateway_node() public view { @@ -102,7 +102,8 @@ contract DotnsPopControllerInvariant is BaseDotns { bytes32 subnode = handler.subnodesCreated(i); for (uint256 j = 0; j < gatewayCount; j++) { string memory label = handler.gatewayLabelsSeen(j); - bytes32 gatewayNode = _carriesSeparator(label) ? _liteNodeOf(label) : _nodeOf(label); + bytes32 gatewayNode = + _carriesSeparator(label) ? _deviceNodeOf(label) : _nodeOf(label); assertTrue(subnode != gatewayNode, "subnode collided with a gateway name"); } } @@ -149,11 +150,11 @@ contract DotnsPopControllerInvariant is BaseDotns { string memory label = handler.gatewayLabelsSeen(i); assertFalse(handler.isPublicLabel(label), "gateway label taken publicly"); if (_carriesSeparator(label)) { - // A lite name is a subname owned in the registry, non-transferable because there is - // no token behind it, not through the soulbound flag. - bytes32 node = _liteNodeOf(label); - assertTrue(dotnsRegistry.owner(node) != address(0), "gateway lite name unowned"); - assertFalse(dotnsRegistrar.exists(uint256(node)), "gateway lite name is a token"); + // A device name is a subname owned in the registry, non-transferable because there + // is no token behind it, not through the soulbound flag. + bytes32 node = _deviceNodeOf(label); + assertTrue(dotnsRegistry.owner(node) != address(0), "gateway device name unowned"); + assertFalse(dotnsRegistrar.exists(uint256(node)), "gateway device name is a token"); } else { assertTrue(dotnsRegistrar.isSoulbound(uint256(_nodeOf(label))), "gateway name free"); } @@ -236,56 +237,72 @@ contract DotnsPopControllerInvariant is BaseDotns { } } - /// @notice For every historic (liteLabelhash, fullNode) pair the resolver's + /// @notice For every historic (deviceLabelhash, personhoodNode) pair the resolver's /// forward and reverse indexes either still round-trip to each /// other or have both been cleared by a later overwrite. A partial /// overwrite, where one side still points at a stale partner, is /// the corruption signature this invariant guards against. - function invariant_fullClaim_liteLink_are_inverse() public view { + function invariant_personhoodLink_deviceLink_are_inverse() public view { uint256 n = handler.claimedCount(); for (uint256 i = 0; i < n; i++) { - bytes32 liteLabelhash = handler.claimedLiteLabelhashes(i); - bytes32 fullNode = handler.claimedFullNodes(i); + bytes32 deviceLabelhash = handler.claimedDeviceLabelhashes(i); + bytes32 personhoodNode = handler.claimedPersonhoodNodes(i); - bytes32 currentFullForLite = dotnsPopResolver.fullClaim(liteLabelhash); - bytes32 currentLiteForFull = dotnsPopResolver.liteLink(fullNode); + bytes32 currentPersonhoodForDevice = dotnsPopResolver.personhoodNodeOf(deviceLabelhash); + bytes32 currentDeviceForPersonhood = dotnsPopResolver.deviceLabelhashOf(personhoodNode); // Either the pair is still live on both sides, or both sides // have been cleared. Anything else is a partial overwrite. - if (currentFullForLite == fullNode) { - assertEq(currentLiteForFull, liteLabelhash, "live fullClaim but liteLink drifted"); - } else if (currentLiteForFull == liteLabelhash) { - assertEq(currentFullForLite, fullNode, "live liteLink but fullClaim drifted"); + if (currentPersonhoodForDevice == personhoodNode) { + assertEq( + currentDeviceForPersonhood, + deviceLabelhash, + "live personhoodNodeOf but deviceLabelhashOf drifted" + ); + } else if (currentDeviceForPersonhood == deviceLabelhash) { + assertEq( + currentPersonhoodForDevice, + personhoodNode, + "live deviceLabelhashOf but personhoodNodeOf drifted" + ); } // Else: both sides were overwritten. Covered by the stale // invariants below. } } - /// @notice No stale `liteLink`: for every touched fullNode, a non-zero - /// liteLink value round-trips through `fullClaim` back to the same - /// fullNode. A drifting liteLink is the corruption footprint this + /// @notice No stale `deviceLabelhashOf`: for every touched personhoodNode, a non-zero + /// device labelhash round-trips through `personhoodNodeOf` back to the same + /// personhoodNode. A drifting device labelhash is the corruption footprint this /// invariant guards against. - function invariant_no_stale_liteLink() public view { + function invariant_no_stale_deviceLabelhash() public view { uint256 n = handler.claimedCount(); for (uint256 i = 0; i < n; i++) { - bytes32 fullNode = handler.claimedFullNodes(i); - bytes32 currentLite = dotnsPopResolver.liteLink(fullNode); - if (currentLite == bytes32(0)) continue; - assertEq(dotnsPopResolver.fullClaim(currentLite), fullNode, "stale liteLink"); + bytes32 personhoodNode = handler.claimedPersonhoodNodes(i); + bytes32 currentDevice = dotnsPopResolver.deviceLabelhashOf(personhoodNode); + if (currentDevice == bytes32(0)) continue; + assertEq( + dotnsPopResolver.personhoodNodeOf(currentDevice), + personhoodNode, + "stale deviceLabelhashOf" + ); } } - /// @notice No stale `fullClaim`: symmetric to `invariant_no_stale_liteLink`, - /// every claimed liteLabelhash with a non-zero fullClaim round-trips - /// through `liteLink` back to the same liteLabelhash. - function invariant_no_stale_fullClaim() public view { + /// @notice No stale `personhoodNodeOf`: symmetric to `invariant_no_stale_deviceLabelhash`, + /// every claimed deviceLabelhash with a non-zero personhood node round-trips + /// through `deviceLabelhashOf` back to the same deviceLabelhash. + function invariant_no_stale_personhoodNode() public view { uint256 n = handler.claimedCount(); for (uint256 i = 0; i < n; i++) { - bytes32 liteLabelhash = handler.claimedLiteLabelhashes(i); - bytes32 currentFull = dotnsPopResolver.fullClaim(liteLabelhash); - if (currentFull == bytes32(0)) continue; - assertEq(dotnsPopResolver.liteLink(currentFull), liteLabelhash, "stale fullClaim"); + bytes32 deviceLabelhash = handler.claimedDeviceLabelhashes(i); + bytes32 currentPersonhood = dotnsPopResolver.personhoodNodeOf(deviceLabelhash); + if (currentPersonhood == bytes32(0)) continue; + assertEq( + dotnsPopResolver.deviceLabelhashOf(currentPersonhood), + deviceLabelhash, + "stale personhoodNodeOf" + ); } } @@ -349,22 +366,22 @@ contract DotnsPopControllerInvariant is BaseDotns { assertEq(page.length, count, "count != enumeration length"); } - /// @notice Settlement writes labels and never strands a minted name. A full-person name is a - /// token whose settled label reads back from the registrar; a lite name is a registry + /// @notice Settlement writes labels and never strands a minted name. A personhood name is a + /// token whose settled label reads back from the registrar; a device name is a registry /// subname whose settled label lives in the owner's store. Either is settled or still /// staged in the owner's pending queue, so a minted name is never left in neither /// place. - /// @dev The stranded case the old model allowed, a lapsed entry swept out of the queue with - /// nothing written, is now unreachable: settlement always writes the label regardless of - /// the reservation deadline. A deployed store and a pending claim are mutually exclusive - /// in this suite, so a lite name whose owner holds a store has necessarily been settled. + /// @dev A pending entry is never dropped without its label being written: settlement always + /// writes the label, whatever the claim's age. A deployed store and a pending claim are + /// mutually exclusive in this suite, so a device name whose owner holds a store has + /// necessarily been settled. function invariant_settled_names_written_and_never_stranded() public view { - uint256 n = handler.mintedLiteTokenCount(); + uint256 n = handler.mintedDeviceTokenCount(); for (uint256 i = 0; i < n; i++) { - uint256 id = handler.mintedLiteTokenIds(i); + uint256 id = handler.mintedDeviceTokenIds(i); if (dotnsRegistrar.exists(id)) { - // Full-person name: a tokenised second-level name. A settled name reads its label + // Personhood name: a tokenised second-level name. A settled name reads its label // back from the registrar; otherwise it must still be staged in its owner's queue. if (bytes(dotnsRegistrar.labelOf(id)).length != 0) continue; assertTrue( @@ -374,28 +391,28 @@ contract DotnsPopControllerInvariant is BaseDotns { continue; } - // Lite name: a registry subname, not a token. It is owned in the registry, and settled - // into the owner's store or still staged in pending. Store and pending are mutually - // exclusive here, so a deployed store means the label was written. + // Device name: a registry subname, not a token. It is owned in the registry, and + // settled into the owner's store or still staged in pending. Store and pending are + // mutually exclusive here, so a deployed store means the label was written. bytes32 node = bytes32(id); address nameOwner = dotnsRegistry.owner(node); - assertTrue(nameOwner != address(0), "lite name lost its registry owner"); + assertTrue(nameOwner != address(0), "device name lost its registry owner"); if (IStoreFactory(address(storeFactory)).getLabelStore(nameOwner) != address(0)) { continue; } - assertTrue(_stagedInPending(nameOwner, node), "lite name neither settled nor staged"); + assertTrue(_stagedInPending(nameOwner, node), "device name neither settled nor staged"); } } /// @notice Whether `owner` holds a pending claim whose label derives to `node`. - /// @dev A lite label carries the separator and derives to its subnode; any other label derives + /// @dev A device name carries the separator and derives to its subnode; any other label derives /// to a second-level node under the TLD. function _stagedInPending(address owner, bytes32 node) internal view returns (bool staged) { IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(owner, 0, type(uint256).max); for (uint256 j = 0; j < pending.length; j++) { bytes32 pendingNode = _carriesSeparator(pending[j].label) - ? _liteNodeOf(pending[j].label) + ? _deviceNodeOf(pending[j].label) : _nodeOf(pending[j].label); if (pendingNode == node) return true; } diff --git a/test/invariant/registrar/DotnsRegistrarControllerInvariant.t.sol b/test/invariant/registrar/DotnsRegistrarControllerInvariant.t.sol index 70b4ae56e..45467623d 100644 --- a/test/invariant/registrar/DotnsRegistrarControllerInvariant.t.sol +++ b/test/invariant/registrar/DotnsRegistrarControllerInvariant.t.sol @@ -31,15 +31,15 @@ contract DotnsRegistrarControllerInvariantTest is BaseDotns { vm.deal(address(handler), 1000 ether); - handler.addActor(ed, IPopRules.PopStatus.PopFull); - handler.addActor(leonardo, IPopRules.PopStatus.PopLite); + handler.addActor(ed, IPopRules.PopStatus.Personhood); + handler.addActor(leonardo, IPopRules.PopStatus.Devicehood); handler.addActor(tiago, IPopRules.PopStatus.NoStatus); // Additional actors for richer multi-user transfer chains. address alice = _createUser("alice"); address bob = _createUser("bob"); - handler.addActor(alice, IPopRules.PopStatus.PopFull); - handler.addActor(bob, IPopRules.PopStatus.PopLite); + handler.addActor(alice, IPopRules.PopStatus.Personhood); + handler.addActor(bob, IPopRules.PopStatus.Devicehood); targetContract(address(handler)); diff --git a/test/invariant/registrar/DotnsRegistrarReservedGrantInvariant.t.sol b/test/invariant/registrar/DotnsRegistrarReservedGrantInvariant.t.sol index 78621251f..aff8791de 100644 --- a/test/invariant/registrar/DotnsRegistrarReservedGrantInvariant.t.sol +++ b/test/invariant/registrar/DotnsRegistrarReservedGrantInvariant.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; import {BaseDotns} from "../../base/BaseDotns.t.sol"; import {ReservedGrantHandler} from "./ReservedGrantHandler.t.sol"; -/// @title Dotns Reserved Grant Invariant Suite +/// @title dotNS Reserved Grant Invariant Suite /// @notice Asserts the properties the grant-gated reserved path exists to provide, across any /// reachable interleaving of grants, relayed mints, ungranted attempts and double spends: /// only granted labels mint, they mint to the beneficiary rather than the submitter, a diff --git a/test/invariant/registrar/DotnsRegistrarSoulboundInvariant.t.sol b/test/invariant/registrar/DotnsRegistrarSoulboundInvariant.t.sol index 014fe288f..5f7495bc1 100644 --- a/test/invariant/registrar/DotnsRegistrarSoulboundInvariant.t.sol +++ b/test/invariant/registrar/DotnsRegistrarSoulboundInvariant.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; import {BaseDotns} from "../../base/BaseDotns.t.sol"; import {SoulboundHandler} from "./SoulboundHandler.t.sol"; -/// @title Dotns Registrar Soulbound Invariant Suite +/// @title dotNS Registrar Soulbound Invariant Suite /// @notice Asserts that PoP-gateway (soulbound) names never change owner across any reachable /// sequence of mints and transfer attempts, and that the campaign actually exercises /// soulbound tokens rather than passing vacuously. diff --git a/test/invariant/registrar/PopControllerHandler.t.sol b/test/invariant/registrar/PopControllerHandler.t.sol index 32819c7c3..8ebc2c613 100644 --- a/test/invariant/registrar/PopControllerHandler.t.sol +++ b/test/invariant/registrar/PopControllerHandler.t.sol @@ -24,7 +24,7 @@ import {IPersonhood} from "../../../contracts/external/personhood/IPersonhood.so /// @notice Bounded random-action handler for @custom:contract DotnsPopController invariant tests. /// @dev Cycles through an actor set and a fixed base-label set so the fuzzer /// explores combinations deterministically. Tracks every labelhash that has -/// hosted a reservation, every minted lite token, and every successful +/// hosted a reservation, every minted device name, and every successful /// claim so invariants can iterate over just what exists. contract PopControllerHandler is Test { /// @notice The PoP controller under test. @@ -35,7 +35,7 @@ contract PopControllerHandler is Test { DotnsRegistrar public immutable REGISTRAR; /// @notice Pricing and classification, read to quote a public registration. IPopRules public immutable POP_RULES; - /// @notice The hierarchical registry, where a lite name lives as a subname of its container. + /// @notice The hierarchical registry, where a device name lives as a subname of its container. IDotnsRegistry public immutable REGISTRY; /// @notice Node hash of the suite's TLD, injected from the deployed protocol registry. /// @dev Keeps the handler rooted at the same TLD the protocol under test uses, without a @@ -53,32 +53,32 @@ contract PopControllerHandler is Test { bytes32[] public reservedLabelsSeen; /// @notice Dedup set for `reservedLabelsSeen` to keep iteration cheap. mapping(bytes32 labelhash => bool) internal _tracked; - /// @notice Monotonic per-actor counter feeding the lite-label suffix so + /// @notice Monotonic per-actor counter feeding the device-name suffix so /// each generated label is unique inside an actor's namespace. - mapping(address actor => uint64 suffix) internal _liteSuffix; + mapping(address actor => uint64 suffix) internal _deviceSuffix; - /// @notice Lite tokens minted through the handler (one push per successful - /// reserve, plus the lite and full nodes pushed on claim and the - /// full node pushed on reLink). Used by the labelOf-non-empty + /// @notice Device names minted through the handler (one push per successful + /// reserve, plus the device and personhood nodes pushed on claim and the + /// personhood node pushed on reLink). Used by the labelOf-non-empty /// invariant to enumerate the token space without scanning the /// full uint256 id range. - uint256[] public mintedLiteTokenIds; - - /// @notice Full nodes minted through successful claims, captured alongside - /// the lite labelhash they were linked against. Used by the - /// fullClaim/liteLink inverse invariant: for each entry, - /// fullClaim(liteHash) == node. - bytes32[] public claimedFullNodes; - /// @notice Lite labelhashes paired index-for-index with `claimedFullNodes`. - bytes32[] public claimedLiteLabelhashes; - - /// @notice Prior lite labels reserved by the handler. Used by reLink - /// actions so the fuzzer can re-use an existing lite label + uint256[] public mintedDeviceTokenIds; + + /// @notice Personhood nodes minted through successful claims, captured alongside + /// the device-name labelhash they were linked against. Used by the + /// personhoodNodeOf/deviceLabelhashOf inverse invariant: for each entry, + /// personhoodNodeOf(deviceLabelhash) == node. + bytes32[] public claimedPersonhoodNodes; + /// @notice Device-name labelhashes paired index-for-index with `claimedPersonhoodNodes`. + bytes32[] public claimedDeviceLabelhashes; + + /// @notice Prior device names reserved by the handler. Used by reLink + /// actions so the fuzzer can re-use an existing device name /// against a fresh base claim, driving the resolver overwrite /// paths under repeated `(baseLabel, actor)` reuse. Kept as /// the raw string because the controller re-hashes internally /// on every call. - string[] public priorLiteLabels; + string[] public priorDeviceLabels; /// @notice Every label the gateway has issued, deduplicated. /// @dev Provenance is written once at mint, so this set only grows. The monotonicity @@ -125,7 +125,7 @@ contract PopControllerHandler is Test { mapping(address actor => bool) internal _pendingActorTracked; /// @notice Seeds the actor pool, base-label set, and personhood mocks so - /// every action call admits both lite and base classifications. + /// every action call admits both device-name and base classifications. /// @param controller_ The PoP controller under test. /// @param publicController_ The public commit-reveal controller. /// @param registrar_ The ERC-721 registrar both controllers mint through. @@ -149,19 +149,19 @@ contract PopControllerHandler is Test { REGISTRY = registry_; TLD_NODE = tldNode_; actors = actors_; - // baselength 8, no trailing digits: PopFull classification. + // baselength 8, no trailing digits: Personhood classification. baseLabels.push("alicebob"); // length 12, no trailing digits: NoStatus classification. A reservable base label - // keys the reservation queue, which only ever holds a digit-free stem. + // keys the reservation queue, which only ever holds a digit-free base name. baseLabels.push("wonderlandxy"); // baselength 10, no trailing digits: NoStatus classification. baseLabels.push("carolcarol"); - // Every actor needs PopFull status on the personhood precompile so the + // Every actor needs Personhood status on the personhood precompile so the // classification/tier guard in PopRules.priceWithCheck admits every - // label the handler can generate: PopLite lite labels (PopFull is a - // superset of PopLite), PopFull base labels, and NoStatus base labels - // (which merely require userStatus != PopLite). + // label the handler can generate: Devicehood device names (Personhood is a + // superset of Devicehood), Personhood base labels, and NoStatus base labels + // (which merely require userStatus != Devicehood). for (uint256 i = 0; i < actors_.length; i++) { _mockPersonhoodTier(actors_[i], 2); } @@ -178,7 +178,7 @@ contract PopControllerHandler is Test { /// @notice Mocks the personhood precompile so `account` reports the given /// status byte for the protocol's personhood context. /// @dev Status byte mirrors the precompile's wire format: 0 = NoStatus, - /// 1 = PopLite, 2 = PopFull. A zero status clears the context alias. + /// 1 = Devicehood, 2 = Personhood. A zero status clears the context alias. function _mockPersonhoodTier(address account, uint8 statusByte) internal { bytes32 contextAlias = statusByte == 0 ? bytes32(0) : keccak256(abi.encode(account, statusByte)); @@ -212,19 +212,19 @@ contract PopControllerHandler is Test { } /// @notice Number of token ids the handler has ever recorded as minted. - function mintedLiteTokenCount() external view returns (uint256) { - return mintedLiteTokenIds.length; + function mintedDeviceTokenCount() external view returns (uint256) { + return mintedDeviceTokenIds.length; } - /// @notice Number of successful (lite, full) claim pairs the handler has + /// @notice Number of successful (device, personhood) claim pairs the handler has /// recorded. function claimedCount() external view returns (uint256) { - return claimedFullNodes.length; + return claimedPersonhoodNodes.length; } - /// @notice Number of prior lite labels available for reLink replay. - function priorLiteLabelCount() external view returns (uint256) { - return priorLiteLabels.length; + /// @notice Number of prior device names available for reLink replay. + function priorDeviceLabelCount() external view returns (uint256) { + return priorDeviceLabels.length; } /// @notice Number of actors the handler has ever seen with a pending claim. @@ -232,30 +232,31 @@ contract PopControllerHandler is Test { return pendingClaimActorsSeen.length; } - /// @notice Reserves a lite label for an actor, optionally enqueueing on a + /// @notice Reserves a device name for an actor, optionally enqueueing on a /// base label. /// @dev Swallows known-good reverts (QueueFull, AlreadyReserved, ERC721 /// collision) so the runner keeps exploring. function reserve(uint256 actorIndex, uint256 baseIndex, bool attachReservation) external { address actor = _actor(actorIndex); - _liteSuffix[actor]++; - string memory liteLabel = _buildLiteLabel("rsv", actor, _liteSuffix[actor]); + _deviceSuffix[actor]++; + string memory deviceLabel = _buildDeviceLabel("rsv", actor, _deviceSuffix[actor]); string memory reservedBase = attachReservation ? _baseLabel(baseIndex) : ""; - IDotnsPopController.BaseReservation memory params = IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: liteLabel, user: actor, chatKey: "" - }), - reservedBaseLabel: reservedBase - }); + IDotnsPopController.DeviceNameIssuanceWithReservation memory params = + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: deviceLabel, user: actor, chatKey: "" + }), + reservedLabel: reservedBase + }); - if (_callReserveBaseName(params)) { + if (_callIssueDeviceNameWithReservation(params)) { if (attachReservation) _track(keccak256(bytes(reservedBase))); - // A lite name is a subname beneath its numeric container, not a token, so record its + // A device name is a subname beneath its numeric container, not a token, so record its // subnode rather than the whole-label hash. - mintedLiteTokenIds.push(uint256(SubnodeUtils.liteSubnodeOf(TLD_NODE, liteLabel))); - priorLiteLabels.push(liteLabel); - _trackGatewayLabel(liteLabel); + mintedDeviceTokenIds.push(uint256(SubnodeUtils.deviceSubnodeOf(TLD_NODE, deviceLabel))); + priorDeviceLabels.push(deviceLabel); + _trackGatewayLabel(deviceLabel); _trackPendingActor(actor); } } @@ -264,7 +265,7 @@ contract PopControllerHandler is Test { /// head of the queue for the picked base label. /// @dev Missing preconditions (wrong actor, expired head, empty queue) /// surface as a revert and are swallowed so the runner keeps - /// leg and the full register leg. + /// leg and the personhood issuance leg. function claim(uint256 actorIndex, uint256 baseIndex) external { address actor = _actor(actorIndex); string memory baseLabel = _baseLabel(baseIndex); @@ -273,73 +274,77 @@ contract PopControllerHandler is Test { if (reservation.labelhash == bytes32(0)) return; if (reservation.labelhash != keccak256(bytes(baseLabel))) return; - _liteSuffix[actor]++; - string memory liteLabel = _buildLiteLabel("clm", actor, _liteSuffix[actor]); + _deviceSuffix[actor]++; + string memory deviceLabel = _buildDeviceLabel("clm", actor, _deviceSuffix[actor]); - IDotnsPopController.BaseReservation memory liteParams = IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: liteLabel, user: actor, chatKey: "" - }), - reservedBaseLabel: "" - }); - if (!_callReserveBaseName(liteParams)) return; - // Recorded here rather than after the full leg below: the subname exists from this point, - // and a full leg that reverts would otherwise leave it outside every invariant's reach. A - // lite name is a subname beneath its numeric container, not a token, so record its subnode. - mintedLiteTokenIds.push(uint256(SubnodeUtils.liteSubnodeOf(TLD_NODE, liteLabel))); - priorLiteLabels.push(liteLabel); - _trackGatewayLabel(liteLabel); + IDotnsPopController.DeviceNameIssuanceWithReservation memory deviceParams = + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: deviceLabel, user: actor, chatKey: "" + }), + reservedLabel: "" + }); + if (!_callIssueDeviceNameWithReservation(deviceParams)) return; + // Recorded here rather than after the personhood leg below: the subname exists from this + // point, and a personhood leg that reverts would otherwise leave it outside every + // invariant's reach. A device name is a subname beneath its numeric container, not a token, + // so record its subnode. + mintedDeviceTokenIds.push(uint256(SubnodeUtils.deviceSubnodeOf(TLD_NODE, deviceLabel))); + priorDeviceLabels.push(deviceLabel); + _trackGatewayLabel(deviceLabel); _trackPendingActor(actor); - // The lite leg stashed a pending claim. Settle it now so the base + // The the issuance stashed a pending claim. Settle it now so the base // registration below takes the warm path; the pending-claim mechanism // forbids a second stash for the same user. vm.prank(actor); try CONTROLLER.settlePendingClaims(actor, type(uint256).max) {} catch {} IDotnsPopController.Link memory link = IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.LiteUsername, liteLabel: liteLabel, chatKey: "" + kind: IDotnsPopController.LinkKind.DeviceName, deviceLabel: deviceLabel, chatKey: "" }); - IDotnsPopController.FullRegistration memory fullParams = - IDotnsPopController.FullRegistration({label: baseLabel, user: actor, link: link}); - if (!_callRegisterBaseName(fullParams)) return; - - bytes32 fullNode = LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); - claimedLiteLabelhashes.push(LabelUtils.labelhashMemory(liteLabel)); - claimedFullNodes.push(fullNode); - mintedLiteTokenIds.push(uint256(fullNode)); + IDotnsPopController.PersonhoodNameIssuance memory personhoodParams = + IDotnsPopController.PersonhoodNameIssuance({label: baseLabel, user: actor, link: link}); + if (!_callIssuePersonhoodName(personhoodParams)) return; + + bytes32 personhoodNode = + LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); + claimedDeviceLabelhashes.push(LabelUtils.labelhashMemory(deviceLabel)); + claimedPersonhoodNodes.push(personhoodNode); + mintedDeviceTokenIds.push(uint256(personhoodNode)); _trackGatewayLabel(baseLabel); } - /// @notice Re-registers an already-used lite label against a fresh - /// base-label claim so the same liteHash maps to a new fullNode. + /// @notice Re-registers an already-used device name against a fresh + /// base-label claim so the same deviceLabelhash maps to a new personhoodNode. /// @dev Drives the resolver overwrite paths. When the handler /// re-uses the same (baseLabel, actor) pair later it also exercises - /// the symmetric case: same fullNode mapped to a new liteHash. - function reLink(uint256 actorIndex, uint256 baseIndex, uint256 liteIndex) external { - uint256 liteCount = priorLiteLabels.length; - if (liteCount == 0) return; + /// the symmetric case: same personhoodNode mapped to a new deviceLabelhash. + function reLink(uint256 actorIndex, uint256 baseIndex, uint256 deviceIndex) external { + uint256 deviceCount = priorDeviceLabels.length; + if (deviceCount == 0) return; address actor = _actor(actorIndex); string memory baseLabel = _baseLabel(baseIndex); - string memory liteLabel = priorLiteLabels[liteIndex % liteCount]; + string memory deviceLabel = priorDeviceLabels[deviceIndex % deviceCount]; IDotnsPopController.UserReservation memory reservation = CONTROLLER.userReservation(actor); if (reservation.labelhash == bytes32(0)) return; if (reservation.labelhash != keccak256(bytes(baseLabel))) return; IDotnsPopController.Link memory link = IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.LiteUsername, liteLabel: liteLabel, chatKey: "" + kind: IDotnsPopController.LinkKind.DeviceName, deviceLabel: deviceLabel, chatKey: "" }); - IDotnsPopController.FullRegistration memory params = - IDotnsPopController.FullRegistration({label: baseLabel, user: actor, link: link}); - if (!_callRegisterBaseName(params)) return; - - bytes32 liteLabelhash = LabelUtils.labelhashMemory(liteLabel); - bytes32 fullNode = LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); - claimedLiteLabelhashes.push(liteLabelhash); - claimedFullNodes.push(fullNode); - mintedLiteTokenIds.push(uint256(fullNode)); + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({label: baseLabel, user: actor, link: link}); + if (!_callIssuePersonhoodName(params)) return; + + bytes32 deviceLabelhash = LabelUtils.labelhashMemory(deviceLabel); + bytes32 personhoodNode = + LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); + claimedDeviceLabelhashes.push(deviceLabelhash); + claimedPersonhoodNodes.push(personhoodNode); + mintedDeviceTokenIds.push(uint256(personhoodNode)); _trackGatewayLabel(baseLabel); } @@ -394,7 +399,7 @@ contract PopControllerHandler is Test { address registrant = publicActors[actorIndex % publicActors.length]; // Alternate between a contested base label and one of the public path's own. The // contested half exercises the race; the fresh half keeps the public side populated - // even in a run where the gateway holds every shared stem, so the provenance + // even in a run where the gateway holds every shared base name, so the provenance // invariant always has a public label to check. Ten characters classifies NoStatus. string memory label = baseIndex % 2 == 0 ? _baseLabel(baseIndex) @@ -406,7 +411,7 @@ contract PopControllerHandler is Test { ) { price = quote.price; } catch { - // A stem the gateway queue holds for someone else, or a tier the registrant does + // A base name the gateway queue holds for someone else, or a tier the registrant does // not meet, is priced nowhere. Both are ordinary contention. return; } @@ -449,13 +454,13 @@ contract PopControllerHandler is Test { /// registered one moves once the fee is paid. The recipient is a public actor either /// way, so a moved name never deposits a `LabelStore` on a gateway actor. function attemptTransfer(uint256 tokenIndex, uint256 toIndex) external { - uint256 gatewayCount = mintedLiteTokenIds.length; + uint256 gatewayCount = mintedDeviceTokenIds.length; uint256 total = gatewayCount + publicTokenIds.length; if (total == 0) return; uint256 pick = tokenIndex % total; uint256 tokenId = - pick < gatewayCount ? mintedLiteTokenIds[pick] : publicTokenIds[pick - gatewayCount]; + pick < gatewayCount ? mintedDeviceTokenIds[pick] : publicTokenIds[pick - gatewayCount]; if (!REGISTRAR.exists(tokenId)) return; address from = REGISTRAR.ownerOf(tokenId); @@ -480,10 +485,10 @@ contract PopControllerHandler is Test { /// @notice Creates an arbitrary subname under a gateway-issued name. /// @dev Interleaves subname creation with gateway mints so no subnode can quietly land on an - /// issued name. A lite parent is skipped: this derives the parent node by hashing the - /// whole label under the TLD, which is not a node the gateway minted for a lite name, so - /// the existence check returns early. The subnode owner comes from `publicActors` so a - /// subname never deposits a `LabelStore` on a gateway actor. + /// issued name. A device-name parent is skipped: this derives the parent node by hashing + /// the whole label under the TLD, which is not a node the gateway minted for a device + /// name, so the existence check returns early. The subnode owner comes from `publicActors` + /// so a subname never deposits a `LabelStore` on a gateway actor. function createSubname(uint256 parentIndex, uint256 subLabelSeed, uint256 toIndex) external { uint256 n = gatewayLabelsSeen.length; if (n == 0) return; @@ -551,12 +556,14 @@ contract PopControllerHandler is Test { /// @dev Returns true on success and false on revert so the caller's /// bookkeeping (ghost arrays) stays consistent with on-chain state. /// @return ok Whether the underlying call succeeded. - function _callReserveBaseName(IDotnsPopController.BaseReservation memory params) + function _callIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation memory params + ) internal returns (bool ok) { _mockOriginIsRoot(true); - try CONTROLLER.reserveBaseName(params) { + try CONTROLLER.issueDeviceNameWithReservation(params) { ok = true; } catch { ok = false; @@ -567,14 +574,14 @@ contract PopControllerHandler is Test { _mockOriginIsRoot(false); } - /// @notice Mirror of `_callReserveBaseName` for `registerBaseName`. + /// @notice Mirror of `_callIssueDeviceNameWithReservation` for `issuePersonhoodName`. /// @return ok Whether the underlying call succeeded. - function _callRegisterBaseName(IDotnsPopController.FullRegistration memory params) + function _callIssuePersonhoodName(IDotnsPopController.PersonhoodNameIssuance memory params) internal returns (bool ok) { _mockOriginIsRoot(true); - try CONTROLLER.registerBaseName(params) { + try CONTROLLER.issuePersonhoodName(params) { ok = true; } catch { ok = false; @@ -591,15 +598,15 @@ contract PopControllerHandler is Test { ); } - /// @notice Builds a classification-valid PoP lite label. + /// @notice Builds a classification-valid PoP device name. /// @dev Shape: `<4 letters from actor>.<2 digits>`, the separated form the gateway - /// accepts. The stem is 7 characters, which classifies as PopLite under PopRules, and + /// accepts. The stem is 7 characters, which classifies as Devicehood under PopRules, and /// the separator and digits are the suffix. Tag disambiguates the reserve vs claim call /// sites so neither collides with the other in the ERC721 namespace. The letter block - /// is derived from the actor address via keccak so each actor lives in its own lite + /// is derived from the actor address via keccak so each actor lives in its own device-name /// namespace. Suffix wraps modulo 100 so the label keeps exactly two digits; /// collisions past 100 reuses are swallowed by the caller's try/catch. - function _buildLiteLabel( + function _buildDeviceLabel( string memory tag, address actor, uint64 suffix diff --git a/test/invariant/registrar/RegistrarControllerHandler.t.sol b/test/invariant/registrar/RegistrarControllerHandler.t.sol index bd81e23b4..81706442d 100644 --- a/test/invariant/registrar/RegistrarControllerHandler.t.sol +++ b/test/invariant/registrar/RegistrarControllerHandler.t.sol @@ -166,13 +166,13 @@ contract RegistrarControllerHandler is Test { } /// @notice Mocks the personhood precompile so it reports `tier` for `account`. - /// @dev Mirrors the `BaseDotns._setUserPopStatus` mapping (PopFull = 2, - /// PopLite = 1, anything else = 0) so the handler stays consistent + /// @dev Mirrors the `BaseDotns._setUserPopStatus` mapping (Personhood = 2, + /// Devicehood = 1, anything else = 0) so the handler stays consistent /// with the test base. function _mockPersonhoodTier(address account, IPopRules.PopStatus tier) internal { uint8 statusByte; - if (tier == IPopRules.PopStatus.PopFull) statusByte = 2; - else if (tier == IPopRules.PopStatus.PopLite) statusByte = 1; + if (tier == IPopRules.PopStatus.Personhood) statusByte = 2; + else if (tier == IPopRules.PopStatus.Devicehood) statusByte = 1; bytes32 contextAlias = statusByte == 0 ? bytes32(0) : keccak256(abi.encode(account, statusByte)); diff --git a/test/invariant/registry/DotnsRegistryInvariant.t.sol b/test/invariant/registry/DotnsRegistryInvariant.t.sol index 3d90c3d20..28f73787e 100644 --- a/test/invariant/registry/DotnsRegistryInvariant.t.sol +++ b/test/invariant/registry/DotnsRegistryInvariant.t.sol @@ -6,7 +6,7 @@ import {IDotnsRegistry} from "../../../contracts/registry/IDotnsRegistry.sol"; import {IPopRules} from "../../../contracts/pop/IPopRules.sol"; import {RegistryHandler} from "./RegistryHandler.t.sol"; -/// @title Dotns Registry Invariant Suite +/// @title dotNS Registry Invariant Suite /// @notice Asserts authorisation, persistence, and parent-reassignment properties of the /// hierarchical registry across randomised registration, subnode creation, and /// transfer flows. @@ -29,12 +29,12 @@ contract DotnsRegistryInvariantTest is BaseDotns { vm.deal(address(handler), 1000 ether); - handler.addActor(ed, IPopRules.PopStatus.PopFull); - handler.addActor(leonardo, IPopRules.PopStatus.PopLite); + handler.addActor(ed, IPopRules.PopStatus.Personhood); + handler.addActor(leonardo, IPopRules.PopStatus.Devicehood); handler.addActor(tiago, IPopRules.PopStatus.NoStatus); address alice = _createUser("alice"); - handler.addActor(alice, IPopRules.PopStatus.PopFull); + handler.addActor(alice, IPopRules.PopStatus.Personhood); targetContract(address(handler)); diff --git a/test/invariant/registry/RegistryHandler.t.sol b/test/invariant/registry/RegistryHandler.t.sol index 5d95f71e1..91fa7e26e 100644 --- a/test/invariant/registry/RegistryHandler.t.sol +++ b/test/invariant/registry/RegistryHandler.t.sol @@ -122,8 +122,8 @@ contract RegistryHandler is Test { /// @param tier Verification tier to report for `account`. function _mockPersonhoodTier(address account, IPopRules.PopStatus tier) internal { uint8 statusByte; - if (tier == IPopRules.PopStatus.PopFull) statusByte = 2; - else if (tier == IPopRules.PopStatus.PopLite) statusByte = 1; + if (tier == IPopRules.PopStatus.Personhood) statusByte = 2; + else if (tier == IPopRules.PopStatus.Devicehood) statusByte = 1; bytes32 contextAlias = statusByte == 0 ? bytes32(0) : keccak256(abi.encode(account, statusByte)); diff --git a/test/unit/escrow/DotnsNameEscrow.t.sol b/test/unit/escrow/DotnsNameEscrow.t.sol index 87227ca7b..2fe4feb4a 100644 --- a/test/unit/escrow/DotnsNameEscrow.t.sol +++ b/test/unit/escrow/DotnsNameEscrow.t.sol @@ -34,8 +34,8 @@ contract GhostNft is ERC721 { /// @custom:contract DotnsNameEscrow, plus the pull-payment and solvency guarantees. contract DotnsNameEscrowTest is BaseDotns { /// @notice Default label used across most tests. - /// @dev Nine-character stem that classifies as NoStatus; the flat deposit equals - /// BASE_DEPOSIT. + /// @dev Eleven characters measured as written, which classifies as NoStatus; the flat deposit + /// equals BASE_DEPOSIT. string internal constant LABEL = "labelnine01"; /// @notice Register `label` for `nameOwner` under the NoStatus PoP tier and return its tokenId. @@ -219,7 +219,7 @@ contract DotnsNameEscrowTest is BaseDotns { function test_self_registration_seeds_funded_position() public { string memory popLabel = "selfregis"; - bytes32 node = _register(popLabel, ed, IPopRules.PopStatus.PopFull); + bytes32 node = _register(popLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = uint256(node); vm.prank(ed); @@ -410,9 +410,9 @@ contract DotnsNameEscrowTest is BaseDotns { function test_funded_position_rebinds_to_new_holder_on_transfer() public { string memory label = BASE_LABEL_A; - _grantPopFull(ed); - _grantPopFull(leonardo); - _register(label, ed, IPopRules.PopStatus.PopFull); + _grantPersonhood(ed); + _grantPersonhood(leonardo); + _register(label, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(label); IDotnsNameEscrow.ReleasePosition memory before = dotnsNameEscrow.getReleasePosition(tokenId); @@ -456,8 +456,8 @@ contract DotnsNameEscrowTest is BaseDotns { function test_downgrade_transfer_pays_name_price() public { string memory label = "lights01"; - _grantPopFull(ed); - _grantPopLite(leonardo); + _grantPersonhood(ed); + _grantDevicehood(leonardo); _commitAndRegister(label, ed, false); @@ -484,17 +484,17 @@ contract DotnsNameEscrowTest is BaseDotns { // Downward cross-tier transfer of a funded NoStatus name: the friction fee settles to // protocol fees and the deposit travels with the NFT. There is no transfer-time refund: // `position.recipient` rebinds to the new holder, the locked deposit follows, and only - // the new holder can later release into escrow. Promoting `ed` to PopFull before the + // the new holder can later release into escrow. Promoting `ed` to Personhood before the // transfer forces `PopRules.transferFloor` to return the base fee D while the position // still carries the original `BASE_DEPOSIT` deposit, so both legs of `chargeTransferFee` // run in one call. uint256 tokenId = _registerNoStatus(LABEL, ed); - _grantPopFull(ed); + _grantPersonhood(ed); uint256 startingPrice = popRules.price(LABEL); uint256 quotedFee = dotnsRegistrar.quoteTransferFee(tokenId, leonardo); - assertEq(quotedFee, startingPrice, "PopFull holder downgrading to NoStatus pays D"); + assertEq(quotedFee, startingPrice, "Personhood holder downgrading to NoStatus pays D"); uint256 reservesBefore = dotnsNameEscrow.reserves(address(0)); uint256 protocolFeesBefore = dotnsNameEscrow.protocolFees(); @@ -761,14 +761,14 @@ contract DotnsNameEscrowTest is BaseDotns { /// @notice Verified payer sponsoring a NoStatus owner on a NoStatus-tier label pays /// exactly D under the max-not-sum rule. /// @dev `priced.price` equals D because the owner is NoStatus, and `transferFloor` returns - /// D because the payer's PopFull tier downgrades into the owner's NoStatus tier. The + /// D because the payer's Personhood tier downgrades into the owner's NoStatus tier. The /// controller charges `max(priced.price, friction) = D` and routes the entire charge /// into protocol fees; the owner-side refundable position is seeded with zero /// amount, so reserves must not move. function test_cross_payer_verified_sponsors_nostatus_pays_only_D() public { string memory label = "crosspayr01"; - _grantPopFull(leonardo); + _grantPersonhood(leonardo); // ed left at default NoStatus tier. uint256 priorProtocolFees = dotnsNameEscrow.protocolFees(); @@ -810,8 +810,8 @@ contract DotnsNameEscrowTest is BaseDotns { function test_cross_payer_pays_the_curve_into_fees() public { string memory label = BASE_LABEL_A; - _grantPopFull(leonardo); - _grantPopFull(ed); + _grantPersonhood(leonardo); + _grantPersonhood(ed); uint256 ownPrice = popRules.price(label); uint256 priorProtocolFees = dotnsNameEscrow.protocolFees(); @@ -845,12 +845,12 @@ contract DotnsNameEscrowTest is BaseDotns { /// it, so `max(ownerPrice, friction)` collapses to the owner price. The whole charge /// routes to protocol fees with no refundable deposit. function test_cross_payer_downgrade_charges_only_owner_price() public { - // LITE_LABEL_A carries the gateway's separator, which the public path this test drives + // DEVICE_LABEL_A carries the gateway's separator, which the public path this test drives // rejects, so the label here is a flat one in the same tier. string memory label = "michael01"; - _grantPopFull(leonardo); - _grantPopLite(ed); + _grantPersonhood(leonardo); + _grantDevicehood(ed); uint256 ownPrice = popRules.price(label); uint256 priorProtocolFees = dotnsNameEscrow.protocolFees(); diff --git a/test/unit/escrow/DotnsNameEscrowRedeem.t.sol b/test/unit/escrow/DotnsNameEscrowRedeem.t.sol index 4f182c3c2..a479125b7 100644 --- a/test/unit/escrow/DotnsNameEscrowRedeem.t.sol +++ b/test/unit/escrow/DotnsNameEscrowRedeem.t.sol @@ -20,8 +20,8 @@ contract DotnsNameEscrowRedeemTest is BaseDotns { /// @notice 14-char label classifying as NoStatus, so registration seeds a funded position. string internal constant FUNDED_LABEL = "redeemlabela01"; - /// @notice 6-char digit-free label classifying as PopFull, registered on the cross-payer path - /// so its deposit position carries a zero amount. + /// @notice 6-char digit-free label classifying as Personhood, registered on the cross-payer + /// path so its deposit position carries a zero amount. string internal constant FREE_LABEL = "redeem"; /// @notice Register `label` for `nameOwner` at `status` and return its tokenId. @@ -322,7 +322,8 @@ contract DotnsNameEscrowRedeemTest is BaseDotns { /// right to recover their own name for no consideration whatsoever. `withdraw` is also a /// step a holder has every reason to make before a name can be recycled. function test_zero_amount_withdrawal_does_not_forfeit_the_redeem_right() public { - uint256 tokenId = _registerCrossPayer(FREE_LABEL, ed, leonardo, IPopRules.PopStatus.PopFull); + uint256 tokenId = + _registerCrossPayer(FREE_LABEL, ed, leonardo, IPopRules.PopStatus.Personhood); assertEq(_positionOf(tokenId).amount, 0, "this case needs a zero-amount position"); _approveAndRelease(tokenId, ed); @@ -439,7 +440,8 @@ contract DotnsNameEscrowRedeemTest is BaseDotns { /// Under a `released && claimed` reclaim gate that would make the name permanently /// unregisterable. function test_zero_amount_release_becomes_reclaimable_without_any_withdrawal() public { - uint256 tokenId = _registerCrossPayer(FREE_LABEL, ed, leonardo, IPopRules.PopStatus.PopFull); + uint256 tokenId = + _registerCrossPayer(FREE_LABEL, ed, leonardo, IPopRules.PopStatus.Personhood); assertEq( _positionOf(tokenId).amount, diff --git a/test/unit/pop/PopRules.t.sol b/test/unit/pop/PopRules.t.sol index ee8599a6b..131402c5f 100644 --- a/test/unit/pop/PopRules.t.sol +++ b/test/unit/pop/PopRules.t.sol @@ -23,20 +23,20 @@ contract PopRulesTests is BaseDotns { assertEq(classificationMessage, "Reserved for Governance"); } - function test_classify_poplite() public view { + function test_classify_devicehood() public view { (IPopRules.PopStatus classificationStatus, string memory classificationMessage) = popRules.classifyName("lights.01"); - assertEq(uint256(classificationStatus), uint256(IPopRules.PopStatus.PopLite)); - assertEq(classificationMessage, "Requires Lite personhood verification"); + assertEq(uint256(classificationStatus), uint256(IPopRules.PopStatus.Devicehood)); + assertEq(classificationMessage, "Requires devicehood"); } - function test_classify_popfull() public view { + function test_classify_personhood() public view { (IPopRules.PopStatus classificationStatus, string memory classificationMessage) = popRules.classifyName("alicebob"); - assertEq(uint256(classificationStatus), uint256(IPopRules.PopStatus.PopFull)); - assertEq(classificationMessage, "Requires Full personhood verification"); + assertEq(uint256(classificationStatus), uint256(IPopRules.PopStatus.Personhood)); + assertEq(classificationMessage, "Requires personhood"); } function test_classify_nostatus() public view { @@ -59,10 +59,10 @@ contract PopRulesTests is BaseDotns { /// these is measured as written: `andrew1` is seven characters and `andrew123` is nine. function test_classify_accepts_any_flat_digit_count() public view { (IPopRules.PopStatus oneDigit,) = popRules.classifyName("andrew1"); - assertEq(uint256(oneDigit), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(oneDigit), uint256(IPopRules.PopStatus.Personhood)); (IPopRules.PopStatus twoDigits,) = popRules.classifyName("andrew01"); - assertEq(uint256(twoDigits), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(twoDigits), uint256(IPopRules.PopStatus.Personhood)); (IPopRules.PopStatus threeDigits,) = popRules.classifyName("andrew123"); assertEq(uint256(threeDigits), uint256(IPopRules.PopStatus.NoStatus)); @@ -75,32 +75,36 @@ contract PopRulesTests is BaseDotns { assertEq(uint256(webThree), uint256(IPopRules.PopStatus.Reserved)); (IPopRules.PopStatus interior,) = popRules.classifyName("micha3l"); - assertEq(uint256(interior), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(interior), uint256(IPopRules.PopStatus.Personhood)); } - /// @dev A lite label's suffix is fixed by its shape, so a wrong digit count fails the shape + /// @dev A device name's suffix is fixed by its shape, so a wrong digit count fails the shape /// check rather than the count check and never reaches classification. - function test_classify_reverts_for_a_lite_suffix_of_the_wrong_length() public { + function test_classify_reverts_for_a_device_suffix_of_the_wrong_length() public { vm.expectRevert( abi.encodeWithSelector( IPopRules.PopError.selector, - "Name must be a lowercase ASCII DNS label or a lite label" + "Name must be a lowercase ASCII DNS label or a device name" ) ); popRules.classifyName("andrew.1"); } - /// @dev The gateway's suffix is not part of the name the candidate chose, so a lite label + /// @dev The gateway's suffix is not part of the name the candidate chose, so a device name /// prices as its stem, while its flat spelling is a different, longer name. The flat /// launch model charges one deposit at every length, so the rule is only observable on /// a length-sensitive curve; this switches to the scarcity model to see it. - function test_a_lite_suffix_prices_as_its_stem() public { + function test_a_device_suffix_prices_as_its_stem() public { vm.startPrank(owner); costModelRegistry.register(IDotnsPricing(address(scarcityPricing))); costModelRegistry.setCurrentVersion(scarcityPricing.version()); vm.stopPrank(); - assertEq(popRules.price("andrew.01"), popRules.price("andrew"), "lite prices as its stem"); + assertEq( + popRules.price("andrew.01"), + popRules.price("andrew"), + "a device name prices as its stem" + ); assertTrue( popRules.price("andrew01") != popRules.price("andrew"), "the flat spelling is a longer name" @@ -108,14 +112,14 @@ contract PopRulesTests is BaseDotns { } function test_verified_person_pays_the_deposit_for_premium() public { - _grantPopFull(ed); + _grantPersonhood(ed); assertEq(popRules.priceWithCheck("alicebob", ed).price, BASE_DEPOSIT); assertEq(popRules.priceWithCheck("lights", ed).price, BASE_DEPOSIT); } function test_transfer_reprices_at_own_length() public { - _grantPopFull(leonardo); + _grantPersonhood(leonardo); assertEq(popRules.transferFloor("lights", leonardo, tiago), BASE_DEPOSIT); assertEq(popRules.transferFloor("lights", leonardo, leonardo), 0); @@ -128,40 +132,36 @@ contract PopRulesTests is BaseDotns { popRules.priceWithCheck("hello", ed); } - function test_price_with_check_revert_full_needed() public { - vm.expectRevert( - abi.encodeWithSelector( - IPopRules.PopError.selector, "Requires Full personhood verification" - ) - ); + function test_price_with_check_revert_personhood_needed() public { + vm.expectRevert(abi.encodeWithSelector(IPopRules.PopError.selector, "Requires personhood")); popRules.priceWithCheck("alicebob", ed); } - function test_popfull_user_can_access_poplite_name() public { - _grantPopFull(ed); + function test_personhood_user_can_access_devicehood_name() public { + _grantPersonhood(ed); IPopRules.PriceWithMeta memory priceMetadata = popRules.priceWithCheck("lights.01", ed); - assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.PopLite)); - assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.Devicehood)); + assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.Personhood)); assertEq(priceMetadata.price, BASE_DEPOSIT); } - function test_poplite_user_can_access_nostatus_name() public { - _grantPopLite(ed); + function test_devicehood_user_can_access_nostatus_name() public { + _grantDevicehood(ed); IPopRules.PriceWithMeta memory priceMetadata = popRules.priceWithCheck("longnamehere.01", ed); assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.NoStatus)); - assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.PopLite)); + assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.Devicehood)); assertEq(priceMetadata.price, BASE_DEPOSIT); } function test_base_reservation_blocks_others() public { // Authorise this test contract as a registrar controller so it may call - // reserveBaseName (gated by DotnsRegistrar.controllers). Passes the stem - // directly per the stems-only public boundary. + // reserveBaseName (gated by DotnsRegistrar.controllers). Passes the base name + // directly, as the public boundary takes base names only. _authoriseTestAsController(); popRules.reserveBaseName("lights", leonardo); @@ -175,7 +175,7 @@ contract PopRulesTests is BaseDotns { vm.expectRevert( abi.encodeWithSelector( - IPopRules.PopError.selector, "Base name reserved for original Lite registrant" + IPopRules.PopError.selector, "Reserved for a device-name holder's personhood claim" ) ); popRules.priceWithCheck("lights", tiago); @@ -194,7 +194,7 @@ contract PopRulesTests is BaseDotns { function test_short_names_closed_reverts_direct_path() public { _setShortNames(false); - _grantPopFull(ed); + _grantPersonhood(ed); vm.expectRevert( abi.encodeWithSelector(IPopRules.PopError.selector, "Short names are not for sale") ); @@ -211,14 +211,14 @@ contract PopRulesTests is BaseDotns { function test_open_band_priced_while_short_names_closed() public { _setShortNames(false); - _grantPopLite(ed); + _grantDevicehood(ed); // longnamehere is 12 characters, so the switch never gates it. assertEq(popRules.priceWithCheck("longnamehere", ed).price, BASE_DEPOSIT); } function test_enabling_short_names_opens_the_market() public { _setShortNames(false); - _grantPopFull(ed); + _grantPersonhood(ed); _setShortNames(true); assertEq(popRules.priceWithCheck("alicebob", ed).price, BASE_DEPOSIT); } @@ -268,10 +268,10 @@ contract PopRulesTests is BaseDotns { } function test_transferFloor_matches_model() public { - // A PopFull sender handing a NoStatus name to a NoStatus recipient pays the name's own + // A Personhood sender handing a NoStatus name to a NoStatus recipient pays the name's own // price, which is the registered model's amount for its base length. string memory label = "longnamehere"; - _grantPopFull(ed); + _grantPersonhood(ed); uint256 floor = popRules.transferFloor(label, ed, leonardo); assertEq(floor, costModelRegistry.priceForBaseLength(bytes(label).length)); } diff --git a/test/unit/pop/PopRulesClassification.t.sol b/test/unit/pop/PopRulesClassification.t.sol index 2e39dc3bf..af45f2b11 100644 --- a/test/unit/pop/PopRulesClassification.t.sol +++ b/test/unit/pop/PopRulesClassification.t.sol @@ -12,9 +12,9 @@ import {IPopRules} from "../../../contracts/pop/IPopRules.sol"; /// and reads no storage, so a tier is a function of the label alone. Anything that needs a /// cost model or a reservation belongs in the proxy-backed suite instead. contract PopRulesClassificationTests is Test { - /// @notice Guard message for a string that is neither a DNS label nor a lite label. + /// @notice Guard message for a string that is neither a DNS label nor a device name. string internal constant SHAPE_ERROR = - "Name must be a lowercase ASCII DNS label or a lite label"; + "Name must be a lowercase ASCII DNS label or a device name"; PopRules internal rules; @@ -31,24 +31,24 @@ contract PopRulesClassificationTests is Test { vm.expectRevert(abi.encodeWithSelector(IPopRules.PopError.selector, reason)); } - /// @dev Base length is the stem, so a two-digit suffix does not count towards the band - /// whether or not it carries the gateway's separator. - function test_classify_measures_a_lite_label_by_its_stem() public view { + /// @dev Base length is the length of the base name, so a two-digit suffix does not count + /// towards the band whether or not it carries the gateway's separator. + function test_classify_measures_a_device_name_by_its_stem() public view { _assertTier("alice.42", IPopRules.PopStatus.Reserved); - _assertTier("joseph.42", IPopRules.PopStatus.PopLite); - _assertTier("benjamin.42", IPopRules.PopStatus.PopLite); + _assertTier("joseph.42", IPopRules.PopStatus.Devicehood); + _assertTier("benjamin.42", IPopRules.PopStatus.Devicehood); _assertTier("elizabeth.42", IPopRules.PopStatus.NoStatus); } /// @notice The separator is meaning, not presentation: it is what makes a name an identity. - /// @dev A lite label is measured by the stem the candidate chose, because the gateway + /// @dev A device name is measured by the stem the candidate chose, because the gateway /// allocated the digits. An ordinary label is measured as written. So the two spellings - /// are different names in different bands, and only the separated one is PopLite. + /// are different names in different bands, and only the separated one is Devicehood. function test_classify_differs_with_and_without_the_separator() public view { - _assertTier("joseph.42", IPopRules.PopStatus.PopLite); - _assertTier("joseph42", IPopRules.PopStatus.PopFull); + _assertTier("joseph.42", IPopRules.PopStatus.Devicehood); + _assertTier("joseph42", IPopRules.PopStatus.Personhood); - _assertTier("michael.01", IPopRules.PopStatus.PopLite); + _assertTier("michael.01", IPopRules.PopStatus.Devicehood); _assertTier("michael01", IPopRules.PopStatus.NoStatus); _assertTier("elizabeth.42", IPopRules.PopStatus.NoStatus); @@ -56,28 +56,33 @@ contract PopRulesClassificationTests is Test { } /// @notice The regression test for deriving the digit count by subtraction. - /// @dev `bytes(name).length - baseLength` is 3 for a lite label, never 2, so a subtraction - /// based check classifies every lite name in the 6-8 band as PopFull. These rows catch - /// it, which is why they assert PopLite rather than merely "not Reserved". - function test_classify_puts_a_lite_label_in_the_lite_tier_not_the_full_tier() public view { + /// @dev `bytes(name).length - baseLength` is 3 for a device name, never 2, so a subtraction + /// based check classifies every device name in the 6-8 band as Personhood. These rows + /// catch it, which is why they assert Devicehood rather than merely "not Reserved". + function test_classify_puts_a_device_name_in_the_devicehood_tier_not_the_personhood_tier() + public + view + { (IPopRules.PopStatus sixChar,) = rules.classifyName("joseph.42"); - assertEq(uint256(sixChar), uint256(IPopRules.PopStatus.PopLite), "stem 6 is PopLite"); + assertEq(uint256(sixChar), uint256(IPopRules.PopStatus.Devicehood), "stem 6 is Devicehood"); (IPopRules.PopStatus eightChar,) = rules.classifyName("benjamin.42"); - assertEq(uint256(eightChar), uint256(IPopRules.PopStatus.PopLite), "stem 8 is PopLite"); + assertEq( + uint256(eightChar), uint256(IPopRules.PopStatus.Devicehood), "stem 8 is Devicehood" + ); } function test_classify_measures_a_label_without_a_suffix_whole() public view { _assertTier("alice", IPopRules.PopStatus.Reserved); - _assertTier("joseph", IPopRules.PopStatus.PopFull); - _assertTier("benjamin", IPopRules.PopStatus.PopFull); + _assertTier("joseph", IPopRules.PopStatus.Personhood); + _assertTier("benjamin", IPopRules.PopStatus.Personhood); _assertTier("elizabeth", IPopRules.PopStatus.NoStatus); } /// @dev An ordinary name carrying digits stays registrable, measured as written. function test_classify_accepts_an_ordinary_flat_digit_suffix() public view { _assertTier("longnamebob01", IPopRules.PopStatus.NoStatus); - _assertTier("lights01", IPopRules.PopStatus.PopFull); + _assertTier("lights01", IPopRules.PopStatus.Personhood); } /// @dev No digit count is privileged or rejected, and none is stripped: a name ending in @@ -85,15 +90,15 @@ contract PopRulesClassificationTests is Test { function test_classify_accepts_a_flat_suffix_of_any_length() public view { _assertTier("iamtherealbob0", IPopRules.PopStatus.NoStatus); _assertTier("elizabeth12345", IPopRules.PopStatus.NoStatus); - _assertTier("blink182", IPopRules.PopStatus.PopFull); + _assertTier("blink182", IPopRules.PopStatus.Personhood); _assertTier("web3", IPopRules.PopStatus.Reserved); } - /// @dev A lite label's suffix is fixed by its shape, so a wrong count fails the shape check + /// @dev A device name's suffix is fixed by its shape, so a wrong count fails the shape check /// before the count check can see it. - /// @dev A separator is legal only on a lite label, so a string carrying one that misses - /// the shape in any way is neither a DNS label nor a lite label. - function test_classify_reverts_for_a_malformed_lite_label() public { + /// @dev A separator is legal only on a device name, so a string carrying one that misses + /// the shape in any way is neither a DNS label nor a device name. + function test_classify_reverts_for_a_malformed_device_name() public { _expectRevert(SHAPE_ERROR); rules.classifyName("joseph.4"); @@ -108,7 +113,7 @@ contract PopRulesClassificationTests is Test { rules.classifyName("jos3ph.42"); } - /// @dev Only a lite label is shortened, so `joseph.42` contends with a reservation on + /// @dev Only a device name is shortened, so `joseph.42` contends with a reservation on /// `joseph` while `joseph42` is an unrelated name that contends with nothing. function test_stripDigits_shortens_only_the_separated_form() public view { assertEq(rules.stripDigits("joseph.42"), "joseph"); @@ -116,13 +121,13 @@ contract PopRulesClassificationTests is Test { assertEq(rules.stripDigits("blink182"), "blink182"); } - /// @dev A stem returned as `joseph.` would fail `isSingleLabelMemory` in the registrar + /// @dev A base name returned as `joseph.` would fail `isSingleLabelMemory` in the registrar /// controller and silently skip the reclaim branch's reservation release. function test_stripDigits_leaves_no_trailing_separator() public view { - string memory stem = rules.stripDigits("joseph.42"); - bytes memory raw = bytes(stem); - assertTrue(raw.length > 0, "stem is not empty"); - assertTrue(raw[raw.length - 1] != ".", "stem carries no trailing separator"); + string memory baseName = rules.stripDigits("joseph.42"); + bytes memory raw = bytes(baseName); + assertTrue(raw.length > 0, "base name is not empty"); + assertTrue(raw[raw.length - 1] != ".", "base name carries no trailing separator"); } function test_stripDigits_returns_a_suffixless_label_verbatim() public view { @@ -130,8 +135,8 @@ contract PopRulesClassificationTests is Test { } /// @dev Must answer the question rather than revert, since the controller asks it of - /// labels that may be lite. - function test_isBaseName_answers_false_for_a_lite_label() public view { + /// labels that may be device-name. + function test_isBaseName_answers_false_for_a_device_name() public view { assertFalse(rules.isBaseName("joseph.42")); assertTrue(rules.isBaseName("elizabeth")); } diff --git a/test/unit/registrar/DotnsPopController.t.sol b/test/unit/registrar/DotnsPopController.t.sol index 787820977..1706ff3b6 100644 --- a/test/unit/registrar/DotnsPopController.t.sol +++ b/test/unit/registrar/DotnsPopController.t.sol @@ -22,16 +22,16 @@ import {Vm} from "forge-std/Vm.sol"; /// tests assert specific behaviours that do not benefit from input /// variation. contract DotnsPopControllerTests is BaseDotns { - function test_reserveBaseName_mints_and_wires_registry_and_resolver() public { - // Lite path requires PopLite tier and a classification-valid lite label. - _grantPopFull(ed); + function test_issueDeviceNameWithReservation_mints_and_wires_registry_and_resolver() public { + // Device-name path requires Devicehood tier and a classification-valid device name. + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x01); - _reservePop(ed, LITE_LABEL_A, chatKey, ""); + _reservePop(ed, DEVICE_LABEL_A, chatKey, ""); - // A lite username is a subnode under its numeric container, not a tokenised name, so its + // A device name is a subnode under its numeric container, not a tokenised name, so its // ownership lives in the registry record rather than the registrar's ERC-721 ledger. - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); assertEq(dotnsRegistry.owner(node), ed); assertEq(dotnsPopResolver.chatKey(node), chatKey); @@ -51,9 +51,9 @@ contract DotnsPopControllerTests is BaseDotns { /// does not re-mint it. /// @dev A re-mint would revert on the already-registered container, so the second reservation /// succeeding, with the container owner unchanged, is proof it took the reuse path. - function test_second_lite_stem_reuses_the_container() public { - _grantPopFull(ed); - _grantPopFull(leonardo); + function test_second_device_stem_reuses_the_container() public { + _grantPersonhood(ed); + _grantPersonhood(leonardo); _reservePop(ed, "michael.01", _validChatKey(0x01), ""); bytes32 containerNode = _nodeOf("01"); @@ -62,177 +62,199 @@ contract DotnsPopControllerTests is BaseDotns { _reservePop(leonardo, "matthew.01", _validChatKey(0x02), ""); assertEq(dotnsRegistry.owner(containerNode), containerOwner, "container not re-owned"); - assertEq(dotnsRegistry.owner(_liteNodeOf("michael.01")), ed, "first stem owned by ed"); + assertEq(dotnsRegistry.owner(_deviceNodeOf("michael.01")), ed, "first stem owned by ed"); assertEq( - dotnsRegistry.owner(_liteNodeOf("matthew.01")), + dotnsRegistry.owner(_deviceNodeOf("matthew.01")), leonardo, "second stem owned by leonardo" ); } - function test_reserveBaseName_reverts_when_origin_is_not_root() public { + function test_issueDeviceNameWithReservation_reverts_when_origin_is_not_root() public { _mockOriginIsRoot(false); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.reserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: "" + dotnsPopController.issueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: "" }), - reservedBaseLabel: "" + reservedLabel: "" }) ); } - function test_reserveBaseName_enqueues_when_reserved_label_provided() public { + function test_issueDeviceNameWithReservation_enqueues_when_reserved_label_provided() public { // `PopRules.priceWithCheck` admits only the live reservation holder on // a given base stem, so the queue is single-occupant. Multi-occupant // queue coverage lives in the invariant suite. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), PERSONHOOD_LABEL_A); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, ed); } - function test_registerBaseName_claim_emits_claim_event_and_not_standalone() public { - // ed gets a different PopFull-classified base label via standalone mint. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_claim_emits_reservation_claimed() public { + // ed gets a different Personhood-classified base label via standalone mint. + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); vm.recordLogs(); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); Vm.Log[] memory logs = vm.getRecordedLogs(); - _assertEventEmittedOnce(logs, keccak256("BaseNameClaimed(bytes32,address,string)")); - _assertEventNotEmitted(logs, keccak256("StandaloneNameRegistered(bytes32,address,string)")); + _assertEventEmittedOnce(logs, keccak256("ReservationClaimed(bytes32,address)")); + _assertEventEmittedOnce(logs, keccak256("PersonhoodNameIssued(bytes32,address,string)")); - (bool reserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(reserved); } - function test_registerBaseName_standalone_emits_standalone_event_and_not_claim() public { - _grantPopFull(tiago); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_standalone_emits_no_reservation_claimed() public { + _grantPersonhood(tiago); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x01), PERSONHOOD_LABEL_A); // `priceWithCheck` admits only the live reservation holder on a given // base stem, so the standalone mint runs against a separate, free stem // owned by a different user; this asserts neither user sees state leak // from the other. - _grantPopFull(ed); + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); vm.recordLogs(); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_C, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_C, user: ed, link: link + }) ); Vm.Log[] memory logs = vm.getRecordedLogs(); - _assertEventEmittedOnce(logs, keccak256("StandaloneNameRegistered(bytes32,address,string)")); - _assertEventNotEmitted(logs, keccak256("BaseNameClaimed(bytes32,address,string)")); + _assertEventEmittedOnce(logs, keccak256("PersonhoodNameIssued(bytes32,address,string)")); + _assertEventNotEmitted(logs, keccak256("ReservationClaimed(bytes32,address)")); } - function test_registerBaseName_claim_inherits_chat_key_from_lite_node() public { - // Promote ed to PopFull so the standalone PopFull-classified mint passes. - _grantPopFull(ed); - bytes memory liteChatKey = _validChatKey(0xaa); - _reservePop(ed, LITE_LABEL_A, liteChatKey, BASE_LABEL_A); + function test_issuePersonhoodName_claim_inherits_chat_key_from_device_node() public { + // Promote ed to Personhood so the standalone Personhood-classified mint passes. + _grantPersonhood(ed); + bytes memory deviceChatKey = _validChatKey(0xaa); + _reservePop(ed, DEVICE_LABEL_A, deviceChatKey, PERSONHOOD_LABEL_A); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - bytes32 fullNode = _nodeOf(BASE_LABEL_A); - assertEq(dotnsPopResolver.chatKey(fullNode), liteChatKey); - assertEq(dotnsPopResolver.liteLink(fullNode), keccak256(bytes(LITE_LABEL_A))); + bytes32 personhoodNode = _nodeOf(PERSONHOOD_LABEL_A); + assertEq(dotnsPopResolver.chatKey(personhoodNode), deviceChatKey); + assertEq( + dotnsPopResolver.deviceLabelhashOf(personhoodNode), keccak256(bytes(DEVICE_LABEL_A)) + ); } - function test_registerBaseName_claim_wipes_entire_queue() public { + function test_issuePersonhoodName_claim_wipes_entire_queue() public { // `priceWithCheck` admits only the live reservation holder on a given // base stem, so the queue stays single-occupant; after the holder // claims, the stem is free for a fresh reservation from any user. - _grantPopFull(ed); - _grantPopFull(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); - - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _grantPersonhood(ed); + _grantPersonhood(tiago); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - _reservePop(tiago, LITE_LABEL_D, _validChatKey(0x04), BASE_LABEL_B); - (, address wonderHolder) = dotnsPopController.isReservedForClaim(BASE_LABEL_B); + _reservePop(tiago, DEVICE_LABEL_D, _validChatKey(0x04), PERSONHOOD_LABEL_B); + (, address wonderHolder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_B); assertEq(wonderHolder, tiago); } - function test_registerBaseName_standalone_auto_relinquishes_users_other_reservation() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_standalone_auto_relinquishes_users_other_reservation() + public + { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - _grantPopFull(ed); + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0x02)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_B, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_B, user: ed, link: link + }) ); - (bool reserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(reserved); } - function test_registerBaseName_standalone_with_lite_link_silently_relinquishes() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_standalone_with_device_link_relinquishes_the_other_reservation() + public + { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - _grantPopFull(ed); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); + _grantPersonhood(ed); + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); vm.recordLogs(); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_B, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_B, user: ed, link: link + }) ); Vm.Log[] memory logs = vm.getRecordedLogs(); - _assertEventEmittedOnce(logs, keccak256("StandaloneNameRegistered(bytes32,address,string)")); - _assertEventEmittedOnce(logs, keccak256("LiteToFullLinked(bytes32,bytes32)")); - _assertEventNotEmitted(logs, keccak256("BaseNameClaimed(bytes32,address,string)")); - _assertEventNotEmitted(logs, keccak256("ReservationRelinquished(bytes32,address)")); + _assertEventEmittedOnce(logs, keccak256("PersonhoodNameIssued(bytes32,address,string)")); + _assertEventEmittedOnce(logs, keccak256("DeviceNameLinked(bytes32,bytes32)")); + _assertEventNotEmitted(logs, keccak256("ReservationClaimed(bytes32,address)")); + _assertEventEmittedOnce(logs, keccak256("ReservationRelinquished(bytes32,address)")); - bytes32 fullNode = _nodeOf(BASE_LABEL_B); - assertEq(dotnsPopResolver.liteLink(fullNode), keccak256(bytes(LITE_LABEL_A))); + bytes32 personhoodNode = _nodeOf(PERSONHOOD_LABEL_B); + assertEq( + dotnsPopResolver.deviceLabelhashOf(personhoodNode), keccak256(bytes(DEVICE_LABEL_A)) + ); - (bool reserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(reserved); } - function test_registerBaseName_reverts_when_origin_is_not_root() public { + function test_issuePersonhoodName_reverts_when_origin_is_not_root() public { IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); _mockOriginIsRoot(false); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.registerBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + dotnsPopController.issuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); } function test_relinquishReservation_promotes_next_waiter_when_head_leaves() public { - _grantPopFull(ed); - _grantPopFull(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _grantPersonhood(ed); + _grantPersonhood(tiago); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); vm.prank(ed); dotnsPopController.relinquishReservation(); - (bool empty,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool empty,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(empty); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x02), BASE_LABEL_A); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x02), PERSONHOOD_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, tiago); } @@ -246,12 +268,12 @@ contract DotnsPopControllerTests is BaseDotns { } function test_setReservationDuration_shortening_retroactively_expires_live_entries() public { - _grantPopFull(ed); + _grantPersonhood(ed); // Enqueue alice under the default duration (7 days). - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); (bool liveBefore, address holderBefore) = - dotnsPopController.isReservedForClaim(BASE_LABEL_A); + dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(liveBefore); assertEq(holderBefore, ed); // Warp 2 days forward (still well within the original 7-day window). @@ -261,7 +283,7 @@ contract DotnsPopControllerTests is BaseDotns { vm.prank(owner); dotnsPopController.setReservationDuration(1 days); - (bool liveAfter,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool liveAfter,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(liveAfter); } @@ -275,34 +297,34 @@ contract DotnsPopControllerTests is BaseDotns { // Two independent reads must agree: the per-user reservation pointer // (`userReservation`) and the per-base claim view (`isReservedForClaim`). // Both change atomically when the same user re-reserves on a new stem. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); IDotnsPopController.UserReservation memory firstReservation = dotnsPopController.userReservation(ed); - assertEq(firstReservation.labelhash, keccak256(bytes(BASE_LABEL_A))); + assertEq(firstReservation.labelhash, keccak256(bytes(PERSONHOOD_LABEL_A))); // Second reservation by the same user on a different stem drops the // first slot and installs the new one. - _reservePop(ed, LITE_LABEL_B, _validChatKey(0x02), BASE_LABEL_B); + _reservePop(ed, DEVICE_LABEL_B, _validChatKey(0x02), PERSONHOOD_LABEL_B); IDotnsPopController.UserReservation memory secondReservation = dotnsPopController.userReservation(ed); - assertEq(secondReservation.labelhash, keccak256(bytes(BASE_LABEL_B))); + assertEq(secondReservation.labelhash, keccak256(bytes(PERSONHOOD_LABEL_B))); - (bool firstReserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool firstReserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(firstReserved); (bool secondReserved, address secondHolder) = - dotnsPopController.isReservedForClaim(BASE_LABEL_B); + dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_B); assertTrue(secondReserved); assertEq(secondHolder, ed); } function test_reEnqueue_after_own_expiry_promotes_same_user_to_head() public { - string memory baseStem = BASE_LABEL_A; - _grantPopFull(ed); + string memory baseStem = PERSONHOOD_LABEL_A; + _grantPersonhood(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), baseStem); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), baseStem); // Warp past the reservation window and fire the GC so ed's pointer gets // cleared by `_advanceExpiredHead`. If the expiry path forgets the // per-user pointer, the second reserve call below hits `AlreadyReserved` @@ -312,8 +334,8 @@ contract DotnsPopControllerTests is BaseDotns { (bool expiredReserved,) = dotnsPopController.isReservedForClaim(baseStem); assertFalse(expiredReserved); - // Same user reserves the same stem again with a fresh lite label. - _reservePop(ed, LITE_LABEL_B, _validChatKey(0x02), baseStem); + // Same user reserves the same stem again with a fresh device name. + _reservePop(ed, DEVICE_LABEL_B, _validChatKey(0x02), baseStem); (bool nowReserved, address holder) = dotnsPopController.isReservedForClaim(baseStem); assertTrue(nowReserved); @@ -321,17 +343,17 @@ contract DotnsPopControllerTests is BaseDotns { } function test_claim_then_reEnqueue_on_same_stem_resets_cleanly() public { - string memory baseStem = BASE_LABEL_A; - _grantPopFull(ed); - _grantPopFull(tiago); + string memory baseStem = PERSONHOOD_LABEL_A; + _grantPersonhood(ed); + _grantPersonhood(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), baseStem); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), baseStem); // Claim wipes the queue via `_clearQueue` which must also drop // `_reservedBaseLabel[labelhash]` and release the PopRules slot. Missing // any one of those lets the next reservation inherit stale state. - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: baseStem, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: baseStem, user: ed, link: link}) ); (bool oldSlot, address oldHolder) = dotnsPopController.isReservedForClaim(baseStem); @@ -339,41 +361,42 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(oldHolder, address(0)); // Fresh stem, different user. If the previous queue leaked, this enqueue // would either revert or land the wrong head address on PopRules. - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x02), BASE_LABEL_B); - (bool newSlot, address newHolder) = dotnsPopController.isReservedForClaim(BASE_LABEL_B); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x02), PERSONHOOD_LABEL_B); + (bool newSlot, address newHolder) = + dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_B); assertTrue(newSlot); assertEq(newHolder, tiago); - (address popHolder,) = popRules.getBaseNameReservation(BASE_LABEL_B); + (address popHolder,) = popRules.getBaseNameReservation(PERSONHOOD_LABEL_B); assertEq(popHolder, tiago); } function test_expireReservation_is_permissionless() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); // Anyone can call. Pinning this prevents a future patch from silently // adding `onlyRoot` and breaking permissionless garbage collection. address stranger = makeAddr("stranger"); vm.prank(stranger); - dotnsPopController.expireReservation(BASE_LABEL_A); + dotnsPopController.expireReservation(PERSONHOOD_LABEL_A); - (bool reserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(reserved); } function test_head_expires_clears_slot_for_next_reserver() public { - string memory baseStem = BASE_LABEL_A; - _grantPopFull(ed); - _grantPopFull(leonardo); + string memory baseStem = PERSONHOOD_LABEL_A; + _grantPersonhood(ed); + _grantPersonhood(leonardo); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), baseStem); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), baseStem); vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); dotnsPopController.expireReservation(baseStem); - _reservePop(leonardo, LITE_LABEL_C, _validChatKey(0x03), baseStem); + _reservePop(leonardo, DEVICE_LABEL_C, _validChatKey(0x03), baseStem); (bool reserved, address holder) = dotnsPopController.isReservedForClaim(baseStem); assertTrue(reserved); @@ -384,51 +407,51 @@ contract DotnsPopControllerTests is BaseDotns { } function test_entry_point_format_rejections() public { - _grantPopFull(ed); + _grantPersonhood(ed); // The shape check runs before any pricing, so "michael" is rejected for carrying no // separator rather than for anything about its tier. - vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({liteLabel: "michael", user: ed, chatKey: ""}) + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({label: "michael", user: ed, chatKey: ""}) ); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "not.valid", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "not.valid", user: ed, link: link}) ); } - function test_same_stem_lite_and_base_occupy_distinct_registrar_tokens() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), ""); - // "michael" (baselength 7, no trailing digits) classifies as PopFull - // and shares the lite's stem, so both tokens coexist on the registrar. - _grantPopFull(tiago); + function test_same_stem_device_and_personhood_names_occupy_distinct_nodes() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), ""); + // "michael" (baselength 7, no trailing digits) classifies as Personhood + // and shares the device name's stem, so both tokens coexist on the registrar. + _grantPersonhood(tiago); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xbb)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "michael", user: tiago, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "michael", user: tiago, link: link}) ); - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL_A)), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("michael"))), tiago); } function test_both_controllers_can_mint_on_shared_registrar() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); _commitAndRegister("longnamebob01", tiago, true); - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL_A)), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob01"))), tiago); } function test_gateway_reserved_name_rejects_public_register_by_other_user() public { - _grantPopFull(tiago); + _grantPersonhood(tiago); // The gateway reserves the stem, and the stem is what the reservation blocks: a public // registrant contends for `longnamebob` itself. - _reservePop(tiago, LITE_LABEL_A, _validChatKey(0x11), "longnamebob"); + _reservePop(tiago, DEVICE_LABEL_A, _validChatKey(0x11), "longnamebob"); (address holder,) = popRules.getBaseNameReservation("longnamebob"); assertEq(holder, tiago); @@ -455,7 +478,7 @@ contract DotnsPopControllerTests is BaseDotns { vm.expectRevert( abi.encodeWithSelector( - IPopRules.PopError.selector, "Base name reserved for original Lite registrant" + IPopRules.PopError.selector, "Reserved for a device-name holder's personhood claim" ) ); vm.prank(ed); @@ -463,8 +486,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_gateway_reserved_name_allows_holder_to_register_via_public() public { - _grantPopFull(tiago); - _reservePop(tiago, LITE_LABEL_A, _validChatKey(0x11), "longnamebob"); + _grantPersonhood(tiago); + _reservePop(tiago, DEVICE_LABEL_A, _validChatKey(0x11), "longnamebob"); _commitAndRegister("longnamebob", tiago, true); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob"))), tiago); @@ -475,50 +498,50 @@ contract DotnsPopControllerTests is BaseDotns { /// shares no stem with it and a stranger may take it. This is the guarantee that /// replaces the old cross-flow, where the two spellings collided. function test_gateway_reservation_does_not_cover_a_digit_suffixed_name() public { - _grantPopFull(tiago); - _reservePop(tiago, LITE_LABEL_A, _validChatKey(0x11), "longnamebob"); + _grantPersonhood(tiago); + _reservePop(tiago, DEVICE_LABEL_A, _validChatKey(0x11), "longnamebob"); - _grantPopFull(ed); + _grantPersonhood(ed); _commitAndRegister("longnamebob01", ed, true); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob01"))), ed); } - function test_second_pop_lite_mint_of_same_label_reverts() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), ""); + function test_second_device_name_issuance_of_same_label_reverts() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), ""); - // Re-issuing a lite name is rejected at the controller before any registry write, so a + // Re-issuing a device name is rejected at the controller before any registry write, so a // duplicate dispatch cannot rehome the identity or overwrite its records. - _grantPopFull(tiago); - vm.expectRevert(IDotnsPopController.LiteNameAlreadyIssued.selector); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: tiago, chatKey: _validChatKey(0xbb) + _grantPersonhood(tiago); + vm.expectRevert(IDotnsPopController.DeviceNameAlreadyIssued.selector); + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: tiago, chatKey: _validChatKey(0xbb) }), - reservedBaseLabel: "" + reservedLabel: "" }) ); } - /// @notice A lite name's owner can host a subname beneath it. - /// @dev A lite name is itself a subname the owner controls in the registry, so the owner holds - /// the parent authority @custom:function IDotnsRegistry.setSubnodeOwner requires and can - /// graft their own subnames beneath it, such as a device name `phone.michael.01`. - function test_lite_name_owner_can_host_a_subname() public { - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xb4) + /// @notice A device name's owner can host a subname beneath it. + /// @dev A device name is itself a subname the owner controls in the registry, so the owner + /// holds the parent authority @custom:function IDotnsRegistry.setSubnodeOwner requires and + /// can graft their own subnames beneath it, such as a device name `phone.michael.01`. + function test_device_name_owner_can_host_a_subname() public { + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xb4) }) ); - // A lite name is a subname the owner controls, so they can host their own subnames beneath - // it, such as a device name `phone.michael.01`. + // A device name is a subname the owner controls, so they can host their own subnames + // beneath it, such as a device name `phone.michael.01`. IDotnsRegistry.SubnodeRecord memory subnodeRecord = IDotnsRegistry.SubnodeRecord({ - parentNode: _liteNodeOf(LITE_LABEL_A), + parentNode: _deviceNodeOf(DEVICE_LABEL_A), subLabel: "phone", - parentLabel: LITE_LABEL_A, + parentLabel: DEVICE_LABEL_A, owner: ed, persist: true }); @@ -534,10 +557,10 @@ contract DotnsPopControllerTests is BaseDotns { /// inside the same call, so the assertion that it still reads false is what proves the /// failed mint took the provenance write with it. A public name that answered /// `isPopIssued` would pass for a person. - function test_pop_full_mint_after_public_register_reverts_and_writes_no_provenance() public { + function test_personhood_mint_after_public_register_reverts_and_writes_no_provenance() public { string memory label = "longnamebobx"; - _grantPopFull(tiago); + _grantPersonhood(tiago); _commitAndRegister(label, tiago, true); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xd1)); @@ -546,20 +569,22 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsRegistrar.NameNotAvailable.selector, uint256(_nodeOf(label)) ) ); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: label, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: label, user: ed, link: link}) ); assertFalse(dotnsPopController.isPopIssued(label), "provenance survived a failed mint"); assertFalse(dotnsRegistrar.isSoulbound(uint256(_nodeOf(label))), "public name locked"); } - function test_public_register_after_pop_full_mint_reverts_at_registrar() public { - // "longnamebobx" is classification-NoStatus, so ed keeps default status. A full-person + function test_public_register_after_personhood_mint_reverts_at_registrar() public { + // "longnamebobx" is classification-NoStatus, so ed keeps default status. A personhood // label carries no digit suffix. IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "longnamebobx", user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: "longnamebobx", user: ed, link: link + }) ); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebobx"))), ed); @@ -591,17 +616,19 @@ contract DotnsPopControllerTests is BaseDotns { } function test_owner_of_pop_minted_name_can_create_subname() public { - _grantPopFull(ed); + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - bytes32 parentNode = _nodeOf(BASE_LABEL_A); + bytes32 parentNode = _nodeOf(PERSONHOOD_LABEL_A); IDotnsRegistry.SubnodeRecord memory subnodeRecord = IDotnsRegistry.SubnodeRecord({ parentNode: parentNode, subLabel: "app", - parentLabel: BASE_LABEL_A, + parentLabel: PERSONHOOD_LABEL_A, owner: leonardo, persist: true }); @@ -613,17 +640,19 @@ contract DotnsPopControllerTests is BaseDotns { } function test_non_owner_cannot_create_subname_under_pop_minted_name() public { - _grantPopFull(ed); + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - bytes32 parentNode = _nodeOf(BASE_LABEL_A); + bytes32 parentNode = _nodeOf(PERSONHOOD_LABEL_A); IDotnsRegistry.SubnodeRecord memory subnodeRecord = IDotnsRegistry.SubnodeRecord({ parentNode: parentNode, subLabel: "app", - parentLabel: BASE_LABEL_A, + parentLabel: PERSONHOOD_LABEL_A, owner: tiago, persist: true }); @@ -637,16 +666,16 @@ contract DotnsPopControllerTests is BaseDotns { // A name minted through the public commit-reveal flow already has an owner, so a PoP // reservation over it could never be redeemed. The guard rejects it at reserve time // rather than admitting it and only failing at claim, which would have locked every - // lite name built on that stem for the full reservation window. + // device name built on that stem for the full reservation window. _commitAndRegister("longnamebob", ed, true); - _grantPopFull(tiago); - vm.expectRevert(IDotnsPopController.BaseNameAlreadyRegistered.selector); - _reservePop(tiago, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(tiago); + vm.expectRevert(IDotnsPopController.PersonhoodNameUnavailable.selector); + _reservePop(tiago, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - // The guard runs before the lite mint, so the whole call aborts and no lite name is - // minted for the candidate. - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(LITE_LABEL_A)))); + // The guard runs before the device-name mint, so the whole call aborts and no device name + // is minted for the candidate. + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(DEVICE_LABEL_A)))); // No reservation was recorded, so the stem family stays open to other candidates. (bool reserved,) = dotnsPopController.isReservedForClaim("longnamebob"); @@ -654,8 +683,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_enqueue_becomesHead_writes_popRules_reservation() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); (address holder, uint64 expires) = popRules.getBaseNameReservation("longnamebob"); assertEq(holder, ed); @@ -663,12 +692,12 @@ contract DotnsPopControllerTests is BaseDotns { } function test_claim_releases_popRules_slot() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "longnamebob", user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "longnamebob", user: ed, link: link}) ); (address holder,) = popRules.getBaseNameReservation("longnamebob"); @@ -676,8 +705,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_relinquish_last_releases_popRules_slot() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); vm.prank(ed); dotnsPopController.relinquishReservation(); @@ -687,8 +716,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_advanceExpiredHead_last_expire_releases_popRules_slot() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); dotnsPopController.expireReservation("longnamebob"); @@ -697,44 +726,47 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(holder, address(0)); } - function test_reserveBaseName_reverts_for_digit_suffixed_reserved_base_label() public { - _grantPopFull(ed); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob01"); + function test_issueDeviceNameWithReservation_reverts_for_digit_suffixed_reserved_label() + public + { + _grantPersonhood(ed); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob01"); } - function test_reserveBaseName_reverts_when_reserved_label_already_registered() public { - // George worked example: ed reserves and then claims the base name, which frees the - // stem slot on PopRules. A later lite candidate must not be able to queue a reservation - // over the now-registered name: the queue keys by stem, so an unclaimable reservation - // would hold that stem for the full reservation window and block every lite name built - // on it. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: "longnamebob", user: ed, link: _linkWithLite(LITE_LABEL_A) + function test_issueDeviceNameWithReservation_reverts_when_reserved_label_already_registered() + public + { + // George worked example: ed reserves and then claims the base name, which frees the stem + // slot on PopRules. A later device-name candidate must not be able to queue a reservation + // over the now-registered name: the queue keys by stem, so an unclaimable reservation would + // hold that stem for the full reservation window and block every device name built on it. + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: "longnamebob", user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) }) ); - _grantPopFull(tiago); - vm.expectRevert(IDotnsPopController.BaseNameAlreadyRegistered.selector); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0xbb), "longnamebob"); + _grantPersonhood(tiago); + vm.expectRevert(IDotnsPopController.PersonhoodNameUnavailable.selector); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0xbb), "longnamebob"); } /// @dev Claiming the stem takes the name, so nothing is left for a stranger to register. /// What the claim releases is the reservation slot on PopRules, which is what this /// asserts; a digit-suffixed spelling would prove nothing, being an unrelated name. function test_claim_clears_the_reservation_slot() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); (address beforeClaim,) = popRules.getBaseNameReservation("longnamebob"); assertEq(beforeClaim, ed, "reserved for the claimant"); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "longnamebob", user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "longnamebob", user: ed, link: link}) ); (address afterClaim,) = popRules.getBaseNameReservation("longnamebob"); @@ -743,13 +775,13 @@ contract DotnsPopControllerTests is BaseDotns { } function test_public_stranger_can_mint_after_reservation_expires() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); dotnsPopController.expireReservation("longnamebob"); - _grantPopFull(tiago); + _grantPersonhood(tiago); _commitAndRegister("longnamebob", tiago, true); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob"))), tiago); } @@ -760,12 +792,12 @@ contract DotnsPopControllerTests is BaseDotns { _mockOriginIsRoot(false); vm.prank(address(dotnsRegistrarController)); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.reserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: "" + dotnsPopController.issueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: "" }), - reservedBaseLabel: "longnamebob" + reservedLabel: "longnamebob" }) ); } @@ -806,259 +838,270 @@ contract DotnsPopControllerTests is BaseDotns { assertFalse(reservedAfter); } - function test_registerBaseName_standalone_succeeds_when_head_is_expired() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_standalone_succeeds_when_head_is_expired() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); // Walk past both the controller and PopRules expiry windows so // nothing blocks tiago's priceWithCheck. vm.warp(block.timestamp + popRules.MAX_RESERVATION_TIME() + 1); - _grantPopFull(tiago); + _grantPersonhood(tiago); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: tiago, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: tiago, link: link + }) ); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), tiago); + assertEq( + IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), tiago + ); } - function test_registerBaseName_standalone_succeeds_when_queue_empty() public { - _grantPopFull(ed); + function test_issuePersonhoodName_standalone_succeeds_when_queue_empty() public { + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), ed); } - function test_registerBaseName_claim_path_bypasses_standalone_holder_guard() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_claim_path_bypasses_standalone_holder_guard() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), ed); } - function test_registerBaseName_guard_blocks_stranger_and_preserves_claim() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_guard_blocks_stranger_and_preserves_claim() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - _grantPopFull(tiago); + _grantPersonhood(tiago); IDotnsPopController.Link memory strangerLink = _linkFresh(_validChatKey(0xbb)); vm.expectPartialRevert(IDotnsPopController.NotHolder.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, user: tiago, link: strangerLink + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: tiago, link: strangerLink }) ); // A's reservation is intact; A claims successfully. - IDotnsPopController.Link memory claimLink = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: claimLink}) + IDotnsPopController.Link memory claimLink = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: claimLink + }) ); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), ed); } - function test_registerBaseName_reverts_for_governance_length_name() public { - _grantPopFull(ed); + function test_issuePersonhoodName_reverts_for_governance_length_name() public { + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); // Stem "alice" has baselength 5; classifies as `Reserved for Governance`, // which the PoP controller's governance guard rejects. - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "alice", user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "alice", user: ed, link: link}) ); } - function test_reserveBaseName_reserved_label_classification_reverts() public { - _grantPopFull(ed); + function test_issueDeviceNameWithReservation_reserved_label_classification_reverts() public { + _grantPersonhood(ed); - // Lite leg uses a valid lite label; reserved leg uses a <=5-char stem, + // The issuance uses a valid device name; reserved leg uses a <=5-char stem, // which classifies as `Reserved for Governance` and is rejected by the // PoP controller's governance guard. - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }), - reservedBaseLabel: "alice" + reservedLabel: "alice" }) ); } - function test_registerBaseName_popFull_user_on_popFull_label_succeeds() public { - _grantPopFull(ed); + function test_issuePersonhoodName_personhood_user_on_personhood_label_succeeds() public { + _grantPersonhood(ed); uint256 controllerBalanceBefore = address(dotnsPopController).balance; IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); // No native token moves on the PoP path. assertEq(address(dotnsPopController).balance, controllerBalanceBefore); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), ed); } - function test_reserveLiteName_succeeds_regardless_of_base_reservation() public { - string memory baseStem = BASE_LABEL_A; + function test_issueDeviceName_succeeds_regardless_of_stem_reservation() public { + string memory baseStem = PERSONHOOD_LABEL_A; // Occupy the base stem with a live reservation. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), baseStem); - // A different user calls reserveLiteName on a different lite label. - address fresh = makeAddr("freshLite"); - _grantPopLite(fresh); - - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "stephen.01", user: fresh, chatKey: _validChatKey(0xcc) + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), baseStem); + // A different user calls issueDeviceName on a different device name. + address fresh = makeAddr("freshDevice"); + _grantDevicehood(fresh); + + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "stephen.01", user: fresh, chatKey: _validChatKey(0xcc) }) ); - assertEq(dotnsRegistry.owner(_liteNodeOf("stephen.01")), fresh); + assertEq(dotnsRegistry.owner(_deviceNodeOf("stephen.01")), fresh); } - function test_reserveLiteName_reverts_for_non_lite_format() public { - _grantPopFull(ed); + function test_issueDeviceName_reverts_for_non_device_format() public { + _grantPersonhood(ed); - vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "alice", user: ed, chatKey: _validChatKey(0xaa) + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "alice", user: ed, chatKey: _validChatKey(0xaa) }) ); } - function test_reserveLiteName_reverts_when_suffix_is_not_exactly_two_digits() public { - _grantPopFull(ed); + function test_issueDeviceName_reverts_when_suffix_is_not_exactly_two_digits() public { + _grantPersonhood(ed); - vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "michael.001", user: ed, chatKey: _validChatKey(0xaa) + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "michael.001", user: ed, chatKey: _validChatKey(0xaa) }) ); } - function test_reserveLiteName_reverts_when_the_stem_is_governance_reserved() public { - _grantPopFull(ed); + function test_issueDeviceName_reverts_when_the_stem_is_governance_reserved() public { + _grantPersonhood(ed); - // `abcd.12` is a well-formed lite label whose four-letter stem classifies as Reserved, + // `abcd.12` is a well-formed device name whose four-letter stem classifies as Reserved, // so classification is what rejects it rather than the shape. - vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "abcd.12", user: ed, chatKey: _validChatKey(0xaa) + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "abcd.12", user: ed, chatKey: _validChatKey(0xaa) }) ); } - function test_reserveLiteName_succeeds_for_long_stem() public { - _grantPopLite(ed); + function test_issueDeviceName_succeeds_for_long_stem() public { + _grantDevicehood(ed); // `andrewsays.01` has a stem of 10, which classifies as NoStatus. The gateway may issue - // it as a lite username regardless of stem length. - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "andrewsays.01", user: ed, chatKey: _validChatKey(0xaa) + // it as a device name regardless of stem length. + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "andrewsays.01", user: ed, chatKey: _validChatKey(0xaa) }) ); - assertEq(dotnsRegistry.owner(_liteNodeOf("andrewsays.01")), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf("andrewsays.01")), ed); } - /// @notice A public registration is not an identity and appears in neither listing. - /// @dev Both listings require provenance, so a name the gateway never minted is absent from - /// both however it is spelled. Without that, `joseph42` would read as a full-person - /// identity purely because it is a single label. - function test_lens_omits_a_public_registration_from_both_listings() public { - _grantPopFull(ed); + /// @notice A public registration is not an identity and is not listed. + /// @dev The listing requires provenance, so a name the gateway never minted is absent however + /// it is spelled. Without that, `joseph42` would read as a personhood name purely because + /// it is a single label. + function test_lens_omits_a_public_registration() public { + _grantPersonhood(ed); _commitAndRegister("joseph42", ed, true); assertFalse(dotnsPopController.isPopIssued("joseph42"), "the gateway did not mint it"); - assertEq(dotnsPopLens.fullNamesOf(ed, 0, 10).length, 0, "not a full-person identity"); - assertEq(dotnsPopLens.liteNamesOf(ed, 0, 10).length, 0, "nor a lite one"); - assertEq(dotnsPopLens.fullNameCountOf(ed), 0); - assertEq(dotnsPopLens.liteNameCountOf(ed), 0); + assertEq(dotnsPopLens.namesOf(ed, 0, 10).length, 0, "not an identity"); + assertEq(dotnsPopLens.nameCountOf(ed), 0); } - /// @notice A full-person gateway name lists as full, not lite. - /// @dev `isPopIssued` covers every name the controller mints, lite and full alike, so it - /// cannot say which kind a name is. The separator does that. Keying the lite listing on - /// provenance alone would move every full-person identity into it. - function test_lens_lists_a_full_person_name_as_full() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); + /// @notice Device names and personhood names come back in one listing. + /// @dev `isPopIssued` covers every name the controller mints, of both kinds, and the label + /// shape tells them apart: a device name carries its separator. + function test_lens_lists_device_and_personhood_names_together() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - assertTrue(dotnsPopController.isPopIssued(BASE_LABEL_A), "both kinds carry provenance"); - assertTrue(dotnsPopController.isPopIssued(LITE_LABEL_A), "both kinds carry provenance"); - - IDotnsPopLens.Name[] memory full = dotnsPopLens.fullNamesOf(ed, 0, 10); - assertEq(full.length, 1, "the full-person name is in the full listing"); - assertEq(full[0].label, BASE_LABEL_A); + assertTrue( + dotnsPopController.isPopIssued(PERSONHOOD_LABEL_A), "both kinds carry provenance" + ); + assertTrue(dotnsPopController.isPopIssued(DEVICE_LABEL_A), "both kinds carry provenance"); - IDotnsPopLens.Name[] memory lite = dotnsPopLens.liteNamesOf(ed, 0, 10); - assertEq(lite.length, 1, "and the lite name is in the lite listing"); - assertEq(lite[0].label, LITE_LABEL_A); + IDotnsPopLens.Name[] memory names = dotnsPopLens.namesOf(ed, 0, 10); + assertEq(names.length, 2, "both names are listed"); + assertTrue(_namesContainNode(names, _nodeOf(PERSONHOOD_LABEL_A)), "the personhood name"); + assertTrue(_namesContainNode(names, _deviceNodeOf(DEVICE_LABEL_A)), "and the device name"); + assertEq(dotnsPopLens.nameCountOf(ed), 2); } - /// @notice A full-person name is letters only, the same rule a lite stem follows. + /// @notice A personhood name is letters only, the same rule a device-name stem follows. /// @dev The hyphen and interior-digit cases are the ones a trailing-digit check misses: /// `alice-bob` and `micha3l` are valid DNS labels and would otherwise be issued as - /// identities, while being impossible as lite stems. All three revert with this + /// identities, while being impossible as device-name stems. All three revert with this /// interface's own error. - function test_registerBaseName_rejects_a_label_that_is_not_letters_only() public { + function test_issuePersonhoodName_rejects_a_label_that_is_not_letters_only() public { IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xa1)); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "alice-bob", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "alice-bob", user: ed, link: link}) ); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "micha3l", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "micha3l", user: ed, link: link}) ); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "Joseph", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "Joseph", user: ed, link: link}) ); } - /// @dev The reservation entrypoints share `_validateBaseLabel`, so a hyphen is rejected + /// @dev The reservation entrypoints share `_validatePersonhoodLabel`, so a hyphen is rejected /// there too rather than resolving to an empty queue. function test_reservation_entrypoints_reject_a_label_that_is_not_letters_only() public { - _grantPopFull(ed); + _grantPersonhood(ed); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xa2), "alice-bob"); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xa2), "alice-bob"); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); dotnsPopController.expireReservation("alice-bob"); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); dotnsPopController.isReservedForClaim("micha3l"); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xa3), "Joseph"); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xa3), "Joseph"); } /// @notice The controller answers its own ERC-165 id and nothing else. @@ -1074,16 +1117,16 @@ contract DotnsPopControllerTests is BaseDotns { /// @dev The node is the hash of the whole string, so it is not the node a subname path /// would produce for the same text. Pinning both is what makes "keep the dot" /// concrete rather than cosmetic. - function test_reserveLiteName_stores_the_label_with_its_separator() public { - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "michael.01", user: ed, chatKey: _validChatKey(0xaa) + function test_issueDeviceName_stores_the_label_with_its_separator() public { + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "michael.01", user: ed, chatKey: _validChatKey(0xaa) }) ); // The name is a subname under its numeric container, owned in the registry record. - bytes32 subnamePathNode = _liteNodeOf("michael.01"); + bytes32 subnamePathNode = _deviceNodeOf("michael.01"); assertEq(dotnsRegistry.owner(subnamePathNode), ed); // The whole-label reading is a different node and is never minted as a token. @@ -1093,12 +1136,12 @@ contract DotnsPopControllerTests is BaseDotns { } function test_isPopIssued_is_set_at_mint() public { - _grantPopLite(ed); + _grantDevicehood(ed); assertFalse(dotnsPopController.isPopIssued("michael.01"), "not issued before the mint"); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "michael.01", user: ed, chatKey: _validChatKey(0xaa) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "michael.01", user: ed, chatKey: _validChatKey(0xaa) }) ); @@ -1117,11 +1160,11 @@ contract DotnsPopControllerTests is BaseDotns { /// name stashed as a pending claim must already answer true. function test_isPopIssued_holds_across_cold_path_settlement() public { address cold = makeAddr("coldClaimant"); - _grantPopLite(cold); + _grantDevicehood(cold); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "william.03", user: cold, chatKey: _validChatKey(0xbb) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "william.03", user: cold, chatKey: _validChatKey(0xbb) }) ); @@ -1134,134 +1177,139 @@ contract DotnsPopControllerTests is BaseDotns { assertTrue(dotnsPopController.isPopIssued("william.03"), "still true once settled"); } - /// @dev Provenance covers every name this controller mints, not only the lite ones, because - /// it records who issued the name rather than which tier it sits in. - function test_isPopIssued_covers_full_person_names() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); + /// @dev Provenance covers every name this controller mints, not only the device-name ones, + /// because it records who issued the name rather than which tier it sits in. + function test_isPopIssued_covers_personhood_names() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - assertTrue(dotnsPopController.isPopIssued(BASE_LABEL_A)); + assertTrue(dotnsPopController.isPopIssued(PERSONHOOD_LABEL_A)); } - function test_reserveLiteName_reverts_when_origin_is_not_root() public { + function test_issueDeviceName_reverts_when_origin_is_not_root() public { _mockOriginIsRoot(false); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.reserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + dotnsPopController.issueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); } - function test_reserveBaseName_lite_and_base_legs_both_succeed_in_one_call() public { - _grantPopFull(ed); + function test_issueDeviceNameWithReservation_issuance_and_reservation_both_succeed_in_one_call() + public + { + _grantPersonhood(ed); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }), - reservedBaseLabel: BASE_LABEL_A + reservedLabel: PERSONHOOD_LABEL_A }) ); - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL_A)), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, ed); } - function test_split_gateway_flow_mints_lite_then_reserves_base() public { - _grantPopFull(ed); + function test_split_gateway_flow_issues_device_name_then_reserves_personhood_name() public { + _grantPersonhood(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL_A)), ed); - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(BASE_LABEL_A)))); + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(PERSONHOOD_LABEL_A)))); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) ); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, ed); - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(BASE_LABEL_A)))); + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(PERSONHOOD_LABEL_A)))); } - function test_reserveBaseNameOnly_reverts_when_origin_is_not_root() public { + function test_reservePersonhoodName_reverts_when_origin_is_not_root() public { _mockOriginIsRoot(false); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.reserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) + dotnsPopController.reservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) ); } - function test_reserveBaseNameOnly_reverts_for_reserved_or_suffixed_labels() public { - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: "alice"}) + function test_reservePersonhoodName_reverts_for_reserved_or_suffixed_labels() public { + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: "alice"}) ); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: "longnamebob01"}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: "longnamebob01"}) ); } - function test_reserveBaseNameOnly_reverts_when_label_already_registered() public { + function test_reservePersonhoodName_reverts_when_label_already_registered() public { // The standalone reservation entrypoint shares the same guard: a base name that already // has an owner on the registrar can never be redeemed, so the reservation is rejected up // front rather than discovered to be unusable at claim time. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: "longnamebob", user: ed, link: _linkWithLite(LITE_LABEL_A) + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: "longnamebob", user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) }) ); - vm.expectRevert(IDotnsPopController.BaseNameAlreadyRegistered.selector); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: tiago, reservedBaseLabel: "longnamebob"}) + vm.expectRevert(IDotnsPopController.PersonhoodNameUnavailable.selector); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: "longnamebob"}) ); } - function test_reserveBaseNameOnly_does_not_mint_lite_or_base_name() public { - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) + function test_reservePersonhoodName_does_not_issue_a_name() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) ); - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(LITE_LABEL_A)))); - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(BASE_LABEL_A)))); + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(DEVICE_LABEL_A)))); + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(PERSONHOOD_LABEL_A)))); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, ed); } - function test_reserveBaseNameOnly_same_user_can_replace_prior_reservation() public { - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) + function test_reservePersonhoodName_same_user_can_replace_prior_reservation() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) ); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_B}) + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_B}) ); - (bool firstReserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool firstReserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(firstReserved); - (bool secondReserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_B); + (bool secondReserved, address holder) = + dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_B); assertTrue(secondReserved); assertEq(holder, ed); } @@ -1271,14 +1319,14 @@ contract DotnsPopControllerTests is BaseDotns { // gateway can settle a user's pending claim, deploying the user's store and writing the // stashed label. The settled name lands in the beneficiary's store, and the settlement // event records the third party as the settler. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); - bytes32 labelhash = keccak256(bytes(LITE_LABEL_A)); + bytes32 labelhash = keccak256(bytes(DEVICE_LABEL_A)); address expectedStore = vm.computeCreateAddress(address(storeFactory), vm.getNonce(address(storeFactory))); @@ -1290,17 +1338,17 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertEq(store, expectedStore); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, ".dot") + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, ".dot") ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } function test_user_settles_own_pending_claim_after_gateway_mint() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); @@ -1310,17 +1358,17 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, ".dot") + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, ".dot") ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } function test_claimLabelStore_settles_callers_own_pending_claim() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); @@ -1331,22 +1379,22 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } function test_settle_deploys_store_when_user_has_none() public { - _grantPopFull(ed); + _grantPersonhood(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); - assertEq(dotnsPopController.pendingClaims(ed, 0, 1)[0].label, LITE_LABEL_A); + assertEq(dotnsPopController.pendingClaims(ed, 0, 1)[0].label, DEVICE_LABEL_A); assertEq(storeFactory.getLabelStore(ed), address(0)); (uint256 settledCount, bool moreRemaining) = @@ -1357,40 +1405,40 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, ".dot") + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, ".dot") ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } - function test_registerBaseName_zero_length_label_reverts() public { - _grantPopFull(ed); + function test_issuePersonhoodName_zero_length_label_reverts() public { + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "", user: ed, link: link}) ); } - function test_reserveBaseName_accepts_65_byte_chat_key() public { - _grantPopFull(ed); + function test_issueDeviceNameWithReservation_accepts_65_byte_chat_key() public { + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x42); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }), - reservedBaseLabel: "" + reservedLabel: "" }) ); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); assertEq(dotnsPopResolver.chatKey(node), chatKey); } @@ -1414,16 +1462,16 @@ contract DotnsPopControllerTests is BaseDotns { } function test_gatewayReserve_stashes_pending_claim_when_user_has_no_label_store() public { - _grantPopFull(ed); + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x01); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }) ); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); assertEq(dotnsRegistry.owner(node), ed); assertEq(storeFactory.getLabelStore(ed), address(0)); // Chat key is now persisted eagerly on the resolver at reserve time, even when @@ -1432,17 +1480,17 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); - assertEq(pending[0].label, LITE_LABEL_A); + assertEq(pending[0].label, DEVICE_LABEL_A); assertGt(pending[0].mintedAt, 0); } function test_settle_deploys_store_and_writes_label_and_chat_key() public { - _grantPopFull(ed); + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x07); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }) ); @@ -1452,9 +1500,9 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); assertEq( - ILabelStore(store).getLabel(node), string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(node), string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopResolver.chatKey(node), chatKey); @@ -1462,16 +1510,16 @@ contract DotnsPopControllerTests is BaseDotns { } function test_settle_emits_settled_and_name_registered() public { - _grantPopFull(ed); + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x03); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }) ); - bytes32 labelhash = keccak256(bytes(LITE_LABEL_A)); + bytes32 labelhash = keccak256(bytes(DEVICE_LABEL_A)); address expectedStore = vm.computeCreateAddress(address(storeFactory), vm.getNonce(address(storeFactory))); @@ -1479,7 +1527,7 @@ contract DotnsPopControllerTests is BaseDotns { vm.expectEmit(true, true, false, true, address(dotnsPopController)); emit IDotnsPopController.PendingClaimSettled(ed, labelhash, expectedStore, ed); vm.expectEmit(true, true, true, true, address(dotnsPopController)); - emit IDotnsPopController.NameRegistered(LITE_LABEL_A, labelhash, ed, expectedStore); + emit IDotnsPopController.NameRegistered(DEVICE_LABEL_A, labelhash, ed, expectedStore); dotnsPopController.settlePendingClaims(ed, type(uint256).max); } @@ -1498,20 +1546,20 @@ contract DotnsPopControllerTests is BaseDotns { // A large queue is drained in bounded batches so a single settlement can never exceed the // block gas limit. Settling with a limit below the queue length reports the residue and a // follow-up call clears it. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: ed, chatKey: _validChatKey(0x07) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: ed, chatKey: _validChatKey(0x07) }) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 3); @@ -1530,21 +1578,21 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } - function test_settle_on_expiry_by_third_party_writes_label() public { - // Age never gates settlement: a claim warped past its reservation deadline still settles + function test_settle_after_reservation_duration_by_third_party_writes_label() public { + // Age never gates settlement: a claim warped past `reservationDuration` still settles // in full. A third party drives the settlement, the store is deployed for the beneficiary, // the label is written, the queue empties, the beneficiary leaves the enumeration set, and // the settler is recorded on the event. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x02) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x02) }) ); vm.warp(block.timestamp + DEFAULT_RESERVATION_DURATION + 1); - bytes32 labelhash = keccak256(bytes(LITE_LABEL_A)); + bytes32 labelhash = keccak256(bytes(DEVICE_LABEL_A)); address expectedStore = vm.computeCreateAddress(address(storeFactory), vm.getNonce(address(storeFactory))); @@ -1559,8 +1607,8 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertEq(store, expectedStore); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); @@ -1570,10 +1618,10 @@ contract DotnsPopControllerTests is BaseDotns { // The old model dropped lapsed entries; stores now always settle. Warping past the // reservation duration and settling writes the label into the store rather than // discarding it. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x04) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x04) }) ); @@ -1585,34 +1633,34 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); } - function test_reserveLiteName_piles_second_pending_claim_when_caller_has_no_store() public { + function test_issueDeviceName_piles_second_pending_claim_when_caller_has_no_store() public { // The Root gateway origin cannot deploy a LabelStore, so a store-less user keeps // accumulating deferred names instead of reverting; a single signed-origin // settlement writes them all at once. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) }) ); IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); assertEq(pending.length, 2); - assertEq(pending[0].label, LITE_LABEL_A); - assertEq(pending[1].label, LITE_LABEL_B); + assertEq(pending[0].label, DEVICE_LABEL_A); + assertEq(pending[1].label, DEVICE_LABEL_B); assertEq(dotnsPopController.pendingClaimUserCount(), 1); vm.prank(ed); @@ -1621,12 +1669,12 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_B)), - string.concat(LITE_LABEL_B, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_B)), + string.concat(DEVICE_LABEL_B, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); @@ -1637,34 +1685,34 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } - function test_registerBaseName_claim_by_store_less_full_person_piles_then_settles() public { - // Regression: a store-less full person reserves a lite name plus a base reservation - // (the lite leg stashes a deferred claim because Root cannot deploy the store), then - // claims the base name. The base mint stashes a second deferred claim instead of + function test_issuePersonhoodName_claim_by_store_less_personhood_piles_then_settles() public { + // Regression: a store-less personhood holder reserves a device name plus a base reservation + // (the device-name issuance stashes a deferred claim because Root cannot deploy the store), + // then claims the base name. The base mint stashes a second deferred claim instead of // reverting; one signed-origin settlement deploys the store and settles both. - _grantPopFull(ed); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x31) + _grantPersonhood(ed); + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x31) }), - reservedBaseLabel: BASE_LABEL_A + reservedLabel: PERSONHOOD_LABEL_A }) ); assertEq(storeFactory.getLabelStore(ed), address(0)); assertEq(dotnsPopController.pendingClaimCountOf(ed), 1); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, user: ed, link: _linkWithLite(LITE_LABEL_A) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) }) ); IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); assertEq(pending.length, 2); - assertEq(pending[0].label, LITE_LABEL_A); - assertEq(pending[1].label, BASE_LABEL_A); + assertEq(pending[0].label, DEVICE_LABEL_A); + assertEq(pending[1].label, PERSONHOOD_LABEL_A); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); @@ -1672,34 +1720,34 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq( - ILabelStore(store).getLabel(_nodeOf(BASE_LABEL_A)), - string.concat(BASE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_nodeOf(PERSONHOOD_LABEL_A)), + string.concat(PERSONHOOD_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } function test_pendingClaimUsers_enumeration_mirrors_stash_and_settle() public { - _grantPopFull(ed); - _grantPopFull(tiago); - _grantPopFull(leonardo); + _grantPersonhood(ed); + _grantPersonhood(tiago); + _grantPersonhood(leonardo); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) ); @@ -1723,10 +1771,10 @@ contract DotnsPopControllerTests is BaseDotns { } function test_pendingClaimUsers_returns_empty_when_offset_past_count() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) ); @@ -1734,13 +1782,13 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(empty.length, 0); } - function test_settle_at_exact_expiry_boundary_writes_label() public { - // Age is irrelevant to settlement: at the exact reservation deadline the claim still - // settles and writes its label rather than being treated as forfeit. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x11) + function test_settle_at_exactly_reservation_duration_writes_label() public { + // Age is irrelevant to settlement: exactly `reservationDuration` after the mint the claim + // still settles and writes its label rather than being treated as forfeit. + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x11) }) ); @@ -1753,8 +1801,8 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } @@ -1762,11 +1810,11 @@ contract DotnsPopControllerTests is BaseDotns { function test_settle_is_keyed_by_user_arg_other_stash_untouched() public { // Settlement targets the `user` argument, not the caller: settling for a user with no // stash is a no-op and does not disturb another user's pending claim. - _grantPopFull(ed); + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x12); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }) ); @@ -1778,7 +1826,7 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); - assertEq(pending[0].label, LITE_LABEL_A); + assertEq(pending[0].label, DEVICE_LABEL_A); assertGt(pending[0].mintedAt, 0); assertEq(storeFactory.getLabelStore(ed), address(0)); assertEq(storeFactory.getLabelStore(tiago), address(0)); @@ -1786,22 +1834,22 @@ contract DotnsPopControllerTests is BaseDotns { } function test_pendingClaimUsers_pagination_boundary_cases() public { - _grantPopFull(ed); - _grantPopFull(tiago); - _grantPopFull(leonardo); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + _grantPersonhood(ed); + _grantPersonhood(tiago); + _grantPersonhood(leonardo); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) ); @@ -1815,28 +1863,28 @@ contract DotnsPopControllerTests is BaseDotns { } function test_settle_with_empty_chat_key_skips_resolver_write() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({liteLabel: LITE_LABEL_A, user: ed, chatKey: ""}) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({label: DEVICE_LABEL_A, user: ed, chatKey: ""}) ); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(node), string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(node), string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopResolver.chatKey(node).length, 0); } function test_gatewayReserve_warm_user_after_settle_writes_directly_without_stashing() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x21) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x21) }) ); @@ -1846,15 +1894,15 @@ contract DotnsPopControllerTests is BaseDotns { assertTrue(store != address(0)); bytes memory secondChatKey = _validChatKey(0x22); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: ed, chatKey: secondChatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: ed, chatKey: secondChatKey }) ); - bytes32 node = _liteNodeOf(LITE_LABEL_B); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_B); assertEq( - ILabelStore(store).getLabel(node), string.concat(LITE_LABEL_B, protocolRegistry.tld()) + ILabelStore(store).getLabel(node), string.concat(DEVICE_LABEL_B, protocolRegistry.tld()) ); assertEq(dotnsPopResolver.chatKey(node), secondChatKey); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); @@ -1864,12 +1912,12 @@ contract DotnsPopControllerTests is BaseDotns { function test_advanceExpiredHead_promotes_waiter_and_resyncs_popRules() public { string memory stem = "longnamebob"; uint64 duration = dotnsPopController.reservationDuration(); - _grantPopFull(ed); - _grantPopFull(tiago); + _grantPersonhood(ed); + _grantPersonhood(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), stem); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), stem); vm.warp(block.timestamp + uint256(duration) / 2); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x02), stem); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x02), stem); bytes32 labelhash = keccak256(bytes(stem)); (uint64 head, uint64 tail) = dotnsPopController.reservationMeta(labelhash); @@ -1900,191 +1948,237 @@ contract DotnsPopControllerTests is BaseDotns { } function test_multiWaiter_standaloneGuard_rejects_non_head_user() public { - _grantPopFull(ed); - _grantPopFull(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x02), BASE_LABEL_A); + _grantPersonhood(ed); + _grantPersonhood(tiago); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x02), PERSONHOOD_LABEL_A); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xbb)); vm.expectPartialRevert(IDotnsPopController.NotHolder.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: tiago, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: tiago, link: link + }) ); } - function test_liteNamesOf_and_fullNamesOf_split_issued_names_by_shape() public { - // Settled names read back from the store; a pending gateway name reads from the queue with - // a live deadline; the two shapes never cross into each other's list; an untouched account - // returns empty lists and zero counts. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + function test_namesOf_lists_issued_names_with_settlement_state() public { + // Settled names read back from the store; a pending gateway name reads from the queue; an + // untouched account returns an empty list and a zero count. + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) }) ); - _grantPopFull(leonardo); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) + _grantPersonhood(leonardo); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) ); - IDotnsPopLens.Name[] memory edLite = dotnsPopLens.liteNamesOf(ed, 0, type(uint256).max); - assertEq(edLite.length, 1); - assertEq(edLite[0].node, _liteNodeOf(LITE_LABEL_A)); - assertEq(edLite[0].label, LITE_LABEL_A); - assertTrue(edLite[0].settled); - assertEq(edLite[0].deadline, 0); - - IDotnsPopLens.Name[] memory edFull = dotnsPopLens.fullNamesOf(ed, 0, type(uint256).max); - assertEq(edFull.length, 1); - assertEq(edFull[0].node, _nodeOf(BASE_LABEL_A)); - assertEq(edFull[0].label, BASE_LABEL_A); - assertTrue(edFull[0].settled); - - assertEq(dotnsPopLens.liteNameCountOf(ed), 1); - assertEq(dotnsPopLens.fullNameCountOf(ed), 1); - - IDotnsPopLens.Name[] memory leoLite = - dotnsPopLens.liteNamesOf(leonardo, 0, type(uint256).max); - assertEq(leoLite.length, 1); - assertEq(leoLite[0].node, _liteNodeOf(LITE_LABEL_C)); - assertEq(leoLite[0].label, LITE_LABEL_C); - assertFalse(leoLite[0].settled); - assertGt(leoLite[0].deadline, 0); - assertEq(dotnsPopLens.liteNameCountOf(leonardo), 1); - assertEq(dotnsPopLens.fullNameCountOf(leonardo), 0); - - assertEq(dotnsPopLens.liteNamesOf(tiago, 0, type(uint256).max).length, 0); - assertEq(dotnsPopLens.fullNamesOf(tiago, 0, type(uint256).max).length, 0); - assertEq(dotnsPopLens.liteNameCountOf(tiago), 0); - assertEq(dotnsPopLens.fullNameCountOf(tiago), 0); - } - - function test_liteNamesOf_pagination_slices_and_clamps() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x02) + IDotnsPopLens.Name[] memory edNames = dotnsPopLens.namesOf(ed, 0, type(uint256).max); + assertEq(edNames.length, 2); + IDotnsPopLens.Name memory edDevice = _nameWithNode(edNames, _deviceNodeOf(DEVICE_LABEL_A)); + assertEq(edDevice.label, DEVICE_LABEL_A); + assertTrue(edDevice.settled); + IDotnsPopLens.Name memory edPersonhood = _nameWithNode(edNames, _nodeOf(PERSONHOOD_LABEL_A)); + assertEq(edPersonhood.label, PERSONHOOD_LABEL_A); + assertTrue(edPersonhood.settled); + assertEq(dotnsPopLens.nameCountOf(ed), 2); + + IDotnsPopLens.Name[] memory leoNames = dotnsPopLens.namesOf(leonardo, 0, type(uint256).max); + assertEq(leoNames.length, 1); + assertEq(leoNames[0].node, _deviceNodeOf(DEVICE_LABEL_C)); + assertEq(leoNames[0].label, DEVICE_LABEL_C); + assertFalse(leoNames[0].settled); + assertEq(dotnsPopLens.nameCountOf(leonardo), 1); + + assertEq(dotnsPopLens.namesOf(tiago, 0, type(uint256).max).length, 0); + assertEq(dotnsPopLens.nameCountOf(tiago), 0); + } + + function test_namesOf_pagination_slices_and_clamps() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: ed, chatKey: _validChatKey(0x02) }) ); - assertEq(dotnsPopLens.liteNameCountOf(ed), 2); + assertEq(dotnsPopLens.nameCountOf(ed), 2); - IDotnsPopLens.Name[] memory first = dotnsPopLens.liteNamesOf(ed, 0, 1); + IDotnsPopLens.Name[] memory first = dotnsPopLens.namesOf(ed, 0, 1); assertEq(first.length, 1); - IDotnsPopLens.Name[] memory second = dotnsPopLens.liteNamesOf(ed, 1, 1); + IDotnsPopLens.Name[] memory second = dotnsPopLens.namesOf(ed, 1, 1); assertEq(second.length, 1); assertTrue(first[0].node != second[0].node); - assertEq(dotnsPopLens.liteNamesOf(ed, 2, 1).length, 0); + assertEq(dotnsPopLens.namesOf(ed, 2, 1).length, 0); // A limit above the internal page ceiling is clamped rather than reverting; the account // holds fewer names than the ceiling, so the full set still comes back. IDotnsPopLens.Name[] memory clamped = - dotnsPopLens.liteNamesOf(ed, 0, DotnsConstants.MAX_PAGE_SIZE + 1); + dotnsPopLens.namesOf(ed, 0, DotnsConstants.MAX_PAGE_SIZE + 1); assertEq(clamped.length, 2); } function test_name_listings_exclude_names_owned_by_others() public { // The listing re-checks registrar ownership per entry, so a name owned by another account // never surfaces in this account's list. - _grantPopFull(ed); - _grantPopFull(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - _reservePop(tiago, LITE_LABEL_C, _validChatKey(0x02), ""); + _grantPersonhood(ed); + _grantPersonhood(tiago); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); + _reservePop(tiago, DEVICE_LABEL_C, _validChatKey(0x02), ""); - IDotnsPopLens.Name[] memory edLite = dotnsPopLens.liteNamesOf(ed, 0, type(uint256).max); - assertEq(edLite.length, 1); - assertFalse(_namesContainNode(edLite, _liteNodeOf(LITE_LABEL_C))); + IDotnsPopLens.Name[] memory edDevice = dotnsPopLens.namesOf(ed, 0, type(uint256).max); + assertEq(edDevice.length, 1); + assertFalse(_namesContainNode(edDevice, _deviceNodeOf(DEVICE_LABEL_C))); - IDotnsPopLens.Name[] memory tiagoLite = - dotnsPopLens.liteNamesOf(tiago, 0, type(uint256).max); - assertEq(tiagoLite.length, 1); - assertFalse(_namesContainNode(tiagoLite, _liteNodeOf(LITE_LABEL_A))); + IDotnsPopLens.Name[] memory tiagoDevice = dotnsPopLens.namesOf(tiago, 0, type(uint256).max); + assertEq(tiagoDevice.length, 1); + assertFalse(_namesContainNode(tiagoDevice, _deviceNodeOf(DEVICE_LABEL_A))); } function test_nameDetail_and_nameDetailByNode_report_record() public { - _grantPopFull(ed); - bytes memory liteChatKey = _validChatKey(0xaa); - _reservePop(ed, LITE_LABEL_A, liteChatKey, BASE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, user: ed, link: _linkWithLite(LITE_LABEL_A) - }) - ); - - bytes32 fullNode = _nodeOf(BASE_LABEL_A); - IDotnsPopLens.NameDetail memory full = dotnsPopLens.nameDetail(BASE_LABEL_A); - assertEq(full.node, fullNode); - assertEq(full.label, BASE_LABEL_A); - assertEq(full.owner, ed); - assertTrue(full.exists); - assertTrue(full.settled); - assertTrue(full.tier == IPopRules.PopStatus.PopFull); - assertEq(full.chatKey, liteChatKey); - assertEq(full.liteLink, keccak256(bytes(LITE_LABEL_A))); - // A base label is never a lite labelhash, so no promoted node is keyed under it. - assertEq(full.fullClaim, bytes32(0)); - - // Holding the lite label lets nameDetail recover the promoted full node. - IDotnsPopLens.NameDetail memory lite = dotnsPopLens.nameDetail(LITE_LABEL_A); - assertEq(lite.fullClaim, fullNode); - - // A settled lite name that was never promoted carries no full claim, and the by-node - // overload leaves it zero. - _grantPopFull(leonardo); - _reservePop(leonardo, LITE_LABEL_C, _validChatKey(0xbb), ""); + _grantPersonhood(ed); + bytes memory deviceChatKey = _validChatKey(0xaa); + _reservePop(ed, DEVICE_LABEL_A, deviceChatKey, PERSONHOOD_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) + }) + ); + + bytes32 personhoodNode = _nodeOf(PERSONHOOD_LABEL_A); + IDotnsPopLens.NameDetail memory personhood = dotnsPopLens.nameDetail(PERSONHOOD_LABEL_A); + assertEq(personhood.node, personhoodNode); + assertEq(personhood.label, PERSONHOOD_LABEL_A); + assertEq(personhood.owner, ed); + assertTrue(personhood.exists); + assertTrue(personhood.settled); + assertTrue(personhood.requiredTier == IPopRules.PopStatus.Personhood); + assertEq(personhood.chatKey, deviceChatKey); + assertEq(personhood.deviceLabelhash, keccak256(bytes(DEVICE_LABEL_A))); + // A base label is never a device-name labelhash, so no promoted node is keyed under it. + assertEq(personhood.personhoodNode, bytes32(0)); + + // Holding the device name lets nameDetail recover the promoted personhood node. + IDotnsPopLens.NameDetail memory device = dotnsPopLens.nameDetail(DEVICE_LABEL_A); + assertEq(device.personhoodNode, personhoodNode); + + // A settled device name that was never promoted carries no personhood claim, and the + // by-node overload leaves it zero. + _grantPersonhood(leonardo); + _reservePop(leonardo, DEVICE_LABEL_C, _validChatKey(0xbb), ""); IDotnsPopLens.NameDetail memory coldByNode = - dotnsPopLens.nameDetailByNode(_liteNodeOf(LITE_LABEL_C)); + dotnsPopLens.nameDetailByNode(_deviceNodeOf(DEVICE_LABEL_C)); assertTrue(coldByNode.exists); - assertEq(coldByNode.fullClaim, bytes32(0)); + assertEq(coldByNode.personhoodNode, bytes32(0)); // Unknown name and node never revert and return a zeroed record. IDotnsPopLens.NameDetail memory unknownName = dotnsPopLens.nameDetail("nothingxx"); assertFalse(unknownName.exists); assertEq(unknownName.owner, address(0)); assertEq(bytes(unknownName.label).length, 0); - assertEq(unknownName.fullClaim, bytes32(0)); + assertEq(unknownName.personhoodNode, bytes32(0)); IDotnsPopLens.NameDetail memory unknownNode = dotnsPopLens.nameDetailByNode(bytes32(uint256(0xdead))); assertFalse(unknownNode.exists); assertEq(unknownNode.owner, address(0)); - assertEq(unknownNode.fullClaim, bytes32(0)); + assertEq(unknownNode.personhoodNode, bytes32(0)); } - /// @notice `nameDetail` classifies a cold-path lite name before it settles, not only after. + /// @notice `nameDetail` classifies a cold-path device name before it settles, not only after. /// @dev A pending subname has no label recoverable from its node, so `nameDetail` classifies /// the caller-supplied label. Before this was wired the tier read `NoStatus` until settlement /// wrote the label into the store. - function test_nameDetail_classifies_a_cold_lite_name_before_and_after_settlement() public { - address fresh = makeAddr("coldlite"); - _grantPopLite(fresh); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: fresh, chatKey: _validChatKey(0x01) + function test_nameDetail_classifies_a_cold_device_name_before_and_after_settlement() public { + address fresh = makeAddr("colddevice"); + _grantDevicehood(fresh); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: fresh, chatKey: _validChatKey(0x01) }) ); // Cold path: the claim is staged and no store is deployed yet. assertEq(storeFactory.getLabelStore(fresh), address(0), "cold user has no store yet"); - IDotnsPopLens.NameDetail memory pending = dotnsPopLens.nameDetail(LITE_LABEL_A); + IDotnsPopLens.NameDetail memory pending = dotnsPopLens.nameDetail(DEVICE_LABEL_A); assertTrue(pending.exists, "subname owned before settlement"); assertFalse(pending.settled, "not settled yet"); - assertEq(pending.label, LITE_LABEL_A, "caller label supplied before settlement"); - assertTrue(pending.tier == IPopRules.PopStatus.PopLite, "classified as lite before settle"); + assertEq(pending.label, DEVICE_LABEL_A, "caller label supplied before settlement"); + assertTrue( + pending.requiredTier == IPopRules.PopStatus.Devicehood, + "classified as devicehood before settle" + ); vm.prank(fresh); dotnsPopController.settlePendingClaims(fresh, type(uint256).max); - IDotnsPopLens.NameDetail memory settled = dotnsPopLens.nameDetail(LITE_LABEL_A); + IDotnsPopLens.NameDetail memory settled = dotnsPopLens.nameDetail(DEVICE_LABEL_A); + assertTrue(settled.settled, "settled after draining the queue"); + assertEq(settled.label, DEVICE_LABEL_A, "label recovered from the store after settlement"); + assertTrue( + settled.requiredTier == IPopRules.PopStatus.Devicehood, + "still devicehood after settlement" + ); + } + + /// @notice `nameDetail` reports an unsettled personhood name's label and tier. + /// @dev The registrar reads a tokenised name's label from the holder's `LabelStore`, which a + /// store-less holder does not have yet, so the lens falls back to the caller's label. + function test_nameDetail_reports_an_unsettled_personhood_name() public { + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh("") + }) + ); + assertEq(dotnsPopController.pendingClaimCountOf(ed), 1, "the claim is pending"); + + IDotnsPopLens.NameDetail memory detail = dotnsPopLens.nameDetail(PERSONHOOD_LABEL_A); + assertTrue(detail.exists, "minted before settlement"); + assertFalse(detail.settled, "not settled yet"); + assertEq(detail.label, PERSONHOOD_LABEL_A, "caller label supplied before settlement"); + assertTrue( + detail.requiredTier == IPopRules.PopStatus.Personhood, + "classified as personhood before settlement" + ); + } + + /// @notice `nameDetailByNode` leaves an unsettled personhood name's label empty until + /// settlement makes it recoverable from the node. + function test_nameDetailByNode_recovers_a_personhood_label_only_after_settlement() public { + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh("") + }) + ); + bytes32 node = _nodeOf(PERSONHOOD_LABEL_A); + + IDotnsPopLens.NameDetail memory pending = dotnsPopLens.nameDetailByNode(node); + assertTrue(pending.exists, "minted before settlement"); + assertFalse(pending.settled, "not settled yet"); + assertEq(pending.label, "", "no label recoverable from the node before settlement"); + assertTrue( + pending.requiredTier == IPopRules.PopStatus.NoStatus, "unclassified without a label" + ); + + vm.prank(ed); + dotnsPopController.settlePendingClaims(ed, type(uint256).max); + + IDotnsPopLens.NameDetail memory settled = dotnsPopLens.nameDetailByNode(node); assertTrue(settled.settled, "settled after draining the queue"); - assertEq(settled.label, LITE_LABEL_A, "label recovered from the store after settlement"); - assertTrue(settled.tier == IPopRules.PopStatus.PopLite, "still lite after settlement"); + assertEq(settled.label, PERSONHOOD_LABEL_A, "label recovered after settlement"); + assertTrue( + settled.requiredTier == IPopRules.PopStatus.Personhood, + "classified as personhood after settlement" + ); } /// @notice A store row whose node key is not its own text's node is neither counted nor listed. @@ -2093,14 +2187,14 @@ contract DotnsPopControllerTests is BaseDotns { /// are /// derived together), so this injects one directly to pin the guard. function test_lens_ignores_a_store_row_whose_node_does_not_match_its_text() public { - _grantPopFull(ed); - _grantPopFull(leonardo); + _grantPersonhood(ed); + _grantPersonhood(leonardo); - // ed holds a legitimate lite name, which deploys ed's store and counts once. - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - assertEq(dotnsPopLens.liteNameCountOf(ed), 1, "one legitimate lite name"); + // ed holds a legitimate device name, which deploys ed's store and counts once. + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); + assertEq(dotnsPopLens.nameCountOf(ed), 1, "one legitimate device name"); - // A second lite name, issued to leonardo, so its text reads as PoP-issued. + // A second device name, issued to leonardo, so its text reads as PoP-issued. _reservePop(leonardo, "another.01", _validChatKey(0x02), ""); // ed owns a storeless subname beneath the numeric container. Only a registered controller @@ -2127,19 +2221,23 @@ contract DotnsPopControllerTests is BaseDotns { // The forged row is owned by ed and its text is PoP-issued, but its node does not match the // text, so the listing excludes it and the count stays one. - assertEq(dotnsPopLens.liteNameCountOf(ed), 1, "forged row excluded from the count"); - IDotnsPopLens.Name[] memory lite = dotnsPopLens.liteNamesOf(ed, 0, type(uint256).max); - assertEq(lite.length, 1, "forged row excluded from the listing"); - assertEq(lite[0].node, _liteNodeOf(LITE_LABEL_A), "only the legitimate lite name is listed"); + assertEq(dotnsPopLens.nameCountOf(ed), 1, "forged row excluded from the count"); + IDotnsPopLens.Name[] memory names = dotnsPopLens.namesOf(ed, 0, type(uint256).max); + assertEq(names.length, 1, "forged row excluded from the listing"); + assertEq( + names[0].node, + _deviceNodeOf(DEVICE_LABEL_A), + "only the legitimate device name is listed" + ); } function test_profileOf_reports_store_pending_and_reservation() public { // A store-less user with a staged claim, a settled user holding a reservation, and an // untouched account each report distinct profile facts. - _grantPopFull(leonardo); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x01) + _grantPersonhood(leonardo); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x01) }) ); IDotnsPopLens.PopProfile memory cold = dotnsPopLens.profileOf(leonardo); @@ -2147,12 +2245,12 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(cold.pendingClaimCount, 1); assertEq(cold.reservationLabelhash, bytes32(0)); - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x02), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x02), PERSONHOOD_LABEL_A); IDotnsPopLens.PopProfile memory warm = dotnsPopLens.profileOf(ed); assertTrue(warm.hasLabelStore); assertEq(warm.pendingClaimCount, 0); - assertEq(warm.reservationLabelhash, keccak256(bytes(BASE_LABEL_A))); + assertEq(warm.reservationLabelhash, keccak256(bytes(PERSONHOOD_LABEL_A))); IDotnsPopLens.PopProfile memory empty = dotnsPopLens.profileOf(tiago); assertFalse(empty.hasLabelStore); @@ -2161,17 +2259,32 @@ contract DotnsPopControllerTests is BaseDotns { } function test_reservedBaseLabelOf_returns_label_or_empty() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); assertEq( - dotnsPopController.reservedBaseLabelOf(keccak256(bytes(BASE_LABEL_A))), BASE_LABEL_A + dotnsPopController.reservedLabelOf(keccak256(bytes(PERSONHOOD_LABEL_A))), + PERSONHOOD_LABEL_A ); assertEq( - bytes(dotnsPopController.reservedBaseLabelOf(keccak256(bytes("unknownbase")))).length, 0 + bytes(dotnsPopController.reservedLabelOf(keccak256(bytes("unknownbase")))).length, 0 ); } + function _nameWithNode( + IDotnsPopLens.Name[] memory names, + bytes32 node + ) + internal + pure + returns (IDotnsPopLens.Name memory) + { + for (uint256 i; i < names.length; ++i) { + if (names[i].node == node) return names[i]; + } + revert("no name with that node"); + } + function _namesContainNode( IDotnsPopLens.Name[] memory names, bytes32 node diff --git a/test/unit/registrar/DotnsPopControllerLegacy.t.sol b/test/unit/registrar/DotnsPopControllerLegacy.t.sol new file mode 100644 index 000000000..de5406c18 --- /dev/null +++ b/test/unit/registrar/DotnsPopControllerLegacy.t.sol @@ -0,0 +1,654 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +import {BaseDotns} from "../../base/BaseDotns.t.sol"; +import {IDotnsPopController} from "../../../contracts/registrars/IDotnsPopController.sol"; +import { + IDotnsPopControllerLegacy +} from "../../../contracts/registrars/IDotnsPopControllerLegacy.sol"; +import {Vm} from "forge-std/Vm.sol"; + +/// @title DotnsPopControllerLegacyTests +/// @notice The legacy entrypoints, and the reservation-queue events. +/// @dev The legacy selectors and error selectors are pinned as literals, because the legacy +/// interface exists to keep them unchanged. Each legacy entrypoint is checked against its +/// replacement from the same state: identical events and state on success, identical revert +/// data on every shared error. Every `InvalidLiteLabel` / `InvalidBaseLabel` revert reachable +/// from a legacy entrypoint is exercised from both the legacy entrypoint and its replacement, +/// which reverts with the new error instead. The shape check inside `_validateDeviceLabel` is +/// preceded by the same check at each of its callers, so it is reached through them and has no +/// test of its own. +contract DotnsPopControllerLegacyTests is BaseDotns { + // Selectors and interfaces + + function test_legacy_selectors_are_pinned() public pure { + assertEq( + IDotnsPopControllerLegacy.reserveLiteName.selector, SELECTOR_LEGACY_RESERVE_LITE_NAME + ); + assertEq( + IDotnsPopControllerLegacy.reserveBaseName.selector, SELECTOR_LEGACY_RESERVE_BASE_NAME + ); + assertEq( + IDotnsPopControllerLegacy.registerBaseName.selector, SELECTOR_LEGACY_REGISTER_BASE_NAME + ); + } + + function test_legacy_error_selectors_are_pinned() public pure { + assertEq(IDotnsPopControllerLegacy.InvalidLiteLabel.selector, bytes4(0x5c6c2eea)); + assertEq(IDotnsPopControllerLegacy.InvalidBaseLabel.selector, bytes4(0x39e67d31)); + } + + function test_new_selectors_differ_from_the_legacy_ones() public pure { + assertTrue( + IDotnsPopController.issueDeviceName.selector != SELECTOR_LEGACY_RESERVE_LITE_NAME + ); + assertTrue( + IDotnsPopController.issueDeviceNameWithReservation.selector + != SELECTOR_LEGACY_RESERVE_BASE_NAME + ); + assertTrue( + IDotnsPopController.issuePersonhoodName.selector != SELECTOR_LEGACY_REGISTER_BASE_NAME + ); + } + + function test_supports_both_interfaces() public view { + assertTrue( + dotnsPopController.supportsInterface(type(IDotnsPopController).interfaceId), + "current interface" + ); + assertTrue( + dotnsPopController.supportsInterface(type(IDotnsPopControllerLegacy).interfaceId), + "legacy interface" + ); + } + + // Legacy entrypoints have the same effect as their replacements + + function test_reserveLiteName_matches_issueDeviceName() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) + ); + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + }); + + _assertSameEffect( + abi.encodeCall(IDotnsPopControllerLegacy.reserveLiteName, (issuance)), + abi.encodeCall(IDotnsPopController.issueDeviceName, (issuance)), + ed, + DEVICE_LABEL_A, + PERSONHOOD_LABEL_A + ); + } + + function test_reserveBaseName_matches_issueDeviceNameWithReservation() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: PERSONHOOD_LABEL_A}) + ); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_B}) + ); + IDotnsPopController.DeviceNameIssuanceWithReservation memory params = + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + }), + reservedLabel: PERSONHOOD_LABEL_A + }); + + _assertSameEffect( + abi.encodeCall(IDotnsPopControllerLegacy.reserveBaseName, (params)), + abi.encodeCall(IDotnsPopController.issueDeviceNameWithReservation, (params)), + ed, + DEVICE_LABEL_A, + PERSONHOOD_LABEL_A + ); + } + + function test_registerBaseName_matches_issuePersonhoodName_on_a_linked_claim() public { + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: PERSONHOOD_LABEL_A}) + ); + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) + }); + + _assertSameEffect( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (params)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (params)), + ed, + DEVICE_LABEL_A, + PERSONHOOD_LABEL_A + ); + } + + function test_registerBaseName_matches_issuePersonhoodName_without_a_reservation() public { + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + }); + + _assertSameEffect( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (params)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (params)), + ed, + DEVICE_LABEL_A, + PERSONHOOD_LABEL_A + ); + } + + function test_legacy_reserveLiteName_issues_a_device_name() public { + vm.recordLogs(); + _rootLegacyReserveLiteName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); + assertTrue(dotnsPopController.isPopIssued(DEVICE_LABEL_A)); + assertEq(dotnsPopResolver.chatKey(_deviceNodeOf(DEVICE_LABEL_A)), _validChatKey(0x01)); + assertEq(_countEvents(logs, keccak256("DeviceNameIssued(bytes32,address,string)")), 1); + } + + function test_legacy_reserveBaseName_issues_and_reserves() public { + _rootLegacyReserveBaseName( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: "" + }), + reservedLabel: PERSONHOOD_LABEL_A + }) + ); + + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); + assertEq( + dotnsPopController.userReservation(ed).labelhash, + keccak256(bytes(PERSONHOOD_LABEL_A)), + "the reservation is queued" + ); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); + assertTrue(reserved); + assertEq(holder, ed); + } + + function test_legacy_registerBaseName_issues_a_personhood_name() public { + vm.recordLogs(); + _rootLegacyRegisterBaseName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + assertEq(dotnsRegistry.owner(_nodeOf(PERSONHOOD_LABEL_A)), ed); + assertTrue(dotnsPopController.isPopIssued(PERSONHOOD_LABEL_A)); + assertEq(_countEvents(logs, keccak256("PersonhoodNameIssued(bytes32,address,string)")), 1); + } + + // Label errors: legacy entrypoints revert with the legacy errors, replacements with the new + // ones + + /// @notice `_issueDeviceName`, shape check: a label with no separator. + function test_device_name_shape_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({label: "michael", user: ed, chatKey: ""}); + + vm.expectRevert(IDotnsPopControllerLegacy.InvalidLiteLabel.selector); + _rootLegacyReserveLiteName(issuance); + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName(issuance); + + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: issuance, reservedLabel: "" + }); + vm.expectRevert(IDotnsPopControllerLegacy.InvalidLiteLabel.selector); + _rootLegacyReserveBaseName(combined); + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceNameWithReservation(combined); + } + + /// @notice `_issueDeviceName`, classification check: a stem short enough to be reserved. + function test_device_name_reserved_stem_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({label: "abc.01", user: ed, chatKey: ""}); + + vm.expectRevert(IDotnsPopControllerLegacy.InvalidLiteLabel.selector); + _rootLegacyReserveLiteName(issuance); + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName(issuance); + } + + /// @notice `_issuePersonhoodName`, classification check: a governance-reserved length. + function test_personhood_name_reserved_length_error_on_both_paths() public { + IDotnsPopController.PersonhoodNameIssuance memory issuance = + IDotnsPopController.PersonhoodNameIssuance({ + label: "abcde", user: ed, link: _linkFresh("") + }); + + vm.expectRevert(IDotnsPopControllerLegacy.InvalidBaseLabel.selector); + _rootLegacyRegisterBaseName(issuance); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName(issuance); + } + + /// @notice `_issuePersonhoodName`, device-link shape check. + function test_device_link_shape_error_on_both_paths() public { + IDotnsPopController.PersonhoodNameIssuance memory issuance = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName("michael") + }); + + vm.expectRevert(IDotnsPopControllerLegacy.InvalidLiteLabel.selector); + _rootLegacyRegisterBaseName(issuance); + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssuePersonhoodName(issuance); + } + + /// @notice `_validatePersonhoodLabel`, shape check, from the personhood issuance and from the + /// reservation of the combined entrypoint. + function test_personhood_name_shape_error_on_both_paths() public { + IDotnsPopController.PersonhoodNameIssuance memory issuance = + IDotnsPopController.PersonhoodNameIssuance({ + label: "alice1", user: ed, link: _linkFresh("") + }); + vm.expectRevert(IDotnsPopControllerLegacy.InvalidBaseLabel.selector); + _rootLegacyRegisterBaseName(issuance); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName(issuance); + + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, "alice1"); + vm.expectRevert(IDotnsPopControllerLegacy.InvalidBaseLabel.selector); + _rootLegacyReserveBaseName(combined); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssueDeviceNameWithReservation(combined); + } + + /// @notice `_validatePersonhoodLabel` from the callers that are not legacy entrypoints: always + /// the new error. + function test_personhood_name_shape_error_on_non_legacy_callers() public { + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + dotnsPopController.expireReservation("alice1"); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + dotnsPopController.isReservedForClaim("alice1"); + } + + /// @notice `_validateReservablePersonhoodLabel`, classification check: a reserved length. + function test_reservable_label_classification_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, "abcde"); + vm.expectRevert(IDotnsPopControllerLegacy.InvalidBaseLabel.selector); + _rootLegacyReserveBaseName(combined); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssueDeviceNameWithReservation(combined); + + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: "abcde"}) + ); + } + + // Errors the legacy entrypoints share with their replacements + + function test_non_root_callers_are_rejected_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({label: DEVICE_LABEL_A, user: ed, chatKey: ""}); + bytes memory expected = abi.encodeWithSelector(IDotnsPopController.NotRoot.selector); + + vm.expectRevert(expected); + IDotnsPopControllerLegacy(address(dotnsPopController)).reserveLiteName(issuance); + vm.expectRevert(expected); + dotnsPopController.issueDeviceName(issuance); + } + + function test_invalid_chat_key_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: hex"01" + }); + bytes memory expected = + abi.encodeWithSelector(IDotnsPopController.InvalidChatKey.selector, uint256(1)); + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveLiteName, (issuance)), + abi.encodeCall(IDotnsPopController.issueDeviceName, (issuance)), + expected + ); + + IDotnsPopController.PersonhoodNameIssuance memory personhood = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(hex"01") + }); + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (personhood)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (personhood)), + expected + ); + } + + function test_second_device_name_issuance_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({label: DEVICE_LABEL_A, user: ed, chatKey: ""}); + _rootIssueDeviceName(issuance); + + bytes memory expected = + abi.encodeWithSelector(IDotnsPopController.DeviceNameAlreadyIssued.selector); + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveLiteName, (issuance)), + abi.encodeCall(IDotnsPopController.issueDeviceName, (issuance)), + expected + ); + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, PERSONHOOD_LABEL_A); + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveBaseName, (combined)), + abi.encodeCall(IDotnsPopController.issueDeviceNameWithReservation, (combined)), + expected + ); + } + + function test_device_link_the_user_does_not_own_error_on_both_paths() public { + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: tiago, chatKey: "" + }) + ); + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) + }); + + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (params)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (params)), + abi.encodeWithSelector( + IDotnsPopController.DeviceNameNotOwned.selector, + ed, + keccak256(bytes(DEVICE_LABEL_A)) + ) + ); + } + + function test_registered_reserved_label_error_on_both_paths() public { + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: tiago, link: _linkFresh("") + }) + ); + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, PERSONHOOD_LABEL_A); + + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveBaseName, (combined)), + abi.encodeCall(IDotnsPopController.issueDeviceNameWithReservation, (combined)), + abi.encodeWithSelector(IDotnsPopController.PersonhoodNameUnavailable.selector) + ); + } + + function test_label_held_by_another_waiter_error_on_both_paths() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: PERSONHOOD_LABEL_A}) + ); + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh("") + }); + + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (params)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (params)), + abi.encodeWithSelector( + IDotnsPopController.NotHolder.selector, ed, keccak256(bytes(PERSONHOOD_LABEL_A)) + ) + ); + } + + function test_full_queue_error_on_both_paths() public { + for (uint256 i; i < dotnsPopController.MAX_RESERVATION_QUEUE(); ++i) { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({ + user: makeAddr(string.concat("waiter", vm.toString(i))), + label: PERSONHOOD_LABEL_A + }) + ); + } + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, PERSONHOOD_LABEL_A); + + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveBaseName, (combined)), + abi.encodeCall(IDotnsPopController.issueDeviceNameWithReservation, (combined)), + abi.encodeWithSelector( + IDotnsPopController.QueueFull.selector, keccak256(bytes(PERSONHOOD_LABEL_A)) + ) + ); + } + + // Every reservation-queue exit emits an event + + function test_claim_emits_claimed_and_evicts_every_other_waiter() public { + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: PERSONHOOD_LABEL_A}) + ); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({ + user: leonardo, label: PERSONHOOD_LABEL_A + }) + ); + + vm.recordLogs(); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + bytes32 claimed = keccak256("ReservationClaimed(bytes32,address)"); + bytes32 evicted = keccak256("ReservationEvicted(bytes32,address)"); + assertEq(_countEventsFor(logs, claimed, ed), 1, "the claimant's claim"); + assertEq(_countEventsFor(logs, evicted, tiago), 1, "first waiter evicted"); + assertEq(_countEventsFor(logs, evicted, leonardo), 1, "second waiter evicted"); + assertEq(_countEventsFor(logs, evicted, ed), 0, "the claimant is not evicted"); + assertEq(dotnsPopController.userReservation(tiago).labelhash, bytes32(0)); + assertEq(dotnsPopController.userReservation(leonardo).labelhash, bytes32(0)); + } + + function test_standalone_issuance_relinquishes_the_users_other_reservation() public { + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + + vm.recordLogs(); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_B, user: ed, link: _linkFresh(_validChatKey(0x02)) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + bytes32 relinquished = keccak256("ReservationRelinquished(bytes32,address)"); + assertEq(_countEventsFor(logs, relinquished, ed), 1, "the dropped entry is recorded"); + assertEq(_countEvents(logs, keccak256("ReservationClaimed(bytes32,address)")), 0); + assertEq(dotnsPopController.userReservation(ed).labelhash, bytes32(0)); + } + + function test_re_reservation_relinquishes_the_previous_entry() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) + ); + + vm.recordLogs(); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_B}) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + bytes32 relinquished = keccak256("ReservationRelinquished(bytes32,address)"); + assertEq(_countEventsFor(logs, relinquished, ed), 1); + assertEq( + dotnsPopController.userReservation(ed).labelhash, keccak256(bytes(PERSONHOOD_LABEL_B)) + ); + } + + function test_relinquishReservation_emits_exactly_once() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) + ); + + vm.recordLogs(); + vm.prank(ed); + dotnsPopController.relinquishReservation(); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + assertEq(_countEvents(logs, keccak256("ReservationRelinquished(bytes32,address)")), 1); + } + + function test_issuance_without_a_reservation_emits_no_relinquish() public { + vm.recordLogs(); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + assertEq(_countEvents(logs, keccak256("ReservationRelinquished(bytes32,address)")), 0); + } + + // Helpers + + function _combined( + string memory deviceLabel, + string memory reservedLabel + ) + internal + view + returns (IDotnsPopController.DeviceNameIssuanceWithReservation memory) + { + return IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: deviceLabel, user: ed, chatKey: "" + }), + reservedLabel: reservedLabel + }); + } + + /// @dev Runs `legacyPayload` and `replacementPayload` from the same state and requires the same + /// events, in order and byte for byte, and the same observable state afterwards. + function _assertSameEffect( + bytes memory legacyPayload, + bytes memory replacementPayload, + address user, + string memory deviceLabel, + string memory personhoodLabel + ) + internal + { + uint256 snapshot = vm.snapshotState(); + vm.recordLogs(); + _dispatchFromRoot(legacyPayload); + Vm.Log[] memory legacyLogs = vm.getRecordedLogs(); + bytes memory legacyState = _observableState(user, deviceLabel, personhoodLabel); + + vm.revertToState(snapshot); + vm.recordLogs(); + _dispatchFromRoot(replacementPayload); + Vm.Log[] memory replacementLogs = vm.getRecordedLogs(); + + assertGt(legacyLogs.length, 0, "the call emits events"); + assertEq(legacyLogs.length, replacementLogs.length, "same number of events"); + for (uint256 i; i < legacyLogs.length; ++i) { + assertEq(legacyLogs[i].emitter, replacementLogs[i].emitter, "same emitter"); + assertEq( + abi.encode(legacyLogs[i].topics), + abi.encode(replacementLogs[i].topics), + "same topics" + ); + assertEq(legacyLogs[i].data, replacementLogs[i].data, "same data"); + } + assertEq(_observableState(user, deviceLabel, personhoodLabel), legacyState, "same state"); + } + + /// @dev Records, reservation and pending claims the gateway entrypoints write for `user`. + function _observableState( + address user, + string memory deviceLabel, + string memory personhoodLabel + ) + internal + view + returns (bytes memory) + { + bytes32 deviceNode = _deviceNodeOf(deviceLabel); + bytes32 personhoodNode = _nodeOf(personhoodLabel); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(personhoodLabel); + return bytes.concat( + abi.encode( + dotnsRegistry.owner(deviceNode), + dotnsRegistry.owner(personhoodNode), + dotnsPopController.isPopIssued(deviceLabel), + dotnsPopController.isPopIssued(personhoodLabel), + dotnsPopResolver.chatKey(deviceNode), + dotnsPopResolver.chatKey(personhoodNode) + ), + abi.encode( + dotnsPopResolver.deviceLabelhashOf(personhoodNode), + dotnsPopResolver.personhoodNodeOf(keccak256(bytes(deviceLabel))), + dotnsPopController.userReservation(user), + reserved, + holder, + dotnsPopController.pendingClaimCountOf(user) + ) + ); + } + + /// @dev Requires both payloads to revert under Root with exactly `expected`. + function _assertBothRevertWith( + bytes memory legacyPayload, + bytes memory replacementPayload, + bytes memory expected + ) + internal + { + assertEq(_rootRevertData(legacyPayload), expected, "legacy entrypoint"); + assertEq(_rootRevertData(replacementPayload), expected, "replacement"); + } + + function _rootRevertData(bytes memory payload) internal returns (bytes memory data) { + _mockOriginIsRoot(true); + bool ok; + (ok, data) = address(dotnsPopController).call(payload); + _mockOriginIsRoot(false); + assertFalse(ok, "the call reverts"); + } + + function _countEvents(Vm.Log[] memory logs, bytes32 sig) internal pure returns (uint256 count) { + for (uint256 i; i < logs.length; ++i) { + if (logs[i].topics.length != 0 && logs[i].topics[0] == sig) ++count; + } + } + + /// @dev Counts `sig` events whose second indexed topic is `user`; every reservation event + /// indexes the labelhash first and the user second. + function _countEventsFor( + Vm.Log[] memory logs, + bytes32 sig, + address user + ) + internal + pure + returns (uint256 count) + { + for (uint256 i; i < logs.length; ++i) { + if ( + logs[i].topics.length > 2 && logs[i].topics[0] == sig + && logs[i].topics[2] == bytes32(uint256(uint160(user))) + ) ++count; + } + } +} diff --git a/test/unit/registrar/DotnsRegistrar.t.sol b/test/unit/registrar/DotnsRegistrar.t.sol index 6092e1374..ed11cfb11 100644 --- a/test/unit/registrar/DotnsRegistrar.t.sol +++ b/test/unit/registrar/DotnsRegistrar.t.sol @@ -363,9 +363,9 @@ contract DotnsRegistrarTests is BaseDotns { function test_transfer_reverts_when_fee_required_but_no_value_attached() public { string memory label = "feerequired01"; - // Grant Full to ed so the mint succeeds, but leave leonardo at NoStatus so the + // Grant personhood to ed so the mint succeeds, but leave leonardo at NoStatus so the // transfer floor charges a non-zero downgrade fee. - _register(label, ed, IPopRules.PopStatus.PopFull); + _register(label, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(label); uint256 fee = dotnsRegistrar.quoteTransferFee(tokenId, leonardo); @@ -444,8 +444,8 @@ contract DotnsRegistrarTests is BaseDotns { assertTrue(dotnsRegistrar.supportsInterface(type(IERC721).interfaceId)); } - /// @notice Mints a name as if through the PoP gateway: the caller is the address registered - /// under `POP_CONTROLLER`, and the gateway path passes an empty label. + /// @notice Mints a name as if through the dotNS gateway pallet: the caller is the address + /// registered under `POP_CONTROLLER`, and the gateway path passes an empty label. function _mintSoulbound( string memory label, address nameOwner diff --git a/test/unit/registrar/DotnsRegistrarController.t.sol b/test/unit/registrar/DotnsRegistrarController.t.sol index 0fa85c112..d66ec3f93 100644 --- a/test/unit/registrar/DotnsRegistrarController.t.sol +++ b/test/unit/registrar/DotnsRegistrarController.t.sol @@ -146,7 +146,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_register_reverts_at_exact_expiry_boundary() public { string memory nameLabel = "alicebobx"; address nameOwner = ed; - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); IDotnsRegistrarController.Registration memory registration = IDotnsRegistrarController.Registration({ @@ -178,11 +178,11 @@ contract DotnsRegistrarControllerTest is BaseDotns { vm.stopPrank(); } - function test_register_popfull_wires_all_records() public { + function test_register_personhood_wires_all_records() public { string memory nameLabel = "web2summit"; address nameOwner = ed; - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); vm.prank(owner); storeFactory.deployLabelStoreFor(nameOwner); @@ -222,13 +222,13 @@ contract DotnsRegistrarControllerTest is BaseDotns { } /// @notice A two-digit second-level name is unreachable, which is what keeps a subname - /// from spelling a lite name. + /// from spelling a device name. /// @dev `joseph.42` reads either as one label or as `joseph` beneath `42`, and the second /// reading needs `42` to exist. No production controller entry point can create it: /// this path requires three characters, and both gateway paths are letters only. An /// owner-authorised controller calling the registrar directly is not bound by either, /// so the invariant holds over entry points rather than over the registrar. Pinned - /// here because it rests on a length floor that reads as unrelated to lite names. + /// here because it rests on a length floor that reads as unrelated to device names. /// `register` is called directly rather than through `_commitAndRegister`, which /// quotes `priceWithCheck` first and would revert on the reserved tier instead. No /// commitment is needed: the label is validated before one is consumed. @@ -250,21 +250,21 @@ contract DotnsRegistrarControllerTest is BaseDotns { dotnsRegistrarController.register(registration); } - /// @notice A public registration reserves no stem. + /// @notice A public registration reserves no base name. /// @dev PopRules' reservation slot belongs to the gateway queue, and `register` writes none - /// of its own. Pinned here because a stem the public path reserved silently would + /// of its own. Pinned here because a base name the public path reserved silently would /// block the gateway's own registrant for the whole reservation window. function test_public_register_reserves_no_base_name() public { string memory nameLabel = "lights01"; address nameOwner = ed; - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); _commitAndRegister(nameLabel, nameOwner, true); assertEq(dotnsRegistrar.ownerOf(_tokenIdForLabel(nameLabel)), nameOwner); (bool isReserved,,) = popRules.isBaseNameReserved(popRules.stripDigits(nameLabel)); - assertFalse(isReserved, "no stem reserved by a public registration"); + assertFalse(isReserved, "no base name reserved by a public registration"); } function test_register_does_not_overwrite_third_party_reverse_record() public { @@ -516,7 +516,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_transfer_writes_label_and_creates_store() public { string memory nameLabel = "alicetransfer01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); assertEq(storeFactory.getLabelStore(leonardo), address(0)); @@ -540,7 +540,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_transfer_back_skips_locked_entry() public { string memory nameLabel = "carolreturn01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(nameLabel); @@ -562,7 +562,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_transfer_clears_primary_reverse_name_when_current_name_is_moved() public { string memory nameLabel = "primarymove01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(nameLabel); assertEq(dotnsReverseResolver.nameOf(ed), "primarymove01.dot"); @@ -580,7 +580,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { assertEq(storeFactory.getLabelStore(address(0)), address(0)); - _register(nameLabel, tiago, IPopRules.PopStatus.PopFull); + _register(nameLabel, tiago, IPopRules.PopStatus.Personhood); ILabelStore tiagoStore = ILabelStore(storeFactory.getLabelStore(tiago)); uint256 tokenId = _tokenIdForLabel(nameLabel); @@ -592,7 +592,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_safe_transfer_writes_to_store() public { string memory nameLabel = "safexfer01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(nameLabel); @@ -614,7 +614,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { address payer = ed; address ownerAddr = leonardo; - _grantPopFull(payer); + _grantPersonhood(payer); bytes32 secret = keccak256(abi.encodePacked(popfullLabel, ownerAddr, block.timestamp)); IDotnsRegistrarController.Registration memory registration = @@ -644,7 +644,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { IPopRules.OwnerStatusInsufficient.selector, popfullLabel, IPopRules.PopStatus.NoStatus, - IPopRules.PopStatus.PopFull + IPopRules.PopStatus.Personhood ) ); dotnsRegistrarController.register{value: charge}(registration); @@ -653,7 +653,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_transfer_via_approved_operator_writes_to_store() public { string memory nameLabel = "opxfer01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(nameLabel); @@ -1096,9 +1096,10 @@ contract DotnsRegistrarControllerTest is BaseDotns { // Regression: the paid path's governance-reserved rejection is unchanged by the grant gate. /// @dev A reserved-tier label (base length five or fewer) is refused on the paid path whoever - /// pays. The cross-payer branch distinguishes a governance-reserved label from a stem held by - /// another user, so it reverts `GovernanceReserved` rather than `NameReserved`. Reserved-tier - /// labels reach circulation only through `registerReserved`, and a grant does not change that. + /// pays. The cross-payer branch distinguishes a governance-reserved label from a base name + /// held by another user: this one reverts `GovernanceReserved`, and `NameReserved` is kept for + /// the held base name. Reserved-tier labels reach circulation only through `registerReserved`, + /// and a grant does not change that. function test_register_rejects_a_governance_reserved_label_for_a_cross_payer() public { string memory nameLabel = "alice"; diff --git a/test/unit/registrar/DotnsRegistrarControllerLifecycle.t.sol b/test/unit/registrar/DotnsRegistrarControllerLifecycle.t.sol index 5c00e657d..e58e8c649 100644 --- a/test/unit/registrar/DotnsRegistrarControllerLifecycle.t.sol +++ b/test/unit/registrar/DotnsRegistrarControllerLifecycle.t.sol @@ -20,15 +20,15 @@ import {ReentrantOverpaymentAttacker} from "../../helpers/ReentrantOverpaymentAt /// and reentrancy guarding. contract DotnsRegistrarControllerLifecycleTest is BaseDotns { /// @dev The subject is the reclaim path, not the tier: release to escrow, withdraw, let the - /// redeem window elapse, then a new owner reclaims. PopFull because `lights01` is - /// measured whole at eight characters and no public label reaches PopLite. + /// redeem window elapse, then a new owner reclaims. Personhood because `lights01` is + /// measured whole at eight characters and no public label reaches Devicehood. function test_register_reclaim_succeeds_for_new_owner() public { string memory label = "lights01"; address originalOwner = ed; address newOwner = leonardo; - _grantPopFull(originalOwner); - _grantPopFull(newOwner); + _grantPersonhood(originalOwner); + _grantPersonhood(newOwner); _commitAndRegister(label, originalOwner, true); @@ -69,10 +69,10 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { dotnsRegistrarController.register{value: registrationPrice}(registration); assertEq(dotnsRegistrar.ownerOf(tokenId), newOwner); - // A public registration reserves no stem, so reclaim has no slot to take over and none + // A public registration reserves no base name, so reclaim has no slot to take over and none // to garbage-collect. (bool isReserved,,) = popRules.isBaseNameReserved(popRules.stripDigits(label)); - assertFalse(isReserved, "no stem reserved by either registration"); + assertFalse(isReserved, "no base name reserved by either registration"); } function test_register_cross_payer_charges_max_not_sum_of_price_and_reach() public { @@ -80,7 +80,7 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { address payer = leonardo; address nameOwner = ed; - _grantPopFull(payer); + _grantPersonhood(payer); _grantNoStatus(nameOwner); uint256 ownerPrice = popRules.priceWithoutCheck(label, nameOwner).price; @@ -124,7 +124,7 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { address payer = leonardo; address nameOwner = ed; - _grantPopFull(payer); + _grantPersonhood(payer); _grantNoStatus(nameOwner); uint256 ownerPrice = popRules.priceWithoutCheck(label, nameOwner).price; @@ -200,7 +200,7 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { function test_register_creates_funded_position_for_self_registration() public { string memory label = BASE_LABEL_A; address nameOwner = ed; - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); IDotnsRegistrarController.Registration memory registration = IDotnsRegistrarController.Registration({ @@ -347,7 +347,7 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { string memory firstLabel = "primary01"; string memory secondLabel = "secondary01"; - _grantPopLite(ed); + _grantDevicehood(ed); _commitAndRegister(firstLabel, ed, true); assertEq(dotnsReverseResolver.nameOf(ed), string.concat(firstLabel, ".dot")); diff --git a/test/unit/registry/DotnsProtocolRegistryDeclarations.t.sol b/test/unit/registry/DotnsProtocolRegistryDeclarations.t.sol index bd02417dd..ea0c81f61 100644 --- a/test/unit/registry/DotnsProtocolRegistryDeclarations.t.sol +++ b/test/unit/registry/DotnsProtocolRegistryDeclarations.t.sol @@ -169,7 +169,7 @@ contract DotnsProtocolRegistryDeclarationTests is BaseDotns { assertEq(registry.expectedCodehash(DotnsConstants.REGISTRAR), bytes32(0)); } - /// @notice `version()` on every DotNS contract mirrors the registry's declared release, so + /// @notice `version()` on every dotNS contract mirrors the registry's declared release, so /// the whole network answers with one synchronised value: empty before the first /// declaration, the declared tag after, with no per-contract bookkeeping. Sampled /// across every mirror the fixture deploys, including a claimed user store. diff --git a/test/unit/registry/DotnsRegistry.t.sol b/test/unit/registry/DotnsRegistry.t.sol index 3d29cb57a..5082d4337 100644 --- a/test/unit/registry/DotnsRegistry.t.sol +++ b/test/unit/registry/DotnsRegistry.t.sol @@ -41,7 +41,7 @@ contract DotnsRegistryTests is BaseDotns { } /// @notice A separator is never valid in a subname label. - /// @dev This is what reserves the dotted-label space to the PoP gateway. Loosen it and a + /// @dev This is what reserves the dotted-label space to the gateway path. Loosen it and a /// subname could be minted whose label equals a person's, putting two claimants on one /// node. Both subnode entry points are checked, since either would open the space. function test_subname_label_carrying_a_separator_is_rejected() public { @@ -75,7 +75,7 @@ contract DotnsRegistryTests is BaseDotns { } /// @notice A subname created outside the gateway carries no person provenance. - /// @dev The gateway issues a lite name as the subname `michael` under `01` and records it in + /// @dev The gateway issues a device name as the subname `michael` under `01` and records it in /// `_popIssued`. This test creates the same subname directly, without the gateway, and /// shows it reads back as not `isPopIssued`: provenance, not the name text, is what marks /// a gateway-issued person. No production controller entry point can create the parent here: @@ -378,7 +378,7 @@ contract DotnsRegistryTests is BaseDotns { function test_revert_subnode_owner_with_parent_label_mismatch() public { string memory parentLabel = "actualparent01"; - bytes32 parentNode = _register(parentLabel, ed, IPopRules.PopStatus.PopFull); + bytes32 parentNode = _register(parentLabel, ed, IPopRules.PopStatus.Personhood); IDotnsRegistry.SubnodeRecord memory subnodeRecord = IDotnsRegistry.SubnodeRecord({ parentNode: parentNode, @@ -616,8 +616,8 @@ contract DotnsRegistryTests is BaseDotns { function test_same_sublabel_under_different_parents_owned_by_same_address() public { string memory parentLabelA = "alphaomega"; string memory parentLabelB = "bravobro"; - bytes32 parentNodeA = _register(parentLabelA, owner, IPopRules.PopStatus.PopFull); - bytes32 parentNodeB = _register(parentLabelB, owner, IPopRules.PopStatus.PopFull); + bytes32 parentNodeA = _register(parentLabelA, owner, IPopRules.PopStatus.Personhood); + bytes32 parentNodeB = _register(parentLabelB, owner, IPopRules.PopStatus.Personhood); string memory subLabel = "app"; diff --git a/test/unit/resolver/DotnsPopResolver.t.sol b/test/unit/resolver/DotnsPopResolver.t.sol index b3cc5d033..8aee730ab 100644 --- a/test/unit/resolver/DotnsPopResolver.t.sol +++ b/test/unit/resolver/DotnsPopResolver.t.sol @@ -31,26 +31,26 @@ contract DotnsPopResolverTests is BaseDotns { dotnsPopResolver.setChatKey(_nodeOf("alice42"), _validChatKey(0x04)); } - function test_setLiteLink_writes_and_emits() public { - bytes32 fullNode = _nodeOf("alice"); - bytes32 liteLabelhash = keccak256(bytes("alice42")); + function test_setDeviceLink_writes_and_emits() public { + bytes32 personhoodNode = _nodeOf("alice"); + bytes32 deviceLabelhash = keccak256(bytes("alice42")); vm.prank(address(dotnsPopController)); vm.expectEmit(true, true, false, false); - emit IDotnsPopResolver.LiteLinkUpdated(fullNode, liteLabelhash); - dotnsPopResolver.setLiteLink(fullNode, liteLabelhash); + emit IDotnsPopResolver.DeviceLinkUpdated(personhoodNode, deviceLabelhash); + dotnsPopResolver.setDeviceLink(personhoodNode, deviceLabelhash); // Both directions must be populated by a single write: forward - // (full => lite) and reverse (lite => full). The reverse index is what - // downstream consumers (Nova) use to answer "given this lite username, - // which full name did they claim?". - assertEq(dotnsPopResolver.liteLink(fullNode), liteLabelhash); - assertEq(dotnsPopResolver.fullClaim(liteLabelhash), fullNode); + // (personhood => device) and reverse (device => personhood). The reverse index is what + // downstream consumers (Nova) use to answer "given this device name, + // which personhood name did they claim?". + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodNode), deviceLabelhash); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceLabelhash), personhoodNode); } - function test_setLiteLink_reverts_for_unauthorised_caller() public { + function test_setDeviceLink_reverts_for_unauthorised_caller() public { vm.prank(ed); vm.expectRevert(abi.encodeWithSelector(IDotnsPopResolver.NotPopController.selector, ed)); - dotnsPopResolver.setLiteLink(_nodeOf("alice"), keccak256(bytes("alice42"))); + dotnsPopResolver.setDeviceLink(_nodeOf("alice"), keccak256(bytes("alice42"))); } function test_setChatKey_accepts_zero_node_as_passthrough() public { @@ -67,11 +67,11 @@ contract DotnsPopResolverTests is BaseDotns { assertEq(dotnsPopResolver.chatKey(bytes32(0)), key); } - function test_setLiteLink_accepts_zero_inputs_as_passthrough() public { + function test_setDeviceLink_accepts_zero_inputs_as_passthrough() public { vm.prank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(bytes32(0), bytes32(0)); - assertEq(dotnsPopResolver.liteLink(bytes32(0)), bytes32(0)); - assertEq(dotnsPopResolver.fullClaim(bytes32(0)), bytes32(0)); + dotnsPopResolver.setDeviceLink(bytes32(0), bytes32(0)); + assertEq(dotnsPopResolver.deviceLabelhashOf(bytes32(0)), bytes32(0)); + assertEq(dotnsPopResolver.personhoodNodeOf(bytes32(0)), bytes32(0)); } function test_rotating_pop_controller_changes_authorised_writer() public { @@ -174,156 +174,156 @@ contract DotnsPopResolverTests is BaseDotns { dotnsPopResolver.setChatKey(_nodeOf("alice42"), key); } - function test_setLiteLink_same_full_node_relink_clears_old_reverse() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 liteX = keccak256(bytes("alice42")); - bytes32 liteY = keccak256(bytes("alice99")); + function test_setDeviceLink_same_personhood_node_relink_clears_old_reverse() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 deviceX = keccak256(bytes("alice42")); + bytes32 deviceY = keccak256(bytes("alice99")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullA, liteY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceY); vm.stopPrank(); // Old reverse must be cleared so downstream consumers stop resolving - // `liteX` to `fullA`. - assertEq(dotnsPopResolver.fullClaim(liteX), bytes32(0)); + // `deviceX` to `personhoodA`. + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), bytes32(0)); // New pair round-trips cleanly. - assertEq(dotnsPopResolver.liteLink(fullA), liteY); - assertEq(dotnsPopResolver.fullClaim(liteY), fullA); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), deviceY); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceY), personhoodA); } - function test_setLiteLink_same_lite_relink_clears_old_forward() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 fullB = _nodeOf("bob"); - bytes32 liteX = keccak256(bytes("alice42")); + function test_setDeviceLink_same_device_relink_clears_old_forward() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 personhoodB = _nodeOf("bob"); + bytes32 deviceX = keccak256(bytes("alice42")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullB, liteX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodB, deviceX); vm.stopPrank(); - // Old forward must be cleared so `fullA` no longer claims `liteX`. - assertEq(dotnsPopResolver.liteLink(fullA), bytes32(0)); + // Old forward must be cleared so `personhoodA` no longer claims `deviceX`. + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), bytes32(0)); // New pair round-trips. - assertEq(dotnsPopResolver.liteLink(fullB), liteX); - assertEq(dotnsPopResolver.fullClaim(liteX), fullB); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodB), deviceX); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), personhoodB); } - function test_setLiteLink_idempotent_relink_keeps_both_indices() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 liteX = keccak256(bytes("alice42")); + function test_setDeviceLink_idempotent_relink_keeps_both_indices() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 deviceX = keccak256(bytes("alice42")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullA, liteX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); vm.stopPrank(); // Writing the same pair twice must not accidentally delete either - // side: the `oldLite == liteLabelhash` and `oldFull == fullNode` + // side: the `oldDevice == deviceLabelhash` and `oldPersonhood == personhoodNode` // guards in the implementation are the things under test here. - assertEq(dotnsPopResolver.liteLink(fullA), liteX); - assertEq(dotnsPopResolver.fullClaim(liteX), fullA); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), deviceX); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), personhoodA); } - function test_setLiteLink_chain_returns_to_original_without_drift() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 liteX = keccak256(bytes("alice42")); - bytes32 liteY = keccak256(bytes("alice99")); + function test_setDeviceLink_chain_returns_to_original_without_drift() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 deviceX = keccak256(bytes("alice42")); + bytes32 deviceY = keccak256(bytes("alice99")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullA, liteY); - dotnsPopResolver.setLiteLink(fullA, liteX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); vm.stopPrank(); // After A -> X -> Y -> X, only the (A, X) pair survives. - assertEq(dotnsPopResolver.liteLink(fullA), liteX); - assertEq(dotnsPopResolver.fullClaim(liteX), fullA); - assertEq(dotnsPopResolver.fullClaim(liteY), bytes32(0)); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), deviceX); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), personhoodA); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceY), bytes32(0)); } - function test_setLiteLink_cross_chain_no_drift() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 fullB = _nodeOf("bob"); - bytes32 liteX = keccak256(bytes("alice42")); + function test_setDeviceLink_cross_chain_no_drift() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 personhoodB = _nodeOf("bob"); + bytes32 deviceX = keccak256(bytes("alice42")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullB, liteX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodB, deviceX); vm.stopPrank(); // A must no longer appear as a claimant of anything, only B. - assertEq(dotnsPopResolver.liteLink(fullA), bytes32(0)); - assertEq(dotnsPopResolver.liteLink(fullB), liteX); - assertEq(dotnsPopResolver.fullClaim(liteX), fullB); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), bytes32(0)); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodB), deviceX); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), personhoodB); } - function test_setLiteLink_quadrangle_clears_both_stale_inverses() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 fullB = _nodeOf("bob"); - bytes32 liteX = keccak256(bytes("alice42")); - bytes32 liteY = keccak256(bytes("bob42")); + function test_setDeviceLink_quadrangle_clears_both_stale_inverses() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 personhoodB = _nodeOf("bob"); + bytes32 deviceX = keccak256(bytes("alice42")); + bytes32 deviceY = keccak256(bytes("bob42")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullB, liteY); - dotnsPopResolver.setLiteLink(fullA, liteY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodB, deviceY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceY); vm.stopPrank(); // After (A,X), (B,Y), (A,Y): only (A,Y) remains. B's forward link // and X's reverse link must both be cleared. - assertEq(dotnsPopResolver.liteLink(fullA), liteY); - assertEq(dotnsPopResolver.liteLink(fullB), bytes32(0)); - assertEq(dotnsPopResolver.fullClaim(liteY), fullA); - assertEq(dotnsPopResolver.fullClaim(liteX), bytes32(0)); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), deviceY); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodB), bytes32(0)); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceY), personhoodA); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), bytes32(0)); } - function test_setLiteLink_with_zero_lite_is_passthrough() public { - bytes32 fullA = _nodeOf("alice"); + function test_setDeviceLink_with_zero_device_is_passthrough() public { + bytes32 personhoodA = _nodeOf("alice"); // Pin current behaviour for the zero-hash edge: the setter does not - // revert on a zero `liteLabelhash` and writes both indices at the + // revert on a zero `deviceLabelhash` and writes both indices at the // zero key. Any future validator that rejects zero inputs lands // here as a failing assertion. vm.prank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, bytes32(0)); + dotnsPopResolver.setDeviceLink(personhoodA, bytes32(0)); - assertEq(dotnsPopResolver.liteLink(fullA), bytes32(0)); - assertEq(dotnsPopResolver.fullClaim(bytes32(0)), fullA); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), bytes32(0)); + assertEq(dotnsPopResolver.personhoodNodeOf(bytes32(0)), personhoodA); } - function test_setLiteLink_long_chain_invariant_holds_at_every_step() public { - // Ten sequential re-links of the same `fullNode` to fresh lite + function test_setDeviceLink_long_chain_invariant_holds_at_every_step() public { + // Ten sequential re-links of the same `personhoodNode` to fresh device-name // labelhashes. At each step the forward and reverse indices must - // round-trip for the current pair, and the previous lite's reverse + // round-trip for the current pair, and the previous device name's reverse // entry must have been cleared. - bytes32 fullA = _nodeOf("alice"); - bytes32 previousLite = bytes32(0); + bytes32 personhoodA = _nodeOf("alice"); + bytes32 previousDevice = bytes32(0); vm.startPrank(address(dotnsPopController)); for (uint256 i = 1; i <= 10; i++) { - bytes32 currentLite = keccak256(abi.encodePacked("alice", i)); - dotnsPopResolver.setLiteLink(fullA, currentLite); + bytes32 currentDevice = keccak256(abi.encodePacked("alice", i)); + dotnsPopResolver.setDeviceLink(personhoodA, currentDevice); // Current pair round-trips. - assertEq(dotnsPopResolver.liteLink(fullA), currentLite); - assertEq(dotnsPopResolver.fullClaim(currentLite), fullA); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), currentDevice); + assertEq(dotnsPopResolver.personhoodNodeOf(currentDevice), personhoodA); // Previous reverse entry was nulled. - if (previousLite != bytes32(0)) { - assertEq(dotnsPopResolver.fullClaim(previousLite), bytes32(0)); + if (previousDevice != bytes32(0)) { + assertEq(dotnsPopResolver.personhoodNodeOf(previousDevice), bytes32(0)); } - previousLite = currentLite; + previousDevice = currentDevice; } vm.stopPrank(); } - function test_setLiteLink_old_lite_reads_zero_after_relink() public { - // Integration-shaped assertion: a consumer that cached the old lite - // hash and later queries `fullClaim` must see `bytes32(0)`, not a - // stale fullNode. - bytes32 fullA = _nodeOf("alice"); - bytes32 liteX = keccak256(bytes("alice42")); - bytes32 liteY = keccak256(bytes("alice99")); + function test_setDeviceLink_old_device_reads_zero_after_relink() public { + // Integration-shaped assertion: a consumer that cached the old device-name + // hash and later queries `personhoodNodeOf` must see `bytes32(0)`, not a + // stale personhoodNode. + bytes32 personhoodA = _nodeOf("alice"); + bytes32 deviceX = keccak256(bytes("alice42")); + bytes32 deviceY = keccak256(bytes("alice99")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullA, liteY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceY); vm.stopPrank(); - assertEq(dotnsPopResolver.fullClaim(liteX), bytes32(0)); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), bytes32(0)); } // 65-byte chat-key helper now lives on BaseDotns as `_validChatKey`. } diff --git a/test/unit/resolver/DotnsReverseResolver.t.sol b/test/unit/resolver/DotnsReverseResolver.t.sol index 2205f46c7..55d0b576a 100644 --- a/test/unit/resolver/DotnsReverseResolver.t.sol +++ b/test/unit/resolver/DotnsReverseResolver.t.sol @@ -11,10 +11,10 @@ import {IDotnsReverseResolver} from "../../../contracts/resolvers/IDotnsReverseR /// @custom:function nameOf read. contract DotnsReverseResolverTests is BaseDotns { /// @notice Label fixture used by tests that need a labelled, transferable name. - /// @dev Stem of nine characters with a two-digit suffix; classifies as NoStatus. + /// @dev Eleven characters measured as written, digits included; classifies as NoStatus. string internal constant CLAIM_LABEL = "claimuser01"; /// @notice Secondary label fixture used by overwrite and transfer tests. - /// @dev Stem of nine characters with a two-digit suffix; classifies as NoStatus. + /// @dev Twelve characters measured as written, digits included; classifies as NoStatus. string internal constant ALT_LABEL = "secondname01"; function test_nameof_returns_empty_when_unset() public view { @@ -116,18 +116,18 @@ contract DotnsReverseResolverTests is BaseDotns { assertEq(dotnsReverseResolver.nameOf(leonardo), string.concat(CLAIM_LABEL, ".dot")); } - /// @notice A lite username owner can claim and reverse-resolve it. - /// @dev A lite name is a registry subname, not a token, so the claim reads ownership through + /// @notice A device name owner can claim and reverse-resolve it. + /// @dev A device name is a registry subname, not a token, so the claim reads ownership through /// the registry at the stem-under-container node rather than from the registrar's ERC-721 /// ledger, and the reverse read resolves the same node. - function test_claim_and_nameof_for_a_lite_username() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); + function test_claim_and_nameof_for_a_device_name() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); vm.prank(ed); - dotnsReverseResolver.claimReverseRecord(LITE_LABEL_A); + dotnsReverseResolver.claimReverseRecord(DEVICE_LABEL_A); - assertEq(dotnsReverseResolver.nameOf(ed), string.concat(LITE_LABEL_A, ".dot")); + assertEq(dotnsReverseResolver.nameOf(ed), string.concat(DEVICE_LABEL_A, ".dot")); } function test_nameof_fails_closed_when_caller_no_longer_owns_stored_name() public { diff --git a/test/unit/store/StoreFactoryMigrator.t.sol b/test/unit/store/StoreFactoryMigrator.t.sol deleted file mode 100644 index 788373c29..000000000 --- a/test/unit/store/StoreFactoryMigrator.t.sol +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseDotns} from "../../base/BaseDotns.t.sol"; -import {StoreFactory} from "../../../contracts/store/StoreFactory.sol"; -import {StoreFactoryMigrator} from "../../../contracts/store/StoreFactoryMigrator.sol"; -import {IStoreFactory} from "../../../contracts/store/IStoreFactory.sol"; -import {IDotnsStore} from "../../../contracts/store/IDotnsStore.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -/// @title StoreFactoryMigratorTests -/// @notice Covers the paged import that carries per-user `LabelStore` bindings onto a new -/// factory, which is the only part of the store migration with behaviour of its own. -/// @dev `storeFactory` from `BaseDotns` stands in for the factory being migrated from, and a -/// fresh proxy for the one being migrated to. What is asserted is what reading the diff -/// cannot show: that a binding is adopted instead of re-deployed, that the guards close, and -/// that the proxy comes back out of the migrator with the imported state intact. -/// @custom:security-contact admin@parity.io -contract StoreFactoryMigratorTests is BaseDotns { - /// @notice The proxy the bindings are imported into. - StoreFactoryMigrator internal target; - - function setUp() public override { - super.setUp(); - - // Two bindings on the old factory, created the ordinary way so they are real stores. - vm.startPrank(owner); - storeFactory.deployLabelStoreFor(ed); - storeFactory.deployLabelStoreFor(tiago); - vm.stopPrank(); - - target = StoreFactoryMigrator( - address( - new ERC1967Proxy( - address(new StoreFactoryMigrator()), - abi.encodeCall(StoreFactory.initialize, (owner, address(protocolRegistry))) - ) - ) - ); - } - - /// @notice Imported users keep the stores they already had. - /// @dev The whole point of the migration: the binding is adopted, not re-created. A fresh - /// factory would hand them a new empty store instead, and their labels would drop out of - /// per-address enumeration while their names stayed put. - function test_import_adopts_the_existing_stores() public { - address edStore = storeFactory.getLabelStore(ed); - address tiagoStore = storeFactory.getLabelStore(tiago); - - vm.prank(owner); - target.importStores(address(storeFactory), 0, 10); - - assertEq(target.getLabelStore(ed), edStore, "ed keeps the store he already had"); - assertEq(target.getLabelStore(tiago), tiagoStore, "tiago keeps the store he already had"); - assertEq(target.getLabelStoreCount(), 2, "both bindings land in the enumeration"); - } - - /// @notice The holder set is read from the old factory, not supplied by the caller. - /// @dev An earlier version took the list as an argument, which left a window between an - /// operator reading the set and the import running. A store created in that window is - /// silently left unbound, and its holder is handed an empty store on their next - /// registration. Reading inside the call closes the window, so a binding created right up - /// to the transaction is still carried. - function test_import_sees_a_binding_created_after_the_decision_to_migrate() public { - address latecomer = makeAddr("latecomer"); - - vm.prank(owner); - storeFactory.deployLabelStoreFor(latecomer); - address lateStore = storeFactory.getLabelStore(latecomer); - - vm.prank(owner); - target.importStores(address(storeFactory), 0, 10); - - assertEq(target.getLabelStore(latecomer), lateStore, "the late binding is carried too"); - assertEq(target.getLabelStoreCount(), 3, "and is counted"); - } - - /// @notice A store whose owner disagrees with the old factory's mapping is rejected. - /// @dev The store list and the per-user mapping are separate state. Trusting a store's own - /// `owner` alone would bind a user the old factory does not hold that store for, and - /// bindings here are permanent, so that user could never be given their real one. - function test_import_reverts_when_a_store_owner_disagrees_with_the_mapping() public { - address edStore = storeFactory.getLabelStore(ed); - address impostor = makeAddr("impostor"); - - // Only the store's answer is changed; the factory still has it bound to ed. - vm.mockCall( - edStore, abi.encodeWithSelector(IDotnsStore.owner.selector), abi.encode(impostor) - ); - - vm.prank(owner); - vm.expectRevert( - abi.encodeWithSelector( - StoreFactoryMigrator.ImportBindingMismatch.selector, impostor, edStore - ) - ); - target.importStores(address(storeFactory), 0, 10); - } - - /// @notice The import lands identically when carried one binding per page. - /// @dev The pages exist because a whole-list import does not fit in a pallet-revive block, - /// so the property that matters is that page boundaries are invisible in the result: - /// same bindings, same count, no matter how the list was sliced. - function test_import_is_the_same_result_page_by_page() public { - address edStore = storeFactory.getLabelStore(ed); - address tiagoStore = storeFactory.getLabelStore(tiago); - - vm.startPrank(owner); - target.importStores(address(storeFactory), 0, 1); - target.importStores(address(storeFactory), 1, 1); - vm.stopPrank(); - - assertEq(target.getLabelStore(ed), edStore, "ed keeps his store across a page boundary"); - assertEq(target.getLabelStore(tiago), tiagoStore, "tiago keeps his on the later page"); - assertEq(target.getLabelStoreCount(), 2, "both pages land, neither twice"); - } - - /// @notice Replaying pages already carried is a no-op instead of a failure. - /// @dev Partial-failure recovery: a death between pages is finished by running the same pages - /// again, so a page that already landed must skip its bindings silently. Loud rejection - /// here would make the interrupted import unfinishable. - function test_replaying_an_imported_page_is_a_noop() public { - vm.startPrank(owner); - target.importStores(address(storeFactory), 0, 10); - target.importStores(address(storeFactory), 0, 10); - vm.stopPrank(); - - assertEq(target.getLabelStoreCount(), 2, "the replay carried nothing twice"); - assertEq( - target.getLabelStore(ed), - storeFactory.getLabelStore(ed), - "the binding is the one the old factory holds" - ); - } - - /// @notice A user already bound to a DIFFERENT store is rejected, not repointed. - /// @dev The skip above is strictly for identical pairs. Bindings are permanent everywhere - /// else in the factory, and the migration does not get to be the exception: a - /// conflicting pair fails loudly instead of rewriting history. - function test_import_reverts_on_a_conflicting_binding() public { - address edStore = storeFactory.getLabelStore(ed); - address tiagoStore = storeFactory.getLabelStore(tiago); - - vm.prank(owner); - target.importStores(address(storeFactory), 0, 10); - - // The old factory now claims ed holds tiago's store: list slot, store owner and mapping - // all agree with each other, and disagree only with the binding already imported. - address[] memory page = new address[](1); - page[0] = tiagoStore; - vm.mockCall( - address(storeFactory), - abi.encodeWithSelector(IStoreFactory.getLabelStores.selector, 0, 1), - abi.encode(page) - ); - vm.mockCall(tiagoStore, abi.encodeWithSelector(IDotnsStore.owner.selector), abi.encode(ed)); - vm.mockCall( - address(storeFactory), - abi.encodeWithSelector(IStoreFactory.getLabelStore.selector, ed), - abi.encode(tiagoStore) - ); - - vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(IStoreFactory.AlreadyDeployed.selector, ed, edStore)); - target.importStores(address(storeFactory), 0, 1); - } - - /// @notice A zero-sized page is a caller bug, named as such. - /// @dev Importing nothing while looking like progress is how a wrong pagination loop would - /// hide; the guard turns it into a revert at the first page. - function test_import_rejects_an_empty_page() public { - vm.prank(owner); - vm.expectRevert(StoreFactoryMigrator.ImportPageEmpty.selector); - target.importStores(address(storeFactory), 0, 0); - } - - /// @notice Only the owner may import. - /// @dev The import writes bindings directly, bypassing every check `deployLabelStoreFor` - /// applies, so it is the most privileged entrypoint the factory has ever carried. - function test_import_is_owner_only() public { - vm.prank(ed); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, ed)); - target.importStores(address(storeFactory), 0, 10); - } - - /// @notice The imported bindings survive the upgrade back to the shipped implementation. - /// @dev The migrator is meant to be transient. This is the assertion that the round trip - /// leaves an ordinary factory holding the imported state, instead of a deployment parked - /// on migration tooling. - function test_bindings_survive_the_return_to_the_shipped_implementation() public { - address edStore = storeFactory.getLabelStore(ed); - - vm.startPrank(owner); - target.importStores(address(storeFactory), 0, 10); - target.upgradeToAndCall(address(new StoreFactory()), ""); - vm.stopPrank(); - - StoreFactory restored = StoreFactory(address(target)); - assertEq(restored.getLabelStore(ed), edStore, "binding survives the swap back"); - assertEq(restored.getLabelStoreCount(), 2, "enumeration survives the swap back"); - } -} diff --git a/test/unit/upgrade/LayoutCompatibility.t.sol b/test/unit/upgrade/LayoutCompatibility.t.sol index 3f379ecf9..1bc57ce90 100644 --- a/test/unit/upgrade/LayoutCompatibility.t.sol +++ b/test/unit/upgrade/LayoutCompatibility.t.sol @@ -38,92 +38,22 @@ contract LayoutCompatibilityTests is Test { ); } - function test_protocolRegistry_layout_is_compatible() public { - // The one proxy that genuinely adds storage: #304 appends `_protocolVersion` and - // `_expectedCodehash` and shrinks the gap to match, which is an append, not a move. - _assertCompatible( - "DotnsProtocolRegistry.sol:DotnsProtocolRegistry", - "DotnsProtocolRegistryOld.sol:DotnsProtocolRegistryOld" - ); - } - - function test_registrar_layout_is_compatible() public { - _assertCompatible( - "DotnsRegistrar.sol:DotnsRegistrar", "DotnsRegistrarOld.sol:DotnsRegistrarOld" - ); - } - - function test_registrarController_layout_is_compatible() public { - // The retained `__whiteListSlot` placeholder is what keeps `protocolRegistry` on the slot - // the live proxy uses. Without it this assertion is what fails. - _assertCompatible( - "DotnsRegistrarController.sol:DotnsRegistrarController", - "DotnsRegistrarControllerOld.sol:DotnsRegistrarControllerOld" - ); - } - - function test_popController_layout_is_compatible() public { - _assertCompatible( - "DotnsPopController.sol:DotnsPopController", - "DotnsPopControllerOld.sol:DotnsPopControllerOld" - ); - } - function test_popRules_layout_is_compatible() public { _assertCompatible("PopRules.sol:PopRules", "PopRulesOld.sol:PopRulesOld"); } - function test_nameEscrow_layout_is_compatible() public { - _assertCompatible( - "DotnsNameEscrow.sol:DotnsNameEscrow", "DotnsNameEscrowOld.sol:DotnsNameEscrowOld" - ); - } - - function test_nameWhitelist_layout_is_compatible() public { - _assertCompatible( - "DotnsNameWhitelist.sol:DotnsNameWhitelist", - "DotnsNameWhitelistOld.sol:DotnsNameWhitelistOld" - ); - } - - function test_resolver_layout_is_compatible() public { - _assertCompatible( - "DotnsResolver.sol:DotnsResolver", "DotnsResolverOld.sol:DotnsResolverOld" - ); - } - - function test_reverseResolver_layout_is_compatible() public { - _assertCompatible( - "DotnsReverseResolver.sol:DotnsReverseResolver", - "DotnsReverseResolverOld.sol:DotnsReverseResolverOld" - ); - } - - function test_contentResolver_layout_is_compatible() public { - _assertCompatible( - "DotnsContentResolver.sol:DotnsContentResolver", - "DotnsContentResolverOld.sol:DotnsContentResolverOld" - ); - } - function test_popResolver_layout_is_compatible() public { + // `_liteLinks` becomes `_deviceLinks` in slot 2 and `_fullClaims` becomes + // `_personhoodNodes` in slot 3, types unchanged, declared by renamed-from annotations. _assertCompatible( "DotnsPopResolver.sol:DotnsPopResolver", "DotnsPopResolverOld.sol:DotnsPopResolverOld" ); } - /// @notice The store-factory migration swaps the implementation twice, so both directions have - /// to be layout-compatible, not just the way in. - /// @dev The migrator is the shipped factory with four fields widened and one entrypoint added, - /// so these hold by construction. Asserting them anyway is what catches someone editing - /// the migrator into something that can no longer be swapped back out, which would strand - /// the proxy on migration tooling. - function test_storeFactory_migration_is_compatible_in_both_directions() public { - _assertCompatible( - "StoreFactoryMigrator.sol:StoreFactoryMigrator", "StoreFactory.sol:StoreFactory" - ); + function test_popController_layout_is_compatible() public { _assertCompatible( - "StoreFactory.sol:StoreFactory", "StoreFactoryMigrator.sol:StoreFactoryMigrator" + "DotnsPopController.sol:DotnsPopController", + "DotnsPopControllerOld.sol:DotnsPopControllerOld" ); } } diff --git a/test/unit/utils/StringUtils.t.sol b/test/unit/utils/StringUtils.t.sol index 9d8ee0047..15ad10615 100644 --- a/test/unit/utils/StringUtils.t.sol +++ b/test/unit/utils/StringUtils.t.sol @@ -17,22 +17,26 @@ contract StringUtilsHarness { return value.isSingleLabel(); } - function isPersonLabel(string calldata value) external pure returns (bool) { - return value.isPersonLabel(); + function isPersonhoodLabel(string calldata value) external pure returns (bool) { + return value.isPersonhoodLabel(); } - function isLitePersonLabel(string calldata value) external pure returns (bool) { - return value.isLitePersonLabel(); + function isDeviceLabel(string calldata value) external pure returns (bool) { + return value.isDeviceLabel(); } - function isLitePersonLabelMemory(string calldata value) external pure returns (bool) { - return StringUtils.isLitePersonLabelMemory(value); + function isDeviceLabelMemory(string calldata value) external pure returns (bool) { + return StringUtils.isDeviceLabelMemory(value); + } + + function isNamePath(string calldata value) external pure returns (bool) { + return value.isNamePath(); } } /// @title StringUtilsTests -/// @notice Unit tests for the lite-label shape predicate and the letters-only name predicate. -/// @dev A lite label is the only shape in DotNS permitted to carry a separator, and its stem +/// @notice Unit tests for the device-name shape predicate and the letters-only name predicate. +/// @dev A device name is the only shape in dotNS permitted to carry a separator, and its stem /// is letters only, which is stricter than a DNS label. A digit suffix is not exclusive: /// an ordinary label may end in digits. These tests are the definition of that boundary. contract StringUtilsTests is Test { @@ -52,86 +56,110 @@ contract StringUtilsTests is Test { } /// @notice Asserts both data locations agree, then that they agree with `expected`. - function _assertLite(string memory value, bool expected) internal view { - bool fromCalldata = utils.isLitePersonLabel(value); - bool fromMemory = utils.isLitePersonLabelMemory(value); + function _assertDeviceLabel(string memory value, bool expected) internal view { + bool fromCalldata = utils.isDeviceLabel(value); + bool fromMemory = utils.isDeviceLabelMemory(value); assertEq(fromCalldata, fromMemory, "calldata and memory variants disagree"); assertEq(fromCalldata, expected, value); } - /// @notice The same letters-only rule a lite stem uses, applied to a whole label. - /// @dev Mirrors the gateway pallet's `is_valid_person`, so a full-person label admits no + /// @notice The same letters-only rule a device-name stem uses, applied to a whole label. + /// @dev Mirrors the gateway pallet's `is_valid_person`, so a personhood name admits no /// digits and no hyphens. No floor on length: that is the governance-reserved band. - function test_isPersonLabel_accepts_letters_only() public view { - assertTrue(utils.isPersonLabel("alicebob")); - assertTrue(utils.isPersonLabel("a")); - assertTrue(utils.isPersonLabel(_stem(63))); + function test_isPersonhoodLabel_accepts_letters_only() public view { + assertTrue(utils.isPersonhoodLabel("alicebob")); + assertTrue(utils.isPersonhoodLabel("a")); + assertTrue(utils.isPersonhoodLabel(_stem(63))); - assertFalse(utils.isPersonLabel("alice-bob"), "no hyphens"); - assertFalse(utils.isPersonLabel("micha3l"), "no interior digits"); - assertFalse(utils.isPersonLabel("alicebob01"), "no digit suffix"); - assertFalse(utils.isPersonLabel("Alicebob"), "no uppercase"); - assertFalse(utils.isPersonLabel(""), "not empty"); - assertFalse(utils.isPersonLabel(_stem(64)), "bounded at 63 octets"); + assertFalse(utils.isPersonhoodLabel("alice-bob"), "no hyphens"); + assertFalse(utils.isPersonhoodLabel("micha3l"), "no interior digits"); + assertFalse(utils.isPersonhoodLabel("alicebob01"), "no digit suffix"); + assertFalse(utils.isPersonhoodLabel("Alicebob"), "no uppercase"); + assertFalse(utils.isPersonhoodLabel(""), "not empty"); + assertFalse(utils.isPersonhoodLabel(_stem(64)), "bounded at 63 octets"); } - function test_isLitePersonLabel_accepts_the_dotted_shape() public view { - _assertLite("joseph.42", true); - _assertLite("joseph.00", true); - _assertLite("elizabeth.42", true); - _assertLite(string.concat(_stem(63), ".42"), true); + function test_isDeviceLabel_accepts_the_dotted_shape() public view { + _assertDeviceLabel("joseph.42", true); + _assertDeviceLabel("joseph.00", true); + _assertDeviceLabel("elizabeth.42", true); + _assertDeviceLabel(string.concat(_stem(63), ".42"), true); } /// @dev The shape puts no floor on the stem: how short a name may be is policy, held by - /// PopRules as the governance-reserved band, so a short stem is a well-formed lite + /// PopRules as the governance-reserved band, so a short stem is a well-formed device-name /// label that classification then rejects. - function test_isLitePersonLabel_puts_no_floor_on_the_stem() public view { - _assertLite("a.42", true); - _assertLite("josep.42", true); - _assertLite(string.concat(_stem(1), ".42"), true); + function test_isDeviceLabel_puts_no_floor_on_the_stem() public view { + _assertDeviceLabel("a.42", true); + _assertDeviceLabel("josep.42", true); + _assertDeviceLabel(string.concat(_stem(1), ".42"), true); } /// @dev The stem is the name a person chose, which People Chain restricts to lowercase /// letters, so it is stricter than a DNS label: no digits, no hyphens, no uppercase. - function test_isLitePersonLabel_requires_a_letters_only_stem() public view { - _assertLite("alice-bob.99", false); - _assertLite("josep4.42", false); - _assertLite("jos3ph.42", false); - _assertLite("joseph1.42", false); - _assertLite("Joseph.42", false); + function test_isDeviceLabel_requires_a_letters_only_stem() public view { + _assertDeviceLabel("alice-bob.99", false); + _assertDeviceLabel("josep4.42", false); + _assertDeviceLabel("jos3ph.42", false); + _assertDeviceLabel("joseph1.42", false); + _assertDeviceLabel("Joseph.42", false); } - function test_isLitePersonLabel_rejects_a_missing_or_repeated_separator() public view { - _assertLite("joseph42", false); - _assertLite("jos.eph.42", false); - _assertLite(".42", false); - _assertLite("joseph.", false); + function test_isDeviceLabel_rejects_a_missing_or_repeated_separator() public view { + _assertDeviceLabel("joseph42", false); + _assertDeviceLabel("jos.eph.42", false); + _assertDeviceLabel(".42", false); + _assertDeviceLabel("joseph.", false); } - function test_isLitePersonLabel_rejects_any_suffix_but_two_digits() public view { - _assertLite("joseph.4", false); - _assertLite("joseph.123", false); - _assertLite("joseph.4x", false); - _assertLite("joseph.x4", false); + function test_isDeviceLabel_rejects_any_suffix_but_two_digits() public view { + _assertDeviceLabel("joseph.4", false); + _assertDeviceLabel("joseph.123", false); + _assertDeviceLabel("joseph.4x", false); + _assertDeviceLabel("joseph.x4", false); } - function test_isLitePersonLabel_rejects_a_non_canonical_stem() public view { - _assertLite("jos_eph.42", false); - _assertLite("-joseph.42", false); - _assertLite("joseph-.42", false); - _assertLite(string.concat(_stem(64), ".42"), false); + function test_isDeviceLabel_rejects_a_non_canonical_stem() public view { + _assertDeviceLabel("jos_eph.42", false); + _assertDeviceLabel("-joseph.42", false); + _assertDeviceLabel("joseph-.42", false); + _assertDeviceLabel(string.concat(_stem(64), ".42"), false); } - function test_isLitePersonLabel_rejects_the_empty_string() public view { - _assertLite("", false); + function test_isDeviceLabel_rejects_the_empty_string() public view { + _assertDeviceLabel("", false); } - /// @dev The bound moved from the whole label to the stem, so a lite label runs three octets + /// @dev The bound moved from the whole label to the stem, so a device name runs three octets /// past the single-label limit. Pinned because it is a deliberate widening. - function test_isLitePersonLabel_bounds_the_stem_not_the_whole_label() public view { + function test_isDeviceLabel_bounds_the_stem_not_the_whole_label() public view { string memory longest = string.concat(_stem(63), ".42"); - assertEq(bytes(longest).length, 66, "longest lite label is 66 octets"); - _assertLite(longest, true); - assertFalse(utils.isSingleLabel(longest), "a lite label is not a single DNS label"); + assertEq(bytes(longest).length, 66, "longest device name is 66 octets"); + _assertDeviceLabel(longest, true); + assertFalse(utils.isSingleLabel(longest), "a device name is not a single DNS label"); + } + + /// @notice A path of exactly @custom:constant StringUtils.MAX_NAME_PATH_OCTETS octets passes. + /// @dev The per-segment bound leaves the path itself unbounded, and `setSubnodeOwner` stores + /// the full name composed from a caller's path in a `LabelStore` row that has no delete + /// path. The two cases below are what fix that ceiling: moving the constant without + /// moving them is a failing build. + function test_isNamePath_accepts_a_path_at_the_ceiling() public view { + string memory segment = _stem(63); + string memory path = string.concat(segment, ".", segment, ".", segment, ".", segment); + assertEq(bytes(path).length, StringUtils.MAX_NAME_PATH_OCTETS, "fixture is not at the cap"); + assertTrue(utils.isNamePath(path)); + } + + /// @notice One octet over the ceiling is refused, with every segment individually legal so + /// the refusal cannot come from the per-segment bound. + function test_isNamePath_rejects_a_path_one_octet_over_the_ceiling() public view { + string memory segment = _stem(63); + string memory path = + string.concat(segment, ".", segment, ".", segment, ".", _stem(62), ".", _stem(1)); + assertEq( + bytes(path).length, StringUtils.MAX_NAME_PATH_OCTETS + 1, "fixture is not one over" + ); + assertFalse(utils.isNamePath(path)); } }