Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/revive-bench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: pallet-revive Benchmark

# Dry-runs the critical dotNS paths on pallet-revive against live Paseo Asset Hub Next and Polkadot
# Asset Hub state, under the limit each one runs with in production. Read-only: nothing is signed
# or submitted, and no secret is used. `deployed` measures the contracts live on the network,
# `build` this checkout's implementations upgraded into the same state.
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
inputs:
fail-at:
description: Fail a path above this share of its limit, in percent
default: "95"

permissions:
contents: read

concurrency:
group: revive-bench-${{ github.ref }}
cancel-in-progress: true

jobs:
bench:
name: ${{ matrix.network }} (${{ matrix.target }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
network: [paseo, polkadot]
target: [deployed, build]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: ./.github/actions/setup-foundry
if: matrix.target == 'build'

- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2.6"
no-cache: true

# The postinstall (`setup.bash`) fetches `lib/`, which only the build needs.
- run: bun install ${{ matrix.target == 'deployed' && '--ignore-scripts' || '' }}

- run: forge build
if: matrix.target == 'build'

- name: Run benchmark
env:
FAIL_AT: ${{ inputs.fail-at || '95' }}
run: |
bun scripts/bench/revive-bench.mjs \
--network ${{ matrix.network }} \
${{ matrix.target == 'build' && '--build' || '' }} \
--fail-at "$FAIL_AT" \
--json revive-bench-${{ matrix.network }}-${{ matrix.target }}.json

- uses: actions/upload-artifact@v4
if: always()
with:
name: revive-bench-${{ matrix.network }}-${{ matrix.target }}
path: revive-bench-${{ matrix.network }}-${{ matrix.target }}.json
if-no-files-found: ignore
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,17 @@ While a fork test is in flight, skip it with:
forge test --no-match-path 'test/fork/**'
```

### pallet-revive benchmark

Foundry tests run on its in-process EVM, which does not meter weight and proof size the way pallet-revive does. `scripts/bench/revive-bench.mjs` measures the critical paths on pallet-revive itself: gateway issuance, reservations and claims, settlement, `expireReservation`, and the largest governance batches. It dry-runs each one as a Root batch against live Asset Hub state and compares it with the limit it runs under: the gateway's `MaxContractCallWeight`, the extrinsic limit for a user's own call, or the scheduler's limit for a referendum. Nothing is signed or submitted.

```bash
bun scripts/bench/revive-bench.mjs --network paseo # contracts live on the network
forge build && bun scripts/bench/revive-bench.mjs --network paseo --build # this checkout
```

`--build` upgrades every proxy and store beacon to the implementations in `out/` inside the same dry run, so a change can be measured before it ships. A path fails above `--fail-at` percent of its limit (95 by default). Paths expected to fail for now are listed in the script's `KNOWN_FAILURES`, each with what clears it: they are reported as known, and the run fails on any other failure, or when a listed path passes, so the list stays current. The `pallet-revive Benchmark` workflow runs both modes on Paseo Asset Hub Next and Polkadot Asset Hub every Monday, and on demand.

### Cleanup checklist before merging an upgrade PR

1. Delete the upgrade script under `scripts/deploy/`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ Addresses are recorded per network in `deployments/<network>/<chain-id>.json`, a

### Build and test

Builds and tests are run with Foundry. Fork tests use the local Paseo Asset Hub adapter described in [DEPLOYMENTS.md](./DEPLOYMENTS.md); ordinary unit, fuzz, and invariant tests run against Foundry's in-process EVM.
Builds and tests are run with Foundry. Fork tests use the local Paseo Asset Hub adapter described in [DEPLOYMENTS.md](./DEPLOYMENTS.md); ordinary unit, fuzz, and invariant tests run against Foundry's in-process EVM. A weekly benchmark measures the critical paths on pallet-revive against live Asset Hub state; see [CONTRIBUTING.md](./CONTRIBUTING.md#pallet-revive-benchmark).

## Security

Expand Down
115 changes: 115 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

20 changes: 12 additions & 8 deletions contracts/utils/DotnsConstants.sol

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was wondering what happens when a name has more claims on the live network, would these upper bound still hold?

The cap only stops new requests, so a name with ~30+ existing claims couldn’t be granted in one call. Should we check live claimantCount before, or note that governance would need to reject some first?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also can we add respective dev comments where required with reason like so, "one call fits a referendum’s weight limit" or smthg like this??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Right, the cap only stops new requests, so a name that collected more claims under a higher cap needs governance to reject some first, one per call, before a grant fits. That's now in the setMaxClaimants NatSpec. Paseo has 4 active names; I'll check Polkadot's claim counts before its upgrade.

Each bound also has a @dev note now: one governance call at the bound still fits the weight a referendum can execute, and the benchmark checks it.

Original file line number Diff line number Diff line change
Expand Up @@ -69,26 +69,30 @@ library DotnsConstants {

/// @notice Default per-name live-claim cap the name whitelist starts with.
/// @dev Governance retunes it on the whitelist within `WHITELIST_MAX_CLAIMANTS_LIMIT`.
uint16 internal constant WHITELIST_DEFAULT_MAX_CLAIMANTS = 64;
uint16 internal constant WHITELIST_DEFAULT_MAX_CLAIMANTS = 24;

/// @notice Default claim-reason byte cap the name whitelist starts with.
/// @dev Governance retunes it on the whitelist within `WHITELIST_MAX_REASON_LIMIT`.
uint256 internal constant WHITELIST_DEFAULT_MAX_REASON_BYTES = 256;

/// @notice Upper bound on the whitelist live-claim cap. Caps the claim clear-loop below the
/// block gas limit.
uint16 internal constant WHITELIST_MAX_CLAIMANTS_LIMIT = 128;
/// @notice Upper bound on the whitelist live-claim cap.
/// @dev A grant clears every claim on the name in one call, so at this bound a referendum's
/// `grantName` still fits the weight one governance call can execute, with each claim
/// carrying the longest reason. The pallet-revive benchmark checks it.
uint16 internal constant WHITELIST_MAX_CLAIMANTS_LIMIT = 30;

/// @notice Upper bound on the whitelist reason byte cap.
uint256 internal constant WHITELIST_MAX_REASON_LIMIT = 256;

/// @notice Default cap on labels granted in one `grantNames` call.
/// @dev Governance retunes it on the whitelist within `WHITELIST_MAX_GRANT_BATCH_LIMIT`.
uint16 internal constant WHITELIST_DEFAULT_MAX_GRANT_BATCH = 100;
uint16 internal constant WHITELIST_DEFAULT_MAX_GRANT_BATCH = 32;

/// @notice Upper bound on the `grantNames` batch cap. Bounds one call below the block gas
/// limit.
uint16 internal constant WHITELIST_MAX_GRANT_BATCH_LIMIT = 256;
/// @notice Upper bound on the `grantNames` batch cap.
/// @dev At this bound a referendum's `grantNames` still fits the weight one governance call
/// can execute, with every label at the longest DNS label length, since the whitelist
/// stores each label it grants. The pallet-revive benchmark checks it.
uint16 internal constant WHITELIST_MAX_GRANT_BATCH_LIMIT = 36;

/// @notice Well-known key the protocol registry is registered under, itself.
/// @dev Not for discovery: the registry is the bootstrap contract every consumer already
Expand Down
3 changes: 3 additions & 0 deletions contracts/whitelist/IDotnsNameWhitelist.sol
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ interface IDotnsNameWhitelist {
/// @notice Sets the live-claim cap per name.
/// @dev Restricted to a substrate Root dispatch. The cap is bounded by
/// `DotnsConstants.WHITELIST_MAX_CLAIMANTS_LIMIT`, which bounds the resolution clear-loop.
/// The cap only stops new requests: a name that collected more claims under a higher cap
/// keeps them, and governance rejects some with @custom:function reject before a
/// @custom:function grantName on it fits one call.
/// @custom:reverts MaxClaimantsOutOfRange when `newMax` is zero or above the ceiling.
/// @custom:emits MaxClaimantsSet.
/// @param newMax New per-name claim cap.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"bun": ">=1.2.6"
},
"devDependencies": {
"@polkadot/api": "16.5.6",
"cross-env": "^10.1.0",
"dotenv": "^16.4.5",
"prettier": "^2.6.2",
Expand Down
Loading
Loading