Merge branch 'main' into sde/contracts-upgrade-environment

This commit is contained in:
Steve Degosserie 2026-03-12 12:12:51 +01:00
commit bfeee2015d
No known key found for this signature in database
GPG key ID: 2F23F0D52ABF408E
63 changed files with 2634 additions and 419 deletions

View file

@ -17,7 +17,8 @@
"!**/html/**/*",
"!**/moonwall/contracts/out/**/*",
"!**/contracts/out/**/*",
"!**/contracts/deployments/state-diff.checksum"
"!**/contracts/deployments/state-diff.checksum",
"!**/bun.lock"
],
"maxSize": 3000000
},

View file

@ -35,7 +35,7 @@
"randaoCommitExpiration": 24,
"minNumRequiredSignatures": 2,
"startBlock": 1,
"rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
"messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
"initialValidatorSetId": 0,
"initialValidatorHashes": [
"0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7",

View file

@ -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,

View file

@ -43,7 +43,7 @@
"randaoCommitExpiration": 24,
"minNumRequiredSignatures": 16,
"startBlock": 1,
"rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
"messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
"initialValidatorSetId": 0,
"initialValidatorHashes": [],
"nextValidatorSetId": 1,

View file

@ -44,7 +44,7 @@
"randaoCommitExpiration": 24,
"minNumRequiredSignatures": 3,
"startBlock": 1303065,
"rewardsMessageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd",
"messageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd",
"initialValidatorSetId": 2186,
"initialValidatorHashes": [
"0x07ce4f2cd558f4d4b529a3362b6ff7d616ca0893b53252dc62829b8218ea5c10",

View file

@ -44,7 +44,7 @@
"randaoCommitExpiration": 24,
"minNumRequiredSignatures": 5,
"startBlock": 1381173,
"rewardsMessageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215",
"messageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215",
"initialValidatorSetId": 2303,
"initialValidatorHashes": [
"0x0ec3102f334aba804c18b843e45ec874005587122a1b49273b1b04d6fd98b1a2",

View file

@ -0,0 +1 @@
{"Agent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"}

View file

@ -1 +1 @@
{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","RewardsAgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"}
{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"}

View file

@ -1 +1 @@
{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","RewardsAgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"}
{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","AgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"}

View file

@ -12,7 +12,7 @@ contract Config {
bytes32[] initialValidatorHashes;
uint128 nextValidatorSetId;
bytes32[] nextValidatorHashes;
bytes32 rewardsMessageOrigin;
bytes32 messageOrigin;
}
// AVS parameters

View file

@ -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));
}
}

View file

@ -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"))))

View file

@ -50,4 +50,3 @@ examples/
Cargo.lock.old
*.toml.old
*.lock.old
**/target/

271
operator/Cargo.lock generated
View file

@ -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",
]

View file

@ -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

View file

@ -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

View file

@ -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",
]

View file

@ -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::<T::AccountId, T::SlashId>::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::<T>::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::<T>::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::<T::AccountId, T::SlashId>::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::<T>::set(queue);

View file

@ -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<u8, ConstU32<256>>),
}
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<AccountId> {
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<u128>;
/// How many queued slashes are being processed per block.
#[pallet::constant]
type QueuedSlashesProcessedPerBlock: Get<u32>;
@ -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<T: Config> = 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<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
SessionIndex,
Twox64Concat,
T::ValidatorId,
OffenceKind,
OptionQuery,
>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
@ -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::<T>::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<T: Config>
OnOffenceHandler<T::AccountId, pallet_session::historical::IdentificationTuple<T>, Weight>
for Pallet<T>
@ -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::<T>::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<T: Config> Pallet<T> {
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<AccountId, SlashId> {
pub percentage: Perbill,
// Whether the slash is confirmed or still needs to go through deferred period
pub confirmed: bool,
}
impl<AccountId, SlashId: One> Slash<AccountId, SlashId> {
/// 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<T: Config>(
slash_era: EraIndex,
stash: T::AccountId,
slash_defer_duration: EraIndex,
offence_kind: OffenceKind,
) -> Option<Slash<T::AccountId, T::SlashId>> {
let prior_slash_p = ValidatorSlashInEra::<T>::get(slash_era, &stash).unwrap_or(Zero::zero());
@ -707,6 +797,7 @@ pub(crate) fn compute_slash<T: Config>(
slash_id,
reporters: Vec::new(),
confirmed,
offence_kind,
})
}
@ -714,3 +805,107 @@ pub(crate) fn compute_slash<T: Config>(
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<ValidatorId> {
fn validator_id(&self) -> &ValidatorId;
}
impl<V, F> HasValidatorId<V> 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<T, Inner, Kind>(PhantomData<(T, Inner, Kind)>);
impl<T, Inner, Kind, R, O, Id> ReportOffence<R, Id, O> for EquivocationReportWrapper<T, Inner, Kind>
where
T: Config,
Inner: ReportOffence<R, Id, O>,
O: Offence<Id>,
Kind: OffenceKindProvider,
Id: HasValidatorId<T::ValidatorId>,
{
fn report_offence(reporters: Vec<R>, offence: O) -> Result<(), OffenceError> {
// Discard offences from before the bonding period.
let offence_session = offence.session_index();
let bonded_eras = pallet::BondedEras::<T>::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::<T>::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::<T>::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
}
}

View file

@ -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<EraIndex> = const { RefCell::new(0) };
pub static DEFER_PERIOD: RefCell<EraIndex> = const { RefCell::new(2) };
pub static SENT_ETHEREUM_MESSAGE_NONCE: RefCell<u64> = const { RefCell::new(0) };
pub static MOCK_REPORT_OFFENCE_SHOULD_FAIL: RefCell<bool> = const { RefCell::new(false) };
pub static MOCK_REPORT_OFFENCE_CALLED: RefCell<bool> = const { RefCell::new(false) };
pub static LAST_SENT_SLASHES: RefCell<Vec<crate::SlashData<AccountId>>> = RefCell::new(Vec::new());
}
impl MockEraIndexProvider {
@ -215,10 +217,16 @@ impl DeferPeriodGetter {
}
pub struct MockOkOutboundQueue;
impl MockOkOutboundQueue {
pub fn last_sent_slashes() -> Vec<crate::SlashData<AccountId>> {
LAST_SENT_SLASHES.with(|r| r.borrow().clone())
}
}
impl crate::SendMessage<AccountId> for MockOkOutboundQueue {
type Ticket = ();
type Message = ();
fn build(_: &Vec<crate::SlashData<AccountId>>, _: u32) -> Option<Self::Ticket> {
fn build(slashes: &Vec<crate::SlashData<AccountId>>, _: u32) -> Option<Self::Ticket> {
LAST_SENT_SLASHES.with(|r| *r.borrow_mut() = slashes.clone());
Some(())
}
fn validate(_: Self::Ticket) -> Result<Self::Ticket, SendError> {
@ -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<u64, Option<u64>> 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<R, Id, O: Offence<Id>> ReportOffence<R, Id, O> for MockInnerReporter {
fn report_offence(_reporters: Vec<R>, _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<Test, MockInnerReporter, crate::BabeEquivocation>;
/// Type alias for the wrapper using the mock reporter with GrandpaEquivocation kind.
pub type MockGrandpaWrapper =
crate::EquivocationReportWrapper<Test, MockInnerReporter, crate::GrandpaEquivocation>;
pub fn run_block() {
run_to_block(System::block_number() + 1);
}

View file

@ -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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 1u64, OffenceKind::LivenessOffence);
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (1, ()),
@ -276,6 +297,7 @@ fn test_on_offence_does_not_work_if_slashing_disabled() {
RuntimeOrigin::root(),
SlashingModeOption::Disabled,
));
PendingOffenceKind::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
let weight = Pallet::<Test>::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::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::BabeEquivocation);
Pallet::<Test>::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::<Test>::insert(0, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(2, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::BabeEquivocation);
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (3, ()),
reporters: vec![],
}],
&[Perbill::from_percent(75)],
0,
);
assert_eq!(
Slashes::<Test>::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::<Test>::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::<Test>::insert(0, 3u64, OffenceKind::BabeEquivocation);
PendingOffenceKind::<Test>::insert(1, 3u64, OffenceKind::GrandpaEquivocation);
// Report at session 0 — should use BabeEquivocation.
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (3, ()),
reporters: vec![],
}],
&[Perbill::from_percent(50)],
0,
);
// Session 0 consumed, session 1 untouched.
assert_eq!(PendingOffenceKind::<Test>::get(0, 3u64), None);
assert_eq!(
PendingOffenceKind::<Test>::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::<u64>::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::<Test>::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::<u64>::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::<Test>::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::<u64>::new(),
MockOffence {
session_index: 0,
offenders: vec![(3, ()), (4, ())],
},
);
// Both offenders should have entries at session 0.
assert_eq!(
PendingOffenceKind::<Test>::get(0, 3u64),
Some(OffenceKind::BabeEquivocation),
);
assert_eq!(
PendingOffenceKind::<Test>::get(0, 4u64),
Some(OffenceKind::BabeEquivocation),
);
// No entry at a different session.
assert_eq!(PendingOffenceKind::<Test>::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::<u64>::new(),
MockOffence {
session_index: 0,
offenders: vec![(3, ()), (4, ())],
},
);
assert!(result.is_err());
// Entries should have been cleaned up.
assert_eq!(PendingOffenceKind::<Test>::get(0, 3u64), None);
assert_eq!(PendingOffenceKind::<Test>::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::<u64>::new(),
MockOffence {
session_index: 0,
offenders: vec![(3, ())],
},
);
assert_eq!(
PendingOffenceKind::<Test>::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::<u64>::new(),
MockOffence {
session_index: 1,
offenders: vec![(3, ())],
},
);
assert!(result.is_err());
// Session 1 cleaned up, session 0 untouched.
assert_eq!(PendingOffenceKind::<Test>::get(1, 3u64), None);
assert_eq!(
PendingOffenceKind::<Test>::get(0, 3u64),
Some(OffenceKind::GrandpaEquivocation),
);
});
}
fn start_era(era_index: EraIndex, session_index: SessionIndex, external_idx: u64) {
Pallet::<Test>::on_era_start(era_index, session_index, external_idx);
crate::mock::MockEraIndexProvider::with_era(era_index);

View file

@ -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<T: Config + pallet_balances::Config>(
user
}
/// Helper: insert a single entry into the ring buffer at slot 0.
fn push_unsent_entry<T: Config>(era_index: u32, timestamp: u32, inflation: u128) {
ExternalValidatorsRewards::<T>::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<T: Config + pallet_balances::Config>(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::<T>("candidate", i, 100);
era_reward_points.individual.insert(account_id, 20);
}
<RewardPointsForEra<T>>::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::<T>::unsent_queue_is_empty());
#[block]
{
ExternalValidatorsRewards::<T>::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::<T>(999, 0, 42);
#[block]
{
ExternalValidatorsRewards::<T>::process_unsent_reward_eras();
}
// Entry should have been removed
assert!(ExternalValidatorsRewards::<T>::unsent_queue_is_empty());
Ok(())
}
// on_initialize: oldest entry retried successfully
#[benchmark]
fn process_unsent_reward_eras_success() -> Result<(), BenchmarkError> {
frame_system::Pallet::<T>::set_block_number(0u32.into());
T::BenchmarkHelper::setup();
setup_era_reward_points::<T>(1);
push_unsent_entry::<T>(1, 0, 42);
#[block]
{
ExternalValidatorsRewards::<T>::process_unsent_reward_eras();
}
assert!(ExternalValidatorsRewards::<T>::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::<T>::set_block_number(0u32.into());
T::BenchmarkHelper::setup();
setup_era_reward_points::<T>(1);
push_unsent_entry::<T>(1, 0, 42);
#[block]
{
ExternalValidatorsRewards::<T>::process_unsent_reward_eras();
}
Ok(())
}
// Governance extrinsic: retry a specific unsent era
#[benchmark]
fn retry_unsent_reward_era() -> Result<(), BenchmarkError> {
frame_system::Pallet::<T>::set_block_number(0u32.into());
T::BenchmarkHelper::setup();
setup_era_reward_points::<T>(1);
push_unsent_entry::<T>(1, 0, 42);
let origin =
T::GovernanceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
#[extrinsic_call]
_(origin as T::RuntimeOrigin, 1u32);
assert!(ExternalValidatorsRewards::<T>::unsent_queue_is_empty());
Ok(())
}
impl_benchmark_test_suite!(
ExternalValidatorsRewards,
crate::mock::new_test_ext(),

View file

@ -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<Self::AccountId>;
/// Origin for governance calls (e.g., retrying unsent reward messages).
type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: types::BenchmarkHelper;
}
@ -175,6 +178,62 @@ pub mod pallet {
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::hooks]
impl<T: Config> Hooks<frame_system::pallet_prelude::BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_n: frame_system::pallet_prelude::BlockNumberFor<T>) -> Weight {
Self::process_unsent_reward_eras()
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// 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<T>,
era_index: EraIndex,
) -> DispatchResult {
T::GovernanceOrigin::ensure_origin(origin)?;
// Scan the ring buffer for the requested era
let head = UnsentRewardHead::<T>::get();
let tail = UnsentRewardTail::<T>::get();
let mut found = None;
let mut slot = head;
while slot != tail {
if let Some(entry @ (idx, _, _)) = UnsentRewardEra::<T>::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::<T>::EraNotInUnsentQueue)?;
let reward_points = RewardPointsForEra::<T>::get(era_index);
let info = reward_points
.generate_era_rewards_info(era_index, inflation, timestamp)
.ok_or(Error::<T>::RewardPointsPruned)?;
let message_id =
Self::send_rewards_message(&info).ok_or(Error::<T>::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<T: Config> {
@ -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<T> {
/// 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<T: Config> =
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<T: Config> = 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<T: Config> = 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<T: Config> = StorageValue<_, u32, ValueQuery>;
impl<T: Config> Pallet<T> {
/// 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<Item = (T::AccountId, RewardPoints)>) {
@ -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<H256> {
let outbound = T::SendMessage::build(utils).or_else(|| {
fn send_rewards_message(info: &EraRewardsUtils) -> Option<H256> {
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::<T>::get() == UnsentRewardTail::<T>::get()
}
/// Number of entries currently in the ring buffer.
#[allow(dead_code)]
pub(crate) fn unsent_queue_len() -> u32 {
let head = UnsentRewardHead::<T>::get();
let tail = UnsentRewardTail::<T>::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::<T>::get();
let tail = UnsentRewardTail::<T>::get();
let next_tail = (tail + 1) % UNSENT_QUEUE_CAPACITY;
if next_tail == head {
// Buffer full
return false;
}
UnsentRewardEra::<T>::insert(tail, entry);
UnsentRewardTail::<T>::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::<T>::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::<T>::get(next) {
UnsentRewardEra::<T>::insert(cur, entry);
}
cur = next;
}
// Remove the now-duplicate last entry and shrink tail
UnsentRewardEra::<T>::remove(cur);
let new_tail = if tail == 0 {
UNSENT_QUEUE_CAPACITY - 1
} else {
tail - 1
};
UnsentRewardTail::<T>::put(new_tail);
// If head was after the removed slot, adjust it too
let head = UnsentRewardHead::<T>::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::<T>::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::<T>::get();
let tail = UnsentRewardTail::<T>::get();
if head == tail {
return T::WeightInfo::process_unsent_reward_eras_empty();
}
let Some((era_index, timestamp, inflation)) = UnsentRewardEra::<T>::get(head) else {
// Slot unexpectedly empty — advance head past it
UnsentRewardHead::<T>::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::<T>::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::<T>::remove(head);
UnsentRewardHead::<T>::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::<T>::remove(head);
UnsentRewardHead::<T>::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::<T>::remove(head);
UnsentRewardHead::<T>::put((head + 1) % UNSENT_QUEUE_CAPACITY);
UnsentRewardEra::<T>::insert(tail, (era_index, timestamp, inflation));
UnsentRewardTail::<T>::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::<T>::remove(era_index_to_delete);
BlocksProducedInEra::<T>::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::<T>::get();
let mut tail = UnsentRewardTail::<T>::get();
let mut slot = head;
while slot != tail {
if let Some((idx, _, _)) = UnsentRewardEra::<T>::get(slot) {
if idx <= era_index_to_delete {
Self::unsent_queue_remove_slot(slot);
tail = UnsentRewardTail::<T>::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::<T>::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 });
}
}
}
}
}

View file

@ -131,6 +131,9 @@ impl crate::types::SendMessage for MockOkOutboundQueue {
}
fn validate(ticket: Self::Ticket) -> Result<Self::Ticket, SendError> {
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<H160>;
type WeightInfo = ();
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
@ -292,6 +296,8 @@ pub mod mock_data {
pub offline_validators: sp_std::vec::Vec<sp_core::H160>,
/// 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]

View file

@ -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::<Test>::get(1);
let inflation =
<Test as pallet_external_validators_rewards::Config>::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::<Test>::get(1);
let inflation =
<Test as pallet_external_validators_rewards::Config>::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::<Test>::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::<Test>::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::<Test>::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());
})
}

View file

@ -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<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
.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()
}
}

View file

@ -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);

View file

@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime {
type KeyOwnerProof =
<Historical as KeyOwnerProofSystem<(KeyTypeId, pallet_babe::AuthorityId)>>::Proof;
type EquivocationReportSystem =
pallet_babe::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
}
@ -424,7 +436,11 @@ impl pallet_grandpa::Config for Runtime {
type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::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 = <Historical as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type EquivocationReportSystem =
pallet_beefy::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<EnsureRoot<AccountId>, governance::custom_origins::GeneralAdmin>;
type WeightInfo = mainnet_weights::pallet_external_validators_rewards::WeightInfo<Runtime>;
#[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<Address> {
@ -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<Runtime>;
type SendMessage = SlashesSendAdapter;

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -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,

View file

@ -74,4 +74,29 @@ impl<T: frame_system::Config> 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()
}
}

View file

@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime {
type KeyOwnerProof =
<Historical as KeyOwnerProofSystem<(KeyTypeId, pallet_babe::AuthorityId)>>::Proof;
type EquivocationReportSystem =
pallet_babe::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
}
@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime {
type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::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 = <Historical as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type EquivocationReportSystem =
pallet_beefy::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<EnsureRoot<AccountId>, governance::custom_origins::GeneralAdmin>;
type WeightInfo = stagenet_weights::pallet_external_validators_rewards::WeightInfo<Runtime>;
#[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<Runtime>;
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\

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -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,

View file

@ -74,4 +74,29 @@ impl<T: frame_system::Config> 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()
}
}

View file

@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime {
type KeyOwnerProof =
<Historical as KeyOwnerProofSystem<(KeyTypeId, pallet_babe::AuthorityId)>>::Proof;
type EquivocationReportSystem =
pallet_babe::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<Runtime>;
}
@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime {
type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::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 = <Historical as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type EquivocationReportSystem =
pallet_beefy::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
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<EnsureRoot<AccountId>, governance::custom_origins::GeneralAdmin>;
type WeightInfo = testnet_weights::pallet_external_validators_rewards::WeightInfo<Runtime>;
#[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<Address> {
@ -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<Runtime>;
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\

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -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,

View file

@ -74,4 +74,29 @@ impl<T: frame_system::Config> 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()
}
}

15
test/.dockerignore Normal file
View file

@ -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/**

View file

@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.13357056092938763018",
"version": "0.1.0-autogenerated.18139584469151706411",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

Binary file not shown.

View file

@ -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=="],

View file

@ -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<Hex>
};
/**
* 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<Hex | null> => {
const fetchAgentOrigin = async (rpcUrl: string): Promise<Hex | null> => {
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<Hex | null> => {
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<Hex> => {
* 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<void> => {
const updateConfigFile = async (networkId: string, messageOrigin: Hex): Promise<void> => {
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<void> => {
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}`);

View file

@ -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 <value>", "Target chain (hoodi, ethereum, anvil)")
.option(
"--environment <value>",
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
)
.option(
"--rpc-url <value>",
"WebSocket RPC URL of the DataHaven chain to fetch RewardsAgentOrigin from"
)
.option("--rpc-url <value>", "WebSocket RPC URL of the DataHaven chain to fetch AgentOrigin from")
.option(
"--genesis-hash <value>",
"Chain genesis hash (32 bytes hex). If not provided, will be fetched from the chain."

View file

@ -8,7 +8,7 @@
"value": null
},
{
"name": "RewardsAgentOrigin",
"name": "AgentOrigin",
"value": null
},
{

View file

@ -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<void> {
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()) ||
"<no logs captured>";
`${logResult.stdout.toString()}${logResult.stderr.toString()}`.trim() || "<no logs captured>";
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}`,

View file

@ -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
});

View file

@ -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",

View file

@ -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);
});

View file

@ -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<Uint8Array>
});
// 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})`;
});
});

View file

@ -84,7 +84,7 @@ export const getPortMappingForNode = (nodeId: string, networkId: string): string
export const launchLocalDataHavenSolochain = async (
options: DataHavenOptions,
launchedNetwork: LaunchedNetwork
): Promise<void> => {
): Promise<string> => {
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}`;
};
/**

View file

@ -144,6 +144,7 @@ export const launchNetwork = async (
options: NetworkLaunchOptions
): Promise<LaunchNetworkResult> => {
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";

View file

@ -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<void> => {
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<void> => {
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",

View file

@ -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<void> => {
): Promise<string> => {
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<void> => {
): Promise<string> => {
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}`;
};
/**

View file

@ -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",

View file

@ -212,7 +212,7 @@ export async function verifyProvidersRegistered(
): Promise<boolean> {
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}`);

View file

@ -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

View file

@ -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;
}

View file

@ -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`

View file

@ -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) };
};

View file

@ -184,7 +184,7 @@ export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint =>
const DATAHAVEN_PARAM_NAMES = [
"EthereumGatewayAddress",
"RewardsUpdateSelector",
"RewardsAgentOrigin",
"AgentOrigin",
"DatahavenServiceManagerAddress"
] as const;

View file

@ -8,6 +8,8 @@
export const COMMON_LAUNCH_ARGS = [
"--unsafe-force-node-key-generation",
"--tmp",
"--chain",
"local",
"--validator",
"--discover-local",
"--no-prometheus",