From 0110a94978ee3e39bd26070cc14174a048c7c340 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:38:29 +0200 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=F0=9F=94=A7=20Fix=20invalid=20`runs?= =?UTF-8?q?-on`=20label=20in=20Publish=20runtime=20task=20(#207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/task-publish-runtime.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/task-publish-runtime.yml b/.github/workflows/task-publish-runtime.yml index 7d41ef4a..8b7742c7 100644 --- a/.github/workflows/task-publish-runtime.yml +++ b/.github/workflows/task-publish-runtime.yml @@ -46,7 +46,8 @@ jobs: build-srtool-runtimes: needs: ["setup-scripts", "read-rust-version"] - runs-on: DH-runners + runs-on: + group: DH-runners permissions: contents: read strategy: From b4f697f9546b49f33ffc9a38a2ccb2832b1dd82c Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:12:55 +0200 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20=E2=99=BB=EF=B8=8F=20=20Update=20R?= =?UTF-8?q?EADME=20files=20(#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> --- README.md | 317 +++++++++++++++++++++++++++++++------------- contracts/README.md | 67 ++++++++-- deploy/README.md | 69 +++++++--- operator/README.md | 177 ++++++++++++++++++++++--- test/README.md | 124 +++++++++++++++-- tools/README.md | 86 ++++++++++++ 6 files changed, 693 insertions(+), 147 deletions(-) create mode 100644 tools/README.md diff --git a/README.md b/README.md index 72ee20fc..fc846154 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,190 @@ # DataHaven 🫎 -An EVM compatible Substrate chain, powered by StorageHub and secured by EigenLayer. +An EVM-compatible Substrate blockchain secured by EigenLayer, bridging Ethereum and Substrate ecosystems through trustless cross-chain communication. -## Repo Structure +## Overview + +DataHaven is an EigenLayer Actively Validated Service (AVS) that combines: + +- **EVM Compatibility**: Full Ethereum support via Frontier pallets for smart contracts and dApps +- **EigenLayer Security**: Validator set secured by Ethereum's economic security through restaking +- **Cross-chain Bridge**: Seamless asset and message transfers with Ethereum via Snowbridge +- **Dynamic Validators**: Operator registry managed on-chain through EigenLayer contracts +- **Performance Rewards**: Validator incentives distributed cross-chain from Ethereum + +## Architecture + +DataHaven bridges two major blockchain ecosystems: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Ethereum (L1) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ EigenLayer AVS Contracts β”‚ β”‚ +β”‚ β”‚ β€’ DataHavenServiceManager (operator lifecycle) β”‚ β”‚ +β”‚ β”‚ β€’ RewardsRegistry (performance tracking) β”‚ β”‚ +β”‚ β”‚ β€’ VetoableSlasher (misbehavior penalties) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ ↕ β”‚ +β”‚ Snowbridge Protocol β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + ↕ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ DataHaven (Substrate) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Custom Pallets β”‚ β”‚ +β”‚ β”‚ β€’ External Validators (sync validator set) β”‚ β”‚ +β”‚ β”‚ β€’ Native Transfer (cross-chain tokens) β”‚ β”‚ +β”‚ β”‚ β€’ Rewards (distribute validator rewards) β”‚ β”‚ +β”‚ β”‚ β€’ Frontier (EVM compatibility) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Repository Structure + +``` +datahaven/ +β”œβ”€β”€ contracts/ # EigenLayer AVS smart contracts +β”‚ β”œβ”€β”€ src/ # Service Manager, Rewards Registry, Slasher +β”‚ β”œβ”€β”€ script/ # Deployment scripts +β”‚ └── test/ # Foundry test suites +β”œβ”€β”€ operator/ # Substrate-based DataHaven node +β”‚ β”œβ”€β”€ node/ # Node implementation & chain spec +β”‚ β”œβ”€β”€ pallets/ # Custom pallets (validators, rewards, transfers) +β”‚ └── runtime/ # Runtime configurations (mainnet/stagenet/testnet) +β”œβ”€β”€ test/ # E2E testing framework +β”‚ β”œβ”€β”€ suites/ # Integration test scenarios +β”‚ β”œβ”€β”€ framework/ # Test utilities and helpers +β”‚ └── launcher/ # Network deployment automation +β”œβ”€β”€ deploy/ # Kubernetes deployment charts +β”‚ β”œβ”€β”€ charts/ # Helm charts for nodes and relayers +β”‚ └── environments/ # Environment-specific configurations +β”œβ”€β”€ tools/ # GitHub automation and release scripts +└── .github/ # CI/CD workflows +``` + +Each directory contains its own README with detailed information. See: +- [contracts/README.md](contracts/README.md) - Smart contract development +- [operator/README.md](operator/README.md) - Node building and runtime development +- [test/README.md](test/README.md) - E2E testing and network deployment +- [deploy/README.md](deploy/README.md) - Kubernetes deployment +- [tools/README.md](tools/README.md) - Development tools + +## Quick Start + +### Prerequisites + +- [Kurtosis](https://docs.kurtosis.com/install) - Network orchestration +- [Bun](https://bun.sh/) v1.2+ - TypeScript runtime +- [Docker](https://www.docker.com/) - Container management +- [Foundry](https://getfoundry.sh/) - Solidity toolkit +- [Rust](https://www.rust-lang.org/tools/install) - For building the operator +- [Helm](https://helm.sh/) - Kubernetes deployments (optional) +- [Zig](https://ziglang.org/) - For macOS cross-compilation (macOS only) + +### Launch Local Network + +The fastest way to get started is with the interactive CLI: ```bash -datahaven/ -β”œβ”€β”€ .github/ # GitHub Actions workflows. -β”œβ”€β”€ contracts/ # Implementation of the DataHaven AVS (Autonomous Verifiable Service) smart contracts to interact with EigenLayer. -β”œβ”€β”€ operator/ # DataHaven node based on Substrate. The "Operator" in EigenLayer terms. -β”œβ”€β”€ test/ # Integration tests for the AVS and Operator. -β”œβ”€β”€ resources/ # Miscellaneous resources for the DataHaven project. -└── README.md +cd test +bun i # Install dependencies +bun cli launch # Interactive launcher with prompts ``` -## E2E CLI +This deploys a complete environment including: +- **Ethereum network**: 2x EL clients (reth), 2x CL clients (lodestar) +- **Block explorers**: Blockscout (optional), Dora consensus explorer +- **DataHaven node**: Single validator with fast block times +- **AVS contracts**: Deployed and configured on Ethereum +- **Snowbridge relayers**: Bidirectional message passing -This repo comes with a CLI for launching a local DataHaven network, packaged with: +For more options and detailed instructions, see the [test README](./test/README.md). -1. A full Ethereum network with: - - 2 x Execution Layer clients (e.g., reth) - - 2 x Consensus Layer clients (e.g., lodestar) - - Blockscout Explorer services for EL (if enabled with --blockscout) - - Dora Explorer service for CL - - Contracts deployed and configured for the DataHaven network. -2. A DataHaven solochain. -3. Snowbridge relayers for cross-chain communication. +### Run Tests -To launch the network, follow the instructions in the [test README](./test/README.md). - -## Docker - -This repo publishes images to [DockerHub](https://hub.docker.com/r/datahavenxyz/datahaven). - -> [!TIP] -> -> If you cannot see this repo you must be added to the permission list for the private repo. - -To aid with speed it employs the following: - -- [sccache](https://github.com/mozilla/sccache/tree/main): De-facto caching tool to speed up rust builds. -- [cargo chef](https://lpalmieri.com/posts/fast-rust-docker-builds/): A method of caching building the dependencies as a docker layer to cut down compilation times. -- [buildx cache mounts](https://docs.docker.com/build/cache/optimize/#use-cache-mounts): Using buildx's new feature to mount an externally restored cache into a container. -- [cache dance](https://github.com/reproducible-containers/buildkit-cache-dance): Weird workaround (endorsed by docker themselves) to inject caches into containers and return the result back to the CI. - -To run a docker image locally (`datahavenxyz/datahaven:local`), from the `/test` folder run: - -```sh -bun build:docker:operator +```bash +cd test +bun test:e2e # Run all integration tests +bun test:e2e:parallel # Run with limited concurrency ``` -## Working with IDEs +### Development Workflows -### VS Code (and its forks) +**Smart Contract Development**: +```bash +cd contracts +forge build # Compile contracts +forge test # Run contract tests +``` -IDE configurations are ignored from this repo's version control, to allow for personalisation. However, there are a few key configurations that we suggest for a better experience. Here are the key suggested configurations to add to your `.vscode/settings.json` file: +**Node Development**: +```bash +cd operator +cargo build --release --features fast-runtime +cargo test +./scripts/run-benchmarks.sh +``` -#### Rust +**After Making Changes**: +```bash +cd test +bun generate:wagmi # Regenerate contract bindings +bun generate:types # Regenerate runtime types +``` +## Key Features + +### EVM Compatibility +Full Ethereum Virtual Machine support via Frontier pallets: +- Deploy Solidity smart contracts +- Use existing Ethereum tooling (MetaMask, Hardhat, etc.) +- Compatible with ERC-20, ERC-721, and other standards + +### EigenLayer Integration +Validator security anchored to Ethereum: +- Operators register via `DataHavenServiceManager` contract +- Economic security through ETH restaking +- Slashing protection with veto period via `VetoableSlasher` +- Performance-based rewards through `RewardsRegistry` + +### Cross-chain Communication +Trustless bridging via Snowbridge: +- Native token transfers between Ethereum ↔ DataHaven +- Cross-chain message passing +- Finality proofs via BEEFY consensus +- Three specialized relayers (beacon, BEEFY, execution) + +### Dynamic Validator Set +Validator management synchronized with Ethereum: +- EigenLayer operator registry as source of truth +- On-chain validator set updates via External Validators pallet +- Automatic consensus participation changes +- Cross-chain coordination for validator lifecycle + +## Docker Images + +Production images published to [DockerHub](https://hub.docker.com/r/datahavenxyz/datahaven). + +**Build optimizations**: +- [sccache](https://github.com/mozilla/sccache) - Rust compilation caching +- [cargo-chef](https://lpalmieri.com/posts/fast-rust-docker-builds/) - Dependency layer caching +- [BuildKit cache mounts](https://docs.docker.com/build/cache/optimize/#use-cache-mounts) - External cache restoration + +**Build locally**: +```bash +cd test +bun build:docker:operator # Creates datahavenxyz/datahaven:local +``` + +## Development Environment + +### VS Code Configuration + +IDE configurations are excluded from version control for personalization, but these settings are recommended for optimal developer experience. Add to your `.vscode/settings.json`: + +**Rust Analyzer**: ```json { "rust-analyzer.linkedProjects": ["./operator/Cargo.toml"], @@ -72,17 +199,13 @@ IDE configurations are ignored from this repo's version control, to allow for pe } ``` -These settings optimise Rust Analyzer for the DataHaven codebase: - -- Marks the `operator/` folder as a linked project for analysis. The root of this repo is a workspace, and this is the rust project that should be analysed by `rust-analyzer`. -- Disables proc macros and build scripts to improve performance. Otherwise, Substrate's proc macros will make iterative checks from `rust-analyzer` unbearably slow. -- Sets a dedicated target directory for Rust Analyzer to avoid conflicts with other build targets like `release` builds. -- Disables WASM builds during analysis for faster feedback. - -#### Solidity - -For [Juan Blanco's Solidity Extension](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity), add the following to your `.vscode/settings.json` file: +Optimizations: +- Links `operator/` directory as the primary Rust project +- Disables proc macros and build scripts for faster analysis (Substrate macros are slow) +- Uses dedicated target directory to avoid conflicts +- Skips WASM builds during development +**Solidity** ([Juan Blanco's extension](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity)): ```json { "solidity.formatter": "forge", @@ -93,16 +216,9 @@ For [Juan Blanco's Solidity Extension](https://marketplace.visualstudio.com/item } ``` -These settings configure Solidity support: - -- Uses Forge as the formatter for consistency with the project's tooling. -- Sets a specific Solidity version for compilation. This one should match the version used in [foundry.toml](./contracts/foundry.toml). -- Sets the Solidity extension as the default formatter. - -#### Typescript - -This repo uses [Biome](https://github.com/biomejs/biome) for TypeScript linting and formatting. To make the extension work nicely with this repo, add the following to your `.vscode/settings.json` file: +Note: Solidity version must match [foundry.toml](./contracts/foundry.toml) +**TypeScript** ([Biome](https://github.com/biomejs/biome)): ```json { "biome.lsp.bin": "test/node_modules/.bin/biome", @@ -115,38 +231,59 @@ This repo uses [Biome](https://github.com/biomejs/biome) for TypeScript linting } ``` -- Sets the Biome binary to the one in the `test/` folder. -- Sets Biome as the default formatter for TypeScript. -- Sets Biome to always organise imports on save. +## CI/CD -## CI +### Local CI Testing -Using the [act](https://github.com/nektos/act) binary, you can run GitHub Actions locally. - -For example, to run the entire `e2e` workflow: +Run GitHub Actions workflows locally using [act](https://github.com/nektos/act): ```bash +# Run E2E workflow act -W .github/workflows/e2e.yml -s GITHUB_TOKEN="$(gh auth token)" + +# Run specific job +act -W .github/workflows/e2e.yml -j test-job-name ``` -Which results in: +### Automated Workflows -```bash -INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' -INFO[0000] Start server on http://192.168.1.97:34567 -[E2E - Kurtosis Deploy and Verify/kurtosis] ⭐ Run Set up job -[E2E - Kurtosis Deploy and Verify/kurtosis] πŸš€ Start image=catthehacker/ubuntu:rust-24.04 -[E2E - Kurtosis Deploy and Verify/kurtosis] 🐳 docker pull image=catthehacker/ubuntu:rust-24.04 platform= username= forcePull=true -[E2E - Kurtosis Deploy and Verify/kurtosis] using DockerAuthConfig authentication for docker pull -[E2E - Kurtosis Deploy and Verify/kurtosis] 🐳 docker create image=catthehacker/ubuntu:rust-24.04 platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host" -[E2E - Kurtosis Deploy and Verify/kurtosis] 🐳 docker run image=catthehacker/ubuntu:rust-24.04 platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host" -[E2E - Kurtosis Deploy and Verify/kurtosis] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= -[E2E - Kurtosis Deploy and Verify/kurtosis] βœ… Success - Set up job -[E2E - Kurtosis Deploy and Verify/kurtosis] ☁ git clone 'https://github.com/oven-sh/setup-bun' # ref=v2 -... -[E2E - Kurtosis Deploy and Verify/kurtosis] βœ… Success - Post Install Foundry [212.864597ms] -[E2E - Kurtosis Deploy and Verify/kurtosis] ⭐ Run Complete job -[E2E - Kurtosis Deploy and Verify/kurtosis] Cleaning up container for job kurtosis -[E2E - Kurtosis Deploy and Verify/kurtosis] βœ… Success - Complete job -[E2E - Kurtosis Deploy and Verify/kurtosis] 🏁 Job succeeded -``` +The repository includes GitHub Actions for: +- **E2E Testing**: Full integration tests on PR and main branch +- **Contract Testing**: Foundry test suites for smart contracts +- **Rust Testing**: Unit and integration tests for operator +- **Docker Builds**: Multi-platform image builds with caching +- **Release Automation**: Version tagging and changelog generation + +See `.github/workflows/` for workflow definitions. + +## Contributing + +### Development Cycle + +1. **Make Changes**: Edit contracts, runtime, or tests +2. **Run Tests**: Component-specific tests (`forge test`, `cargo test`) +3. **Regenerate Types**: Update bindings if contracts/runtime changed +4. **Integration Test**: Run E2E tests to verify cross-component behavior +5. **Code Quality**: Format and lint (`cargo fmt`, `forge fmt`, `bun fmt:fix`) + +### Common Pitfalls + +- **Type mismatches**: Regenerate with `bun generate:types` after runtime changes +- **Contract changes not reflected**: Run `bun generate:wagmi` after modifications +- **Kurtosis issues**: Ensure Docker is running and Kurtosis engine is started +- **Slow development**: Use `--features fast-runtime` for faster block times +- **Network launch hangs**: Check Blockscout - forge output can appear frozen + +See [CLAUDE.md](./CLAUDE.md) for detailed development guidance. + +## License + +GPL-3.0 - See LICENSE file for details + +## Links + +- [EigenLayer Documentation](https://docs.eigenlayer.xyz/) +- [Substrate Documentation](https://docs.substrate.io/) +- [Snowbridge Documentation](https://docs.snowbridge.network/) +- [Foundry Book](https://book.getfoundry.sh/) +- [Polkadot-API Documentation](https://papi.how/) diff --git a/contracts/README.md b/contracts/README.md index ca651b3c..d3d03221 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -4,18 +4,31 @@ This directory contains the smart contracts for the DataHaven Actively Validated ## Overview -DataHaven is an AVS that provides secure and decentralised data storage services. The contracts in this repository implement the Service Manager, middleware, and associated utilities required for the DataHaven protocol. +DataHaven is an EVM-compatible Substrate blockchain secured by EigenLayer. These contracts implement the AVS Service Manager, middleware, and associated utilities that integrate with EigenLayer's operator registration, slashing, and rewards infrastructure. ## Project Structure -- `src/`: Smart contract source code - - `DataHavenServiceManager.sol`: Main service manager contract - - `interfaces/`: Contract interfaces - - `libraries/`: Utility libraries - - `middleware/`: Middleware contracts (similar to EigenLayer's [middleware contracts](https://github.com/Layr-Labs/eigenlayer-middleware)) -- `script/`: Deployment scripts -- `test/`: Test cases -- `foundry.toml`: Foundry configuration +``` +contracts/ +β”œβ”€β”€ src/ # Smart contract source code +β”‚ β”œβ”€β”€ DataHavenServiceManager.sol # Core AVS service manager +β”‚ β”œβ”€β”€ RewardsRegistry.sol # Validator performance & rewards tracking +β”‚ β”œβ”€β”€ VetoableSlasher.sol # Slashing with veto period +β”‚ β”œβ”€β”€ interfaces/ # Contract interfaces +β”‚ β”œβ”€β”€ libraries/ # Utility libraries +β”‚ └── middleware/ # EigenLayer middleware integration +β”œβ”€β”€ script/ # Deployment & setup scripts +β”‚ └── deploy/ # Environment-specific deployment +β”œβ”€β”€ test/ # Foundry test suites +└── foundry.toml # Foundry configuration +``` + +### Key Contracts + +- **DataHavenServiceManager**: Manages operator lifecycle, registration, and deregistration with EigenLayer +- **RewardsRegistry**: Tracks validator performance metrics and handles reward distribution via Snowbridge +- **VetoableSlasher**: Implements slashing mechanism with dispute resolution veto period +- **Middleware**: Integration layer with EigenLayer's core contracts (based on [eigenlayer-middleware](https://github.com/Layr-Labs/eigenlayer-middleware)) ## Prerequisites @@ -52,16 +65,22 @@ For maximum verbosity including stack traces: forge test -vvvv ``` -Run specific test suites: +Run specific test contracts: ```bash -forge test --match-contract RewardsRegistry --no-match-contract SnowbridgeIntegration +forge test --match-contract RewardsRegistry ``` -Run specific tests: +Run specific test functions: ```bash -forge test --match-test test_getRewardstest_newRewardsMessage --no-match-test test_newRewardsMessage_OnlyRewardsAgent +forge test --match-test test_newRewardsMessage +``` + +Exclude specific tests: + +```bash +forge test --no-match-test test_newRewardsMessage_OnlyRewardsAgent ``` ## Deployment @@ -102,3 +121,25 @@ The deployment configuration can be modified in: - `script/deploy/Config.sol`: Environment-specific configuration - `script/deploy/DeployParams.s.sol`: Deployment parameters + +## Code Generation + +After making changes to contracts, regenerate TypeScript bindings for the test framework: + +```bash +cd ../test +bun generate:wagmi +``` + +This generates type-safe contract interfaces used by the E2E test suite. + +## Integration with DataHaven + +These contracts integrate with the DataHaven Substrate node through: + +1. **Operator Registration**: Validators register on-chain via `DataHavenServiceManager` +2. **Performance Tracking**: Node submits validator metrics to `RewardsRegistry` +3. **Cross-chain Rewards**: Rewards distributed from Ethereum to DataHaven via Snowbridge +4. **Slashing**: Misbehavior triggers slashing through `VetoableSlasher` with veto period + +For full network integration testing, see the [test directory](../test/README.md). diff --git a/deploy/README.md b/deploy/README.md index 93eea2f1..8be4be4f 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,6 +1,6 @@ # DataHaven Deployment -This directory contains all the necessary files and configurations for deploying DataHaven to various environments. +This directory contains Helm charts and configurations for deploying DataHaven nodes and relayers to Kubernetes clusters across various environments (local development, staging, production). ## Directory Structure @@ -50,16 +50,20 @@ Available environments: ## Environment Details ### Local -- Single replica -- Minimal resources (256Mi memory, 100m CPU) -- Local image tags -- Small persistence size +- **Purpose**: Local development and testing +- **Replicas**: 1 (bootnode + validator) +- **Resources**: Minimal (256Mi memory, 100m CPU) +- **Image**: Local Docker builds (`datahavenxyz/datahaven:local`) +- **Storage**: Small persistence (1-5Gi) +- **Network**: Single-node network with fast block times ### Stagenet -- 2 replicas -- Medium resources (512Mi memory, 200m CPU) -- Stagenet image tags -- 20Gi persistence size +- **Purpose**: Pre-production testing and staging +- **Replicas**: 2+ validators +- **Resources**: Medium (512Mi memory, 200m CPU) +- **Image**: Stagenet tags from DockerHub +- **Storage**: 20Gi+ persistent volumes +- **Network**: Multi-validator network simulating production ## Configuration Structure @@ -87,9 +91,44 @@ The deployment process: - **Bootnode**: Entry point for the network - **Validator**: Validates transactions and produces blocks -### Relays -- **Snowbridge Relays**: Handle cross-chain communication with Ethereum - - **Beacon Relay**: Relays Ethereum beacon chain data - - **BEEFY Relay**: Relays BEEFY consensus data for finality - - **Execution Relay**: Relays Ethereum execution layer data -- **Solochain Relayers**: Handle standalone chain operations and cross-chain communication +### Relays (Snowbridge) +- **Beacon Relay**: Relays Ethereum beacon chain finality to DataHaven +- **BEEFY Relay**: Relays DataHaven BEEFY finality proofs to Ethereum +- **Execution Relay**: Relays Ethereum execution layer messages to DataHaven +- **Solochain Relayers**: Relays DataHaven chain operations to the DataHaven AVS + +These relayers enable trustless bidirectional token and message passing between Ethereum and DataHaven. + +## Development Workflow + +1. **Local Testing**: + ```bash + cd test + bun cli launch # Starts local network without K8s + ``` + +2. **K8s Deployment**: + ```bash + cd test + bun cli deploy --e local + ``` + +3. **Building Local Images**: + ```bash + cd test + bun build:docker:operator # Builds datahavenxyz/datahaven:local + ``` + +4. **Updating Configurations**: + - Modify `environments//values.yaml` for environment-specific changes + - Modify chart templates in `charts/` for structural changes + - Redeploy with `bun cli deploy --e ` + +## Troubleshooting + +- **Pods not starting**: Check logs with `kubectl logs ` +- **Image pull failures**: Verify Docker registry access and image tags +- **Persistent volume issues**: Ensure storage class is available with `kubectl get sc` +- **Network connectivity**: Check service endpoints with `kubectl get svc` + +For more detailed deployment and testing workflows, see the [test directory](../test/README.md). diff --git a/operator/README.md b/operator/README.md index 086df50f..319b0309 100644 --- a/operator/README.md +++ b/operator/README.md @@ -1,6 +1,88 @@ -# DataHaven 🫎 +# DataHaven Operator (Substrate Node) 🫎 -Based on [polkadot-sdk-solochain-template](https://github.com/paritytech/polkadot-sdk-solochain-template) +The DataHaven operator is a Substrate-based blockchain node that serves as an EigenLayer AVS operator. It combines Substrate's modular framework with EVM compatibility (via Frontier) and cross-chain capabilities (via Snowbridge). + +## Overview + +Built on the [polkadot-sdk-solochain-template](https://github.com/paritytech/polkadot-sdk-solochain-template), this node implements: + +- **EVM Compatibility**: Full Ethereum compatibility via Frontier pallets +- **EigenLayer Integration**: Operator registration and management via AVS contracts +- **External Validators**: Dynamic validator set controlled by EigenLayer registry +- **Cross-chain Communication**: Token and message passing via Snowbridge +- **Rewards System**: Performance-based validator rewards from Ethereum + +## Project Structure + +``` +operator/ +β”œβ”€β”€ node/ # Node implementation +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ chain_spec.rs # Chain specification & genesis config +β”‚ β”‚ β”œβ”€β”€ cli.rs # CLI interface +β”‚ β”‚ β”œβ”€β”€ command.rs # Command handlers +β”‚ β”‚ β”œβ”€β”€ rpc.rs # RPC configuration +β”‚ β”‚ └── service.rs # Node service setup +β”œβ”€β”€ pallets/ # Custom pallets +β”‚ β”œβ”€β”€ external-validators/ # EigenLayer validator set management +β”‚ β”œβ”€β”€ native-transfer/ # Cross-chain token transfers +β”‚ └── rewards/ # Validator rewards distribution +β”œβ”€β”€ runtime/ # Runtime configurations +β”‚ β”œβ”€β”€ mainnet/ # Mainnet runtime +β”‚ β”œβ”€β”€ stagenet/ # Stagenet runtime +β”‚ └── testnet/ # Testnet runtime (with fast-runtime feature) +└── scripts/ # Utility scripts + └── run-benchmarks.sh # Runtime benchmarking automation +``` + +## Prerequisites + +- [Rust](https://www.rust-lang.org/tools/install) (latest stable) +- [Substrate dependencies](https://docs.substrate.io/install/) +- [Zig](https://ziglang.org/) (macOS only, for cross-compilation) + +## Building + +### Development Build (Fast Runtime) + +For local development with faster block times: + +```bash +cargo build --release --features fast-runtime +``` + +This enables 3-second block times instead of the production 12-second blocks. + +### Production Build + +For production or stagenet deployments: + +```bash +cargo build --release +``` + +### Running Tests + +```bash +# Run all tests +cargo test + +# Run tests for specific pallet +cargo test -p pallet-external-validators + +# Run with output +cargo test -- --nocapture +``` + +### Code Quality + +```bash +# Format code +cargo fmt + +# Lint with clippy +cargo clippy --all-targets --all-features +``` ## Benchmarking @@ -8,13 +90,12 @@ DataHaven uses runtime benchmarking to generate accurate weight calculations for ### Requirements -Make sure you have the lastest rust version - -- `frame-omni-bencher` - Install with: `cargo install frame-omni-bencher --profile=production` (or `cargo install --git https://github.com/paritytech/polkadot-sdk frame-omni-bencher --profile=production --locked`) +- Latest Rust stable version +- `frame-omni-bencher`: Install with `cargo install frame-omni-bencher --profile=production` ### Running Benchmarks -Execute the benchmarking script from the project root: +Execute from the operator directory: ```bash # Benchmark all pallets for testnet runtime (default) @@ -28,23 +109,87 @@ Execute the benchmarking script from the project root: ``` The script will: -1. Automatically discover all available pallets -2. Build the runtime WASM with `runtime-benchmarks` feature +1. Discover all available pallets +2. Build runtime WASM with `runtime-benchmarks` feature 3. Generate weight files in `runtime/{runtime}/src/weights/` -4. Provide a summary of successful and failed benchmarks - -### Script Parameters +4. Provide summary of results +**Parameters**: - `runtime`: Runtime to benchmark (testnet, stagenet, mainnet). Default: testnet -- `steps`: Number of steps for benchmarking. Default: 50 +- `steps`: Number of steps. Default: 50 - `repeat`: Number of repetitions. Default: 20 -## Zombienet testing +## Zombienet Testing -First, install [zombienet](https://github.com/paritytech/zombienet). +[Zombienet](https://github.com/paritytech/zombienet) provides local multi-validator network testing. -To spawn a local solo chain with four validators and BABE finality, run: +### Setup + +1. Install Zombienet: + ```bash + # Download binary from releases + # Or install via npm + npm install -g @zombienet/cli + ``` + +2. Spawn local network with four validators: + ```bash + zombienet -p native spawn test/config/zombie-datahaven-local.toml + ``` + +This launches a local solochain with BABE consensus for testing validator coordination. + +## Docker Image + +Build local Docker image for testing: ```bash -zombienet -p native spawn test/config/zombie-datahaven-local.toml +cd ../test +bun build:docker:operator ``` + +This creates `datahavenxyz/datahaven:local` using optimized caching: +- [sccache](https://github.com/mozilla/sccache): Rust build caching +- [cargo-chef](https://lpalmieri.com/posts/fast-rust-docker-builds/): Dependency layer caching +- BuildKit cache mounts: External cache restoration + +## Type Generation + +After runtime changes, regenerate Polkadot-API TypeScript types: + +```bash +cd ../test +bun generate:types # Production runtime +bun generate:types:fast # Fast runtime (development) +``` + +## Integration Testing + +For full network integration tests with Ethereum, Snowbridge, and contracts: + +```bash +cd ../test +bun cli launch # Interactive launcher +bun test:e2e # Run E2E test suite +``` + +See the [test directory](../test/README.md) for comprehensive testing documentation. + +## Custom Pallets + +### External Validators +Manages the dynamic validator set based on EigenLayer operator registry. Syncs validator changes from Ethereum to the Substrate consensus layer. + +**Location**: `pallets/external-validators/` + +### Native Transfer +Handles cross-chain token transfers between Ethereum and DataHaven via Snowbridge messaging. + +**Location**: `pallets/native-transfer/` + +### Rewards +Distributes performance-based rewards to validators, processing reward messages from the Ethereum `RewardsRegistry` contract. + +**Location**: `pallets/rewards/` + +Each pallet includes its own tests and benchmarks. See pallet-specific README files for details. diff --git a/test/README.md b/test/README.md index ae428f28..a26493ab 100644 --- a/test/README.md +++ b/test/README.md @@ -1,6 +1,8 @@ # DataHaven E2E Testing -Quick start guide for running DataHaven end-to-end tests. For comprehensive documentation, see [E2E Testing Guide](./docs/E2E_TESTING_GUIDE.md). +End-to-end testing framework for DataHaven, providing automated network deployment, contract interaction, and cross-chain scenario testing. This directory contains all tools needed to launch a complete local DataHaven network with Ethereum, Snowbridge relayers, and run comprehensive integration tests. + +For comprehensive documentation, see [E2E Testing Guide](./docs/E2E_TESTING_GUIDE.md). ## Pre-requisites @@ -51,20 +53,68 @@ bun test:e2e:parallel # Run a specific test suite bun test suites/some-test.test.ts - ``` +## What Gets Launched + +The `bun cli launch` command deploys a complete local environment: + +1. **Ethereum Network** (via Kurtosis): + - 2x Execution Layer clients (reth) + - 2x Consensus Layer clients (lodestar) + - Blockscout Explorer (optional: `--blockscout`) + - Dora Consensus Explorer + +2. **DataHaven Network**: + - Single validator solochain + - EVM compatibility via Frontier + - Fast block times (3s with `--fast-runtime`) + +3. **Smart Contracts**: + - EigenLayer AVS contracts deployed to Ethereum + - Optional Blockscout verification (`--verified`) + +4. **Snowbridge Relayers**: + - Beacon relay (Ethereum β†’ DataHaven) + - BEEFY relay (DataHaven β†’ Ethereum) + - Execution relay (Ethereum β†’ DataHaven) + - Solochain relay (DataHaven β†’ Ethereum) + +5. **Network Configuration**: + - Validator registration and funding + - Parameter initialization + - Validator set updates + For more information on the E2E testing framework, see the [E2E Testing Framework Overview](./docs/E2E_FRAMEWORK_OVERVIEW.md). -## Other Common Commands +## Common Commands -| Command | Description | -| ------------------------- | ----------------------------------------------------------------------------------------------------------- | -| `bun cli stop` | Stop all local DataHaven networks (interactive, will ask for confirmation on each component of the network) | -| `bun cli deploy` | Deploy the DataHaven network to a remote Kubernetes cluster | -| `bun generate:wagmi` | Generate contract TypeScript bindings for the contracts in the `contracts` directory | -| `bun generate:types` | Generate Polkadot API types | -| `bun generate:types:fast` | Generate Polkadot API types with the `--fast-runtime` feature enabled | +| Command | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------- | +| **Network Management** | | +| `bun cli` | Interactive CLI menu for all operations | +| `bun cli launch` | Launch full local network (interactive options) | +| `bun start:e2e:local` | Launch local network (non-interactive) | +| `bun start:e2e:verified` | Launch with Blockscout and contract verification | +| `bun start:e2e:ci` | CI-optimized network launch | +| `bun cli stop` | Stop all services (interactive) | +| `bun stop:dh` | Stop DataHaven only | +| `bun stop:sb` | Stop Snowbridge relayers only | +| `bun stop:eth` | Stop Ethereum network only | +| **Testing** | | +| `bun test:e2e` | Run all E2E test suites | +| `bun test:e2e:parallel` | Run tests with limited concurrency | +| `bun test ` | Run specific test file | +| **Code Generation** | | +| `bun generate:wagmi` | Generate TypeScript contract bindings (after contract changes) | +| `bun generate:types` | Generate Polkadot-API types from runtime | +| `bun generate:types:fast` | Generate types with fast-runtime feature | +| **Code Quality** | | +| `bun fmt:fix` | Fix TypeScript formatting with Biome | +| `bun typecheck` | TypeScript type checking | +| **Deployment** | | +| `bun cli deploy` | Deploy to Kubernetes cluster (interactive) | +| `bun build:docker:operator` | Build local Docker image (`datahavenxyz/datahaven:local`) | ## Local Network Deployment @@ -178,8 +228,56 @@ This script will: > > The script uses the `--release` flag by default, meaning it uses the WASM binary from `./operator/target/release`. If you need to use a different build target, you may need to adjust the script or run the steps manually. +## Project Structure + +``` +test/ +β”œβ”€β”€ suites/ # E2E test suites +β”‚ β”œβ”€β”€ contracts.test.ts # Contract deployment & configuration +β”‚ β”œβ”€β”€ cross-chain.test.ts # Cross-chain message passing +β”‚ β”œβ”€β”€ datahaven-substrate.test.ts # Block production & finality +β”‚ β”œβ”€β”€ ethereum-basic.test.ts # Ethereum network validation +β”‚ β”œβ”€β”€ native-token-transfer.test.ts # Cross-chain token transfers +β”‚ β”œβ”€β”€ rewards-message.test.ts # Validator rewards distribution +β”‚ └── validator-set-update.test.ts # Dynamic validator set updates +β”œβ”€β”€ framework/ # Test utilities & helpers +β”‚ β”œβ”€β”€ connectors.ts # Network connectors +β”‚ β”œβ”€β”€ manager.ts # Test environment manager +β”‚ β”œβ”€β”€ suite.ts # Test suite utilities +β”‚ └── index.ts # Framework exports +β”œβ”€β”€ launcher/ # Network deployment tools +β”‚ β”œβ”€β”€ kurtosis/ # Ethereum network launcher +β”‚ β”œβ”€β”€ snowbridge/ # Relayer management +β”‚ └── datahaven/ # DataHaven node management +β”œβ”€β”€ generated/ # Generated types +β”‚ β”œβ”€β”€ wagmi/ # Contract bindings +β”‚ └── polkadot-api/ # Runtime types +└── docs/ # Testing documentation + β”œβ”€β”€ E2E_TESTING_GUIDE.md + └── E2E_FRAMEWORK_OVERVIEW.md +``` + +## Test Suites + +- **contracts.test.ts**: Contract deployment and configuration validation +- **cross-chain.test.ts**: Cross-chain message passing between Ethereum and DataHaven +- **datahaven-substrate.test.ts**: Block production, finalization, and consensus +- **ethereum-basic.test.ts**: Ethereum network health and basic functionality +- **native-token-transfer.test.ts**: Cross-chain token transfers via Snowbridge +- **rewards-message.test.ts**: Validator reward distribution from Ethereum to DataHaven +- **validator-set-update.test.ts**: Dynamic validator registration/deregistration via EigenLayer + +Run individual suites: +```bash +bun test suites/rewards-message.test.ts +bun test suites/native-token-transfer.test.ts +bun test suites/validator-set-update.test.ts +``` + ## Further Information -- [Kurtosis](https://docs.kurtosis.com/): Used for launching a full Ethereum network -- [Zombienet](https://paritytech.github.io/zombienet/): Used for launching a Polkadot-SDK based network -- [Bun](https://bun.sh/): TypeScript runtime and ecosystem tooling +- [Kurtosis](https://docs.kurtosis.com/): Ethereum network orchestration +- [Zombienet](https://paritytech.github.io/zombienet/): Polkadot-SDK network testing +- [Bun](https://bun.sh/): TypeScript runtime and tooling +- [Foundry](https://book.getfoundry.sh/): Solidity development framework +- [Polkadot-API](https://papi.how/): Type-safe Substrate interactions diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..9735b232 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,86 @@ +# DataHaven Development Tools + +Utility scripts for GitHub automation and release management. + +## Overview + +This directory contains Node.js/TypeScript tools for automating release workflows and generating standardized release notes for both runtime and client releases. + +## Structure + +``` +tools/ +β”œβ”€β”€ github/ # GitHub automation utilities +β”‚ β”œβ”€β”€ github-utils.ts # Common GitHub API utilities +β”‚ β”œβ”€β”€ generate-release-body.ts # Client release notes generator +β”‚ └── generate-runtime-body.ts # Runtime release notes generator +β”œβ”€β”€ package.json # Dependencies and scripts +└── tsconfig.json # TypeScript configuration +``` + +## Prerequisites + +- Node.js (version specified in `.nvmrc`) +- npm (for dependency management) + +## Setup + +```bash +cd tools +npm install +``` + +## Available Scripts + +### Generate Client Release Notes + +Creates formatted release notes for client (node binary) releases: + +```bash +npm run print-client-release-issue +``` + +This script: +- Generates changelog from Git history +- Formats release notes for GitHub +- Includes version information and breaking changes +- Outputs markdown suitable for GitHub releases + +### Generate Runtime Release Notes + +Creates formatted release notes for runtime (WASM) releases: + +```bash +npm run print-runtime-release-issue +``` + +This script: +- Generates runtime-specific changelog +- Highlights runtime version bumps +- Includes migration information +- Formats for GitHub release pages + +## Usage in CI/CD + +These tools are typically invoked by GitHub Actions workflows during the release process. They can also be run manually for testing or preparing draft releases. + +## GitHub Integration + +The tools use the [Octokit](https://github.com/octokit/octokit.js) library to interact with the GitHub API. Authentication is typically handled via `GITHUB_TOKEN` environment variable in CI/CD contexts. + +## Development + +The tools are written in TypeScript and use: +- `@polkadot/api`: For parsing runtime metadata +- `octokit`: For GitHub API interactions +- `yargs`: For CLI argument parsing +- `ts-node`: For direct TypeScript execution + +## Customization + +To modify release note formatting or add new automation: + +1. Edit the respective generator in `github/` +2. Update `package.json` scripts if adding new commands +3. Test locally with `npm run ` +4. Update CI workflows in `.github/workflows/` if needed From ac09a4f2bb87ed05976ce9703a52529f704c8c66 Mon Sep 17 00:00:00 2001 From: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:00:10 +0200 Subject: [PATCH 3/4] feat: Add SafeMode and TxPause Pallets (#192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Overview This PR integrates the `pallet-safe-mode` and `pallet-tx-pause` from Polkadot SDK to provide comprehensive emergency governance controls across all DataHaven runtime networks (mainnet, stagenet, testnet). ### Key Changes #### πŸ”§ **Core Integration** - **Dependencies**: Added `pallet-safe-mode` and `pallet-tx-pause` from `polkadot-stable2412-6` - **Runtime Integration**: Integrated both pallets across all three runtime networks with pallet indices 103 and 104 - **Call Filtering**: Implemented unified `RuntimeCallFilter` that combines Normal, SafeMode, and TxPause restrictions #### πŸ›‘οΈ **SafeMode Pallet Configuration** - **Duration**: 1 day activation period (`DAYS` constant) - **Deposits**: Disabled permissionless entry/extension (all `None`) - **Origins**: Root-only for all force operations (`force_enter`, `force_exit`, `force_extend`, etc.) - **Whitelisting**: SafeMode and Sudo calls are immune to restrictions #### ⏸️ **TxPause Pallet Configuration** - **Origins**: Root-only pause/unpause control - **Whitelisting**: SafeMode and Sudo calls cannot be paused - **Max Call Name Length**: 256 characters #### πŸ—οΈ **Architecture** - **Shared Types**: Created `operator/runtime/common/src/safe_mode.rs` with reusable configurations - **Combined Filtering**: `RuntimeCallFilter` applies all three filter layers (Normal + SafeMode + TxPause) - **Consistent Config**: Identical configuration across mainnet, stagenet, and testnet #### πŸ“Š **Infrastructure Updates** - **Benchmarking**: Added both pallets to benchmark suites across all networks - **Weight Mappings**: Placeholder weights using Substrate defaults (ready for chain-specific benchmarking) - **Metadata**: Updated runtime metadata for new pallet exposure #### πŸ§ͺ **Testing Framework** - **Coverage**: Tests for individual pallet behavior, combined restrictions, whitelisting, and edge cases ### Emergency Control Capabilities **SafeMode Pallet** (8 calls): - User calls: `enter`, `extend`, `release_deposit` - Force calls: `force_enter`, `force_exit`, `force_extend`, `force_slash_deposit`, `force_release_deposit` **TxPause Pallet** (2 calls): - `pause_call` / `unpause_call` - Granular transaction type pausing --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> --- operator/Cargo.lock | 43 ++ operator/Cargo.toml | 2 + operator/runtime/common/Cargo.toml | 6 + operator/runtime/common/src/lib.rs | 2 + operator/runtime/common/src/safe_mode.rs | 73 +++ operator/runtime/mainnet/Cargo.toml | 8 + operator/runtime/mainnet/src/benchmarks.rs | 2 + operator/runtime/mainnet/src/configs/mod.rs | 74 ++- operator/runtime/mainnet/src/lib.rs | 6 + operator/runtime/mainnet/src/weights/mod.rs | 2 + .../mainnet/src/weights/pallet_safe_mode.rs | 7 + .../mainnet/src/weights/pallet_tx_pause.rs | 7 + operator/runtime/mainnet/tests/lib.rs | 1 + .../mainnet/tests/safe_mode_tx_pause.rs | 431 ++++++++++++++ operator/runtime/stagenet/Cargo.toml | 8 + operator/runtime/stagenet/src/benchmarks.rs | 2 + operator/runtime/stagenet/src/configs/mod.rs | 74 ++- operator/runtime/stagenet/src/lib.rs | 6 + operator/runtime/stagenet/src/weights/mod.rs | 2 + .../stagenet/src/weights/pallet_safe_mode.rs | 7 + .../stagenet/src/weights/pallet_tx_pause.rs | 7 + operator/runtime/stagenet/tests/lib.rs | 1 + .../stagenet/tests/safe_mode_tx_pause.rs | 432 ++++++++++++++ operator/runtime/testnet/Cargo.toml | 8 + operator/runtime/testnet/src/benchmarks.rs | 2 + operator/runtime/testnet/src/configs/mod.rs | 74 ++- operator/runtime/testnet/src/lib.rs | 6 + operator/runtime/testnet/src/weights/mod.rs | 2 + .../testnet/src/weights/pallet_safe_mode.rs | 7 + .../testnet/src/weights/pallet_tx_pause.rs | 7 + operator/runtime/testnet/tests/lib.rs | 1 + .../testnet/tests/safe_mode_tx_pause.rs | 535 ++++++++++++++++++ test/.papi/descriptors/package.json | 2 +- test/.papi/metadata/datahaven.scale | Bin 608513 -> 616961 bytes 34 files changed, 1834 insertions(+), 13 deletions(-) create mode 100644 operator/runtime/common/src/safe_mode.rs create mode 100644 operator/runtime/mainnet/src/weights/pallet_safe_mode.rs create mode 100644 operator/runtime/mainnet/src/weights/pallet_tx_pause.rs create mode 100644 operator/runtime/mainnet/tests/safe_mode_tx_pause.rs create mode 100644 operator/runtime/stagenet/src/weights/pallet_safe_mode.rs create mode 100644 operator/runtime/stagenet/src/weights/pallet_tx_pause.rs create mode 100644 operator/runtime/stagenet/tests/safe_mode_tx_pause.rs create mode 100644 operator/runtime/testnet/src/weights/pallet_safe_mode.rs create mode 100644 operator/runtime/testnet/src/weights/pallet_tx_pause.rs create mode 100644 operator/runtime/testnet/tests/safe_mode_tx_pause.rs diff --git a/operator/Cargo.lock b/operator/Cargo.lock index 1447cbcb..d402defa 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -2892,6 +2892,7 @@ dependencies = [ "pallet-proxy", "pallet-randomness", "pallet-referenda", + "pallet-safe-mode", "pallet-scheduler", "pallet-session", "pallet-storage-providers", @@ -2901,6 +2902,7 @@ dependencies = [ "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", + "pallet-tx-pause", "pallet-utility", "pallet-whitelist", "parity-scale-codec", @@ -3081,7 +3083,9 @@ dependencies = [ "pallet-evm", "pallet-evm-precompile-proxy", "pallet-migrations", + "pallet-safe-mode", "pallet-treasury", + "pallet-tx-pause", "parity-scale-codec", "polkadot-primitives", "polkadot-runtime-common", @@ -3166,6 +3170,7 @@ dependencies = [ "pallet-proxy", "pallet-randomness", "pallet-referenda", + "pallet-safe-mode", "pallet-scheduler", "pallet-session", "pallet-storage-providers", @@ -3175,6 +3180,7 @@ dependencies = [ "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", + "pallet-tx-pause", "pallet-utility", "pallet-whitelist", "parity-scale-codec", @@ -3306,6 +3312,7 @@ dependencies = [ "pallet-proxy", "pallet-randomness", "pallet-referenda", + "pallet-safe-mode", "pallet-scheduler", "pallet-session", "pallet-storage-providers", @@ -3315,6 +3322,7 @@ dependencies = [ "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", + "pallet-tx-pause", "pallet-utility", "pallet-whitelist", "parity-scale-codec", @@ -9589,6 +9597,24 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-safe-mode" +version = "20.0.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2412-6#bbc435c7667d3283ba280a8fec44676357392753" +dependencies = [ + "docify", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-proxy", + "pallet-utility", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-runtime", +] + [[package]] name = "pallet-scheduler" version = "40.2.0" @@ -9789,6 +9815,23 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-tx-pause" +version = "20.0.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2412-6#bbc435c7667d3283ba280a8fec44676357392753" +dependencies = [ + "docify", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-proxy", + "pallet-utility", + "parity-scale-codec", + "scale-info", + "sp-runtime", +] + [[package]] name = "pallet-utility" version = "39.1.0" diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 83412fba..48f0c731 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -121,6 +121,8 @@ pallet-multisig = { git = "https://github.com/paritytech/polkadot-sdk", tag = "p pallet-offences = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-parameters = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-preimage = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } +pallet-safe-mode = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } +pallet-tx-pause = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2412-6", default-features = false } pallet-collective = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-conviction-voting = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } diff --git a/operator/runtime/common/Cargo.toml b/operator/runtime/common/Cargo.toml index 777b04d6..baeceea0 100644 --- a/operator/runtime/common/Cargo.toml +++ b/operator/runtime/common/Cargo.toml @@ -14,6 +14,8 @@ pallet-balances = { workspace = true } pallet-evm = { workspace = true } pallet-evm-precompile-proxy = { workspace = true } pallet-migrations = { workspace = true } +pallet-safe-mode = { workspace = true } +pallet-tx-pause = { workspace = true } pallet-treasury = { workspace = true } polkadot-primitives = { workspace = true } polkadot-runtime-common = { workspace = true } @@ -34,6 +36,8 @@ std = [ "pallet-evm/std", "pallet-evm-precompile-proxy/std", "pallet-migrations/std", + "pallet-safe-mode/std", + "pallet-tx-pause/std", "pallet-treasury/std", "polkadot-primitives/std", "polkadot-runtime-common/std", @@ -48,6 +52,8 @@ std = [ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "pallet-migrations/runtime-benchmarks", + "pallet-safe-mode/runtime-benchmarks", + "pallet-tx-pause/runtime-benchmarks", "polkadot-primitives/runtime-benchmarks", "polkadot-runtime-common/runtime-benchmarks", "sp-runtime/runtime-benchmarks", diff --git a/operator/runtime/common/src/lib.rs b/operator/runtime/common/src/lib.rs index b823e419..92c20e98 100644 --- a/operator/runtime/common/src/lib.rs +++ b/operator/runtime/common/src/lib.rs @@ -24,6 +24,8 @@ pub mod deal_with_fees; pub mod impl_on_charge_evm_transaction; pub mod migrations; pub use migrations::*; +pub mod safe_mode; +pub use safe_mode::*; use fp_account::EthereumSignature; pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; diff --git a/operator/runtime/common/src/safe_mode.rs b/operator/runtime/common/src/safe_mode.rs new file mode 100644 index 00000000..8c5c88bd --- /dev/null +++ b/operator/runtime/common/src/safe_mode.rs @@ -0,0 +1,73 @@ +// Copyright 2019-2025 DataHaven Inc. +// This file is part of DataHaven. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// DataHaven is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +//! Safe Mode and Tx Pause shared types, constants, and utilities + +use crate::time::DAYS; +use crate::Balance; +use frame_support::{parameter_types, traits::Contains}; +use pallet_tx_pause::RuntimeCallNameOf; +use polkadot_primitives::BlockNumber; +use sp_std::marker::PhantomData; + +// Safe Mode Constants +parameter_types! { + /// Default duration for safe mode activation (1 day) + pub const SafeModeDuration: BlockNumber = DAYS; + pub const SafeModeEnterDeposit: Option = None; + /// Safe mode extend deposit - None disables permissionless extend + pub const SafeModeExtendDeposit: Option = None; + /// Release delay - None disables permissionless release + pub const ReleaseDelayNone: Option = None; +} + +/// Calls that cannot be paused by the tx-pause pallet. +pub struct TxPauseWhitelistedCalls(PhantomData); +/// Whitelist `Balances::transfer_keep_alive`, all others are pauseable. +impl Contains> for TxPauseWhitelistedCalls +where + R: pallet_tx_pause::Config, +{ + fn contains(full_name: &RuntimeCallNameOf) -> bool { + match (full_name.0.as_slice(), full_name.1.as_slice()) { + // sudo calls + (b"Sudo", _) => true, + // SafeMode calls + (b"SafeMode", _) => true, + _ => false, + } + } +} + +/// Combined Call Filter that applies Normal, SafeMode, and TxPause filters +/// This filter is generic over the runtime call type and identical across all runtimes +pub struct RuntimeCallFilter( + PhantomData<(Call, NormalFilter, SafeModeFilter, TxPauseFilter)>, +); + +impl Contains + for RuntimeCallFilter +where + NormalFilter: Contains, + SafeModeFilter: Contains, + TxPauseFilter: Contains, +{ + fn contains(call: &Call) -> bool { + NormalFilter::contains(call) + && SafeModeFilter::contains(call) + && TxPauseFilter::contains(call) + } +} diff --git a/operator/runtime/mainnet/Cargo.toml b/operator/runtime/mainnet/Cargo.toml index 8e402059..6c877e14 100644 --- a/operator/runtime/mainnet/Cargo.toml +++ b/operator/runtime/mainnet/Cargo.toml @@ -64,6 +64,8 @@ pallet-outbound-commitment-store = { workspace = true } pallet-datahaven-native-transfer = { workspace = true } pallet-parameters = { workspace = true } pallet-preimage = { workspace = true } +pallet-safe-mode = { workspace = true } +pallet-tx-pause = { workspace = true } pallet-proxy = { workspace = true } pallet-referenda = { workspace = true } pallet-scheduler = { workspace = true } @@ -211,6 +213,8 @@ std = [ "pallet-offences/std", "pallet-parameters/std", "pallet-preimage/std", + "pallet-safe-mode/std", + "pallet-tx-pause/std", "pallet-referenda/std", "pallet-proxy/std", "pallet-scheduler/std", @@ -310,6 +314,8 @@ runtime-benchmarks = [ "pallet-offences/runtime-benchmarks", "pallet-parameters/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", + "pallet-safe-mode/runtime-benchmarks", + "pallet-tx-pause/runtime-benchmarks", "pallet-referenda/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", @@ -358,6 +364,8 @@ try-runtime = [ "pallet-offences/try-runtime", "pallet-parameters/try-runtime", "pallet-preimage/try-runtime", + "pallet-safe-mode/try-runtime", + "pallet-tx-pause/try-runtime", "pallet-referenda/try-runtime", "pallet-proxy/try-runtime", "pallet-scheduler/try-runtime", diff --git a/operator/runtime/mainnet/src/benchmarks.rs b/operator/runtime/mainnet/src/benchmarks.rs index c0683a0f..3677711d 100644 --- a/operator/runtime/mainnet/src/benchmarks.rs +++ b/operator/runtime/mainnet/src/benchmarks.rs @@ -53,6 +53,8 @@ frame_benchmarking::define_benchmarks!( [pallet_transaction_payment, TransactionPayment] [pallet_parameters, Parameters] [pallet_message_queue, MessageQueue] + [pallet_safe_mode, SafeMode] + [pallet_tx_pause, TxPause] // EVM pallets [pallet_evm, Evm] diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index 11edcc24..8b2f8e63 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -35,8 +35,8 @@ use super::{ ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations, Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, - RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury, - BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, + RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury, + TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, }; use codec::{Decode, Encode, MaxEncodedLen}; @@ -91,6 +91,10 @@ use datahaven_runtime_common::{ FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen, MigrationIdentifierMaxLen, MigrationStatusHandler, }, + safe_mode::{ + ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit, + SafeModeExtendDeposit, TxPauseWhitelistedCalls, + }, time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK}, }; use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor}; @@ -233,6 +237,36 @@ impl Contains for NormalCallFilter { } } +/// Calls that can bypass the safe-mode pallet. +/// These calls are essential for emergency governance and system maintenance. +pub struct SafeModeWhitelistedCalls; +impl Contains for SafeModeWhitelistedCalls { + fn contains(call: &RuntimeCall) -> bool { + match call { + // Core system calls + RuntimeCall::System(_) => true, + // Safe mode management + RuntimeCall::SafeMode(_) => true, + // Transaction pause management + RuntimeCall::TxPause(_) => true, + // Emergency admin access (testnet/dev only) + RuntimeCall::Sudo(_) => true, + // Governance infrastructure - critical for emergency responses + RuntimeCall::Whitelist(_) => true, + RuntimeCall::Preimage(_) => true, + RuntimeCall::Scheduler(_) => true, + RuntimeCall::ConvictionVoting(_) => true, + RuntimeCall::Referenda(_) => true, + RuntimeCall::TechnicalCommittee(_) => true, + RuntimeCall::TreasuryCouncil(_) => true, + _ => false, + } + } +} + +pub type MainnetRuntimeCallFilter = + RuntimeCallFilter; + /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from /// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`), /// but overridden as needed. @@ -265,8 +299,8 @@ impl frame_system::Config for Runtime { type MaxConsumers = frame_support::traits::ConstU32<16>; type SystemWeightInfo = mainnet_weights::frame_system::WeightInfo; type MultiBlockMigrator = MultiBlockMigrations; - /// Use the NormalCallFilter to restrict certain runtime calls - type BaseCallFilter = NormalCallFilter; + /// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions + type BaseCallFilter = MainnetRuntimeCallFilter; } // 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks. @@ -1480,6 +1514,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime { type WeightInfo = mainnet_weights::pallet_datahaven_native_transfer::WeightInfo; } +//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ +//β•‘ SAFE MODE & TX PAUSE PALLETS β•‘ +//β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• + +impl pallet_safe_mode::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type RuntimeHoldReason = RuntimeHoldReason; + type WhitelistedCalls = SafeModeWhitelistedCalls; + type EnterDuration = SafeModeDuration; + type ExtendDuration = SafeModeDuration; + type EnterDepositAmount = SafeModeEnterDeposit; + type ExtendDepositAmount = SafeModeExtendDeposit; + type ForceEnterOrigin = EnsureRootWithSuccess; + type ForceExtendOrigin = EnsureRootWithSuccess; + type ForceExitOrigin = EnsureRoot; + type ForceDepositOrigin = EnsureRoot; + type ReleaseDelay = ReleaseDelayNone; + type Notify = (); + type WeightInfo = mainnet_weights::pallet_safe_mode::WeightInfo; +} + +impl pallet_tx_pause::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PauseOrigin = EnsureRoot; + type UnpauseOrigin = EnsureRoot; + type WhitelistedCalls = TxPauseWhitelistedCalls; + type MaxNameLen = ConstU32<256>; + type WeightInfo = mainnet_weights::pallet_tx_pause::WeightInfo; +} + #[cfg(test)] mod tests { use super::*; diff --git a/operator/runtime/mainnet/src/lib.rs b/operator/runtime/mainnet/src/lib.rs index 954238e5..726c9ae0 100644 --- a/operator/runtime/mainnet/src/lib.rs +++ b/operator/runtime/mainnet/src/lib.rs @@ -378,6 +378,12 @@ mod runtime { #[runtime::pallet_index(39)] pub type MultiBlockMigrations = pallet_migrations; + + #[runtime::pallet_index(103)] + pub type SafeMode = pallet_safe_mode; + + #[runtime::pallet_index(104)] + pub type TxPause = pallet_tx_pause; // β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• Polkadot SDK Utility Pallets ══════════════════╝ // ╔═════════════════════════ Governance Pallets ════════════════════╗ diff --git a/operator/runtime/mainnet/src/weights/mod.rs b/operator/runtime/mainnet/src/weights/mod.rs index d314d39c..9428a40f 100644 --- a/operator/runtime/mainnet/src/weights/mod.rs +++ b/operator/runtime/mainnet/src/weights/mod.rs @@ -41,11 +41,13 @@ pub mod pallet_multisig; pub mod pallet_parameters; pub mod pallet_preimage; pub mod pallet_proxy; +pub mod pallet_safe_mode; pub mod pallet_scheduler; pub mod pallet_sudo; pub mod pallet_timestamp; pub mod pallet_transaction_payment; pub mod pallet_treasury; +pub mod pallet_tx_pause; pub mod pallet_utility; // Governance pallets diff --git a/operator/runtime/mainnet/src/weights/pallet_safe_mode.rs b/operator/runtime/mainnet/src/weights/pallet_safe_mode.rs new file mode 100644 index 00000000..e7a54199 --- /dev/null +++ b/operator/runtime/mainnet/src/weights/pallet_safe_mode.rs @@ -0,0 +1,7 @@ +// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks. +// +// We reuse the upstream Substrate weight assumptions which are conservative enough for +// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate +// weights in this module via the runtime benchmarking CLI. + +pub type WeightInfo = pallet_safe_mode::weights::SubstrateWeight; diff --git a/operator/runtime/mainnet/src/weights/pallet_tx_pause.rs b/operator/runtime/mainnet/src/weights/pallet_tx_pause.rs new file mode 100644 index 00000000..28676e27 --- /dev/null +++ b/operator/runtime/mainnet/src/weights/pallet_tx_pause.rs @@ -0,0 +1,7 @@ +// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks. +// +// We reuse the upstream Substrate weight assumptions which are conservative enough for +// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in +// this module via the runtime benchmarking CLI. + +pub type WeightInfo = pallet_tx_pause::weights::SubstrateWeight; diff --git a/operator/runtime/mainnet/tests/lib.rs b/operator/runtime/mainnet/tests/lib.rs index c6d5eaef..e94b5930 100644 --- a/operator/runtime/mainnet/tests/lib.rs +++ b/operator/runtime/mainnet/tests/lib.rs @@ -5,6 +5,7 @@ pub mod governance; mod migrations; mod native_token_transfer; mod proxy; +mod safe_mode_tx_pause; use common::*; use datahaven_mainnet_runtime::{ diff --git a/operator/runtime/mainnet/tests/safe_mode_tx_pause.rs b/operator/runtime/mainnet/tests/safe_mode_tx_pause.rs new file mode 100644 index 00000000..d7a56d78 --- /dev/null +++ b/operator/runtime/mainnet/tests/safe_mode_tx_pause.rs @@ -0,0 +1,431 @@ +#![allow(clippy::too_many_arguments)] + +#[path = "common.rs"] +mod common; + +use common::{account_id, ExtBuilder, ALICE, BOB}; +use datahaven_mainnet_runtime::{ + Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, System, UncheckedExtrinsic, +}; +use frame_support::{assert_noop, assert_ok, BoundedVec}; +use pallet_safe_mode::EnteredUntil; +use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf}; +use sp_runtime::{ + traits::Dispatchable, + transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, +}; +use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3; + +fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf { + use frame_support::traits::GetCallMetadata; + let metadata = call.get_call_metadata(); + ( + BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(), + BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(), + ) +} + +fn transfer_call(amount: u128) -> RuntimeCall { + RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: account_id(BOB), + value: amount, + }) +} + +mod safe_mode { + use super::*; + + #[test] + fn force_enter_requires_root() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + assert_noop!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + sp_runtime::DispatchError::BadOrigin + ); + + assert!(EnteredUntil::::get().is_some()); + System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::< + Runtime, + >::Entered { + until: EnteredUntil::::get().unwrap(), + })); + }); + } + + #[test] + fn active_safe_mode_blocks_non_whitelisted_calls() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let xt = transfer_call(1u128); + let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into()); + let validity = Runtime::validate_transaction( + TransactionSource::External, + unchecked_xt, + Default::default(), + ); + assert_eq!( + validity, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + }); + } + + #[test] + fn whitelisted_calls_dispatch_in_safe_mode() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + + assert!(EnteredUntil::::get().is_none()); + }); + } +} + +mod tx_pause { + use super::*; + + #[test] + fn pause_requires_root() { + ExtBuilder::default().build().execute_with(|| { + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + assert_noop!( + RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + sp_runtime::DispatchError::BadOrigin + ); + + assert_ok!( + RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name }) + .dispatch(RuntimeOrigin::root()) + ); + }); + } + + #[test] + fn paused_call_is_blocked() { + ExtBuilder::default().build().execute_with(|| { + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + assert_eq!( + Runtime::validate_transaction(TransactionSource::External, xt, Default::default()), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_noop!( + call.clone() + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + frame_system::Error::::CallFiltered + ); + + assert_ok!( + RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name }) + .dispatch(RuntimeOrigin::root()) + ); + + // After unpause, the call should be dispatchable + assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE)))); + }); + } + + #[test] + fn whitelisted_call_cannot_be_paused() { + ExtBuilder::default().build().execute_with(|| { + let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}); + let call_name = call_name(&call); + + assert_noop!( + RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root()), + TxPauseError::::Unpausable + ); + }); + } +} + +mod combined_behaviour { + use super::*; + + #[test] + fn dual_restrictions_require_both_to_clear() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + let validity = Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default(), + ); + assert_eq!( + validity, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { + ident: call_name.clone() + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + let still_blocked = Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default(), + ); + assert_eq!( + still_blocked, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + + // After exiting safe mode and unpausing, call should be dispatchable + assert_ok!(call + .clone() + .dispatch(RuntimeOrigin::signed(account_id(ALICE)))); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.into()); + assert_eq!( + Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default() + ), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + }); + } + + #[test] + fn control_plane_calls_work_under_restrictions() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { + ident: call_name.clone() + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + }); + } + + #[test] + fn governance_whitelisted_calls_work_during_safe_mode() { + use sp_core::H256; + + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .with_balances(vec![(account_id(ALICE), 1_000_000_000_000)]) + .build() + .execute_with(|| { + // Enter safe mode + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + // Verify safe mode is active + assert!(EnteredUntil::::get().is_some()); + + // Verify normal calls are blocked during safe mode + let normal_call = transfer_call(100); + assert_noop!( + normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))), + frame_system::Error::::CallFiltered + ); + + // Test Whitelist pallet - critical for emergency runtime upgrades + let call_hash = H256::random(); + assert_ok!( + RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash }) + .dispatch(RuntimeOrigin::root()) + ); + + // Test Preimage pallet - required for storing governance call data + let dummy_preimage = vec![1u8; 32]; + let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage { + bytes: dummy_preimage, + }) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))); + + match preimage_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Preimage calls should not be filtered by safe mode" + ); + } + } + + // Test Scheduler pallet - needed for time-delayed governance actions + let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel { + when: 100, + index: 0, + }) + .dispatch(RuntimeOrigin::root()); + + match scheduler_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Scheduler calls should not be filtered by safe mode" + ); + } + } + + // Test Referenda pallet - core OpenGov proposal system + let referenda_result = + RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 }) + .dispatch(RuntimeOrigin::root()); + + match referenda_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Referenda calls should not be filtered by safe mode" + ); + } + } + + // Test ConvictionVoting - allows token holders to vote during emergencies + let voting_result = RuntimeCall::ConvictionVoting( + pallet_conviction_voting::Call::remove_other_vote { + target: account_id(BOB), + class: 0, + index: 0, + }, + ) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))); + + match voting_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "ConvictionVoting calls should not be filtered by safe mode" + ); + } + } + + // Test TechnicalCommittee - expert oversight for emergency actions + let tech_committee_result = + RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members { + new_members: vec![account_id(ALICE)], + prime: None, + old_count: 0, + }) + .dispatch(RuntimeOrigin::root()); + + match tech_committee_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "TechnicalCommittee calls should not be filtered by safe mode" + ); + } + } + }); + } +} diff --git a/operator/runtime/stagenet/Cargo.toml b/operator/runtime/stagenet/Cargo.toml index 49882ac1..1d7a9ff1 100644 --- a/operator/runtime/stagenet/Cargo.toml +++ b/operator/runtime/stagenet/Cargo.toml @@ -64,6 +64,8 @@ pallet-outbound-commitment-store = { workspace = true } pallet-datahaven-native-transfer = { workspace = true } pallet-parameters = { workspace = true } pallet-preimage = { workspace = true } +pallet-safe-mode = { workspace = true } +pallet-tx-pause = { workspace = true } pallet-proxy = { workspace = true } pallet-referenda = { workspace = true } pallet-scheduler = { workspace = true } @@ -212,6 +214,8 @@ std = [ "pallet-offences/std", "pallet-parameters/std", "pallet-preimage/std", + "pallet-safe-mode/std", + "pallet-tx-pause/std", "pallet-referenda/std", "pallet-proxy/std", "pallet-scheduler/std", @@ -311,6 +315,8 @@ runtime-benchmarks = [ "pallet-offences/runtime-benchmarks", "pallet-parameters/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", + "pallet-safe-mode/runtime-benchmarks", + "pallet-tx-pause/runtime-benchmarks", "pallet-referenda/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", @@ -359,6 +365,8 @@ try-runtime = [ "pallet-offences/try-runtime", "pallet-parameters/try-runtime", "pallet-preimage/try-runtime", + "pallet-safe-mode/try-runtime", + "pallet-tx-pause/try-runtime", "pallet-referenda/try-runtime", "pallet-proxy/try-runtime", "pallet-scheduler/try-runtime", diff --git a/operator/runtime/stagenet/src/benchmarks.rs b/operator/runtime/stagenet/src/benchmarks.rs index 7e752f52..22db3d3c 100644 --- a/operator/runtime/stagenet/src/benchmarks.rs +++ b/operator/runtime/stagenet/src/benchmarks.rs @@ -53,6 +53,8 @@ frame_benchmarking::define_benchmarks!( [pallet_transaction_payment, TransactionPayment] [pallet_parameters, Parameters] [pallet_message_queue, MessageQueue] + [pallet_safe_mode, SafeMode] + [pallet_tx_pause, TxPause] // Governance pallets [pallet_collective_technical_committee, TechnicalCommittee] diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index 6df2639f..049a20c3 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -35,8 +35,8 @@ use super::{ ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations, Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, - RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury, - BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, + RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury, + TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, }; use codec::{Decode, Encode, MaxEncodedLen}; @@ -91,6 +91,10 @@ use datahaven_runtime_common::{ FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen, MigrationIdentifierMaxLen, MigrationStatusHandler, }, + safe_mode::{ + ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit, + SafeModeExtendDeposit, TxPauseWhitelistedCalls, + }, time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK}, }; use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor}; @@ -233,6 +237,36 @@ impl Contains for NormalCallFilter { } } +/// Calls that can bypass the safe-mode pallet. +/// These calls are essential for emergency governance and system maintenance. +pub struct SafeModeWhitelistedCalls; +impl Contains for SafeModeWhitelistedCalls { + fn contains(call: &RuntimeCall) -> bool { + match call { + // Core system calls + RuntimeCall::System(_) => true, + // Safe mode management + RuntimeCall::SafeMode(_) => true, + // Transaction pause management + RuntimeCall::TxPause(_) => true, + // Emergency admin access (testnet/dev only) + RuntimeCall::Sudo(_) => true, + // Governance infrastructure - critical for emergency responses + RuntimeCall::Whitelist(_) => true, + RuntimeCall::Preimage(_) => true, + RuntimeCall::Scheduler(_) => true, + RuntimeCall::ConvictionVoting(_) => true, + RuntimeCall::Referenda(_) => true, + RuntimeCall::TechnicalCommittee(_) => true, + RuntimeCall::TreasuryCouncil(_) => true, + _ => false, + } + } +} + +pub type StagenetRuntimeCallFilter = + RuntimeCallFilter; + /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from /// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`), /// but overridden as needed. @@ -265,8 +299,8 @@ impl frame_system::Config for Runtime { type MaxConsumers = frame_support::traits::ConstU32<16>; type SystemWeightInfo = stagenet_weights::frame_system::WeightInfo; type MultiBlockMigrator = MultiBlockMigrations; - /// Use the NormalCallFilter to restrict certain runtime calls - type BaseCallFilter = NormalCallFilter; + /// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions + type BaseCallFilter = StagenetRuntimeCallFilter; } // 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks. @@ -1481,6 +1515,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime { type WeightInfo = stagenet_weights::pallet_datahaven_native_transfer::WeightInfo; } +//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ +//β•‘ SAFE MODE & TX PAUSE PALLETS β•‘ +//β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• + +impl pallet_safe_mode::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type RuntimeHoldReason = RuntimeHoldReason; + type WhitelistedCalls = SafeModeWhitelistedCalls; + type EnterDuration = SafeModeDuration; + type ExtendDuration = SafeModeDuration; + type EnterDepositAmount = SafeModeEnterDeposit; + type ExtendDepositAmount = SafeModeExtendDeposit; + type ForceEnterOrigin = EnsureRootWithSuccess; + type ForceExtendOrigin = EnsureRootWithSuccess; + type ForceExitOrigin = EnsureRoot; + type ForceDepositOrigin = EnsureRoot; + type ReleaseDelay = ReleaseDelayNone; + type Notify = (); + type WeightInfo = stagenet_weights::pallet_safe_mode::WeightInfo; +} + +impl pallet_tx_pause::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PauseOrigin = EnsureRoot; + type UnpauseOrigin = EnsureRoot; + type WhitelistedCalls = TxPauseWhitelistedCalls; + type MaxNameLen = ConstU32<256>; + type WeightInfo = stagenet_weights::pallet_tx_pause::WeightInfo; +} + #[cfg(test)] mod tests { use super::*; diff --git a/operator/runtime/stagenet/src/lib.rs b/operator/runtime/stagenet/src/lib.rs index d536edd9..3407f555 100644 --- a/operator/runtime/stagenet/src/lib.rs +++ b/operator/runtime/stagenet/src/lib.rs @@ -380,6 +380,12 @@ mod runtime { #[runtime::pallet_index(39)] pub type MultiBlockMigrations = pallet_migrations; + + #[runtime::pallet_index(103)] + pub type SafeMode = pallet_safe_mode; + + #[runtime::pallet_index(104)] + pub type TxPause = pallet_tx_pause; // β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• Polkadot SDK Utility Pallets ══════════════════╝ // ╔═════════════════════════ Governance Pallets ════════════════════╗ diff --git a/operator/runtime/stagenet/src/weights/mod.rs b/operator/runtime/stagenet/src/weights/mod.rs index b39064f7..a430d426 100644 --- a/operator/runtime/stagenet/src/weights/mod.rs +++ b/operator/runtime/stagenet/src/weights/mod.rs @@ -43,11 +43,13 @@ pub mod pallet_multisig; pub mod pallet_parameters; pub mod pallet_preimage; pub mod pallet_proxy; +pub mod pallet_safe_mode; pub mod pallet_scheduler; pub mod pallet_sudo; pub mod pallet_timestamp; pub mod pallet_transaction_payment; pub mod pallet_treasury; +pub mod pallet_tx_pause; pub mod pallet_utility; // Governance pallets diff --git a/operator/runtime/stagenet/src/weights/pallet_safe_mode.rs b/operator/runtime/stagenet/src/weights/pallet_safe_mode.rs new file mode 100644 index 00000000..e7a54199 --- /dev/null +++ b/operator/runtime/stagenet/src/weights/pallet_safe_mode.rs @@ -0,0 +1,7 @@ +// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks. +// +// We reuse the upstream Substrate weight assumptions which are conservative enough for +// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate +// weights in this module via the runtime benchmarking CLI. + +pub type WeightInfo = pallet_safe_mode::weights::SubstrateWeight; diff --git a/operator/runtime/stagenet/src/weights/pallet_tx_pause.rs b/operator/runtime/stagenet/src/weights/pallet_tx_pause.rs new file mode 100644 index 00000000..28676e27 --- /dev/null +++ b/operator/runtime/stagenet/src/weights/pallet_tx_pause.rs @@ -0,0 +1,7 @@ +// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks. +// +// We reuse the upstream Substrate weight assumptions which are conservative enough for +// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in +// this module via the runtime benchmarking CLI. + +pub type WeightInfo = pallet_tx_pause::weights::SubstrateWeight; diff --git a/operator/runtime/stagenet/tests/lib.rs b/operator/runtime/stagenet/tests/lib.rs index 4213f411..3766015d 100644 --- a/operator/runtime/stagenet/tests/lib.rs +++ b/operator/runtime/stagenet/tests/lib.rs @@ -4,6 +4,7 @@ pub mod common; pub mod governance; mod native_token_transfer; mod proxy; +mod safe_mode_tx_pause; use common::*; use datahaven_stagenet_runtime::{ diff --git a/operator/runtime/stagenet/tests/safe_mode_tx_pause.rs b/operator/runtime/stagenet/tests/safe_mode_tx_pause.rs new file mode 100644 index 00000000..9f55a04b --- /dev/null +++ b/operator/runtime/stagenet/tests/safe_mode_tx_pause.rs @@ -0,0 +1,432 @@ +#![allow(clippy::too_many_arguments)] + +#[path = "common.rs"] +mod common; + +use common::{account_id, ExtBuilder, ALICE, BOB}; +use datahaven_stagenet_runtime::{ + Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SafeMode, System, TxPause, + UncheckedExtrinsic, +}; +use frame_support::{assert_noop, assert_ok, BoundedVec}; +use pallet_safe_mode::EnteredUntil; +use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf}; +use sp_runtime::{ + traits::Dispatchable, + transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, +}; +use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3; + +fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf { + use frame_support::traits::GetCallMetadata; + let metadata = call.get_call_metadata(); + ( + BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(), + BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(), + ) +} + +fn transfer_call(amount: u128) -> RuntimeCall { + RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: account_id(BOB), + value: amount, + }) +} + +mod safe_mode { + use super::*; + + #[test] + fn force_enter_requires_root() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + assert_noop!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + sp_runtime::DispatchError::BadOrigin + ); + + assert!(EnteredUntil::::get().is_some()); + System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::< + Runtime, + >::Entered { + until: EnteredUntil::::get().unwrap(), + })); + }); + } + + #[test] + fn active_safe_mode_blocks_non_whitelisted_calls() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let xt = transfer_call(1u128); + let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into()); + let validity = Runtime::validate_transaction( + TransactionSource::External, + unchecked_xt, + Default::default(), + ); + assert_eq!( + validity, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + }); + } + + #[test] + fn whitelisted_calls_dispatch_in_safe_mode() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + + assert!(EnteredUntil::::get().is_none()); + }); + } +} + +mod tx_pause { + use super::*; + + #[test] + fn pause_requires_root() { + ExtBuilder::default().build().execute_with(|| { + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + assert_noop!( + RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + sp_runtime::DispatchError::BadOrigin + ); + + assert_ok!( + RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name }) + .dispatch(RuntimeOrigin::root()) + ); + }); + } + + #[test] + fn paused_call_is_blocked() { + ExtBuilder::default().build().execute_with(|| { + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + assert_eq!( + Runtime::validate_transaction(TransactionSource::External, xt, Default::default()), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_noop!( + call.clone() + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + frame_system::Error::::CallFiltered + ); + + assert_ok!( + RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name }) + .dispatch(RuntimeOrigin::root()) + ); + + // After unpause, the call should be dispatchable + assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE)))); + }); + } + + #[test] + fn whitelisted_call_cannot_be_paused() { + ExtBuilder::default().build().execute_with(|| { + let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}); + let call_name = call_name(&call); + + assert_noop!( + RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root()), + TxPauseError::::Unpausable + ); + }); + } +} + +mod combined_behaviour { + use super::*; + + #[test] + fn dual_restrictions_require_both_to_clear() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + let validity = Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default(), + ); + assert_eq!( + validity, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { + ident: call_name.clone() + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + let still_blocked = Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default(), + ); + assert_eq!( + still_blocked, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + + // After exiting safe mode and unpausing, call should be dispatchable + assert_ok!(call + .clone() + .dispatch(RuntimeOrigin::signed(account_id(ALICE)))); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.into()); + assert_eq!( + Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default() + ), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + }); + } + + #[test] + fn control_plane_calls_work_under_restrictions() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { + ident: call_name.clone() + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + }); + } + + #[test] + fn governance_whitelisted_calls_work_during_safe_mode() { + use sp_core::H256; + + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .with_balances(vec![(account_id(ALICE), 1_000_000_000_000)]) + .build() + .execute_with(|| { + // Enter safe mode + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + // Verify safe mode is active + assert!(EnteredUntil::::get().is_some()); + + // Verify normal calls are blocked during safe mode + let normal_call = transfer_call(100); + assert_noop!( + normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))), + frame_system::Error::::CallFiltered + ); + + // Test Whitelist pallet - critical for emergency runtime upgrades + let call_hash = H256::random(); + assert_ok!( + RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash }) + .dispatch(RuntimeOrigin::root()) + ); + + // Test Preimage pallet - required for storing governance call data + let dummy_preimage = vec![1u8; 32]; + let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage { + bytes: dummy_preimage, + }) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))); + + match preimage_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Preimage calls should not be filtered by safe mode" + ); + } + } + + // Test Scheduler pallet - needed for time-delayed governance actions + let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel { + when: 100, + index: 0, + }) + .dispatch(RuntimeOrigin::root()); + + match scheduler_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Scheduler calls should not be filtered by safe mode" + ); + } + } + + // Test Referenda pallet - core OpenGov proposal system + let referenda_result = + RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 }) + .dispatch(RuntimeOrigin::root()); + + match referenda_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Referenda calls should not be filtered by safe mode" + ); + } + } + + // Test ConvictionVoting - allows token holders to vote during emergencies + let voting_result = RuntimeCall::ConvictionVoting( + pallet_conviction_voting::Call::remove_other_vote { + target: account_id(BOB), + class: 0, + index: 0, + }, + ) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))); + + match voting_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "ConvictionVoting calls should not be filtered by safe mode" + ); + } + } + + // Test TechnicalCommittee - expert oversight for emergency actions + let tech_committee_result = + RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members { + new_members: vec![account_id(ALICE)], + prime: None, + old_count: 0, + }) + .dispatch(RuntimeOrigin::root()); + + match tech_committee_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "TechnicalCommittee calls should not be filtered by safe mode" + ); + } + } + }); + } +} diff --git a/operator/runtime/testnet/Cargo.toml b/operator/runtime/testnet/Cargo.toml index 98f22639..3f30753e 100644 --- a/operator/runtime/testnet/Cargo.toml +++ b/operator/runtime/testnet/Cargo.toml @@ -64,6 +64,8 @@ pallet-offences = { workspace = true } pallet-outbound-commitment-store = { workspace = true } pallet-parameters = { workspace = true } pallet-preimage = { workspace = true } +pallet-safe-mode = { workspace = true } +pallet-tx-pause = { workspace = true } pallet-proxy = { workspace = true } pallet-referenda = { workspace = true } pallet-scheduler = { workspace = true } @@ -209,6 +211,8 @@ std = [ "pallet-offences/std", "pallet-parameters/std", "pallet-preimage/std", + "pallet-safe-mode/std", + "pallet-tx-pause/std", "pallet-referenda/std", "pallet-proxy/std", "pallet-scheduler/std", @@ -310,6 +314,8 @@ runtime-benchmarks = [ "pallet-offences/runtime-benchmarks", "pallet-parameters/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", + "pallet-safe-mode/runtime-benchmarks", + "pallet-tx-pause/runtime-benchmarks", "pallet-referenda/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", @@ -358,6 +364,8 @@ try-runtime = [ "pallet-offences/try-runtime", "pallet-parameters/try-runtime", "pallet-preimage/try-runtime", + "pallet-safe-mode/try-runtime", + "pallet-tx-pause/try-runtime", "pallet-referenda/try-runtime", "pallet-proxy/try-runtime", "pallet-scheduler/try-runtime", diff --git a/operator/runtime/testnet/src/benchmarks.rs b/operator/runtime/testnet/src/benchmarks.rs index 830165b4..9f5147b4 100644 --- a/operator/runtime/testnet/src/benchmarks.rs +++ b/operator/runtime/testnet/src/benchmarks.rs @@ -52,6 +52,8 @@ frame_benchmarking::define_benchmarks!( [pallet_transaction_payment, TransactionPayment] [pallet_parameters, Parameters] [pallet_message_queue, MessageQueue] + [pallet_safe_mode, SafeMode] + [pallet_tx_pause, TxPause] // EVM pallets [pallet_evm, Evm] diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index d3163efa..e0757667 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -35,8 +35,8 @@ use super::{ ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations, Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, - RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury, - BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, + RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury, + TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION, }; use codec::{Decode, Encode, MaxEncodedLen}; @@ -91,6 +91,10 @@ use datahaven_runtime_common::{ FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen, MigrationIdentifierMaxLen, MigrationStatusHandler, }, + safe_mode::{ + ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit, + SafeModeExtendDeposit, TxPauseWhitelistedCalls, + }, time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK}, }; use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor}; @@ -233,6 +237,36 @@ impl Contains for NormalCallFilter { } } +/// Calls that can bypass the safe-mode pallet. +/// These calls are essential for emergency governance and system maintenance. +pub struct SafeModeWhitelistedCalls; +impl Contains for SafeModeWhitelistedCalls { + fn contains(call: &RuntimeCall) -> bool { + match call { + // Core system calls + RuntimeCall::System(_) => true, + // Safe mode management + RuntimeCall::SafeMode(_) => true, + // Transaction pause management + RuntimeCall::TxPause(_) => true, + // Emergency admin access (testnet/dev only) + RuntimeCall::Sudo(_) => true, + // Governance infrastructure - critical for emergency responses + RuntimeCall::Whitelist(_) => true, + RuntimeCall::Preimage(_) => true, + RuntimeCall::Scheduler(_) => true, + RuntimeCall::ConvictionVoting(_) => true, + RuntimeCall::Referenda(_) => true, + RuntimeCall::TechnicalCommittee(_) => true, + RuntimeCall::TreasuryCouncil(_) => true, + _ => false, + } + } +} + +pub type TestnetRuntimeCallFilter = + RuntimeCallFilter; + /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from /// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`), /// but overridden as needed. @@ -265,8 +299,8 @@ impl frame_system::Config for Runtime { type MaxConsumers = frame_support::traits::ConstU32<16>; type SystemWeightInfo = testnet_weights::frame_system::WeightInfo; type MultiBlockMigrator = MultiBlockMigrations; - /// Use the NormalCallFilter to restrict certain runtime calls - type BaseCallFilter = NormalCallFilter; + /// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions + type BaseCallFilter = TestnetRuntimeCallFilter; } // 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks. @@ -1479,6 +1513,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime { type WeightInfo = testnet_weights::pallet_datahaven_native_transfer::WeightInfo; } +//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ +//β•‘ SAFE MODE & TX PAUSE PALLETS β•‘ +//β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• + +impl pallet_safe_mode::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type RuntimeHoldReason = RuntimeHoldReason; + type WhitelistedCalls = SafeModeWhitelistedCalls; + type EnterDuration = SafeModeDuration; + type ExtendDuration = SafeModeDuration; + type EnterDepositAmount = SafeModeEnterDeposit; + type ExtendDepositAmount = SafeModeExtendDeposit; + type ForceEnterOrigin = EnsureRootWithSuccess; + type ForceExtendOrigin = EnsureRootWithSuccess; + type ForceExitOrigin = EnsureRoot; + type ForceDepositOrigin = EnsureRoot; + type ReleaseDelay = ReleaseDelayNone; + type Notify = (); + type WeightInfo = testnet_weights::pallet_safe_mode::WeightInfo; +} + +impl pallet_tx_pause::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PauseOrigin = EnsureRoot; + type UnpauseOrigin = EnsureRoot; + type WhitelistedCalls = TxPauseWhitelistedCalls; + type MaxNameLen = ConstU32<256>; + type WeightInfo = testnet_weights::pallet_tx_pause::WeightInfo; +} + #[cfg(test)] mod tests { use super::*; diff --git a/operator/runtime/testnet/src/lib.rs b/operator/runtime/testnet/src/lib.rs index 1e217379..87ea4db4 100644 --- a/operator/runtime/testnet/src/lib.rs +++ b/operator/runtime/testnet/src/lib.rs @@ -377,6 +377,12 @@ mod runtime { #[runtime::pallet_index(39)] pub type MultiBlockMigrations = pallet_migrations; + + #[runtime::pallet_index(103)] + pub type SafeMode = pallet_safe_mode; + + #[runtime::pallet_index(104)] + pub type TxPause = pallet_tx_pause; // β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• Polkadot SDK Utility Pallets ══════════════════╝ // ╔═════════════════════════ Governance Pallets ════════════════════╗ diff --git a/operator/runtime/testnet/src/weights/mod.rs b/operator/runtime/testnet/src/weights/mod.rs index e335ea69..93b00589 100644 --- a/operator/runtime/testnet/src/weights/mod.rs +++ b/operator/runtime/testnet/src/weights/mod.rs @@ -43,11 +43,13 @@ pub mod pallet_multisig; pub mod pallet_parameters; pub mod pallet_preimage; pub mod pallet_proxy; +pub mod pallet_safe_mode; pub mod pallet_scheduler; pub mod pallet_sudo; pub mod pallet_timestamp; pub mod pallet_transaction_payment; pub mod pallet_treasury; +pub mod pallet_tx_pause; pub mod pallet_utility; // Governance pallets diff --git a/operator/runtime/testnet/src/weights/pallet_safe_mode.rs b/operator/runtime/testnet/src/weights/pallet_safe_mode.rs new file mode 100644 index 00000000..e7a54199 --- /dev/null +++ b/operator/runtime/testnet/src/weights/pallet_safe_mode.rs @@ -0,0 +1,7 @@ +// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks. +// +// We reuse the upstream Substrate weight assumptions which are conservative enough for +// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate +// weights in this module via the runtime benchmarking CLI. + +pub type WeightInfo = pallet_safe_mode::weights::SubstrateWeight; diff --git a/operator/runtime/testnet/src/weights/pallet_tx_pause.rs b/operator/runtime/testnet/src/weights/pallet_tx_pause.rs new file mode 100644 index 00000000..28676e27 --- /dev/null +++ b/operator/runtime/testnet/src/weights/pallet_tx_pause.rs @@ -0,0 +1,7 @@ +// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks. +// +// We reuse the upstream Substrate weight assumptions which are conservative enough for +// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in +// this module via the runtime benchmarking CLI. + +pub type WeightInfo = pallet_tx_pause::weights::SubstrateWeight; diff --git a/operator/runtime/testnet/tests/lib.rs b/operator/runtime/testnet/tests/lib.rs index dd8d1ed3..63cf45ef 100644 --- a/operator/runtime/testnet/tests/lib.rs +++ b/operator/runtime/testnet/tests/lib.rs @@ -4,6 +4,7 @@ pub mod common; pub mod governance; mod native_token_transfer; mod proxy; +mod safe_mode_tx_pause; use common::*; use datahaven_testnet_runtime::{ diff --git a/operator/runtime/testnet/tests/safe_mode_tx_pause.rs b/operator/runtime/testnet/tests/safe_mode_tx_pause.rs new file mode 100644 index 00000000..7c086ab9 --- /dev/null +++ b/operator/runtime/testnet/tests/safe_mode_tx_pause.rs @@ -0,0 +1,535 @@ +#![allow(clippy::too_many_arguments)] + +#[path = "common.rs"] +mod common; + +use common::{account_id, ExtBuilder, ALICE, BOB}; +use datahaven_testnet_runtime::{ + Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SafeMode, System, TxPause, + UncheckedExtrinsic, +}; +use frame_support::{assert_noop, assert_ok, BoundedVec}; +use pallet_safe_mode::EnteredUntil; +use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf}; +use sp_runtime::{ + traits::Dispatchable, + transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, +}; +use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3; + +fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf { + use frame_support::traits::GetCallMetadata; + let metadata = call.get_call_metadata(); + ( + BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(), + BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(), + ) +} + +fn transfer_call(amount: u128) -> RuntimeCall { + RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: account_id(BOB), + value: amount, + }) +} + +mod safe_mode { + use super::*; + + #[test] + fn force_enter_requires_root() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + assert_noop!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + sp_runtime::DispatchError::BadOrigin + ); + + assert!(EnteredUntil::::get().is_some()); + System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::< + Runtime, + >::Entered { + until: EnteredUntil::::get().unwrap(), + })); + }); + } + + #[test] + fn active_safe_mode_blocks_non_whitelisted_calls() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let xt = transfer_call(1u128); + let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into()); + let validity = Runtime::validate_transaction( + TransactionSource::External, + unchecked_xt, + Default::default(), + ); + assert_eq!( + validity, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + }); + } + + #[test] + fn whitelisted_calls_dispatch_in_safe_mode() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + + assert!(EnteredUntil::::get().is_none()); + }); + } + + #[test] + fn exit_restores_normal_flow() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .with_balances(vec![(account_id(ALICE), 1_000_000)]) + .build() + .execute_with(|| { + // Enter safe mode + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let call = transfer_call(100); + + // Verify call is blocked in safe mode + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + assert_eq!( + Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default() + ), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + // Exit safe mode + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + + // Verify call now works + assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE)))); + }); + } + + #[test] + fn sudo_bypass() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .with_balances(vec![(account_id(ALICE), 1_000_000)]) + .build() + .execute_with(|| { + // Enter safe mode + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let transfer = transfer_call(100); + + // Wrap in sudo call + let sudo_call = RuntimeCall::Sudo(pallet_sudo::Call::sudo { + call: Box::new(transfer), + }); + + // Sudo should bypass safe mode filter + assert_ok!(sudo_call.dispatch(RuntimeOrigin::signed(account_id(ALICE)))); + }); + } +} + +mod tx_pause { + use super::*; + + #[test] + fn pause_requires_root() { + ExtBuilder::default().build().execute_with(|| { + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + assert_noop!( + RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + sp_runtime::DispatchError::BadOrigin + ); + + assert_ok!( + RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name }) + .dispatch(RuntimeOrigin::root()) + ); + }); + } + + #[test] + fn paused_call_is_blocked() { + ExtBuilder::default().build().execute_with(|| { + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + assert_eq!( + Runtime::validate_transaction(TransactionSource::External, xt, Default::default()), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_noop!( + call.clone() + .dispatch(RuntimeOrigin::signed(account_id(ALICE))), + frame_system::Error::::CallFiltered + ); + + assert_ok!( + RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name }) + .dispatch(RuntimeOrigin::root()) + ); + + // After unpause, the call should be dispatchable + assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE)))); + }); + } + + #[test] + fn whitelisted_call_cannot_be_paused() { + ExtBuilder::default().build().execute_with(|| { + let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}); + let call_name = call_name(&call); + + assert_noop!( + RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root()), + TxPauseError::::Unpausable + ); + }); + } +} + +mod combined_behaviour { + use super::*; + + #[test] + fn dual_restrictions_require_both_to_clear() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + let validity = Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default(), + ); + assert_eq!( + validity, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { + ident: call_name.clone() + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + let still_blocked = Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default(), + ); + assert_eq!( + still_blocked, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + + // After exiting safe mode and unpausing, call should be dispatchable + assert_ok!(call + .clone() + .dispatch(RuntimeOrigin::signed(account_id(ALICE)))); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root())); + + let xt = UncheckedExtrinsic::new_bare(call.into()); + assert_eq!( + Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default() + ), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + }); + } + + #[test] + fn control_plane_calls_work_under_restrictions() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .build() + .execute_with(|| { + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let call = transfer_call(1u128); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name.clone(), + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { + ident: call_name.clone() + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root())); + + assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {}) + .dispatch(RuntimeOrigin::root())); + }); + } + + #[test] + fn governance_whitelisted_calls_work_during_safe_mode() { + use sp_core::H256; + + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .with_balances(vec![(account_id(ALICE), 1_000_000_000_000)]) + .build() + .execute_with(|| { + // Enter safe mode + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + // Verify safe mode is active + assert!(EnteredUntil::::get().is_some()); + + // Verify normal calls are blocked during safe mode + let normal_call = transfer_call(100); + assert_noop!( + normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))), + frame_system::Error::::CallFiltered + ); + + // Test Whitelist pallet - critical for emergency runtime upgrades + let call_hash = H256::random(); + assert_ok!( + RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash }) + .dispatch(RuntimeOrigin::root()) + ); + + // Test Preimage pallet - required for storing governance call data + let dummy_preimage = vec![1u8; 32]; + let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage { + bytes: dummy_preimage, + }) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))); + + match preimage_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Preimage calls should not be filtered by safe mode" + ); + } + } + + // Test Scheduler pallet - needed for time-delayed governance actions + let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel { + when: 100, + index: 0, + }) + .dispatch(RuntimeOrigin::root()); + + match scheduler_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Scheduler calls should not be filtered by safe mode" + ); + } + } + + // Test Referenda pallet - core OpenGov proposal system + let referenda_result = + RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 }) + .dispatch(RuntimeOrigin::root()); + + match referenda_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "Referenda calls should not be filtered by safe mode" + ); + } + } + + // Test ConvictionVoting - allows token holders to vote during emergencies + let voting_result = RuntimeCall::ConvictionVoting( + pallet_conviction_voting::Call::remove_other_vote { + target: account_id(BOB), + class: 0, + index: 0, + }, + ) + .dispatch(RuntimeOrigin::signed(account_id(ALICE))); + + match voting_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "ConvictionVoting calls should not be filtered by safe mode" + ); + } + } + + // Test TechnicalCommittee - expert oversight for emergency actions + let tech_committee_result = + RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members { + new_members: vec![account_id(ALICE)], + prime: None, + old_count: 0, + }) + .dispatch(RuntimeOrigin::root()); + + match tech_committee_result { + Ok(_) => {} + Err(e) => { + let call_filtered_error: sp_runtime::DispatchError = + frame_system::Error::::CallFiltered.into(); + assert_ne!( + format!("{:?}", e.error), + format!("{:?}", call_filtered_error), + "TechnicalCommittee calls should not be filtered by safe mode" + ); + } + } + }); + } + + #[test] + fn error_surface_consistency() { + ExtBuilder::default() + .with_sudo(account_id(ALICE)) + .with_balances(vec![(account_id(ALICE), 1_000_000)]) + .build() + .execute_with(|| { + // Activate both restrictions + assert_ok!( + RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {}) + .dispatch(RuntimeOrigin::root()) + ); + + let call = transfer_call(100); + let call_name = call_name(&call); + + assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause { + full_name: call_name, + }) + .dispatch(RuntimeOrigin::root())); + + // Validate the blocked call - should return consistent error + let xt = UncheckedExtrinsic::new_bare(call.clone().into()); + let validation_result = Runtime::validate_transaction( + TransactionSource::External, + xt, + Default::default(), + ); + + // Should return InvalidTransaction::Call + assert_eq!( + validation_result, + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) + ); + + // Dispatch should also fail with consistent error + assert_noop!( + call.dispatch(RuntimeOrigin::signed(account_id(ALICE))), + frame_system::Error::::CallFiltered + ); + }); + } +} diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index 021754cd..e5757e41 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.10311212470204876148", + "version": "0.1.0-autogenerated.11494808361211823293", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/test/.papi/metadata/datahaven.scale b/test/.papi/metadata/datahaven.scale index c935d7e428413a105d061cfe29958e6d1f8cd842..98e2427e1967d74c2178defc0223411a566fd751 100644 GIT binary patch delta 20287 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 From 72bfe9bb62c58fdf69de8f48c8b1bc870f9504e3 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:34:21 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=F0=9F=94=A7=20Add=20revision=20numb?= =?UTF-8?q?er=20to=20rust=20toolchain=20channel=20version=20to=20match=20w?= =?UTF-8?q?ith=20srtool=20image=20version=20(#209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/task-publish-runtime.yml | 4 ++-- operator/rust-toolchain.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/task-publish-runtime.yml b/.github/workflows/task-publish-runtime.yml index 8b7742c7..e65ddc3c 100644 --- a/.github/workflows/task-publish-runtime.yml +++ b/.github/workflows/task-publish-runtime.yml @@ -83,8 +83,8 @@ jobs: RUNTIME_BUILD_PROFILE: "production" run: | # Ensure we have permissions to write to the runtime folder target for the docker user - mkdir -p runtime/${GH_WORKFLOW_MATRIX_CHAIN}/target - chmod uog+rwX runtime/${GH_WORKFLOW_MATRIX_CHAIN}/target + mkdir -p operator/runtime/${GH_WORKFLOW_MATRIX_CHAIN}/target + chmod uog+rwX operator/runtime/${GH_WORKFLOW_MATRIX_CHAIN}/target chmod u+x ./original-scripts/build-runtime-srtool.sh ./original-scripts/build-runtime-srtool.sh diff --git a/operator/rust-toolchain.toml b/operator/rust-toolchain.toml index 16588d25..d2b485d3 100644 --- a/operator/rust-toolchain.toml +++ b/operator/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.88" +channel = "1.88.0" components = [ "cargo", "clippy",