diff --git a/biome.json b/biome.json index aed14936..2b197a48 100644 --- a/biome.json +++ b/biome.json @@ -17,7 +17,8 @@ "!**/html/**/*", "!**/moonwall/contracts/out/**/*", "!**/contracts/out/**/*", - "!**/contracts/deployments/state-diff.checksum" + "!**/contracts/deployments/state-diff.checksum", + "!**/bun.lock" ], "maxSize": 3000000 }, diff --git a/contracts/config/anvil.json b/contracts/config/anvil.json index 8e3796c3..4a134ced 100644 --- a/contracts/config/anvil.json +++ b/contracts/config/anvil.json @@ -35,7 +35,7 @@ "randaoCommitExpiration": 24, "minNumRequiredSignatures": 2, "startBlock": 1, - "rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", + "messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", "initialValidatorSetId": 0, "initialValidatorHashes": [ "0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7", diff --git a/contracts/config/example.jsonc b/contracts/config/example.jsonc index e57c1ec7..aeef7cc4 100644 --- a/contracts/config/example.jsonc +++ b/contracts/config/example.jsonc @@ -99,9 +99,9 @@ /// Initial BEEFY block number. Set to latest finalized block by update-beefy-checkpoint. /// The BeefyClient will only accept BEEFY commitments with block numbers > startBlock. "startBlock": 1, - /// The origin linked to the Rewards Agent, the Agent contract who's allowed to submit - /// new reward merkle roots to the RewardsRegistry contract. - "rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", + /// The origin linked to the Agent, the Agent contract who's allowed to submit + /// new reward merkle roots or slashes. + "messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", /// The BEEFY validator set ID for the initial validators. /// This is fetched from Beefy.ValidatorSetId on the DataHaven chain. "initialValidatorSetId": 0, diff --git a/contracts/config/mainnet-ethereum.json b/contracts/config/mainnet-ethereum.json index 5a0e13fb..28775dad 100644 --- a/contracts/config/mainnet-ethereum.json +++ b/contracts/config/mainnet-ethereum.json @@ -43,7 +43,7 @@ "randaoCommitExpiration": 24, "minNumRequiredSignatures": 16, "startBlock": 1, - "rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", + "messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", "initialValidatorSetId": 0, "initialValidatorHashes": [], "nextValidatorSetId": 1, diff --git a/contracts/config/stagenet-hoodi.json b/contracts/config/stagenet-hoodi.json index a79ac73a..40d80dbc 100644 --- a/contracts/config/stagenet-hoodi.json +++ b/contracts/config/stagenet-hoodi.json @@ -44,7 +44,7 @@ "randaoCommitExpiration": 24, "minNumRequiredSignatures": 3, "startBlock": 1303065, - "rewardsMessageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd", + "messageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd", "initialValidatorSetId": 2186, "initialValidatorHashes": [ "0x07ce4f2cd558f4d4b529a3362b6ff7d616ca0893b53252dc62829b8218ea5c10", diff --git a/contracts/config/testnet-hoodi.json b/contracts/config/testnet-hoodi.json index 1aa56fee..3396b7b4 100644 --- a/contracts/config/testnet-hoodi.json +++ b/contracts/config/testnet-hoodi.json @@ -44,7 +44,7 @@ "randaoCommitExpiration": 24, "minNumRequiredSignatures": 5, "startBlock": 1381173, - "rewardsMessageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215", + "messageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215", "initialValidatorSetId": 2303, "initialValidatorHashes": [ "0x0ec3102f334aba804c18b843e45ec874005587122a1b49273b1b04d6fd98b1a2", diff --git a/contracts/deployments/anvil-agent-info.json b/contracts/deployments/anvil-agent-info.json new file mode 100644 index 00000000..b0bc6882 --- /dev/null +++ b/contracts/deployments/anvil-agent-info.json @@ -0,0 +1 @@ +{"Agent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"} \ No newline at end of file diff --git a/contracts/deployments/anvil-rewards-info.json b/contracts/deployments/anvil-rewards-info.json index ea1ee44d..c91dba57 100644 --- a/contracts/deployments/anvil-rewards-info.json +++ b/contracts/deployments/anvil-rewards-info.json @@ -1 +1 @@ -{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","RewardsAgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"} \ No newline at end of file +{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"} \ No newline at end of file diff --git a/contracts/deployments/stagenet-hoodi-rewards-info.json b/contracts/deployments/stagenet-hoodi-rewards-info.json index 32bba16a..fc588b28 100644 --- a/contracts/deployments/stagenet-hoodi-rewards-info.json +++ b/contracts/deployments/stagenet-hoodi-rewards-info.json @@ -1 +1 @@ -{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","RewardsAgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"} \ No newline at end of file +{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","AgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"} \ No newline at end of file diff --git a/contracts/script/deploy/Config.sol b/contracts/script/deploy/Config.sol index 63558988..4d076407 100644 --- a/contracts/script/deploy/Config.sol +++ b/contracts/script/deploy/Config.sol @@ -12,7 +12,7 @@ contract Config { bytes32[] initialValidatorHashes; uint128 nextValidatorSetId; bytes32[] nextValidatorHashes; - bytes32 rewardsMessageOrigin; + bytes32 messageOrigin; } // AVS parameters diff --git a/contracts/script/deploy/DeployBase.s.sol b/contracts/script/deploy/DeployBase.s.sol index fe1d127c..27f0d6fd 100644 --- a/contracts/script/deploy/DeployBase.s.sol +++ b/contracts/script/deploy/DeployBase.s.sol @@ -123,7 +123,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { BeefyClient beefyClient, AgentExecutor agentExecutor, IGatewayV2 gateway, - address payable rewardsAgentAddress + address payable agentAddress ) = _deploySnowbridge(snowbridgeConfig); Logging.logFooter(); _logProgress(); @@ -133,14 +133,14 @@ abstract contract DeployBase is Script, DeployParams, Accounts { DataHavenServiceManager serviceManager, DataHavenServiceManager serviceManagerImplementation, ProxyAdmin actualProxyAdmin - ) = _deployDataHavenContracts(avsConfig, proxyAdmin, gateway); + ) = _deployDataHavenContracts(avsConfig, proxyAdmin, gateway, agentAddress); Logging.logFooter(); _logProgress(); // Final configuration (same for both modes) Logging.logHeader("FINAL CONFIGURATION"); - Logging.logContractDeployed("Rewards Agent Address", rewardsAgentAddress); + Logging.logContractDeployed("Agent Address", agentAddress); Logging.logFooter(); _logProgress(); @@ -151,11 +151,11 @@ abstract contract DeployBase is Script, DeployParams, Accounts { gateway, serviceManager, serviceManagerImplementation, - rewardsAgentAddress, + agentAddress, actualProxyAdmin ); - _outputRewardsAgentInfo(rewardsAgentAddress, snowbridgeConfig.rewardsMessageOrigin); + _outputAgentInfo(agentAddress, snowbridgeConfig.messageOrigin); } /** @@ -202,11 +202,11 @@ abstract contract DeployBase is Script, DeployParams, Accounts { // Create Agent Logging.logSection("Creating Snowbridge Agent"); vm.broadcast(_deployerPrivateKey); - gateway.v2_createAgent(config.rewardsMessageOrigin); - address payable rewardsAgentAddress = payable(gateway.agentOf(config.rewardsMessageOrigin)); - Logging.logContractDeployed("Rewards Agent", rewardsAgentAddress); + gateway.v2_createAgent(config.messageOrigin); + address payable agentAddress = payable(gateway.agentOf(config.messageOrigin)); + Logging.logContractDeployed("Rewards Agent", agentAddress); - return (beefyClient, agentExecutor, gateway, rewardsAgentAddress); + return (beefyClient, agentExecutor, gateway, agentAddress); } /** @@ -241,7 +241,8 @@ abstract contract DeployBase is Script, DeployParams, Accounts { function _deployDataHavenContracts( AVSConfig memory avsConfig, ProxyAdmin proxyAdmin, - IGatewayV2 gateway + IGatewayV2 gateway, + address agentAddress ) internal returns (DataHavenServiceManager, DataHavenServiceManager, ProxyAdmin) { Logging.logHeader("DATAHAVEN CUSTOM CONTRACTS DEPLOYMENT"); @@ -270,7 +271,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { // Create service manager initialisation parameters struct ServiceManagerInitParams memory initParams = ServiceManagerInitParams({ avsOwner: avsConfig.avsOwner, - rewardsInitiator: avsConfig.rewardsInitiator, + rewardsInitiator: agentAddress, validatorsStrategiesAndMultipliers: strategiesAndMultipliers, gateway: address(gateway), validatorSetSubmitter: avsConfig.validatorSetSubmitter, @@ -316,38 +317,38 @@ abstract contract DeployBase is Script, DeployParams, Accounts { IGatewayV2 gateway, DataHavenServiceManager serviceManager, DataHavenServiceManager serviceManagerImplementation, - address rewardsAgent, + address agent, ProxyAdmin proxyAdmin ) internal virtual; /** - * @notice Output rewards agent info (shared across all deployment types) + * @notice Output agent info (shared across all deployment types) */ - function _outputRewardsAgentInfo( - address rewardsAgent, - bytes32 rewardsAgentOrigin + function _outputAgentInfo( + address agent, + bytes32 agentOrigin ) internal { - Logging.logHeader("REWARDS AGENT INFO"); - Logging.logContractDeployed("RewardsAgent", rewardsAgent); - Logging.logAgentOrigin("RewardsAgentOrigin", vm.toString(rewardsAgentOrigin)); + Logging.logHeader("AGENT INFO"); + Logging.logContractDeployed("Agent", agent); + Logging.logAgentOrigin("AgentOrigin", vm.toString(agentOrigin)); Logging.logFooter(); // Write to deployment file for future reference string memory network = _getNetworkName(); - string memory rewardsInfoPath = - string.concat(vm.projectRoot(), "/deployments/", network, "-rewards-info.json"); + string memory agentInfoPath = + string.concat(vm.projectRoot(), "/deployments/", network, "-agent-info.json"); // Create directory if it doesn't exist vm.createDir(string.concat(vm.projectRoot(), "/deployments"), true); // Create JSON with rewards info string memory json = "{"; - json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",'); - json = string.concat(json, '"RewardsAgentOrigin": "', vm.toString(rewardsAgentOrigin), '"'); + json = string.concat(json, '"Agent": "', vm.toString(agent), '",'); + json = string.concat(json, '"AgentOrigin": "', vm.toString(agentOrigin), '"'); json = string.concat(json, "}"); // Write to file - vm.writeFile(rewardsInfoPath, json); - Logging.logInfo(string.concat("Rewards info saved to: ", rewardsInfoPath)); + vm.writeFile(agentInfoPath, json); + Logging.logInfo(string.concat("Agent info saved to: ", agentInfoPath)); } } diff --git a/contracts/script/deploy/DeployParams.s.sol b/contracts/script/deploy/DeployParams.s.sol index eda4630f..c77f99b9 100644 --- a/contracts/script/deploy/DeployParams.s.sol +++ b/contracts/script/deploy/DeployParams.s.sol @@ -24,8 +24,7 @@ contract DeployParams is Script, Config { config.minNumRequiredSignatures = vm.parseJsonUint(configJson, ".snowbridge.minNumRequiredSignatures"); config.startBlock = vm.parseJsonUint(configJson, ".snowbridge.startBlock").toUint64(); - config.rewardsMessageOrigin = - vm.parseJsonBytes32(configJson, ".snowbridge.rewardsMessageOrigin"); + config.messageOrigin = vm.parseJsonBytes32(configJson, ".snowbridge.messageOrigin"); // Load validators from file or generate placeholder ones in dev mode bool isDevMode = keccak256(abi.encodePacked(vm.envOr("DEV_MODE", string("false")))) diff --git a/operator/.dockerignore b/operator/.dockerignore index 8f7393e4..13c345c5 100644 --- a/operator/.dockerignore +++ b/operator/.dockerignore @@ -50,4 +50,3 @@ examples/ Cargo.lock.old *.toml.old *.lock.old -**/target/ \ No newline at end of file diff --git a/operator/Cargo.lock b/operator/Cargo.lock index 702cc2d4..586435c3 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -1521,7 +1521,7 @@ dependencies = [ "pallet-message-queue", "parity-scale-codec", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "sp-core", "sp-runtime", "sp-std", @@ -2607,7 +2607,7 @@ dependencies = [ [[package]] name = "datahaven-mainnet-runtime" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bridge-hub-common 0.13.1", @@ -2720,8 +2720,8 @@ dependencies = [ "shp-treasury-funding", "shp-tx-implicits-runtime-api", "smallvec", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", @@ -2764,7 +2764,7 @@ dependencies = [ [[package]] name = "datahaven-node" -version = "0.25.0" +version = "0.26.0" dependencies = [ "async-channel 1.9.0", "clap", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "datahaven-runtime-common" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "fp-account", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "datahaven-stagenet-runtime" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bridge-hub-common 0.13.1", @@ -3024,8 +3024,8 @@ dependencies = [ "shp-treasury-funding", "shp-tx-implicits-runtime-api", "smallvec", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", @@ -3068,7 +3068,7 @@ dependencies = [ [[package]] name = "datahaven-testnet-runtime" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bridge-hub-common 0.13.1", @@ -3181,8 +3181,8 @@ dependencies = [ "shp-treasury-funding", "shp-tx-implicits-runtime-api", "smallvec", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", @@ -3374,7 +3374,7 @@ dependencies = [ [[package]] name = "dhp-bridge" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "frame-system", @@ -3382,7 +3382,7 @@ dependencies = [ "pallet-datahaven-native-transfer", "pallet-external-validators", "parity-scale-codec", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "sp-core", "sp-std", @@ -8642,8 +8642,8 @@ dependencies = [ [[package]] name = "pallet-bucket-nfts" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -8699,8 +8699,8 @@ dependencies = [ [[package]] name = "pallet-cr-randomness" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "frame-system", @@ -8719,7 +8719,7 @@ dependencies = [ [[package]] name = "pallet-datahaven-native-transfer" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -8727,7 +8727,7 @@ dependencies = [ "pallet-balances", "parity-scale-codec", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "sp-core", "sp-io", @@ -8831,7 +8831,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-balances-erc20" -version = "0.25.0" +version = "0.26.0" dependencies = [ "fp-evm", "frame-support", @@ -8854,7 +8854,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-batch" -version = "0.25.0" +version = "0.26.0" dependencies = [ "evm", "fp-evm", @@ -8893,7 +8893,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-call-permit" -version = "0.25.0" +version = "0.26.0" dependencies = [ "evm", "fp-evm", @@ -8959,7 +8959,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-datahaven-native-transfer" -version = "0.25.0" +version = "0.26.0" dependencies = [ "evm", "fp-evm", @@ -8973,7 +8973,7 @@ dependencies = [ "parity-scale-codec", "precompile-utils", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "sp-core", "sp-io", @@ -8983,8 +8983,8 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-file-system" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "fp-account", "fp-evm", @@ -9052,7 +9052,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-proxy" -version = "0.25.0" +version = "0.26.0" dependencies = [ "evm", "fp-evm", @@ -9096,7 +9096,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-registry" -version = "0.25.0" +version = "0.26.0" dependencies = [ "fp-evm", "frame-support", @@ -9142,12 +9142,11 @@ dependencies = [ "log", "pallet-external-validators", "pallet-session", - "pallet-staking", "pallet-timestamp", "parity-scale-codec", "scale-info", "serde", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "sp-core", "sp-io", @@ -9157,7 +9156,7 @@ dependencies = [ [[package]] name = "pallet-external-validators" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9181,7 +9180,7 @@ dependencies = [ [[package]] name = "pallet-external-validators-rewards" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9194,7 +9193,7 @@ dependencies = [ "pallet-timestamp", "parity-scale-codec", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "sp-core", "sp-io", @@ -9223,8 +9222,8 @@ dependencies = [ [[package]] name = "pallet-file-system" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9252,8 +9251,8 @@ dependencies = [ [[package]] name = "pallet-file-system-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9287,7 +9286,7 @@ dependencies = [ [[package]] name = "pallet-grandpa-benchmarking" -version = "0.25.0" +version = "0.26.0" dependencies = [ "finality-grandpa", "frame-benchmarking", @@ -9439,7 +9438,7 @@ dependencies = [ [[package]] name = "pallet-outbound-commitment-store" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "frame-system", @@ -9467,8 +9466,8 @@ dependencies = [ [[package]] name = "pallet-payment-streams" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9487,8 +9486,8 @@ dependencies = [ [[package]] name = "pallet-payment-streams-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9515,8 +9514,8 @@ dependencies = [ [[package]] name = "pallet-proofs-dealer" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9541,8 +9540,8 @@ dependencies = [ [[package]] name = "pallet-proofs-dealer-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9563,7 +9562,7 @@ dependencies = [ [[package]] name = "pallet-proxy-genesis-companion" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "frame-system", @@ -9580,8 +9579,8 @@ dependencies = [ [[package]] name = "pallet-randomness" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9674,7 +9673,7 @@ dependencies = [ [[package]] name = "pallet-session-benchmarking" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9718,8 +9717,8 @@ dependencies = [ [[package]] name = "pallet-storage-providers" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9740,8 +9739,8 @@ dependencies = [ [[package]] name = "pallet-storage-providers-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -13903,8 +13902,8 @@ dependencies = [ [[package]] name = "shc-actors-derive" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "once_cell", "proc-macro2", @@ -13916,8 +13915,8 @@ dependencies = [ [[package]] name = "shc-actors-framework" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "bincode", @@ -13935,8 +13934,8 @@ dependencies = [ [[package]] name = "shc-blockchain-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -13991,8 +13990,8 @@ dependencies = [ [[package]] name = "shc-blockchain-service-db" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "chrono", "diesel", @@ -14015,8 +14014,8 @@ dependencies = [ [[package]] name = "shc-client" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14090,8 +14089,8 @@ dependencies = [ [[package]] name = "shc-common" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "bigdecimal", @@ -14155,8 +14154,8 @@ dependencies = [ [[package]] name = "shc-file-manager" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "bincode", "hash-db", @@ -14180,8 +14179,8 @@ dependencies = [ [[package]] name = "shc-file-transfer-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14211,8 +14210,8 @@ dependencies = [ [[package]] name = "shc-fisherman-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "async-trait", "diesel", @@ -14242,8 +14241,8 @@ dependencies = [ [[package]] name = "shc-forest-manager" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "async-trait", @@ -14268,8 +14267,8 @@ dependencies = [ [[package]] name = "shc-indexer-db" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "bigdecimal", "chrono", @@ -14296,8 +14295,8 @@ dependencies = [ [[package]] name = "shc-indexer-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14347,8 +14346,8 @@ dependencies = [ [[package]] name = "shc-rpc" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "array-bytes", "async-trait", @@ -14393,8 +14392,8 @@ dependencies = [ [[package]] name = "shc-telemetry" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "log", "substrate-prometheus-endpoint", @@ -14410,8 +14409,8 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "shp-constants" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-core", "sp-runtime", @@ -14419,8 +14418,8 @@ dependencies = [ [[package]] name = "shp-data-price-updater" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14434,8 +14433,8 @@ dependencies = [ [[package]] name = "shp-file-key-verifier" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14452,8 +14451,8 @@ dependencies = [ [[package]] name = "shp-file-metadata" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "hex", "num-bigint", @@ -14468,8 +14467,8 @@ dependencies = [ [[package]] name = "shp-forest-verifier" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14485,16 +14484,16 @@ dependencies = [ [[package]] name = "shp-opaque" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-runtime", ] [[package]] name = "shp-session-keys" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "async-trait", "parity-scale-codec", @@ -14508,8 +14507,8 @@ dependencies = [ [[package]] name = "shp-traits" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14522,8 +14521,8 @@ dependencies = [ [[package]] name = "shp-treasury-funding" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "log", "shp-traits", @@ -14533,8 +14532,8 @@ dependencies = [ [[package]] name = "shp-tx-implicits-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -14546,8 +14545,8 @@ dependencies = [ [[package]] name = "shp-types" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-core", "sp-runtime", @@ -14827,7 +14826,7 @@ dependencies = [ [[package]] name = "snowbridge-beacon-primitives" -version = "0.25.0" +version = "0.26.0" dependencies = [ "byte-slice-cast", "frame-support", @@ -14872,7 +14871,7 @@ dependencies = [ [[package]] name = "snowbridge-core" -version = "0.25.0" +version = "0.26.0" dependencies = [ "bp-relayers", "ethabi-decode", @@ -14949,8 +14948,8 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-verification-primitives", "sp-core", "sp-io", @@ -14963,7 +14962,7 @@ dependencies = [ [[package]] name = "snowbridge-merkle-tree" -version = "0.25.0" +version = "0.26.0" dependencies = [ "array-bytes", "hex", @@ -15004,7 +15003,7 @@ dependencies = [ [[package]] name = "snowbridge-outbound-queue-primitives" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "ethabi-decode", @@ -15016,7 +15015,7 @@ dependencies = [ "parity-scale-codec", "polkadot-parachain-primitives", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-verification-primitives", "sp-arithmetic", "sp-core", @@ -15030,12 +15029,12 @@ dependencies = [ [[package]] name = "snowbridge-outbound-queue-v2-runtime-api" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "parity-scale-codec", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", "sp-api", @@ -15045,7 +15044,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-ethereum-client" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -15058,8 +15057,8 @@ dependencies = [ "scale-info", "serde", "serde_json", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-ethereum 0.3.0", "snowbridge-inbound-queue-primitives", "snowbridge-pallet-ethereum-client-fixtures", @@ -15075,8 +15074,8 @@ name = "snowbridge-pallet-ethereum-client-fixtures" version = "0.9.0" dependencies = [ "hex-literal 0.3.4", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "sp-core", "sp-std", @@ -15084,7 +15083,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-inbound-queue-v2" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bp-relayers", @@ -15098,8 +15097,8 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-inbound-queue-v2-fixtures", @@ -15120,8 +15119,8 @@ name = "snowbridge-pallet-inbound-queue-v2-fixtures" version = "0.10.0" dependencies = [ "hex-literal 0.3.4", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "sp-core", "sp-std", @@ -15151,7 +15150,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-outbound-queue-v2" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bp-relayers", @@ -15165,8 +15164,8 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", @@ -15197,7 +15196,7 @@ dependencies = [ "parity-scale-codec", "polkadot-primitives", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "snowbridge-pallet-outbound-queue", "sp-core", @@ -15210,7 +15209,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-system-v2" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -15222,7 +15221,7 @@ dependencies = [ "parity-scale-codec", "polkadot-primitives", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "snowbridge-pallet-outbound-queue-v2", "snowbridge-pallet-system", @@ -15238,10 +15237,10 @@ dependencies = [ [[package]] name = "snowbridge-system-v2-runtime-api" -version = "0.25.0" +version = "0.26.0" dependencies = [ "parity-scale-codec", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "sp-api", "sp-std", "staging-xcm", @@ -15249,7 +15248,7 @@ dependencies = [ [[package]] name = "snowbridge-test-utils" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -15269,12 +15268,12 @@ dependencies = [ [[package]] name = "snowbridge-verification-primitives" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "parity-scale-codec", "scale-info", - "snowbridge-beacon-primitives 0.25.0", + "snowbridge-beacon-primitives 0.26.0", "sp-core", "sp-std", ] diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 194e5f4e..40a689e9 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" homepage = "https://datahaven.xyz/" license = "GPL-3" repository = "https://github.com/datahavenxyz/datahaven" -version = "0.25.0" +version = "0.26.0" [workspace] members = [ @@ -146,7 +146,6 @@ pallet-referenda = { git = "https://github.com/paritytech/polkadot-sdk", tag = " pallet-safe-mode = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-scheduler = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-session = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } -pallet-staking = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-sudo = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } @@ -273,42 +272,42 @@ fc-storage = { git = "https://github.com/polkadot-evm/frontier", branch = "stabl # StorageHub ## Runtime -pallet-bucket-nfts = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-cr-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-file-system-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-payment-streams = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-payment-streams-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-proofs-dealer = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-proofs-dealer-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-storage-providers = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-storage-providers-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-constants = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-data-price-updater = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-file-key-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-file-metadata = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-forest-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-traits = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-treasury-funding = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +pallet-bucket-nfts = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-cr-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-file-system-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-payment-streams = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-payment-streams-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-proofs-dealer = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-proofs-dealer-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-storage-providers = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-storage-providers-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-constants = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-data-price-updater = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-file-key-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-file-metadata = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-forest-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-traits = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-treasury-funding = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } ## Client -shc-actors-derive = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-actors-framework = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-blockchain-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-client = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-common = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-file-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-file-transfer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-fisherman-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-forest-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-indexer-db = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-indexer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-rpc = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-opaque = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-tx-implicits-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-types = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +shc-actors-derive = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-actors-framework = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-blockchain-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-client = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-common = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-file-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-file-transfer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-fisherman-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-forest-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-indexer-db = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-indexer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-rpc = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-opaque = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-tx-implicits-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-types = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } ## Precompiles -pallet-evm-precompile-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +pallet-evm-precompile-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } # Static linking diff --git a/operator/Dockerfile b/operator/Dockerfile index 7e52508d..c21849c3 100644 --- a/operator/Dockerfile +++ b/operator/Dockerfile @@ -55,7 +55,7 @@ COPY --from=builder \ RUN useradd -m -u 1001 -U -s /bin/sh -d /datahaven datahaven && \ mkdir -p /datahaven/.local/share /data && \ chown -R datahaven:datahaven /data && \ - ln -s /data /datahaven/.local/share/datahaven + ln -s /data /datahaven/.local/share/datahaven-node USER datahaven diff --git a/operator/pallets/external-validator-slashes/Cargo.toml b/operator/pallets/external-validator-slashes/Cargo.toml index cf9a9804..7dd77a28 100644 --- a/operator/pallets/external-validator-slashes/Cargo.toml +++ b/operator/pallets/external-validator-slashes/Cargo.toml @@ -18,7 +18,6 @@ frame-support = { workspace = true } frame-system = { workspace = true } log = { workspace = true } pallet-session = { workspace = true } -pallet-staking = { workspace = true } parity-scale-codec = { workspace = true, features = ["derive", "max-encoded-len"] } scale-info = { workspace = true } snowbridge-core = { workspace = true } @@ -42,7 +41,6 @@ std = [ "frame-system/std", "log/std", "pallet-session/std", - "pallet-staking/std", "pallet-timestamp/std", "parity-scale-codec/std", "pallet-external-validators/std", @@ -58,7 +56,6 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", - "pallet-staking/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-external-validators/runtime-benchmarks", "snowbridge-core/runtime-benchmarks", @@ -70,7 +67,6 @@ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "pallet-session/try-runtime", - "pallet-staking/try-runtime", "pallet-timestamp/try-runtime", "sp-runtime/try-runtime", ] diff --git a/operator/pallets/external-validator-slashes/src/benchmarking.rs b/operator/pallets/external-validator-slashes/src/benchmarking.rs index 51693fb7..7e8d7a00 100644 --- a/operator/pallets/external-validator-slashes/src/benchmarking.rs +++ b/operator/pallets/external-validator-slashes/src/benchmarking.rs @@ -41,7 +41,14 @@ mod benchmarks { let era = T::EraIndexProvider::active_era().index; let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); for _ in 0..MAX_SLASHES { - existing_slashes.push(Slash::::default_from(dummy())); + existing_slashes.push(Slash { + validator: dummy(), + reporters: vec![], + slash_id: One::one(), + percentage: Perbill::from_percent(1), + confirmed: false, + offence_kind: OffenceKind::LivenessOffence, + }); } Slashes::::insert( era.saturating_add(T::SlashDeferDuration::get()) @@ -74,7 +81,13 @@ mod benchmarks { let era = T::EraIndexProvider::active_era().index; let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); #[extrinsic_call] - _(RawOrigin::Root, era, dummy(), Perbill::from_percent(50)); + _( + RawOrigin::Root, + era, + dummy(), + Perbill::from_percent(50), + OffenceKind::LivenessOffence, + ); assert_eq!( Slashes::::get( @@ -93,7 +106,14 @@ mod benchmarks { let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); for _ in 0..(s + 1) { - queue.push_back(Slash::::default_from(dummy())); + queue.push_back(Slash { + validator: dummy(), + reporters: vec![], + slash_id: One::one(), + percentage: Perbill::from_percent(1), + confirmed: false, + offence_kind: OffenceKind::LivenessOffence, + }); } UnreportedSlashesQueue::::set(queue); diff --git a/operator/pallets/external-validator-slashes/src/lib.rs b/operator/pallets/external-validator-slashes/src/lib.rs index 8ef718d3..b24b85db 100644 --- a/operator/pallets/external-validator-slashes/src/lib.rs +++ b/operator/pallets/external-validator-slashes/src/lib.rs @@ -31,7 +31,7 @@ extern crate alloc; use pallet_external_validators::apply; use snowbridge_outbound_queue_primitives::SendError; use { - alloc::{collections::vec_deque::VecDeque, vec, vec::Vec}, + alloc::{collections::vec_deque::VecDeque, string::String, vec, vec::Vec}, frame_support::{pallet_prelude::*, traits::DefensiveSaturating}, frame_system::pallet_prelude::*, log::log, @@ -46,7 +46,7 @@ use { DispatchResult, Perbill, }, sp_staking::{ - offence::{OffenceDetails, OnOffenceHandler}, + offence::{Offence, OffenceDetails, OffenceError, OnOffenceHandler, ReportOffence}, EraIndex, SessionIndex, }, }; @@ -63,10 +63,45 @@ mod tests; mod benchmarking; pub mod weights; +/// Identifies the type of consensus offence for EigenLayer slash reporting. +#[derive( + Encode, + Decode, + DecodeWithMemTracking, + RuntimeDebug, + TypeInfo, + Clone, + PartialEq, + Eq, + MaxEncodedLen, +)] +pub enum OffenceKind { + /// Liveness offence (i.e. Unresponsiveness) + LivenessOffence, + BabeEquivocation, + GrandpaEquivocation, + BeefyEquivocation, + Custom(BoundedVec>), +} + +impl OffenceKind { + pub fn to_description(&self) -> String { + match self { + Self::LivenessOffence => "Liveness offence".into(), + Self::BabeEquivocation => "BABE equivocation".into(), + Self::GrandpaEquivocation => "GRANDPA equivocation".into(), + Self::BeefyEquivocation => "BEEFY equivocation".into(), + Self::Custom(desc) => String::from_utf8(desc.to_vec()) + .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()), + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct SlashData { pub validator: AccountId, pub wad_to_slash: u128, + pub description: String, } // FIXME (nice to have): Merge with SendMessage trait from pallet external-validator-reward (similar trait) @@ -153,6 +188,11 @@ pub mod pallet { /// Provider to retrieve the current external index of validators type ExternalIndexProvider: ExternalIndexProvider; + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// Default: 5e16 = 5% in WAD format (1e18 = 100%). + #[pallet::constant] + type MaxSlashWad: Get; + /// How many queued slashes are being processed per block. #[pallet::constant] type QueuedSlashesProcessedPerBlock: Get; @@ -183,6 +223,9 @@ pub mod pallet { EthereumDeliverFail, /// Invalid params for root_test_send_msg_to_eth RootTestInvalidParams, + /// No PendingOffenceKind found for (session, validator) — offence was not + /// reported through EquivocationReportWrapper, so the offence kind is unknown. + MissingOffenceKind, } #[apply(derive_storage_traits)] @@ -237,6 +280,27 @@ pub mod pallet { #[pallet::storage] pub type SlashingMode = StorageValue<_, SlashingModeOption, ValueQuery>; + /// Temporarily stores the offence kind per (session, offender), set by + /// `EquivocationReportWrapper` before `on_offence` is called synchronously within + /// the same block. Keyed by session index and validator ID so that offences from + /// different sessions or for different validators cannot interfere with each other. + /// + /// SAFETY: relies on `pallet_offences::report_offence` calling `on_offence` + /// synchronously in the same block. Entries are cleaned up via `take()` in + /// `on_offence` on success, or explicit `remove()` in the wrapper on error. + /// If the offence pipeline ever becomes asynchronous, this storage should be + /// replaced with an offence-payload-based approach. + #[pallet::storage] + pub type PendingOffenceKind = StorageDoubleMap< + _, + Twox64Concat, + SessionIndex, + Twox64Concat, + T::ValidatorId, + OffenceKind, + OptionQuery, + >; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -305,6 +369,7 @@ pub mod pallet { era: EraIndex, validator: T::AccountId, percentage: Perbill, + offence_kind: OffenceKind, ) -> DispatchResult { ensure_root(origin)?; let active_era = T::EraIndexProvider::active_era().index; @@ -324,6 +389,7 @@ pub mod pallet { era, validator, slash_defer_duration, + offence_kind, ) .ok_or(Error::::ErrorComputingSlash)?; @@ -374,7 +440,8 @@ pub mod pallet { } } -/// This is intended to be used with `FilterHistoricalOffences`. +/// This is intended to be used with `EquivocationReportWrapper`, which filters +/// out historical offences (before the bonding period) and tags the offence kind. impl OnOffenceHandler, Weight> for Pallet @@ -453,6 +520,25 @@ where for (details, slash_fraction) in offenders.iter().zip(slash_fraction) { let (stash, _) = &details.offender; + // Read the per-(session, offender) offence kind set by EquivocationReportWrapper. + // This is set synchronously before on_offence is called, so take() clears it. + // Type safety: `stash` is T::ValidatorId (from IdentificationTuple), matching + // the key used by the wrapper. The trait bounds above enforce ValidatorId == AccountId. + let offence_kind = match pallet::PendingOffenceKind::::take(slash_session, stash) { + Some(kind) => kind, + None => { + log!( + log::Level::Error, + "MissingOffenceKind for session {:?}, validator {:?} — skipping slash", + slash_session, + stash, + ); + add_db_reads_writes(1, 1); + continue; + } + }; + add_db_reads_writes(1, 1); + // Skip if the validator is invulnerable. if invulnerables.contains(stash) { continue; @@ -477,6 +563,7 @@ where slash_era, stash.clone(), slash_defer_duration, + offence_kind.clone(), ); if let Some(mut slash) = slash { @@ -594,9 +681,22 @@ impl Pallet { break; }; + // Convert Perbill to EigenLayer WAD format with linear mapping. + // Perbill(100%) → MaxSlashWad (e.g. 5% WAD = 5e16). + // Formula: perbill_inner * MaxSlashWad / 1e9 + // Clamp to MaxSlashWad to guard against overflow if governance + // sets MaxSlashWad high enough for saturating_mul to hit u128::MAX. + let max_wad = T::MaxSlashWad::get(); + let wad_to_slash = (slash.percentage.deconstruct() as u128) + .saturating_mul(max_wad) + .checked_div(1_000_000_000u128) + .unwrap_or(0) + .min(max_wad); + slashes_to_send.push(SlashData { validator: slash.validator, - wad_to_slash: u128::from_str_radix("10000000000000000", 10).unwrap(), // TODO: need to compute how much we slash (for now it is 1e16) + wad_to_slash, + description: slash.offence_kind.to_description(), }); } }); @@ -655,19 +755,8 @@ pub struct Slash { pub percentage: Perbill, // Whether the slash is confirmed or still needs to go through deferred period pub confirmed: bool, -} - -impl Slash { - /// Initializes the default object using the given `validator`. - pub fn default_from(validator: AccountId) -> Self { - Self { - validator, - reporters: vec![], - slash_id: One::one(), - percentage: Perbill::from_percent(50), - confirmed: false, - } - } + /// The type of consensus offence (relayed to EigenLayer as a description string). + pub offence_kind: OffenceKind, } /// Computes a slash of a validator and nominators. It returns an unapplied @@ -682,6 +771,7 @@ pub(crate) fn compute_slash( slash_era: EraIndex, stash: T::AccountId, slash_defer_duration: EraIndex, + offence_kind: OffenceKind, ) -> Option> { let prior_slash_p = ValidatorSlashInEra::::get(slash_era, &stash).unwrap_or(Zero::zero()); @@ -707,6 +797,7 @@ pub(crate) fn compute_slash( slash_id, reporters: Vec::new(), confirmed, + offence_kind, }) } @@ -714,3 +805,107 @@ pub(crate) fn compute_slash( fn is_sorted_and_unique(list: &[u32]) -> bool { list.windows(2).all(|w| w[0] < w[1]) } + +/// Trait for associating an `OffenceKind` with a reporter type. +pub trait OffenceKindProvider { + fn kind() -> OffenceKind; +} + +/// Extracts the validator (account) ID from an offender identification tuple. +pub trait HasValidatorId { + fn validator_id(&self) -> &ValidatorId; +} + +impl HasValidatorId for (V, F) { + fn validator_id(&self) -> &V { + &self.0 + } +} + +/// Wraps a `ReportOffence` implementation to: +/// 1. **Filter historical offences**: discard reports whose session predates the bonding +/// period (similar to `FilterHistoricalOffences` in `pallet_staking`, but using this +/// pallet's own `BondedEras` storage instead of staking eras). +/// 2. **Tag offence kind**: store the `OffenceKind` per offender in `PendingOffenceKind` +/// before delegating to the inner reporter, so that `on_offence` can read it via +/// `PendingOffenceKind::take()`. +/// +/// If the inner `report_offence` fails (e.g. duplicate report), stale `PendingOffenceKind` +/// entries are cleaned up to prevent leaking into unrelated future offences. +pub struct EquivocationReportWrapper(PhantomData<(T, Inner, Kind)>); + +impl ReportOffence for EquivocationReportWrapper +where + T: Config, + Inner: ReportOffence, + O: Offence, + Kind: OffenceKindProvider, + Id: HasValidatorId, +{ + fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError> { + // Discard offences from before the bonding period. + let offence_session = offence.session_index(); + let bonded_eras = pallet::BondedEras::::get(); + if bonded_eras + .first() + .filter(|(_, start, _)| offence_session >= *start) + .is_none() + { + log!( + log::Level::Debug, + "discarding offence from session {} — predates bonded eras {:?}", + offence_session, + bonded_eras.first(), + ); + return Ok(()); + } + + let offenders = offence.offenders(); + for offender in &offenders { + pallet::PendingOffenceKind::::insert( + offence_session, + offender.validator_id(), + Kind::kind(), + ); + } + let result = Inner::report_offence(reporters, offence); + if result.is_err() { + for offender in &offenders { + pallet::PendingOffenceKind::::remove(offence_session, offender.validator_id()); + } + } + result + } + + fn is_known_offence(offenders: &[Id], time_slot: &O::TimeSlot) -> bool { + Inner::is_known_offence(offenders, time_slot) + } +} + +pub struct BabeEquivocation; +impl OffenceKindProvider for BabeEquivocation { + fn kind() -> OffenceKind { + OffenceKind::BabeEquivocation + } +} + +pub struct GrandpaEquivocation; +impl OffenceKindProvider for GrandpaEquivocation { + fn kind() -> OffenceKind { + OffenceKind::GrandpaEquivocation + } +} + +pub struct BeefyEquivocation; +impl OffenceKindProvider for BeefyEquivocation { + fn kind() -> OffenceKind { + OffenceKind::BeefyEquivocation + } +} + +pub struct ImOnlineUnresponsive; +impl OffenceKindProvider for ImOnlineUnresponsive { + fn kind() -> OffenceKind { + OffenceKind::LivenessOffence + } +} diff --git a/operator/pallets/external-validator-slashes/src/mock.rs b/operator/pallets/external-validator-slashes/src/mock.rs index c21f13c5..efe2c509 100644 --- a/operator/pallets/external-validator-slashes/src/mock.rs +++ b/operator/pallets/external-validator-slashes/src/mock.rs @@ -25,7 +25,7 @@ use { core::cell::RefCell, frame_support::{ parameter_types, - traits::{ConstU16, ConstU32, ConstU64, Get}, + traits::{ConstU128, ConstU16, ConstU32, ConstU64, Get}, weights::constants::RocksDbWeight, }, frame_system as system, @@ -132,7 +132,9 @@ thread_local! { pub static ERA_INDEX: RefCell = const { RefCell::new(0) }; pub static DEFER_PERIOD: RefCell = const { RefCell::new(2) }; pub static SENT_ETHEREUM_MESSAGE_NONCE: RefCell = const { RefCell::new(0) }; - + pub static MOCK_REPORT_OFFENCE_SHOULD_FAIL: RefCell = const { RefCell::new(false) }; + pub static MOCK_REPORT_OFFENCE_CALLED: RefCell = const { RefCell::new(false) }; + pub static LAST_SENT_SLASHES: RefCell>> = RefCell::new(Vec::new()); } impl MockEraIndexProvider { @@ -215,10 +217,16 @@ impl DeferPeriodGetter { } pub struct MockOkOutboundQueue; +impl MockOkOutboundQueue { + pub fn last_sent_slashes() -> Vec> { + LAST_SENT_SLASHES.with(|r| r.borrow().clone()) + } +} impl crate::SendMessage for MockOkOutboundQueue { type Ticket = (); type Message = (); - fn build(_: &Vec>, _: u32) -> Option { + fn build(slashes: &Vec>, _: u32) -> Option { + LAST_SENT_SLASHES.with(|r| *r.borrow_mut() = slashes.clone()); Some(()) } fn validate(_: Self::Ticket) -> Result { @@ -258,6 +266,7 @@ impl external_validator_slashes::Config for Test { type EraIndexProvider = MockEraIndexProvider; type InvulnerablesProvider = MockInvulnerableProvider; type ExternalIndexProvider = TimestampProvider; + type MaxSlashWad = ConstU128<50_000_000_000_000_000>; type QueuedSlashesProcessedPerBlock = ConstU32<20>; type WeightInfo = (); type SendMessage = MockOkOutboundQueue; @@ -289,6 +298,75 @@ impl sp_runtime::traits::Convert> for IdentityValidator { } } +// --- Mock infrastructure for testing EquivocationReportWrapper --- + +use sp_staking::offence::{Offence, OffenceError, ReportOffence}; + +/// A mock inner ReportOffence that can be configured to succeed or fail. +pub struct MockInnerReporter; + +impl MockInnerReporter { + pub fn set_should_fail(fail: bool) { + MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow_mut() = fail); + } + pub fn was_called() -> bool { + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow()) + } + pub fn reset() { + MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow_mut() = false); + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow_mut() = false); + } +} + +impl> ReportOffence for MockInnerReporter { + fn report_offence(_reporters: Vec, _offence: O) -> Result<(), OffenceError> { + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow_mut() = true); + if MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow()) { + Err(OffenceError::DuplicateReport) + } else { + Ok(()) + } + } + fn is_known_offence(_offenders: &[Id], _time_slot: &O::TimeSlot) -> bool { + false + } +} + +/// A minimal mock Offence for testing the wrapper. +pub struct MockOffence { + pub session_index: SessionIndex, + pub offenders: Vec<(u64, ())>, +} + +impl Offence<(u64, ())> for MockOffence { + const ID: sp_staking::offence::Kind = *b"mock:offence0000"; + type TimeSlot = u128; + + fn offenders(&self) -> Vec<(u64, ())> { + self.offenders.clone() + } + fn session_index(&self) -> SessionIndex { + self.session_index + } + fn validator_set_count(&self) -> u32 { + 3 + } + fn time_slot(&self) -> Self::TimeSlot { + self.session_index as u128 + } + fn slash_fraction(&self, _offenders_count: u32) -> sp_runtime::Perbill { + sp_runtime::Perbill::from_percent(50) + } +} + +/// Type alias for the wrapper using the mock reporter with BabeEquivocation kind. +pub type MockBabeWrapper = + crate::EquivocationReportWrapper; + +/// Type alias for the wrapper using the mock reporter with GrandpaEquivocation kind. +pub type MockGrandpaWrapper = + crate::EquivocationReportWrapper; + pub fn run_block() { run_to_block(System::block_number() + 1); } diff --git a/operator/pallets/external-validator-slashes/src/tests.rs b/operator/pallets/external-validator-slashes/src/tests.rs index 0c21466c..126485b8 100644 --- a/operator/pallets/external-validator-slashes/src/tests.rs +++ b/operator/pallets/external-validator-slashes/src/tests.rs @@ -18,12 +18,14 @@ use { super::*, crate::{ mock::{ - new_test_ext, run_block, DeferPeriodGetter, ExternalValidatorSlashes, - MockEraIndexProvider, RuntimeEvent, RuntimeOrigin, System, Test, + new_test_ext, run_block, DeferPeriodGetter, ExternalValidatorSlashes, MockBabeWrapper, + MockEraIndexProvider, MockGrandpaWrapper, MockInnerReporter, MockOffence, + MockOkOutboundQueue, RuntimeEvent, RuntimeOrigin, System, Test, }, - Slash, + OffenceKind, Slash, }, - frame_support::{assert_noop, assert_ok}, + frame_support::{assert_noop, assert_ok, BoundedVec}, + sp_staking::offence::ReportOffence, }; #[test] @@ -35,6 +37,7 @@ fn root_can_inject_manual_offence() { 0, 1u64, Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( Slashes::::get(get_slashing_era(0)), @@ -43,7 +46,10 @@ fn root_can_inject_manual_offence() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); assert_eq!(NextSlashId::::get(), 1); @@ -59,7 +65,8 @@ fn cannot_inject_future_era_offence() { RuntimeOrigin::root(), 1, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), ), Error::::ProvidedFutureEra ); @@ -76,7 +83,8 @@ fn cannot_inject_era_offence_too_far_in_the_past() { RuntimeOrigin::root(), 1, 4u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), ), Error::::ProvidedNonSlashableEra ); @@ -91,7 +99,8 @@ fn root_can_cancel_deferred_slash() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_ok!(ExternalValidatorSlashes::cancel_deferred_slash( RuntimeOrigin::root(), @@ -111,7 +120,8 @@ fn root_cannot_cancel_deferred_slash_if_outside_deferring_period() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); start_era(4, 0, 4); @@ -131,7 +141,8 @@ fn root_cannot_cancel_out_of_bounds() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash( @@ -152,7 +163,8 @@ fn root_cannot_cancel_duplicates() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 3, vec![0, 0]), @@ -169,13 +181,15 @@ fn root_cannot_cancel_if_not_sorted() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_ok!(ExternalValidatorSlashes::force_inject_slash( RuntimeOrigin::root(), 0, 2u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 3, vec![1, 0]), @@ -196,7 +210,8 @@ fn test_after_bonding_period_we_can_remove_slashes() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( @@ -206,7 +221,10 @@ fn test_after_bonding_period_we_can_remove_slashes() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); @@ -226,6 +244,7 @@ fn test_on_offence_injects_offences() { new_test_ext().execute_with(|| { start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -242,7 +261,8 @@ fn test_on_offence_injects_offences() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); }); @@ -253,7 +273,8 @@ fn test_on_offence_does_not_work_for_invulnerables() { new_test_ext().execute_with(|| { start_era(0, 0, 0); start_era(1, 1, 1); - // account 1 invulnerable + // account 1 invulnerable — populate kind so we test the invulnerable check, not missing kind + PendingOffenceKind::::insert(0, 1u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { offender: (1, ()), @@ -276,6 +297,7 @@ fn test_on_offence_does_not_work_if_slashing_disabled() { RuntimeOrigin::root(), SlashingModeOption::Disabled, )); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); let weight = Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -303,7 +325,8 @@ fn defer_period_of_zero_confirms_immediately_slashes() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( Slashes::::get(get_slashing_era(0)), @@ -312,7 +335,10 @@ fn defer_period_of_zero_confirms_immediately_slashes() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); }); @@ -327,7 +353,8 @@ fn we_cannot_cancel_anything_with_defer_period_zero() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 0, vec![0]), @@ -342,6 +369,7 @@ fn test_on_offence_defer_period_0() { crate::mock::DeferPeriodGetter::with_defer_period(0); start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -359,7 +387,8 @@ fn test_on_offence_defer_period_0() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); start_era(2, 2, 2); @@ -373,6 +402,7 @@ fn test_slashes_command_matches_event() { crate::mock::DeferPeriodGetter::with_defer_period(0); start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -391,7 +421,8 @@ fn test_slashes_command_matches_event() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); start_era(2, 2, 2); @@ -405,6 +436,122 @@ fn test_slashes_command_matches_event() { }); } +// ── WAD conversion tests ── +// MaxSlashWad in mock = 50_000_000_000_000_000 (5e16 = 5% in WAD format). +// Perbill(100%) = 1_000_000_000 inner. +// Formula: wad = perbill_inner * MaxSlashWad / 1e9 + +#[test] +fn wad_conversion_100_percent_slash_maps_to_max_slash_wad() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(100)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 100% → full MaxSlashWad = 5e16 + assert_eq!(sent[0].wad_to_slash, 50_000_000_000_000_000u128); + assert_eq!(sent[0].validator, 3); + }); +} + +#[test] +fn wad_conversion_50_percent_slash_maps_to_half_max_slash_wad() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 50% → MaxSlashWad / 2 = 2.5e16 + assert_eq!(sent[0].wad_to_slash, 25_000_000_000_000_000u128); + }); +} + +#[test] +fn wad_conversion_zero_percent_slash_maps_to_zero() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + // 0% slash → no slash recorded (compute_slash returns None for 0%) + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 0); + }); +} + +#[test] +fn wad_conversion_carries_offence_kind_description() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Pre-populate a BabeEquivocation kind for session 0, validator 3. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(75)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 75% → 75% of MaxSlashWad = 3.75e16 + assert_eq!(sent[0].wad_to_slash, 37_500_000_000_000_000u128); + assert_eq!(sent[0].description, "BABE equivocation"); + }); +} + #[test] fn test_on_offence_defer_period_0_messages_get_queued() { new_test_ext().execute_with(|| { @@ -413,6 +560,7 @@ fn test_on_offence_defer_period_0_messages_get_queued() { start_era(1, 1, 1); // The limit is 20, for i in 0..25 { + PendingOffenceKind::::insert(0, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -450,6 +598,7 @@ fn test_account_id_encoding() { slash_id: 1, percentage: Perbill::default(), confirmed: true, + offence_kind: OffenceKind::LivenessOffence, }; let encoded_account = slash.validator.encode(); @@ -466,6 +615,7 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { start_era(1, 1, 1); // The limit is 20, for i in 0..25 { + PendingOffenceKind::::insert(0, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -487,6 +637,7 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { // We have 5 non-dispatched, which should accumulate // We shoulld have 30 after we initialie era 3 for i in 0..25 { + PendingOffenceKind::::insert(2, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -512,6 +663,213 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { }); } +// ── PendingOffenceKind & EquivocationReportWrapper tests ── + +#[test] +fn on_offence_reads_pending_offence_kind_from_double_map() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Pre-populate PendingOffenceKind for validator 3 at session 0. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(75)], + 0, + ); + + assert_eq!( + Slashes::::get(get_slashing_era(0)), + vec![Slash { + validator: 3, + percentage: Perbill::from_percent(75), + confirmed: false, + reporters: vec![], + slash_id: 0, + offence_kind: OffenceKind::BabeEquivocation, + }] + ); + + // Entry should have been consumed. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + }); +} + +#[test] +fn pending_offence_kind_is_session_isolated() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Same validator, different kinds in different sessions. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + PendingOffenceKind::::insert(1, 3u64, OffenceKind::GrandpaEquivocation); + + // Report at session 0 — should use BabeEquivocation. + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + 0, + ); + + // Session 0 consumed, session 1 untouched. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + assert_eq!( + PendingOffenceKind::::get(1, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + }); +} + +#[test] +fn wrapper_filters_historical_offence_before_bonding_period() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + // BondedEras now contains [(0,0,0), (1,1,1)]. + // An offence at session 0 is within the bonding period — should pass. + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_ok()); + assert!(MockInnerReporter::was_called()); + + // The mock reporter doesn't trigger on_offence, so manually consume the entry. + assert_eq!( + PendingOffenceKind::::take(0, 3u64), + Some(OffenceKind::BabeEquivocation), + ); + + // Advance eras until era 0 drops out of BondedEras. + // BondingDuration = 5, so after era 6 starts, era 0 is pruned. + for i in 2..=7 { + start_era(i, i, i as u64); + } + + MockInnerReporter::reset(); + + // Session 0 now predates the bonding period — should be silently discarded. + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_ok()); + assert!(!MockInnerReporter::was_called()); + + // No PendingOffenceKind should have been written. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + }); +} + +#[test] +fn wrapper_sets_pending_offence_kind_per_session_and_offender() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + let _ = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ()), (4, ())], + }, + ); + + // Both offenders should have entries at session 0. + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::BabeEquivocation), + ); + assert_eq!( + PendingOffenceKind::::get(0, 4u64), + Some(OffenceKind::BabeEquivocation), + ); + // No entry at a different session. + assert_eq!(PendingOffenceKind::::get(1, 3u64), None); + }); +} + +#[test] +fn wrapper_cleans_up_pending_offence_kind_on_error() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + MockInnerReporter::set_should_fail(true); + + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ()), (4, ())], + }, + ); + + assert!(result.is_err()); + // Entries should have been cleaned up. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + assert_eq!(PendingOffenceKind::::get(0, 4u64), None); + }); +} + +#[test] +fn wrapper_error_cleanup_does_not_affect_other_sessions() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + // Successfully report at session 0. + let _ = MockGrandpaWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + + // Now fail a report at session 1 for the same validator. + MockInnerReporter::set_should_fail(true); + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 1, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_err()); + + // Session 1 cleaned up, session 0 untouched. + assert_eq!(PendingOffenceKind::::get(1, 3u64), None); + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + }); +} + fn start_era(era_index: EraIndex, session_index: SessionIndex, external_idx: u64) { Pallet::::on_era_start(era_index, session_index, external_idx); crate::mock::MockEraIndexProvider::with_era(era_index); diff --git a/operator/pallets/external-validators-rewards/src/benchmarking.rs b/operator/pallets/external-validators-rewards/src/benchmarking.rs index 335557f4..4b84bc3f 100644 --- a/operator/pallets/external-validators-rewards/src/benchmarking.rs +++ b/operator/pallets/external-validators-rewards/src/benchmarking.rs @@ -21,9 +21,9 @@ use super::*; #[allow(unused)] use crate::Pallet as ExternalValidatorsRewards; use { - crate::{types::BenchmarkHelper, OnEraEnd}, + crate::types::BenchmarkHelper, frame_benchmarking::{account, v2::*, BenchmarkError}, - frame_support::traits::Currency, + frame_support::traits::{Currency, EnsureOrigin}, sp_std::prelude::*, }; @@ -43,6 +43,11 @@ fn create_funded_user( user } +/// Helper: insert a single entry into the ring buffer at slot 0. +fn push_unsent_entry(era_index: u32, timestamp: u32, inflation: u128) { + ExternalValidatorsRewards::::unsent_queue_push((era_index, timestamp, inflation)); +} + #[allow(clippy::multiple_bound_locations)] #[benchmarks(where T: pallet_balances::Config)] mod benchmarks { @@ -72,6 +77,106 @@ mod benchmarks { Ok(()) } + /// Helper to populate reward points for an era with 1000 validators. + fn setup_era_reward_points(era_index: u32) { + let mut era_reward_points = EraRewardPoints::default(); + era_reward_points.total = 20 * 1000; + + for i in 0..1000 { + let account_id = create_funded_user::("candidate", i, 100); + era_reward_points.individual.insert(account_id, 20); + } + + >::insert(era_index, era_reward_points); + } + + // on_initialize: unsent queue is empty (2 reads for head+tail) + #[benchmark] + fn process_unsent_reward_eras_empty() -> Result<(), BenchmarkError> { + // Ensure queue is empty (default state: head == tail == 0) + assert!(ExternalValidatorsRewards::::unsent_queue_is_empty()); + + #[block] + { + ExternalValidatorsRewards::::process_unsent_reward_eras(); + } + + Ok(()) + } + + // on_initialize: oldest entry has pruned reward points + #[benchmark] + fn process_unsent_reward_eras_expired() -> Result<(), BenchmarkError> { + // Push an entry whose reward points do NOT exist in storage + push_unsent_entry::(999, 0, 42); + + #[block] + { + ExternalValidatorsRewards::::process_unsent_reward_eras(); + } + + // Entry should have been removed + assert!(ExternalValidatorsRewards::::unsent_queue_is_empty()); + + Ok(()) + } + + // on_initialize: oldest entry retried successfully + #[benchmark] + fn process_unsent_reward_eras_success() -> Result<(), BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); + T::BenchmarkHelper::setup(); + setup_era_reward_points::(1); + + push_unsent_entry::(1, 0, 42); + + #[block] + { + ExternalValidatorsRewards::::process_unsent_reward_eras(); + } + + assert!(ExternalValidatorsRewards::::unsent_queue_is_empty()); + + Ok(()) + } + + // Use success weight as upper bound for the failed path + #[benchmark] + fn process_unsent_reward_eras_failed() -> Result<(), BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); + T::BenchmarkHelper::setup(); + setup_era_reward_points::(1); + + push_unsent_entry::(1, 0, 42); + + #[block] + { + ExternalValidatorsRewards::::process_unsent_reward_eras(); + } + + Ok(()) + } + + // Governance extrinsic: retry a specific unsent era + #[benchmark] + fn retry_unsent_reward_era() -> Result<(), BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); + T::BenchmarkHelper::setup(); + setup_era_reward_points::(1); + + push_unsent_entry::(1, 0, 42); + + let origin = + T::GovernanceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, 1u32); + + assert!(ExternalValidatorsRewards::::unsent_queue_is_empty()); + + Ok(()) + } + impl_benchmark_test_suite!( ExternalValidatorsRewards, crate::mock::new_test_ext(), diff --git a/operator/pallets/external-validators-rewards/src/lib.rs b/operator/pallets/external-validators-rewards/src/lib.rs index 376d3a55..8aeaa123 100644 --- a/operator/pallets/external-validators-rewards/src/lib.rs +++ b/operator/pallets/external-validators-rewards/src/lib.rs @@ -66,13 +66,13 @@ pub mod pallet { pub use crate::weights::WeightInfo; use { - super::*, frame_support::pallet_prelude::*, + super::*, frame_support::pallet_prelude::*, frame_system::pallet_prelude::OriginFor, pallet_external_validators::traits::EraIndexProvider, sp_runtime::Saturating, sp_std::collections::btree_map::BTreeMap, }; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); pub type RewardPoints = u32; pub type EraIndex = u32; @@ -168,6 +168,9 @@ pub mod pallet { /// Hook for minting inflation tokens. type HandleInflation: HandleInflation; + /// Origin for governance calls (e.g., retrying unsent reward messages). + type GovernanceOrigin: EnsureOrigin; + #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper: types::BenchmarkHelper; } @@ -175,6 +178,62 @@ pub mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_n: frame_system::pallet_prelude::BlockNumberFor) -> Weight { + Self::process_unsent_reward_eras() + } + } + + #[pallet::call] + impl Pallet { + /// Governance escape hatch: manually retry sending a rewards message for + /// an era that is stuck in the unsent queue. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::retry_unsent_reward_era())] + pub fn retry_unsent_reward_era( + origin: OriginFor, + era_index: EraIndex, + ) -> DispatchResult { + T::GovernanceOrigin::ensure_origin(origin)?; + + // Scan the ring buffer for the requested era + let head = UnsentRewardHead::::get(); + let tail = UnsentRewardTail::::get(); + let mut found = None; + let mut slot = head; + while slot != tail { + if let Some(entry @ (idx, _, _)) = UnsentRewardEra::::get(slot) { + if idx == era_index { + found = Some((slot, entry)); + break; + } + } + slot = (slot + 1) % UNSENT_QUEUE_CAPACITY; + } + let (slot, (_, timestamp, inflation)) = found.ok_or(Error::::EraNotInUnsentQueue)?; + + let reward_points = RewardPointsForEra::::get(era_index); + let info = reward_points + .generate_era_rewards_info(era_index, inflation, timestamp) + .ok_or(Error::::RewardPointsPruned)?; + + let message_id = + Self::send_rewards_message(&info).ok_or(Error::::MessageSendFailed)?; + + Self::unsent_queue_remove_slot(slot); + + Self::deposit_event(Event::RewardsMessageRetried { + message_id, + era_index, + total_points: info.total_points, + inflation_amount: inflation, + }); + + Ok(()) + } + } + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -185,6 +244,29 @@ pub mod pallet { total_points: u128, inflation_amount: u128, }, + /// The rewards message failed to send; era queued for retry. + RewardsMessageSendFailed { era_index: EraIndex }, + /// A previously failed rewards message was retried and sent successfully. + RewardsMessageRetried { + message_id: H256, + era_index: EraIndex, + total_points: u128, + inflation_amount: u128, + }, + /// An unsent era was dropped because its reward points have been pruned. + UnsentEraExpired { era_index: EraIndex }, + /// The unsent queue is full; this era could not be enqueued for retry. + UnsentQueueFull { era_index: EraIndex }, + } + + #[pallet::error] + pub enum Error { + /// The specified era is not in the unsent queue. + EraNotInUnsentQueue, + /// Reward points for the era have been pruned from storage. + RewardPointsPruned, + /// The message delivery still failed on retry. + MessageSendFailed, } /// Keep tracks of distributed points per validator and total. @@ -200,7 +282,7 @@ pub mod pallet { /// - individual_points: (address, points) tuples for each validator. /// - inflation_amount: total inflation tokens to distribute. /// - era_start_timestamp: timestamp when the era started (seconds since Unix epoch). - pub fn generate_era_rewards_utils( + pub fn generate_era_rewards_info( &self, era_index: EraIndex, inflation_amount: u128, @@ -260,6 +342,33 @@ pub mod pallet { pub type BlocksProducedInEra = StorageMap<_, Twox64Concat, EraIndex, u32, ValueQuery>; + /// Maximum number of unsent reward entries in the ring buffer. + pub const UNSENT_QUEUE_CAPACITY: u32 = 64; + + /// Ring buffer of eras whose rewards messages failed to send. + /// Each slot stores (era_index, era_start_timestamp, scaled_inflation). + /// Keyed by slot index [0, UNSENT_QUEUE_CAPACITY). + #[pallet::storage] + pub type UnsentRewardEra = StorageMap< + _, + Twox64Concat, + u32, + ( + EraIndex, + /* era_start_timestamp */ u32, + /* scaled_inflation */ u128, + ), + >; + + /// Ring buffer head: next slot to be processed by `on_initialize`. + #[pallet::storage] + pub type UnsentRewardHead = StorageValue<_, u32, ValueQuery>; + + /// Ring buffer tail: next slot to write a new entry into. + /// When head == tail the buffer is empty. + #[pallet::storage] + pub type UnsentRewardTail = StorageValue<_, u32, ValueQuery>; + impl Pallet { /// Reward validators. Does not check if the validators are valid, caller needs to make sure of that. pub fn reward_by_ids(points: impl IntoIterator) { @@ -276,8 +385,8 @@ pub mod pallet { /// Helper to build, validate and deliver an outbound message. /// Logs any error and returns None on failure. - fn send_rewards_message(utils: &EraRewardsUtils) -> Option { - let outbound = T::SendMessage::build(utils).or_else(|| { + fn send_rewards_message(info: &EraRewardsUtils) -> Option { + let outbound = T::SendMessage::build(info).or_else(|| { log::error!(target: "ext_validators_rewards", "Failed to build outbound message"); None })?; @@ -303,6 +412,147 @@ pub mod pallet { .ok() } + // ── Ring-buffer helpers ────────────────────────────────────────── + + /// Returns true when the ring buffer is empty (head == tail). + #[allow(dead_code)] + pub(crate) fn unsent_queue_is_empty() -> bool { + UnsentRewardHead::::get() == UnsentRewardTail::::get() + } + + /// Number of entries currently in the ring buffer. + #[allow(dead_code)] + pub(crate) fn unsent_queue_len() -> u32 { + let head = UnsentRewardHead::::get(); + let tail = UnsentRewardTail::::get(); + tail.wrapping_sub(head) % UNSENT_QUEUE_CAPACITY + } + + /// Push a new entry into the ring buffer. + /// Returns `true` on success, `false` if the buffer is full. + pub(crate) fn unsent_queue_push(entry: (EraIndex, u32, u128)) -> bool { + let head = UnsentRewardHead::::get(); + let tail = UnsentRewardTail::::get(); + let next_tail = (tail + 1) % UNSENT_QUEUE_CAPACITY; + if next_tail == head { + // Buffer full + return false; + } + UnsentRewardEra::::insert(tail, entry); + UnsentRewardTail::::put(next_tail); + true + } + + /// Remove the entry at a given slot and compact the buffer by shifting + /// subsequent entries back. Used by the extrinsic and `on_era_start`. + fn unsent_queue_remove_slot(slot: u32) { + let tail = UnsentRewardTail::::get(); + // Shift entries after `slot` backward to fill the gap + let mut cur = slot; + loop { + let next = (cur + 1) % UNSENT_QUEUE_CAPACITY; + if next == tail { + break; + } + // Move next → cur + if let Some(entry) = UnsentRewardEra::::get(next) { + UnsentRewardEra::::insert(cur, entry); + } + cur = next; + } + // Remove the now-duplicate last entry and shrink tail + UnsentRewardEra::::remove(cur); + let new_tail = if tail == 0 { + UNSENT_QUEUE_CAPACITY - 1 + } else { + tail - 1 + }; + UnsentRewardTail::::put(new_tail); + + // If head was after the removed slot, adjust it too + let head = UnsentRewardHead::::get(); + // We also need to handle head potentially pointing past the buffer + // after a removal. Since we shifted everything between slot..tail back, + // the head only needs adjustment if it was == tail (now new_tail) — but + // that means the buffer just became empty, which is fine (head == new_tail). + // However, if head was pointing *at* a slot beyond the removed one, the + // entry it pointed to slid back by one, so head should also slide back. + // In practice, removal only happens when we know the slot, so we can + // simply recalculate emptiness. + if head == tail { + // Was already at tail, buffer must be empty now + UnsentRewardHead::::put(new_tail); + } + } + + // ── Core retry logic ────────────────────────────────────────────── + + /// Process at most one unsent reward era per block. + /// On failure the head pointer advances to the next entry so a single + /// stuck era does not block retries for subsequent eras. + pub(crate) fn process_unsent_reward_eras() -> Weight { + let head = UnsentRewardHead::::get(); + let tail = UnsentRewardTail::::get(); + + if head == tail { + return T::WeightInfo::process_unsent_reward_eras_empty(); + } + + let Some((era_index, timestamp, inflation)) = UnsentRewardEra::::get(head) else { + // Slot unexpectedly empty — advance head past it + UnsentRewardHead::::put((head + 1) % UNSENT_QUEUE_CAPACITY); + return T::WeightInfo::process_unsent_reward_eras_empty(); + }; + + // Check if reward points are still available + let reward_points = RewardPointsForEra::::get(era_index); + let info = + match reward_points.generate_era_rewards_info(era_index, inflation, timestamp) { + Some(info) => info, + None => { + // Reward points have been pruned — discard this entry + log::warn!( + target: "ext_validators_rewards", + "Unsent era {era_index} expired: reward points pruned", + ); + UnsentRewardEra::::remove(head); + UnsentRewardHead::::put((head + 1) % UNSENT_QUEUE_CAPACITY); + Self::deposit_event(Event::UnsentEraExpired { era_index }); + return T::WeightInfo::process_unsent_reward_eras_expired(); + } + }; + + // Attempt to resend + match Self::send_rewards_message(&info) { + Some(message_id) => { + UnsentRewardEra::::remove(head); + UnsentRewardHead::::put((head + 1) % UNSENT_QUEUE_CAPACITY); + Self::deposit_event(Event::RewardsMessageRetried { + message_id, + era_index, + total_points: info.total_points, + inflation_amount: inflation, + }); + T::WeightInfo::process_unsent_reward_eras_success() + } + None => { + // Move the failed entry to the back of the queue so the + // next block tries a different era (avoids head-of-line + // blocking). The entry is not lost — it will be retried + // after all other pending entries. + UnsentRewardEra::::remove(head); + UnsentRewardHead::::put((head + 1) % UNSENT_QUEUE_CAPACITY); + UnsentRewardEra::::insert(tail, (era_index, timestamp, inflation)); + UnsentRewardTail::::put((tail + 1) % UNSENT_QUEUE_CAPACITY); + log::warn!( + target: "ext_validators_rewards", + "Retry for unsent era {era_index} still failing, moved to back of queue", + ); + T::WeightInfo::process_unsent_reward_eras_failed() + } + } + } + /// Track a block authored by a validator pub fn note_block_author(author: T::AccountId) { // Track per-session authorship for performance points @@ -619,6 +869,24 @@ pub mod pallet { RewardPointsForEra::::remove(era_index_to_delete); BlocksProducedInEra::::remove(era_index_to_delete); + + // Proactively clean up any unsent entries whose reward points + // have been pruned (this era and any older ones still lingering). + let head = UnsentRewardHead::::get(); + let mut tail = UnsentRewardTail::::get(); + let mut slot = head; + while slot != tail { + if let Some((idx, _, _)) = UnsentRewardEra::::get(slot) { + if idx <= era_index_to_delete { + Self::unsent_queue_remove_slot(slot); + tail = UnsentRewardTail::::get(); + Self::deposit_event(Event::UnsentEraExpired { era_index: idx }); + // Don't advance slot — next entry slid into this position + continue; + } + } + slot = (slot + 1) % UNSENT_QUEUE_CAPACITY; + } } } @@ -671,17 +939,17 @@ pub mod pallet { // Generate era rewards utils with the actual rewards amount (post-treasury split). // This ensures the message to EigenLayer matches the actual minted rewards. - let utils = match era_reward_points.generate_era_rewards_utils( + let info = match RewardPointsForEra::::get(&era_index).generate_era_rewards_info( era_index, mint_result.rewards_amount, era_start_timestamp, ) { - Some(utils) => utils, + Some(info) => info, None => { // Returns None when total_points is zero or no validators have rewards log::error!( target: "ext_validators_rewards", - "Failed to generate era rewards utils (no rewards to distribute)" + "Failed to generate era rewards info (no rewards to distribute)" ); return; } @@ -692,13 +960,31 @@ pub mod pallet { DispatchClass::Mandatory, ); - if let Some(message_id) = Self::send_rewards_message(&utils) { - Self::deposit_event(Event::RewardsMessageSent { - message_id, - era_index, - total_points: utils.total_points, - inflation_amount: mint_result.rewards_amount, - }); + match Self::send_rewards_message(&info) { + Some(message_id) => { + Self::deposit_event(Event::RewardsMessageSent { + message_id, + era_index, + total_points: info.total_points, + inflation_amount: mint_result.rewards_amount, + }); + } + None => { + // Message failed — queue for automatic retry via on_initialize + if Self::unsent_queue_push(( + era_index, + era_start_timestamp, + mint_result.rewards_amount, + )) { + Self::deposit_event(Event::RewardsMessageSendFailed { era_index }); + } else { + log::error!( + target: "ext_validators_rewards", + "Unsent reward queue full, cannot enqueue era {era_index}", + ); + Self::deposit_event(Event::UnsentQueueFull { era_index }); + } + } } } } diff --git a/operator/pallets/external-validators-rewards/src/mock.rs b/operator/pallets/external-validators-rewards/src/mock.rs index 3c892f35..6b99b7c3 100644 --- a/operator/pallets/external-validators-rewards/src/mock.rs +++ b/operator/pallets/external-validators-rewards/src/mock.rs @@ -131,6 +131,9 @@ impl crate::types::SendMessage for MockOkOutboundQueue { } fn validate(ticket: Self::Ticket) -> Result { + if Mock::mock().send_message_fails { + return Err(SendError::MessageTooLarge); + } Ok(ticket) } @@ -223,6 +226,7 @@ impl pallet_external_validators_rewards::Config for Test { type HandleInflation = InflationMinter; type Currency = Balances; type RewardsEthereumSovereignAccount = RewardsEthereumSovereignAccount; + type GovernanceOrigin = frame_system::EnsureRoot; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -292,6 +296,8 @@ pub mod mock_data { pub offline_validators: sp_std::vec::Vec, /// Set of (era_index, validator_id) pairs that are slashed pub slashed_validators: sp_std::vec::Vec<(u32, sp_core::H160)>, + /// When true, MockOkOutboundQueue::validate will return Err(SendError::MessageTooLarge) + pub send_message_fails: bool, } #[pallet::config] diff --git a/operator/pallets/external-validators-rewards/src/tests.rs b/operator/pallets/external-validators-rewards/src/tests.rs index 1a66daa0..752a55c0 100644 --- a/operator/pallets/external-validators-rewards/src/tests.rs +++ b/operator/pallets/external-validators-rewards/src/tests.rs @@ -16,7 +16,7 @@ use { crate::{self as pallet_external_validators_rewards, mock::*}, - frame_support::traits::fungible::Mutate, + frame_support::{assert_noop, assert_ok, traits::fungible::Mutate}, pallet_external_validators::traits::{ActiveEraInfo, OnEraEnd, OnEraStart}, sp_core::H160, sp_std::collections::btree_map::BTreeMap, @@ -165,8 +165,8 @@ fn test_on_era_end() { let treasury_amount = InflationTreasuryProportion::get().mul_floor(inflation); let rewards_amount = inflation - treasury_amount; // Use 0 for era_start_timestamp in tests - let rewards_utils = era_rewards.generate_era_rewards_utils(1, rewards_amount, 0); - assert!(rewards_utils.is_some()); + let rewards_info = era_rewards.generate_era_rewards_info(1, inflation, 0); + assert!(rewards_info.is_some()); System::assert_last_event(RuntimeEvent::ExternalValidatorsRewards( crate::Event::RewardsMessageSent { message_id: Default::default(), @@ -207,8 +207,8 @@ fn test_on_era_end_with_zero_inflation() { let era_rewards = pallet_external_validators_rewards::RewardPointsForEra::::get(1); let inflation = ::EraInflationProvider::get(); - let rewards_utils = era_rewards.generate_era_rewards_utils(1, inflation, 0); - assert!(rewards_utils.is_some()); + let rewards_info = era_rewards.generate_era_rewards_info(1, inflation, 0); + assert!(rewards_info.is_some()); // With zero inflation, no RewardsMessageSent event should be emitted let events = System::events(); assert!( @@ -246,15 +246,15 @@ fn test_on_era_end_with_zero_points() { ExternalValidatorsRewards::reward_by_ids(accounts_points); ExternalValidatorsRewards::on_era_end(1); - // When all validators have zero points, generate_era_rewards_utils should return None + // When all validators have zero points, generate_era_rewards_info should return None // to prevent inflation from being minted with no way to distribute it let era_rewards = pallet_external_validators_rewards::RewardPointsForEra::::get(1); let inflation = ::EraInflationProvider::get(); - let rewards_utils = era_rewards.generate_era_rewards_utils(1, inflation, 0); + let rewards_info = era_rewards.generate_era_rewards_info(1, inflation, 0); assert!( - rewards_utils.is_none(), - "generate_era_rewards_utils should return None when total_points is zero" + rewards_info.is_none(), + "generate_era_rewards_info should return None when total_points is zero" ); // Verify no RewardsMessageSent event was emitted @@ -3722,3 +3722,456 @@ fn test_era_end_uses_correct_era_blocks_not_session() { ); }) } + +// ═══════════════════════════════════════════════════════════════════════════ +// Retry mechanism tests (ring-buffer storage) +// ═══════════════════════════════════════════════════════════════════════════ + +/// Helper: push an entry into the unsent ring buffer via the pallet API. +fn push_unsent(era_index: u32, timestamp: u32, inflation: u128) { + assert!( + ExternalValidatorsRewards::unsent_queue_push((era_index, timestamp, inflation)), + "unsent_queue_push should succeed" + ); +} + +/// Helper: return the number of entries in the unsent ring buffer. +fn unsent_len() -> u32 { + ExternalValidatorsRewards::unsent_queue_len() +} + +/// Helper: check if unsent queue is empty. +fn unsent_is_empty() -> bool { + ExternalValidatorsRewards::unsent_queue_is_empty() +} + +#[test] +fn send_failure_queues_era() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + mock.send_message_fails = true; + }); + + // Give validators some points + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + // Author expected blocks for 100% inflation + for _ in 0..600 { + ExternalValidatorsRewards::note_block_author(H160::from_low_u64_be(1)); + } + + ExternalValidatorsRewards::on_era_end(1); + + // Verify era is queued + assert_eq!(unsent_len(), 1); + + // Verify event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageSendFailed { era_index: 1 }, + )); + }) +} + +#[test] +fn on_initialize_retries_and_succeeds() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + }); + + // Set up reward points for era 1 + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + + // Manually populate the unsent queue + push_unsent(1, 30, 42); + + // Sending should succeed (send_message_fails is false by default) + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // Queue should be empty + assert!(unsent_is_empty()); + + // Verify retry event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 1, + total_points: 100, + inflation_amount: 42, + }, + )); + }) +} + +#[test] +fn on_initialize_moves_failed_entry_to_back() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 2, + start: Some(30_000), + }); + mock.send_message_fails = true; + }); + + // Set up reward points for eras 1 and 2 + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + }); + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 200)]); + + // Push two entries: era 1 then era 2 + push_unsent(1, 30, 42); + push_unsent(2, 30, 84); + + // First call: tries era 1, fails, moves era 1 to back of queue + ExternalValidatorsRewards::process_unsent_reward_eras(); + // Queue length stays the same (entry moved, not removed) + assert_eq!(unsent_len(), 2); + + // Second call: tries era 2 (NOT era 1 again), fails, moves era 2 to back + ExternalValidatorsRewards::process_unsent_reward_eras(); + assert_eq!(unsent_len(), 2); + + // Re-enable sending + Mock::mutate(|mock| mock.send_message_fails = false); + + // Third call: era 1 (now at front again), succeeds + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + assert_eq!(unsent_len(), 1); + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 1, + total_points: 200, + inflation_amount: 42, + }, + )); + + // Fourth call: era 2, succeeds + ExternalValidatorsRewards::process_unsent_reward_eras(); + assert!(unsent_is_empty()); + }) +} + +#[test] +fn on_initialize_removes_expired_era() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Populate unsent queue with era 999 but do NOT add RewardPointsForEra for it + push_unsent(999, 0, 42); + + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // Entry should be removed + assert!(unsent_is_empty()); + + // Verify expired event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::UnsentEraExpired { era_index: 999 }, + )); + }) +} + +#[test] +fn on_initialize_noop_when_queue_empty() { + new_test_ext().execute_with(|| { + run_to_block(1); + System::reset_events(); + + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // No events should be emitted + let events = System::events(); + assert!( + events.is_empty(), + "No events should be emitted when unsent queue is empty" + ); + }) +} + +#[test] +fn on_initialize_processes_only_head() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 3, + start: Some(30_000), + }); + }); + + // Set up reward points for both eras + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 2, + start: Some(30_000), + }); + }); + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(2), 200)]); + + // Push two entries + push_unsent(3, 30, 42); + push_unsent(2, 20, 84); + + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // Only the head entry (era 3) should be processed (and removed on success) + assert_eq!(unsent_len(), 1); + }) +} + +#[test] +fn retry_extrinsic_success() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + }); + + // Set up reward points + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + + // Populate unsent queue + push_unsent(1, 30, 42); + + System::reset_events(); + assert_ok!(ExternalValidatorsRewards::retry_unsent_reward_era( + RuntimeOrigin::root(), + 1 + )); + + // Queue should be empty + assert!(unsent_is_empty()); + + // Verify retry event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 1, + total_points: 100, + inflation_amount: 42, + }, + )); + }) +} + +#[test] +fn retry_extrinsic_era_not_in_queue() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_noop!( + ExternalValidatorsRewards::retry_unsent_reward_era(RuntimeOrigin::root(), 1), + crate::Error::::EraNotInUnsentQueue + ); + }) +} + +#[test] +fn retry_extrinsic_pruned_data() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Queue an era but don't create reward points for it + push_unsent(999, 0, 42); + + assert_noop!( + ExternalValidatorsRewards::retry_unsent_reward_era(RuntimeOrigin::root(), 999), + crate::Error::::RewardPointsPruned + ); + }) +} + +#[test] +fn retry_extrinsic_requires_root() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_noop!( + ExternalValidatorsRewards::retry_unsent_reward_era( + RuntimeOrigin::signed(H160::from_low_u64_be(1)), + 1 + ), + sp_runtime::DispatchError::BadOrigin + ); + }) +} + +#[test] +fn unsent_queue_full() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 65, + start: Some(30_000), + }); + mock.send_message_fails = true; + }); + + // Fill the ring buffer to capacity (63 entries, since capacity=64 + // means 63 usable slots in a ring buffer with head==tail==empty). + for i in 0..63u32 { + push_unsent(i, 0, 42); + } + assert_eq!(unsent_len(), 63); + + // Give validators some points so on_era_end doesn't bail early + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + for _ in 0..600 { + ExternalValidatorsRewards::note_block_author(H160::from_low_u64_be(1)); + } + + System::reset_events(); + ExternalValidatorsRewards::on_era_end(65); + + // Verify UnsentQueueFull event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::UnsentQueueFull { era_index: 65 }, + )); + + // Queue should still be at 63 + assert_eq!(unsent_len(), 63); + }) +} + +#[test] +fn on_era_start_prunes_unsent_entry() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Set up: era 1 has an unsent entry + push_unsent(1, 0, 42); + + // HistoryDepth is 10, so era 11 should prune era 1 + System::reset_events(); + ExternalValidatorsRewards::on_era_start(11, 0, 11); + + // Unsent entry should be removed + assert!(unsent_is_empty()); + + // Verify expired event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::UnsentEraExpired { era_index: 1 }, + )); + }) +} + +#[test] +fn retry_extrinsic_send_still_fails() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + mock.send_message_fails = true; + }); + + // Set up reward points + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + + // Populate unsent queue + push_unsent(1, 30, 42); + + assert_noop!( + ExternalValidatorsRewards::retry_unsent_reward_era(RuntimeOrigin::root(), 1), + crate::Error::::MessageSendFailed + ); + + // Queue should still have the entry + assert_eq!(unsent_len(), 1); + }) +} + +#[test] +fn head_of_line_blocking_avoided() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Set up reward points for eras 1, 2, 3 + for era in 1..=3u32 { + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: era, + start: Some(30_000), + }); + }); + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + } + + // Push eras 1, 2, 3 into the queue + push_unsent(1, 30, 10); + push_unsent(2, 30, 20); + push_unsent(3, 30, 30); + + // Make sending fail + Mock::mutate(|mock| mock.send_message_fails = true); + + // Block 1: tries era 1, fails, advances head → era 2 + ExternalValidatorsRewards::process_unsent_reward_eras(); + // Block 2: tries era 2, fails, advances head → era 3 + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // Now re-enable sending + Mock::mutate(|mock| mock.send_message_fails = false); + + // Block 3: tries era 3, succeeds + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 3, + total_points: 100, + inflation_amount: 30, + }, + )); + + // Block 4: wraps around to era 1, succeeds + ExternalValidatorsRewards::process_unsent_reward_eras(); + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 1, + total_points: 100, + inflation_amount: 10, + }, + )); + + // Block 5: era 2, succeeds + ExternalValidatorsRewards::process_unsent_reward_eras(); + assert!(unsent_is_empty()); + }) +} diff --git a/operator/pallets/external-validators-rewards/src/weights.rs b/operator/pallets/external-validators-rewards/src/weights.rs index 766adfcf..a7585778 100644 --- a/operator/pallets/external-validators-rewards/src/weights.rs +++ b/operator/pallets/external-validators-rewards/src/weights.rs @@ -54,6 +54,11 @@ use sp_std::marker::PhantomData; /// Weight functions needed for pallet_external_validators_rewards. pub trait WeightInfo { fn on_era_end() -> Weight; + fn process_unsent_reward_eras_empty() -> Weight; + fn process_unsent_reward_eras_expired() -> Weight; + fn process_unsent_reward_eras_success() -> Weight; + fn process_unsent_reward_eras_failed() -> Weight; + fn retry_unsent_reward_era() -> Weight; } /// Weights for pallet_external_validators_rewards using the Substrate node and recommended hardware. @@ -84,6 +89,36 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + // 1 read for UnsentRewardEras + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + // 1 read UnsentRewardEras + 1 read RewardPointsForEra + 1 write UnsentRewardEras + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + // Same as on_era_end + queue read/write + Weight::from_parts(1_136_401_000, 39987) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + // Use success weight as upper bound + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + // Same as success path + Self::process_unsent_reward_eras_success() + } } // For backwards compatibility and tests @@ -113,4 +148,29 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + Weight::from_parts(1_136_401_000, 39987) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + Self::process_unsent_reward_eras_success() + } } diff --git a/operator/runtime/common/src/slashes_adapter.rs b/operator/runtime/common/src/slashes_adapter.rs index e28aabc4..74fa5c58 100644 --- a/operator/runtime/common/src/slashes_adapter.rs +++ b/operator/runtime/common/src/slashes_adapter.rs @@ -111,8 +111,8 @@ fn encode_slashing_request( let slashing_request = SlashingRequest { operator: Address::from(slash_operator.validator.0), strategies: strategies.clone(), - wadsToSlash: wads_to_slash, // We only have one strategy deployed - description: "Slashing validator".into(), + wadsToSlash: wads_to_slash, + description: slash_operator.description.clone().into(), }; slashings.push(slashing_request); diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index 2d97fc46..48f1aad9 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -401,7 +409,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -424,7 +436,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -501,8 +517,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1494,7 +1518,7 @@ impl datahaven_runtime_common::rewards_adapter::RewardsSubmissionConfig for Main } fn rewards_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies_and_multipliers() -> Vec<(H160, u128)> { @@ -1574,6 +1598,8 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; + type GovernanceOrigin = + EitherOfDiverse, governance::custom_origins::GeneralAdmin>; type WeightInfo = mainnet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -1668,9 +1694,9 @@ impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for Main runtime_params::dynamic_params::runtime_config::DatahavenServiceManagerAddress::get() } + // TODO: remove `slashes_` prefix and just call it `agent_origin` fn slashes_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() - // TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ? + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies() -> Vec
{ @@ -1701,6 +1727,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = mainnet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; diff --git a/operator/runtime/mainnet/src/configs/runtime_params.rs b/operator/runtime/mainnet/src/configs/runtime_params.rs index 605c9f73..9d3cdeb3 100644 --- a/operator/runtime/mainnet/src/configs/runtime_params.rs +++ b/operator/runtime/mainnet/src/configs/runtime_params.rs @@ -48,9 +48,9 @@ pub mod dynamic_params { #[codec(index = 3)] #[allow(non_upper_case_globals)] - /// The RewardsAgentOrigin is the hash of the string "external_validators_rewards" + /// The AgentOrigin is the hash of the string "external_validators_rewards" /// TODO: Decide which agent origin we want to use. Currently for testing it's the zero hash - pub static RewardsAgentOrigin: H256 = H256::from_slice(&hex!( + pub static AgentOrigin: H256 = H256::from_slice(&hex!( "c505dfb2df107d106d08bd0f1a0acd10052ca9aa078629a4ccfd0c90c6e69b65" )); @@ -417,6 +417,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/operator/runtime/mainnet/src/lib.rs b/operator/runtime/mainnet/src/lib.rs index 94b4ec5b..f295318a 100644 --- a/operator/runtime/mainnet/src/lib.rs +++ b/operator/runtime/mainnet/src/lib.rs @@ -142,7 +142,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 200 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 1300, + spec_version: 1400, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/operator/runtime/mainnet/src/weights/pallet_external_validators_rewards.rs b/operator/runtime/mainnet/src/weights/pallet_external_validators_rewards.rs index b8be1393..10854100 100644 --- a/operator/runtime/mainnet/src/weights/pallet_external_validators_rewards.rs +++ b/operator/runtime/mainnet/src/weights/pallet_external_validators_rewards.rs @@ -74,4 +74,29 @@ impl pallet_external_validators_rewards::WeightInfo for .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + Weight::from_parts(1_905_623_000, 29162) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + Self::process_unsent_reward_eras_success() + } } diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index fb4cc4bb..12b4a960 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -400,7 +408,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -498,8 +514,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1490,7 +1514,7 @@ impl datahaven_runtime_common::rewards_adapter::RewardsSubmissionConfig for Stag } fn rewards_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies_and_multipliers() -> Vec<(H160, u128)> { @@ -1570,6 +1594,8 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; + type GovernanceOrigin = + EitherOfDiverse, governance::custom_origins::GeneralAdmin>; type WeightInfo = stagenet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -1665,7 +1691,7 @@ impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for Stag } fn slashes_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() // TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ? } @@ -1697,6 +1723,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = stagenet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; @@ -1926,8 +1953,8 @@ mod tests { /// Test that the Rewards Agent ID (used for Snowbridge outbound messages from the rewards pallet) /// is correctly computed from the chain's genesis hash and the ExternalValidatorRewardsAccount. /// - /// This test verifies the value that should be set as `RewardsAgentOrigin` in runtime parameters - /// and as `rewardsMessageOrigin` in the AVS contract configuration. + /// This test verifies the value that should be set as `AgentOrigin` in runtime parameters + /// and as `messageOrigin` in the AVS contract configuration. /// /// The Agent ID is computed following Snowbridge's pattern for GlobalConsensus locations: /// blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), compact_len, "AccountKey20", account_key)) @@ -1967,8 +1994,8 @@ mod tests { // Hash with blake2_256 let computed_agent_id = H256(blake2_256(&encoded)); - // Expected Agent ID - this value must match RewardsAgentOrigin in runtime_params.rs - // If this test fails, update RewardsAgentOrigin to match the computed value. + // Expected Agent ID - this value must match AgentOrigin in runtime_params.rs + // If this test fails, update AgentOrigin to match the computed value. let expected_agent_id = H256(hex_literal::hex!( "56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd" )); @@ -1978,8 +2005,8 @@ mod tests { expected_agent_id, "Computed Rewards Agent ID must match expected value.\n\ This value should be set as:\n\ - - RewardsAgentOrigin in runtime_params.rs\n\ - - rewardsMessageOrigin in AVS contract config\n\ + - AgentOrigin in runtime_params.rs\n\ + - messageOrigin in AVS contract config\n\ \n\ Rewards account: 0x{}\n\ Genesis hash: 0x{}\n\ diff --git a/operator/runtime/stagenet/src/configs/runtime_params.rs b/operator/runtime/stagenet/src/configs/runtime_params.rs index f05caff5..dcf136c9 100644 --- a/operator/runtime/stagenet/src/configs/runtime_params.rs +++ b/operator/runtime/stagenet/src/configs/runtime_params.rs @@ -51,13 +51,13 @@ pub mod dynamic_params { #[codec(index = 3)] #[allow(non_upper_case_globals)] - /// The RewardsAgentOrigin is the Agent ID for the rewards pallet's outbound Snowbridge messages. + /// The AgentOrigin is the Agent ID for the rewards/slashes pallet's outbound Snowbridge messages. /// Computed as: blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), interior)) /// where interior = SCALE_ENCODE("AccountKey20", ExternalValidatorRewardsAccount) /// /// For stagenet with genesis hash 0x72d0856fd339e09cb21df7bac8ac3120bd871e327ec0e1658395df68acef9bee /// and rewards account 0x6d6f646c64682f65767265770000000000000000 (from PalletId "dh/evrew"): - pub static RewardsAgentOrigin: H256 = H256::from_slice(&hex!( + pub static AgentOrigin: H256 = H256::from_slice(&hex!( "56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd" )); @@ -424,6 +424,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/operator/runtime/stagenet/src/lib.rs b/operator/runtime/stagenet/src/lib.rs index 0655d3be..df43090f 100644 --- a/operator/runtime/stagenet/src/lib.rs +++ b/operator/runtime/stagenet/src/lib.rs @@ -145,7 +145,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 200 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 1300, + spec_version: 1400, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/operator/runtime/stagenet/src/weights/pallet_external_validators_rewards.rs b/operator/runtime/stagenet/src/weights/pallet_external_validators_rewards.rs index 4d223163..34d31953 100644 --- a/operator/runtime/stagenet/src/weights/pallet_external_validators_rewards.rs +++ b/operator/runtime/stagenet/src/weights/pallet_external_validators_rewards.rs @@ -74,4 +74,29 @@ impl pallet_external_validators_rewards::WeightInfo for .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + Weight::from_parts(1_894_953_000, 29162) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + Self::process_unsent_reward_eras_success() + } } diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index 5b9305a9..27dbc538 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -400,7 +408,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -501,8 +517,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1494,7 +1518,7 @@ impl datahaven_runtime_common::rewards_adapter::RewardsSubmissionConfig for Test } fn rewards_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies_and_multipliers() -> Vec<(H160, u128)> { @@ -1574,6 +1598,8 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; + type GovernanceOrigin = + EitherOfDiverse, governance::custom_origins::GeneralAdmin>; type WeightInfo = testnet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -1669,8 +1695,7 @@ impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for Test } fn slashes_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() - // TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ? + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies() -> Vec
{ @@ -1700,6 +1725,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = testnet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; @@ -1948,8 +1974,8 @@ mod tests { /// Test that the Rewards Agent ID (used for Snowbridge outbound messages from the rewards pallet) /// is correctly computed from the chain's genesis hash and the ExternalValidatorRewardsAccount. /// - /// This test verifies the value that should be set as `RewardsAgentOrigin` in runtime parameters - /// and as `rewardsMessageOrigin` in the AVS contract configuration. + /// This test verifies the value that should be set as `AgentOrigin` in runtime parameters + /// and as `messageOrigin` in the AVS contract configuration. /// /// The Agent ID is computed following Snowbridge's pattern for GlobalConsensus locations: /// blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), compact_len, "AccountKey20", account_key)) @@ -1989,8 +2015,8 @@ mod tests { // Hash with blake2_256 let computed_agent_id = H256(blake2_256(&encoded)); - // Expected Agent ID - this value must match RewardsAgentOrigin in runtime_params.rs - // If this test fails, update RewardsAgentOrigin to match the computed value. + // Expected Agent ID - this value must match AgentOrigin in runtime_params.rs + // If this test fails, update AgentOrigin to match the computed value. let expected_agent_id = H256(hex_literal::hex!( "d0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215" )); @@ -2000,8 +2026,8 @@ mod tests { expected_agent_id, "Computed Rewards Agent ID must match expected value.\n\ This value should be set as:\n\ - - RewardsAgentOrigin in runtime_params.rs\n\ - - rewardsMessageOrigin in AVS contract config\n\ + - AgentOrigin in runtime_params.rs\n\ + - messageOrigin in AVS contract config\n\ \n\ Rewards account: 0x{}\n\ Genesis hash: 0x{}\n\ diff --git a/operator/runtime/testnet/src/configs/runtime_params.rs b/operator/runtime/testnet/src/configs/runtime_params.rs index bc218f6c..40c89108 100644 --- a/operator/runtime/testnet/src/configs/runtime_params.rs +++ b/operator/runtime/testnet/src/configs/runtime_params.rs @@ -49,13 +49,13 @@ pub mod dynamic_params { #[codec(index = 3)] #[allow(non_upper_case_globals)] - /// The RewardsAgentOrigin is the Agent ID for the rewards pallet's outbound Snowbridge messages. + /// The AgentOrigin is the Agent ID for the rewards/slashes pallet's outbound Snowbridge messages. /// Computed as: blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), interior)) /// where interior = SCALE_ENCODE("AccountKey20", ExternalValidatorRewardsAccount) /// /// For testnet with genesis hash 0xdbf403d348916fb0694485bc7f9c0d8c53fdf86664ebac019af209c090c3df99 /// and rewards account 0x6d6f646c64682f65767265770000000000000000 (from PalletId "dh/evrew"): - pub static RewardsAgentOrigin: H256 = H256::from_slice(&hex!( + pub static AgentOrigin: H256 = H256::from_slice(&hex!( "d0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215" )); @@ -419,6 +419,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/operator/runtime/testnet/src/lib.rs b/operator/runtime/testnet/src/lib.rs index c177d49b..a8248dbd 100644 --- a/operator/runtime/testnet/src/lib.rs +++ b/operator/runtime/testnet/src/lib.rs @@ -142,7 +142,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 200 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 1300, + spec_version: 1400, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/operator/runtime/testnet/src/weights/pallet_external_validators_rewards.rs b/operator/runtime/testnet/src/weights/pallet_external_validators_rewards.rs index b2403bcf..9b7e752d 100644 --- a/operator/runtime/testnet/src/weights/pallet_external_validators_rewards.rs +++ b/operator/runtime/testnet/src/weights/pallet_external_validators_rewards.rs @@ -74,4 +74,29 @@ impl pallet_external_validators_rewards::WeightInfo for .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + Weight::from_parts(1_893_280_000, 29162) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + Self::process_unsent_reward_eras_success() + } } diff --git a/test/.dockerignore b/test/.dockerignore new file mode 100644 index 00000000..4731dc5a --- /dev/null +++ b/test/.dockerignore @@ -0,0 +1,15 @@ +# Keep submitter image build context minimal. +* + +!package.json +!bun.lock +!tsconfig.json +!bunfig.toml +!.papi/ +!.papi/** +!tools/validator-set-submitter/ +!tools/validator-set-submitter/** +!contract-bindings/ +!contract-bindings/** +!utils/ +!utils/** diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index d7be6139..185bc678 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.13357056092938763018", + "version": "0.1.0-autogenerated.18139584469151706411", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/test/.papi/metadata/datahaven.scale b/test/.papi/metadata/datahaven.scale index 5b2fcad3..3f4882b2 100644 Binary files a/test/.papi/metadata/datahaven.scale and b/test/.papi/metadata/datahaven.scale differ diff --git a/test/bun.lock b/test/bun.lock index d95f380f..a80af401 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -14,6 +14,9 @@ "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@polkadot-api/descriptors": "file:.papi/descriptors", + "@storagehub-sdk/core": "^0.4.4", + "@storagehub-sdk/msp-client": "^0.4.4", + "@storagehub/api-augment": "^0.4.0", "@types/dockerode": "^3.3.41", "@types/node": "^22.15.32", "@wagmi/cli": "^2.3.1", @@ -568,6 +571,14 @@ "@sqltools/formatter": ["@sqltools/formatter@1.2.5", "", {}, "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="], + "@storagehub-sdk/core": ["@storagehub-sdk/core@0.4.4", "", { "dependencies": { "@polkadot/types": "^16.4.7", "abitype": "^1.0.0", "ethers": "^6.15.0" }, "peerDependencies": { "viem": ">=2.38.3" } }, "sha512-3tvsp5ILx4r1JWzqef02EKKL+u9nZIrl+/PMpj4Ode17v+mDmYI2ME3On9fZ8/+dEIAXWgqGh8/EjkYdP9PAEQ=="], + + "@storagehub-sdk/msp-client": ["@storagehub-sdk/msp-client@0.4.4", "", { "peerDependencies": { "@storagehub-sdk/core": ">=0.0.5", "viem": ">=2.38.3" } }, "sha512-7TLSQAhwJ+RFxU5SbknRw37Qkhts3u2DycdZyA7aUe6e+QyD917QNnlYcM/JJLZFFiqGwy+Nrk07xhKv1zKAZg=="], + + "@storagehub/api-augment": ["@storagehub/api-augment@0.4.2", "", { "dependencies": { "@polkadot/api": "^16.4.7", "@polkadot/api-base": "^16.4.7", "@polkadot/rpc-core": "^16.4.7", "@polkadot/typegen": "^16.4.7", "@polkadot/types": "^16.4.7", "@polkadot/types-codec": "^16.4.7", "@storagehub/types-bundle": "0.4.2", "tsx": "4.20.5", "typescript": "^5.9.2" } }, "sha512-L3q5ZsZD+iLPEdBs2ZTKeH5fDaihiUJQpyxSC3pj0geOdE97m+FqxgOALEvAZT7Eqi0m38B0xneREzwPpIGtnA=="], + + "@storagehub/types-bundle": ["@storagehub/types-bundle@0.4.2", "", { "dependencies": { "@polkadot/api": "^16.4.7", "@polkadot/api-base": "^16.4.7", "@polkadot/rpc-core": "^16.4.6", "@polkadot/typegen": "^16.4.6", "@polkadot/types": "^16.4.7", "@polkadot/types-codec": "^16.4.7", "typescript": "^5.9.2" } }, "sha512-kkWYP1WwiVP0NGQqIWLfcOsIkb1BJXk7Qw+pkNIzf7QW6HpJaPySJybRksK6ClwKdqzNXXyZ4Sw0vBO1//8h0w=="], + "@substrate/connect": ["@substrate/connect@0.8.11", "", { "dependencies": { "@substrate/connect-extension-protocol": "^2.0.0", "@substrate/connect-known-chains": "^1.1.5", "@substrate/light-client-extension-helpers": "^1.0.0", "smoldot": "2.0.26" } }, "sha512-ofLs1PAO9AtDdPbdyTYj217Pe+lBfTLltdHDs3ds8no0BseoLeAGxpz1mHfi7zB4IxI3YyAiLjH6U8cw4pj4Nw=="], "@substrate/connect-extension-protocol": ["@substrate/connect-extension-protocol@2.2.2", "", {}, "sha512-t66jwrXA0s5Goq82ZtjagLNd7DPGCNjHeehRlE/gcJmJ+G56C0W+2plqOMRicJ8XGR1/YFnUSEqUFiSNbjGrAA=="], @@ -2180,6 +2191,10 @@ "@safe-global/safe-apps-sdk/viem": ["viem@2.29.2", "", { "dependencies": { "@noble/curves": "1.8.2", "@noble/hashes": "1.7.2", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", "ox": "0.6.9", "ws": "8.18.1" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-cukRxab90jvQ+TDD84sU3qB3UmejYqgCw4cX8SfWzvh7JPfZXI3kAMUaT5OSR2As1Mgvx1EJawccwPjGqkSSwA=="], + "@storagehub/api-augment/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "@storagehub/types-bundle/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@substrate/connect/smoldot": ["smoldot@2.0.26", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-F+qYmH4z2s2FK+CxGj8moYcd1ekSIKH8ywkdqlOz88Dat35iB1DIYL11aILN46YSGMzQW/lbJNS307zBSDN5Ig=="], "@substrate/light-client-extension-helpers/@polkadot-api/json-rpc-provider": ["@polkadot-api/json-rpc-provider@0.0.1", "", {}, "sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA=="], diff --git a/test/cli/handlers/contracts/rewards-origin.ts b/test/cli/handlers/contracts/rewards-origin.ts index d89c31bd..4da1b116 100644 --- a/test/cli/handlers/contracts/rewards-origin.ts +++ b/test/cli/handlers/contracts/rewards-origin.ts @@ -48,7 +48,7 @@ const palletIdToAccountId20 = (palletId: string): Hex => { * blake2_256(SCALE_ENCODE(("GlobalConsensus", ByGenesis(genesis_hash), ("AccountKey20", account_key)))) * * NOTE: This computation follows Snowbridge's pattern but may need verification against - * the actual on-chain Agent ID. The preferred approach is to set RewardsAgentOrigin on + * the actual on-chain Agent ID. The preferred approach is to set AgentOrigin on * the chain and fetch it via this command. * * @param genesisHash - The chain's genesis hash (32 bytes, hex string with 0x prefix) @@ -116,36 +116,36 @@ const computeAgentId = async (genesisHash: Hex, accountKey20: Hex): Promise }; /** - * Fetches the RewardsAgentOrigin from the runtime parameters. + * Fetches the AgentOrigin from the runtime parameters. * * @param rpcUrl - WebSocket RPC endpoint of the DataHaven chain - * @returns The RewardsAgentOrigin as a hex string, or null if not set or zero + * @returns The AgentOrigin as a hex string, or null if not set or zero */ -const fetchRewardsAgentOrigin = async (rpcUrl: string): Promise => { +const fetchAgentOrigin = async (rpcUrl: string): Promise => { logger.info(`📡 Connecting to DataHaven chain at ${rpcUrl}...`); const { client: papiClient, typedApi: dhApi } = createPapiConnectors(rpcUrl); try { - logger.info("🔍 Fetching RewardsAgentOrigin from runtime parameters..."); + logger.info("🔍 Fetching AgentOrigin from runtime parameters..."); - // Query the Parameters pallet for RewardsAgentOrigin + // Query the Parameters pallet for AgentOrigin const parameter = await dhApi.query.Parameters.Parameters.getValue( { type: "RuntimeConfig", - value: { type: "RewardsAgentOrigin", value: undefined } + value: { type: "AgentOrigin", value: undefined } }, { at: "best" } ); if (!parameter) { - logger.info("ℹ️ RewardsAgentOrigin parameter not found (using default)"); + logger.info("ℹ️ AgentOrigin parameter not found (using default)"); return null; } // Extract the value from the parameter result // The parameter is wrapped in the RuntimeConfig enum variant - if (parameter.type === "RuntimeConfig" && parameter.value.type === "RewardsAgentOrigin") { + if (parameter.type === "RuntimeConfig" && parameter.value.type === "AgentOrigin") { const origin = parameter.value.value; if (origin) { const originHex = origin.asHex(); @@ -153,15 +153,15 @@ const fetchRewardsAgentOrigin = async (rpcUrl: string): Promise => { const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex; if (originHex === zeroHash) { - logger.info("ℹ️ RewardsAgentOrigin is set to zero (placeholder)"); + logger.info("ℹ️ AgentOrigin is set to zero (placeholder)"); return null; } - logger.success(`Found RewardsAgentOrigin: ${originHex}`); + logger.success(`Found AgentOrigin: ${originHex}`); return originHex as Hex; } } - logger.info("ℹ️ RewardsAgentOrigin value not available"); + logger.info("ℹ️ AgentOrigin value not available"); return null; } finally { papiClient.destroy(); @@ -193,9 +193,9 @@ const fetchGenesisHash = async (rpcUrl: string): Promise => { * Updates the config file with the rewards message origin. * * @param networkId - The network identifier (e.g., "hoodi", "stagenet-hoodi") - * @param rewardsMessageOrigin - The rewards message origin (Agent ID) + * @param messageOrigin - The rewards message origin (Agent ID) */ -const updateConfigFile = async (networkId: string, rewardsMessageOrigin: Hex): Promise => { +const updateConfigFile = async (networkId: string, messageOrigin: Hex): Promise => { const configFilePath = `../contracts/config/${networkId}.json`; const configFile = Bun.file(configFilePath); @@ -211,21 +211,21 @@ const updateConfigFile = async (networkId: string, rewardsMessageOrigin: Hex): P configJson.snowbridge = {}; } - const oldOrigin = configJson.snowbridge.rewardsMessageOrigin; - configJson.snowbridge.rewardsMessageOrigin = rewardsMessageOrigin; + const oldOrigin = configJson.snowbridge.messageOrigin; + configJson.snowbridge.messageOrigin = messageOrigin; await Bun.write(configFilePath, `${JSON.stringify(configJson, null, 2)}\n`); logger.success(`Config file updated: ${configFilePath}`); - if (oldOrigin !== rewardsMessageOrigin) { - logger.info(` rewardsMessageOrigin: ${oldOrigin ?? "unset"} -> ${rewardsMessageOrigin}`); + if (oldOrigin !== messageOrigin) { + logger.info(` messageOrigin: ${oldOrigin ?? "unset"} -> ${messageOrigin}`); } }; /** * Main handler for the update-rewards-origin command. - * Fetches or computes the RewardsAgentOrigin and updates the config file. + * Fetches or computes the AgentOrigin and updates the config file. */ export const updateRewardsOrigin = async (options: UpdateRewardsOriginOptions): Promise => { const networkId = buildNetworkId(options.chain, options.environment); @@ -246,17 +246,17 @@ export const updateRewardsOrigin = async (options: UpdateRewardsOriginOptions): printDivider(); try { - // Step 1: Try to fetch RewardsAgentOrigin from the chain - let rewardsMessageOrigin = await fetchRewardsAgentOrigin(options.rpcUrl); + // Step 1: Try to fetch AgentOrigin from the chain + let messageOrigin = await fetchAgentOrigin(options.rpcUrl); printDivider(); - if (rewardsMessageOrigin) { + if (messageOrigin) { // Use the value from the chain - logger.info("✅ Using RewardsAgentOrigin from chain runtime parameters"); + logger.info("✅ Using AgentOrigin from chain runtime parameters"); } else { // Compute the Agent ID from genesis hash and pallet account - logger.info("🔧 Computing RewardsAgentOrigin from genesis hash and pallet account..."); + logger.info("🔧 Computing AgentOrigin from genesis hash and pallet account..."); // Get genesis hash (from option or fetch from chain) const genesisHash = options.genesisHash @@ -272,22 +272,22 @@ export const updateRewardsOrigin = async (options: UpdateRewardsOriginOptions): // Compute the Agent ID logger.info("🔐 Computing Agent ID..."); logger.warn( - "⚠️ Note: Computed Agent ID may need verification. Prefer setting RewardsAgentOrigin on-chain." + "⚠️ Note: Computed Agent ID may need verification. Prefer setting AgentOrigin on-chain." ); - rewardsMessageOrigin = await computeAgentId(genesisHash, rewardsAccount); - logger.info(` Agent ID: ${rewardsMessageOrigin}`); + messageOrigin = await computeAgentId(genesisHash, rewardsAccount); + logger.info(` Agent ID: ${messageOrigin}`); } printDivider(); // Display the final value logger.info("📝 Rewards Message Origin:"); - logger.info(` ${rewardsMessageOrigin}`); + logger.info(` ${messageOrigin}`); printDivider(); // Update the config file - await updateConfigFile(networkId, rewardsMessageOrigin); + await updateConfigFile(networkId, messageOrigin); printDivider(); logger.success(`Rewards message origin updated successfully for ${networkId}`); diff --git a/test/cli/index.ts b/test/cli/index.ts index 1ecf7921..338bf38f 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -212,7 +212,7 @@ const contractsCommand = program - upgrade: Upgrade contracts by deploying new implementations - verify: Verify deployed contracts on block explorer - update-beefy-checkpoint: Fetch BEEFY authorities from a live chain and update config - - update-rewards-origin: Fetch or compute the RewardsAgentOrigin and update config + - update-rewards-origin: Fetch or compute the AgentOrigin and update config - update-metadata: Update the metadata URI of an existing AVS contract Common options: @@ -396,17 +396,14 @@ contractsCommand contractsCommand .command("update-rewards-origin") .description( - "Fetch or compute the RewardsAgentOrigin and update the config file with the rewards message origin" + "Fetch or compute the AgentOrigin and update the config file with the rewards message origin" ) .option("--chain ", "Target chain (hoodi, ethereum, anvil)") .option( "--environment ", "Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value." ) - .option( - "--rpc-url ", - "WebSocket RPC URL of the DataHaven chain to fetch RewardsAgentOrigin from" - ) + .option("--rpc-url ", "WebSocket RPC URL of the DataHaven chain to fetch AgentOrigin from") .option( "--genesis-hash ", "Chain genesis hash (32 bytes hex). If not provided, will be fetched from the chain." diff --git a/test/configs/parameters/datahaven-parameters.json b/test/configs/parameters/datahaven-parameters.json index ce875f03..0072a499 100644 --- a/test/configs/parameters/datahaven-parameters.json +++ b/test/configs/parameters/datahaven-parameters.json @@ -8,7 +8,7 @@ "value": null }, { - "name": "RewardsAgentOrigin", + "name": "AgentOrigin", "value": null }, { diff --git a/test/e2e/framework/submitter.ts b/test/e2e/framework/submitter.ts index cad168eb..66ce466f 100644 --- a/test/e2e/framework/submitter.ts +++ b/test/e2e/framework/submitter.ts @@ -17,13 +17,13 @@ const SUBMITTER_READY_TIMEOUT_SECONDS = 30; const SUBMITTER_LOG_TAIL_LINES = 200; /** - * Builds the validator-set-submitter Docker image from the repo root. + * Builds the validator-set-submitter Docker image from the test directory. */ export async function buildSubmitterImage(): Promise { logger.debug("Building validator-set-submitter Docker image..."); - const repoRoot = path.resolve(import.meta.dir, "../../.."); - await $`docker build -f test/tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .` - .cwd(repoRoot) + const testRoot = path.resolve(import.meta.dir, "../.."); + await $`docker build -f tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .` + .cwd(testRoot) .quiet(); logger.debug("Validator-set-submitter image built successfully"); } @@ -106,9 +106,11 @@ export async function launchSubmitter(options: LaunchSubmitterOptions): Promise< timeoutSeconds: SUBMITTER_READY_TIMEOUT_SECONDS }); } catch (error) { + const logResult = await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}` + .nothrow() + .quiet(); const logs = - (await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}`.nothrow().text()) || - ""; + `${logResult.stdout.toString()}${logResult.stderr.toString()}`.trim() || ""; await stopSubmitter(containerName); throw new Error( `Submitter did not become ready. Expected log "${SUBMITTER_READY_LOG}". Last ${SUBMITTER_LOG_TAIL_LINES} log lines:\n${logs}`, diff --git a/test/e2e/framework/suite.ts b/test/e2e/framework/suite.ts index 5afb7144..4a74252a 100644 --- a/test/e2e/framework/suite.ts +++ b/test/e2e/framework/suite.ts @@ -3,6 +3,7 @@ import readline from "node:readline"; import { isCI } from "launcher/network"; import { logger } from "utils"; import { launchNetwork } from "../../launcher"; +import { getDefaultRelayerImageTag } from "../../launcher/network"; import type { LaunchNetworkResult } from "../../launcher/types"; import { ConnectorFactory, type TestConnectors } from "./connectors"; import { TestSuiteManager } from "./manager"; @@ -57,7 +58,7 @@ export abstract class BaseTestSuite { datahavenImageTag: this.options.networkOptions?.datahavenImageTag || "datahavenxyz/datahaven:local", relayerImageTag: - this.options.networkOptions?.relayerImageTag || "datahavenxyz/snowbridge-relay:latest", + this.options.networkOptions?.relayerImageTag || getDefaultRelayerImageTag(), buildDatahaven: false, // default to false in the test suite so we can speed up the CI ...this.options.networkOptions }); diff --git a/test/e2e/framework/validators.ts b/test/e2e/framework/validators.ts index 2ecee8a9..a6c0ff38 100644 --- a/test/e2e/framework/validators.ts +++ b/test/e2e/framework/validators.ts @@ -54,6 +54,8 @@ export const launchDatahavenValidator = async ( const COMMON_LAUNCH_ARGS = [ "--unsafe-force-node-key-generation", "--tmp", + "--chain", + "local", "--validator", "--discover-local", "--no-prometheus", diff --git a/test/e2e/suites/slash.test.ts b/test/e2e/suites/slash.test.ts index 87e370f6..7547a84c 100644 --- a/test/e2e/suites/slash.test.ts +++ b/test/e2e/suites/slash.test.ts @@ -1,9 +1,12 @@ import { beforeAll, describe, expect, it } from "bun:test"; -import { FixedSizeBinary } from "polkadot-api"; +import { $ } from "bun"; +import { Binary, FixedSizeBinary } from "polkadot-api"; import { CROSS_CHAIN_TIMEOUTS, getPapiSigner, logger } from "utils"; import type { Address } from "viem"; +import { gatewayAbi } from "../../contract-bindings"; import { getContractInstance, parseDeploymentsFile } from "../../utils/contracts"; -import { waitForDataHavenEvent } from "../../utils/events"; +import { waitForDataHavenEvent, waitForEthereumEvent } from "../../utils/events"; +import { waitFor } from "../../utils/waits"; import { BaseTestSuite } from "../framework"; class SlashTestSuite extends BaseTestSuite { @@ -15,6 +18,10 @@ class SlashTestSuite extends BaseTestSuite { // Set up hooks in constructor this.setupHooks(); } + + getNetworkId(): string { + return this.networkId; + } } // Create the test suite instance @@ -116,6 +123,8 @@ describe("Should slash an operator", () => { }, 40000); it("use sudo to slash operator", async () => { + const { publicClient } = suite.getTestConnectors(); + // get era number const activeEra = await dhApi.query.ExternalValidators.ActiveEra.getValue(); @@ -129,7 +138,11 @@ describe("Should slash an operator", () => { const sudoSlashCall = dhApi.tx.ExternalValidatorsSlashes.force_inject_slash({ validator, era: activeEra?.index || 0, // Will fail if active era is 0. !! Important !! Sometimes for the inject to work (because of some latency) we need to inject in the `activeEra.index + 1` - percentage: 20 + percentage: 20, + offence_kind: { + type: "Custom", + value: Binary.fromText("Manual slash: E2E test") + } }); const sudoTx = dhApi.tx.Sudo.sudo({ call: sudoSlashCall.decodedCall @@ -158,5 +171,120 @@ describe("Should slash an operator", () => { throw new Error("SlashesMessageSent event not found"); } logger.info("Slashes message sent"); + + const fromBlock = await publicClient.getBlockNumber(); + const deployments = await parseDeploymentsFile(); + const _ethEvent = await waitForEthereumEvent({ + client: publicClient, + address: deployments.Gateway, + abi: gatewayAbi, + eventName: "SlashingComplete", + fromBlock: fromBlock > 0n ? fromBlock - 1n : fromBlock, + timeout: CROSS_CHAIN_TIMEOUTS.DH_TO_ETH_MS + }); + + logger.info("Got Ethereum event!"); }, 560000); + + it("should detect and slash an unresponsive validator (liveness)", async () => { + const networkId = suite.getNetworkId(); + const bobContainer = `datahaven-bob-${networkId}`; + + // Drain any prior SlashReported events so we only see events from this test. + await dhApi.event.ExternalValidatorsSlashes.SlashReported.pull(); + + const activeEra = await dhApi.query.ExternalValidators.ActiveEra.getValue({ at: "best" }); + const eraAtStart = activeEra?.index ?? 0; + const sessionAtStart = await dhApi.query.Session.CurrentIndex.getValue({ at: "best" }); + logger.info(`Liveness test start — era: ${eraAtStart}, session: ${sessionAtStart}`); + + // Pause bob to simulate a liveness failure (missed heartbeats). + // Using pause/unpause instead of stop/start preserves bob's process + // state (GRANDPA voter, peer connections, keystore) so finality can + // resume quickly once unpaused. + logger.info(`Pausing bob container: ${bobContainer}`); + await $`docker pause ${bobContainer}`.quiet(); + logger.info("Bob container paused. Waiting for sessions to elapse..."); + + let slashReportedEvent: any; + try { + // Wait for at least TWO full sessions so pallet_im_online detects bob's + // missing heartbeats at a session boundary. + // + // Why two sessions: + // 1. Bob may have already sent his heartbeat for the current session + // before being paused, so the first session boundary won't report him. + // 2. The NEXT full session (where bob is offline from start to finish) + // will trigger the unresponsiveness report at its boundary. + // + // Timing with BABE c=(1,4) and PrimaryAndSecondaryVRFSlots: + // - Alice alone produces ~62.5% of blocks → ~9.6s per block on average. + // - 10 blocks/session → ~96s per session with alice only. + // - 2 sessions = ~192s. We use 200s for margin. + await Bun.sleep(200_000); + + // Unpause bob to restore GRANDPA finality (needs 2/2 validators). + // Bob's process resumes immediately with full state, so he can vote + // on the pending blocks that alice produced while he was paused. + logger.info("Unpausing bob container..."); + await $`docker unpause ${bobContainer}`.quiet(); + logger.info("Bob unpaused. Waiting for finality and SlashReported event..."); + + // Poll for a SlashReported event from the ExternalValidatorsSlashes pallet. + // + // Why SlashReported instead of Slashes storage: + // pallet_im_online's UnresponsivenessOffence::slash_fraction() formula + // gives 0% for small validator sets (1 out of 2 offline is below the + // 10%+1 threshold). With 0% fraction, compute_slash returns None and + // no Slashes entry is created. However, on_offence still emits the + // SlashReported event, which proves the full detection pipeline works: + // pallet_im_online → EquivocationReportWrapper → pallet_offences → on_offence. + // + // After unpausing bob, GRANDPA finality catches up and the events from + // alice's blocks (produced during the pause) become finalized and visible + // to the PAPI event subscription. + let pollCount = 0; + const collectedEvents: any[] = []; + await waitFor({ + lambda: async () => { + pollCount++; + const events = await dhApi.event.ExternalValidatorsSlashes.SlashReported.pull(); + for (const event of events) { + const payload = (event as any)?.payload ?? event; + collectedEvents.push(payload); + logger.info( + `[poll ${pollCount}] SlashReported event: validator=${payload?.validator}, ` + + `fraction=${JSON.stringify(payload?.fraction)}, slash_era=${payload?.slash_era}` + ); + } + if (collectedEvents.length > 0) { + slashReportedEvent = collectedEvents[0]; + return true; + } + if (pollCount % 10 === 0) { + const curEra = await dhApi.query.ExternalValidators.ActiveEra.getValue({ at: "best" }); + const curSession = await dhApi.query.Session.CurrentIndex.getValue({ at: "best" }); + logger.info( + `[poll ${pollCount}] era: ${curEra?.index}, session: ${curSession} (started at era=${eraAtStart}, session=${sessionAtStart})` + ); + } + return false; + }, + iterations: 60, + delay: 5000, + errorMessage: "SlashReported event not found after pausing bob for liveness detection" + }); + } finally { + // Ensure bob is always unpaused so the network stays healthy for teardown + await $`docker unpause ${bobContainer}`.nothrow().quiet(); + } + + expect(slashReportedEvent).toBeDefined(); + logger.info( + "Liveness offence confirmed via SlashReported: " + + `validator=${slashReportedEvent.validator}, ` + + `fraction=${JSON.stringify(slashReportedEvent.fraction)}, ` + + `slash_era=${slashReportedEvent.slash_era}` + ); + }, 600_000); }); diff --git a/test/e2e/suites/storagehub.test.ts b/test/e2e/suites/storagehub.test.ts new file mode 100644 index 00000000..229b47ca --- /dev/null +++ b/test/e2e/suites/storagehub.test.ts @@ -0,0 +1,228 @@ +/** + * StorageHub E2E Tests + * + * Tests the uploading a file to storage through Datahaven + * + * Prerequisites: + * - DataHaven network with StorageHub service running + * - Storage hub MSP and BSP + */ +import "@storagehub/api-augment"; +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { TypeRegistry } from "@polkadot/types"; +import { + FileManager, + initWasm, + ReplicationLevel, + SH_FILE_SYSTEM_PRECOMPILE_ADDRESS, + StorageHubClient +} from "@storagehub-sdk/core"; +import { MspClient } from "@storagehub-sdk/msp-client"; +import { $ } from "bun"; +import { Binary } from "polkadot-api"; +import { createPapiConnectors, logger } from "utils"; +import { CHAIN_ID, SUBSTRATE_FUNDED_ACCOUNTS } from "utils/constants"; +import { getEvmEcdsaSigner } from "utils/papi"; +import { createPublicClient, createWalletClient, defineChain, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { launchLocalDataHavenSolochain } from "../../launcher/datahaven"; +import { + launchBackend, + launchBspNode, + launchIndexerNode, + launchMspNode, + launchStorageHubPostgres +} from "../../launcher/storagehub-docker"; +import { LaunchedNetwork } from "../../launcher/types/launchedNetwork"; +import { registerProviders } from "../../scripts/register-providers"; + +const TEST_AUTHORITY_IDS = ["alice", "bob"] as const; +const networkId = `storagehub-${Date.now()}`.toLowerCase().replace(/[^a-z0-9-]/g, "-"); + +describe("test uploading file to storage hub", () => { + let aliceUrl: string; + let _mspUrl: string; + let backendUrl: string; + + beforeAll(async () => { + await initWasm(); + + const datahavenImageTag = "datahavenxyz/datahaven:local"; + const relayerImageTag = "datahavenxyz/snowbridge-relay:latest"; + const authorityIds = TEST_AUTHORITY_IDS; + const buildDatahaven = false; + const datahavenBuildExtraArgs = ""; + + const options = { + networkId, + datahavenImageTag, + relayerImageTag, + authorityIds, + buildDatahaven, + datahavenBuildExtraArgs + }; + + const run = new LaunchedNetwork(); + + // 1. Launch DataHaven validator nodes + logger.info("📦 Launching DataHaven validator nodes..."); + aliceUrl = await launchLocalDataHavenSolochain(options, run); + + // 2. Launch PostgreSQL database + logger.info("🗄️ Launching StorageHub PostgreSQL..."); + await launchStorageHubPostgres(options, run); + + // 3. Launch MSP node + logger.info("📦 Launching MSP node..."); + _mspUrl = await launchMspNode(options, run); + + // 4. Launch BSP node + logger.info("📦 Launching BSP node..."); + await launchBspNode(options, run); + + // 6. Launch Indexer node + logger.info("📦 Launching Indexer node..."); + await launchIndexerNode(options, run); + + // // 7. Launch Fisherman node + // logger.info("📦 Launching Fisherman node..."); + // await launchFishermanNode(options, run); + + // Register providers + logger.info("📝 Registering providers..."); + await registerProviders({ launchedNetwork: run }); + + // Launch Storage Hub Backend + logger.info("📦 Launching Storage hub backend..."); + backendUrl = await launchBackend(options, run); + }); + + it("Create a bucket", async () => { + const { typedApi: dhApi } = createPapiConnectors(aliceUrl); + + const mspCount = await dhApi.query.Providers.MspCount.getValue(); + const bspCount = await dhApi.query.Providers.BspCount.getValue(); + + expect(mspCount).toBe(1); + expect(bspCount).toBe(1); + + const msp_id = await dhApi.query.Providers.AccountIdToMainStorageProviderId.getValue( + SUBSTRATE_FUNDED_ACCOUNTS.CHARLETH.publicKey + ); + expect(msp_id).toBeDefined(); + if (!msp_id) { + throw new Error("mspId for Charleth not found"); + } + + const value_prop_id = + await dhApi.apis.StorageProvidersApi.query_value_propositions_for_msp(msp_id); + + const call = await dhApi.tx.FileSystem.create_bucket({ + msp_id, + name: Binary.fromText("bucket"), + private: false, + value_prop_id: value_prop_id[0].id + }); + const aliceSigner = getEvmEcdsaSigner(SUBSTRATE_FUNDED_ACCOUNTS.ALITH.privateKey); + const mspResult = await call.signAndSubmit(aliceSigner); + expect(mspResult.ok).toBeTrue(); + }, 30000); + + it("Send a request", async () => { + const { typedApi: dhApi } = createPapiConnectors(aliceUrl); + + const msp_id = await dhApi.query.Providers.AccountIdToMainStorageProviderId.getValue( + SUBSTRATE_FUNDED_ACCOUNTS.CHARLETH.publicKey + ); + expect(msp_id).toBeDefined(); + if (!msp_id) { + throw new Error("mspId for Charleth not found"); + } + + const buckets = await dhApi.apis.StorageProvidersApi.query_buckets_for_msp(msp_id); + if (!buckets.success) { + throw new Error("Bucket not found for the registered msp"); + } + expect(buckets.value.length).toBe(1); + + const bucketId = buckets.value[0].asHex(); + const fileContent = "foo bar"; + const location = "foo/bar.txt"; + + // Build FileManager from in-memory file content + const fileBytes = new TextEncoder().encode(fileContent); + const fileManager = new FileManager({ + size: fileBytes.length, + stream: () => + new ReadableStream({ + start(controller) { + controller.enqueue(fileBytes); + controller.close(); + } + }) as ReadableStream + }); + + // Compute fingerprint and file key from the file metadata + const registry = new TypeRegistry(); + const account = privateKeyToAccount(SUBSTRATE_FUNDED_ACCOUNTS.ALITH.privateKey); + const owner = registry.createType("AccountId20", account.address); + const bucketIdH256 = registry.createType("H256", bucketId); + const fingerprint = await fileManager.getFingerprint(); + const _fileKey = await fileManager.computeFileKey(owner, bucketIdH256, location); + + // Set up EVM clients + const httpUrl = aliceUrl.replace("ws://", "http://"); + const chain = defineChain({ + id: CHAIN_ID, + name: "DataHaven", + nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" }, + rpcUrls: { default: { http: [httpUrl] } } + }); + const walletClient = createWalletClient({ account, chain, transport: http(httpUrl) }); + const publicClient = createPublicClient({ chain, transport: http(httpUrl) }); + const storageHubClient = new StorageHubClient({ + rpcUrl: httpUrl, + chain, + walletClient, + filesystemContractAddress: SH_FILE_SYSTEM_PRECOMPILE_ADDRESS + }); + + // Issue storage request + const txHash = await storageHubClient.issueStorageRequest( + bucketId as `0x${string}`, + location, + fingerprint.toHex() as `0x${string}`, + BigInt(fileBytes.length), + msp_id.asHex() as `0x${string}`, + [], + ReplicationLevel.Basic, + 1 + ); + + // Wait for storage request transaction + // Don't proceed until receipt is confirmed on chain + if (!txHash) { + throw new Error("Storage request transaction was not submitted"); + } + const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + if (receipt.status !== "success") { + throw new Error(`Storage request failed: ${txHash}`); + } + console.log("issueStorageRequest() txReceipt:", receipt); + + // Authenticate with the backend via SIWE and upload the file + let sessionRef: { token: string; user: { address: string } } | undefined; + const sessionProvider = async () => sessionRef; + const mspClient = await MspClient.connect({ baseUrl: backendUrl }, sessionProvider); + + const domain = new URL(backendUrl).host; + const siweSession = await mspClient.auth.SIWE(walletClient, domain, backendUrl); + const sessionToken = (siweSession as { token: string }).token; + expect(sessionToken).toBeDefined(); + }, 60000); + + afterAll(async () => { + // Delete all the containers started by this test suite + await $`docker container rm -f $(docker container ls -q --filter name=${networkId})`; + }); +}); diff --git a/test/launcher/datahaven.ts b/test/launcher/datahaven.ts index fcc004cc..429a326a 100644 --- a/test/launcher/datahaven.ts +++ b/test/launcher/datahaven.ts @@ -84,7 +84,7 @@ export const getPortMappingForNode = (nodeId: string, networkId: string): string export const launchLocalDataHavenSolochain = async ( options: DataHavenOptions, launchedNetwork: LaunchedNetwork -): Promise => { +): Promise => { logger.info("🚀 Launching DataHaven network..."); invariant(options.datahavenImageTag, "❌ DataHaven image tag not defined"); @@ -165,6 +165,8 @@ export const launchLocalDataHavenSolochain = async ( await setupDataHavenValidatorConfig(launchedNetwork, "datahaven-"); logger.success(`DataHaven network started, primary node accessible on port ${alicePort}`); + + return `ws://127.0.0.1:${alicePort}`; }; /** diff --git a/test/launcher/network/index.ts b/test/launcher/network/index.ts index 55c1139f..783eb162 100644 --- a/test/launcher/network/index.ts +++ b/test/launcher/network/index.ts @@ -144,6 +144,7 @@ export const launchNetwork = async ( options: NetworkLaunchOptions ): Promise => { const networkId = options.networkId; + const relayerImageTag = options.relayerImageTag || getDefaultRelayerImageTag(); const launchedNetwork = new LaunchedNetwork(); launchedNetwork.networkName = networkId; let injectContracts = false; @@ -177,7 +178,7 @@ export const launchNetwork = async ( { networkId, datahavenImageTag: options.datahavenImageTag || "datahavenxyz/datahaven:local", - relayerImageTag: options.relayerImageTag || "datahavenxyz/snowbridge-relay:latest", + relayerImageTag, authorityIds: TEST_AUTHORITY_IDS, buildDatahaven: options.buildDatahaven ?? !isCI, // if not specified, default to false for CI, true for local testing datahavenBuildExtraArgs: options.datahavenBuildExtraArgs || "--features=fast-runtime" @@ -248,14 +249,10 @@ export const launchNetwork = async ( // 7. Launch relayers logger.info("❄️ Launching Snowbridge relayers..."); - if (!options.relayerImageTag) { - throw new Error("Relayer image tag not specified"); - } - await launchRelayers( { networkId, - relayerImageTag: options.relayerImageTag, + relayerImageTag, kurtosisEnclaveName }, launchedNetwork @@ -297,4 +294,13 @@ export const launchNetwork = async ( } }; +export const getDefaultRelayerImageTag = (): string => { + if (process.env.RELAYER_IMAGE_TAG) { + return process.env.RELAYER_IMAGE_TAG; + } + return process.arch === "arm64" + ? "datahavenxyz/snowbridge-relay:local" + : "datahavenxyz/snowbridge-relay:latest"; +}; + export const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; diff --git a/test/launcher/relayers.ts b/test/launcher/relayers.ts index a64cbad4..d1f33569 100644 --- a/test/launcher/relayers.ts +++ b/test/launcher/relayers.ts @@ -93,6 +93,47 @@ export const RELAYER_CONFIG_PATHS = { SOLOCHAIN: path.join(RELAYER_CONFIG_DIR, "solochain-relay.json") }; +const LOCAL_RELAYER_SOURCE_DIR = path.resolve( + import.meta.dir, + "..", + "..", + "contracts", + "lib", + "snowbridge", + "relayer" +); + +const isLocalRelayerImage = (relayerImageTag: string): boolean => + relayerImageTag.endsWith(":local"); + +const ensureLocalRelayerImage = async (relayerImageTag: string): Promise => { + if (!isLocalRelayerImage(relayerImageTag)) { + return; + } + + const localImageExists = await $`docker image inspect ${relayerImageTag}`.nothrow().quiet(); + if (localImageExists.exitCode === 0) { + logger.debug(`Local relayer image already available: ${relayerImageTag}`); + return; + } + + const dockerfilePath = path.join(LOCAL_RELAYER_SOURCE_DIR, "Dockerfile"); + const dockerfileExists = await Bun.file(dockerfilePath).exists(); + invariant( + dockerfileExists, + `❌ Local relayer Dockerfile not found at ${dockerfilePath}. Cannot build ${relayerImageTag}` + ); + + logger.info( + `🐳 Local relayer image ${relayerImageTag} not found. Building from ${LOCAL_RELAYER_SOURCE_DIR} for ${process.arch}...` + ); + await runShellCommandWithLogger(`docker build -f Dockerfile -t ${relayerImageTag} .`, { + cwd: LOCAL_RELAYER_SOURCE_DIR, + logLevel: "debug" + }); + logger.success(`✅ Built local relayer image: ${relayerImageTag}`); +}; + /** * Generates configuration files for relayers. * @@ -278,16 +319,16 @@ export const initEthClientPallet = async ( process.platform === "linux" ? "--add-host host.docker.internal:host-gateway" : ""; // Opportunistic pull - pull the image from Docker Hub only if it's not a local image - const isLocal = relayerImageTag.endsWith(":local"); + const isLocal = isLocalRelayerImage(relayerImageTag); + const platformParam = isLocal ? "" : "--platform linux/amd64"; logger.debug("Generating beacon checkpoint"); const datastoreHostPath = path.resolve(datastorePath); - const command = `docker run \ + const command = `docker run ${platformParam} \ -v ${beaconConfigHostPath}:${beaconConfigContainerPath}:ro \ -v ${checkpointHostPath}:${checkpointContainerPath} \ -v ${datastoreHostPath}:/data \ --name generate-beacon-checkpoint-${networkId} \ - --platform linux/amd64 \ --workdir /app \ ${addHostParam} \ ${launchedNetwork.networkName ? `--network ${launchedNetwork.networkName}` : ""} \ @@ -400,6 +441,7 @@ export const launchRelayers = async ( const { relayerImageTag, kurtosisEnclaveName } = options; invariant(relayerImageTag, "❌ relayerImageTag is required"); + await ensureLocalRelayerImage(relayerImageTag); await killExistingContainers("snowbridge-"); @@ -623,7 +665,7 @@ const launchRelayerContainers = async ( launchedNetwork: LaunchedNetwork, networkId: string ): Promise => { - const isLocal = relayerImageTag.endsWith(":local"); + const isLocal = isLocalRelayerImage(relayerImageTag); const networkName = launchedNetwork.networkName; invariant(networkName, "❌ Docker network name not found in LaunchedNetwork instance"); const restartArgs = ["--restart", "on-failure:5"]; @@ -641,8 +683,7 @@ const launchRelayerContainers = async ( "docker", "run", "-d", - "--platform", - "linux/amd64", + ...(isLocal ? [] : ["--platform", "linux/amd64"]), "--add-host", "host.docker.internal:host-gateway", "--name", diff --git a/test/launcher/storagehub-docker.ts b/test/launcher/storagehub-docker.ts index a9f64c0e..9cb9c7df 100644 --- a/test/launcher/storagehub-docker.ts +++ b/test/launcher/storagehub-docker.ts @@ -124,7 +124,7 @@ export const injectStorageHubKey = async ( // Use Bun's $ directly with docker exec (no sh -c wrapper needed) // This properly handles the spaces in the seed phrase try { - await $`docker exec ${containerName} datahaven-node key insert --base-path /data --key-type bcsv --scheme ecdsa --suri ${secretKey}`; + await $`docker exec ${containerName} datahaven-node key insert --chain local --key-type bcsv --scheme ecdsa --suri ${secretKey}`; logger.success("Key injected successfully"); } catch (error) { logger.error(`Failed to inject key : ${error}`); @@ -141,7 +141,7 @@ export const injectStorageHubKey = async ( export const launchMspNode = async ( options: DataHavenOptions, launchedNetwork: LaunchedNetwork -): Promise => { +): Promise => { logger.info("🚀 Launching StorageHub MSP node..."); const containerName = `storagehub-msp-${options.networkId}`; @@ -182,7 +182,10 @@ export const launchMspNode = async ( "--max-storage-capacity", "10737418240", // 10 GiB "--jump-capacity", - "1073741824" // 1 GiB + "1073741824", // 1 GiB + "--trusted-file-transfer-server", + "--trusted-file-transfer-server-host", + "0.0.0.0" // Listen on all interfaces so the backend container can reach it ]; logger.debug(`Executing: ${command.join(" ")}`); @@ -217,6 +220,8 @@ export const launchMspNode = async ( launchedNetwork.addContainer(containerName, { ws: wsPort }, { ws: DEFAULT_SUBSTRATE_WS_PORT }); logger.success(`MSP node started on port ${wsPort}`); + + return `ws://127.0.0.1:${wsPort}`; }; /** @@ -457,11 +462,12 @@ export const launchFishermanNode = async ( * * @param options - Configuration options for launching the network * @param launchedNetwork - The launched network instance to track the node + * @returns The HTTP URL of the backend API (e.g. "http://127.0.0.1:8080") */ export const launchBackend = async ( options: DataHavenOptions, launchedNetwork: LaunchedNetwork -): Promise => { +): Promise => { logger.info("🚀 Launching StorageHub Backend..."); const backendImage = "moonsonglabs/storage-hub-msp-backend:latest"; @@ -484,8 +490,10 @@ export const launchBackend = async ( "-e", "RUST_LOG=info", backendImage, - "--chain", - "local", + "--host", + "0.0.0.0", + "--port", + "8080", "--log-format", "text", "--database-url", @@ -507,6 +515,8 @@ export const launchBackend = async ( launchedNetwork.addContainer(containerName, { http: apiPort }, { http: apiPort }); logger.success(`StorageHub Backend container started on port ${apiPort}`); + + return `http://127.0.0.1:${apiPort}`; }; /** diff --git a/test/package.json b/test/package.json index ad1f1156..4d521359 100644 --- a/test/package.json +++ b/test/package.json @@ -54,6 +54,9 @@ "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", "@polkadot-api/descriptors": "file:.papi/descriptors", + "@storagehub-sdk/core": "^0.4.4", + "@storagehub-sdk/msp-client": "^0.4.4", + "@storagehub/api-augment": "^0.4.0", "@types/dockerode": "^3.3.41", "@types/node": "^22.15.32", "@wagmi/cli": "^2.3.1", diff --git a/test/scripts/register-providers.ts b/test/scripts/register-providers.ts index 216d509b..5bc850cc 100644 --- a/test/scripts/register-providers.ts +++ b/test/scripts/register-providers.ts @@ -212,7 +212,7 @@ export async function verifyProvidersRegistered( ): Promise { logger.info("🔍 Verifying provider registration..."); - const aliceContainerName = `datahaven - alice - ${options.launchedNetwork.networkId} `; + const aliceContainerName = `datahaven-alice-${options.launchedNetwork.networkId} `; const alicePort = options.launchedNetwork.getContainerPort(aliceContainerName); const { client, typedApi } = createPapiConnectors(`ws://127.0.0.1:${alicePort}`); diff --git a/test/tools/validator-set-submitter/Dockerfile b/test/tools/validator-set-submitter/Dockerfile index 84edba6d..584a0f20 100644 --- a/test/tools/validator-set-submitter/Dockerfile +++ b/test/tools/validator-set-submitter/Dockerfile @@ -1,7 +1,8 @@ # Validator Set Submitter image # -# Build from the repository root: -# docker build -f test/tools/validator-set-submitter/Dockerfile \ +# Build from the test directory: +# cd test +# docker build -f tools/validator-set-submitter/Dockerfile \ # -t datahavenxyz/validator-set-submitter:local . # # Runtime expectations: @@ -13,8 +14,8 @@ FROM oven/bun:1.3.3-slim AS deps WORKDIR /app -COPY test/package.json test/bun.lock test/tsconfig.json ./ -COPY test/.papi ./.papi +COPY package.json bun.lock tsconfig.json ./ +COPY .papi ./.papi RUN bun install --frozen-lockfile --production FROM oven/bun:1.3.3-slim @@ -24,10 +25,10 @@ WORKDIR /app RUN useradd -m -u 1001 -U -s /bin/sh -d /submitter submitter COPY --from=deps /app/node_modules ./node_modules -COPY test/tsconfig.json test/bunfig.toml ./ -COPY test/tools/validator-set-submitter/ ./tools/validator-set-submitter/ -COPY test/contract-bindings/ ./contract-bindings/ -COPY test/utils/ ./utils/ +COPY tsconfig.json bunfig.toml ./ +COPY tools/validator-set-submitter/ ./tools/validator-set-submitter/ +COPY contract-bindings/ ./contract-bindings/ +COPY utils/ ./utils/ ENV NODE_ENV=production diff --git a/test/tools/validator-set-submitter/config.ts b/test/tools/validator-set-submitter/config.ts index dc9a73d8..4c6107c9 100644 --- a/test/tools/validator-set-submitter/config.ts +++ b/test/tools/validator-set-submitter/config.ts @@ -1,4 +1,3 @@ -import { parseDeploymentsFile } from "utils"; import { parseEther } from "viem"; import { parse as parseYaml } from "yaml"; @@ -37,6 +36,7 @@ export async function loadConfig( let serviceManagerAddress = optionalHexString(raw, "service_manager_address"); if (!serviceManagerAddress) { + const { parseDeploymentsFile } = await import("../../utils/contracts.ts"); const deployments = await parseDeploymentsFile(networkId); serviceManagerAddress = deployments.ServiceManager; } diff --git a/test/utils/docker.ts b/test/utils/docker.ts index 7029e765..9d5b438e 100644 --- a/test/utils/docker.ts +++ b/test/utils/docker.ts @@ -1,5 +1,6 @@ import { existsSync } from "node:fs"; import { type Duplex, PassThrough, Transform } from "node:stream"; +import { $ } from "bun"; import Docker from "dockerode"; import invariant from "tiny-invariant"; import { logger } from "./logger"; @@ -178,6 +179,13 @@ export async function waitForLog(opts: { const { readable } = Transform.toWeb(pass); const decoder = new TextDecoder(); + let bufferedLogs = ""; + const hasHit = (text: string): boolean => { + if (typeof opts.search === "string") return text.includes(opts.search); + // Avoid stateful regex surprises with /g or /y across multiple checks. + opts.search.lastIndex = 0; + return opts.search.test(text); + }; const timer = setTimeout( () => pass.destroy( @@ -190,14 +198,16 @@ export async function waitForLog(opts: { try { for await (const chunk of readable) { - const text = decoder.decode(chunk as Uint8Array, { stream: false }); - - const hit = - typeof opts.search === "string" ? text.includes(opts.search) : opts.search.test(text); - - if (hit) return text.trim(); + bufferedLogs += decoder.decode(chunk as Uint8Array, { stream: true }); + if (hasHit(bufferedLogs)) return bufferedLogs.trim(); + if (bufferedLogs.length > 64_000) { + bufferedLogs = bufferedLogs.slice(-64_000); + } } + bufferedLogs += decoder.decode(); + if (hasHit(bufferedLogs)) return bufferedLogs.trim(); + throw new Error( `Log stream ended before "${opts.search}" appeared for container ${opts.containerName}` ); @@ -229,6 +239,9 @@ export const waitForContainerToStart = async ( logger.debug(`Waiting for container ${containerName} to start...`); const seconds = options?.timeoutSeconds ?? 30; + // sleep 2 seconds to see if the started container didn't exit right away + await Bun.sleep(2000); + for (let i = 0; i < seconds; i++) { const containers = await docker.listContainers(); const container = containers.find((container) => @@ -236,10 +249,17 @@ export const waitForContainerToStart = async ( ); if (container) { logger.debug(`Container ${containerName} started after ${i} seconds`); + const result = await $`docker logs ${containerName}`.nothrow().quiet().text(); + console.log(result); + return; } await Bun.sleep(1000); } + + const result = await $`docker logs ${containerName}`; + console.log(result); + invariant( false, `❌ container ${containerName} cannot be found in running container list after ${seconds} seconds` diff --git a/test/utils/papi.ts b/test/utils/papi.ts index 42a4dc63..1ec97545 100644 --- a/test/utils/papi.ts +++ b/test/utils/papi.ts @@ -43,6 +43,7 @@ export const createPapiConnectors = ( ): { client: PolkadotClient; typedApi: DataHavenApi } => { const url = wsUrl ?? "ws://127.0.0.1:9944"; const client = createClient(withPolkadotSdkCompat(getWsProvider(url))); + return { client, typedApi: client.getTypedApi(datahaven) }; }; diff --git a/test/utils/types.ts b/test/utils/types.ts index 695788ff..df4f8143 100644 --- a/test/utils/types.ts +++ b/test/utils/types.ts @@ -184,7 +184,7 @@ export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint => const DATAHAVEN_PARAM_NAMES = [ "EthereumGatewayAddress", "RewardsUpdateSelector", - "RewardsAgentOrigin", + "AgentOrigin", "DatahavenServiceManagerAddress" ] as const; diff --git a/test/utils/validators.ts b/test/utils/validators.ts index 637c9ae6..5e635200 100644 --- a/test/utils/validators.ts +++ b/test/utils/validators.ts @@ -8,6 +8,8 @@ export const COMMON_LAUNCH_ARGS = [ "--unsafe-force-node-key-generation", "--tmp", + "--chain", + "local", "--validator", "--discover-local", "--no-prometheus",