mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
Merge branch 'main' into sde/contracts-upgrade-environment
This commit is contained in:
commit
bfeee2015d
63 changed files with 2634 additions and 419 deletions
|
|
@ -17,7 +17,8 @@
|
|||
"!**/html/**/*",
|
||||
"!**/moonwall/contracts/out/**/*",
|
||||
"!**/contracts/out/**/*",
|
||||
"!**/contracts/deployments/state-diff.checksum"
|
||||
"!**/contracts/deployments/state-diff.checksum",
|
||||
"!**/bun.lock"
|
||||
],
|
||||
"maxSize": 3000000
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
"randaoCommitExpiration": 24,
|
||||
"minNumRequiredSignatures": 2,
|
||||
"startBlock": 1,
|
||||
"rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"initialValidatorSetId": 0,
|
||||
"initialValidatorHashes": [
|
||||
"0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
"randaoCommitExpiration": 24,
|
||||
"minNumRequiredSignatures": 16,
|
||||
"startBlock": 1,
|
||||
"rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"initialValidatorSetId": 0,
|
||||
"initialValidatorHashes": [],
|
||||
"nextValidatorSetId": 1,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
"randaoCommitExpiration": 24,
|
||||
"minNumRequiredSignatures": 3,
|
||||
"startBlock": 1303065,
|
||||
"rewardsMessageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd",
|
||||
"messageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd",
|
||||
"initialValidatorSetId": 2186,
|
||||
"initialValidatorHashes": [
|
||||
"0x07ce4f2cd558f4d4b529a3362b6ff7d616ca0893b53252dc62829b8218ea5c10",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
"randaoCommitExpiration": 24,
|
||||
"minNumRequiredSignatures": 5,
|
||||
"startBlock": 1381173,
|
||||
"rewardsMessageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215",
|
||||
"messageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215",
|
||||
"initialValidatorSetId": 2303,
|
||||
"initialValidatorHashes": [
|
||||
"0x0ec3102f334aba804c18b843e45ec874005587122a1b49273b1b04d6fd98b1a2",
|
||||
|
|
|
|||
1
contracts/deployments/anvil-agent-info.json
Normal file
1
contracts/deployments/anvil-agent-info.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"Agent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","RewardsAgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"}
|
||||
{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","RewardsAgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"}
|
||||
{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","AgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"}
|
||||
|
|
@ -12,7 +12,7 @@ contract Config {
|
|||
bytes32[] initialValidatorHashes;
|
||||
uint128 nextValidatorSetId;
|
||||
bytes32[] nextValidatorHashes;
|
||||
bytes32 rewardsMessageOrigin;
|
||||
bytes32 messageOrigin;
|
||||
}
|
||||
|
||||
// AVS parameters
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))))
|
||||
|
|
|
|||
|
|
@ -50,4 +50,3 @@ examples/
|
|||
Cargo.lock.old
|
||||
*.toml.old
|
||||
*.lock.old
|
||||
**/target/
|
||||
271
operator/Cargo.lock
generated
271
operator/Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ═══════════════════════╝
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\
|
||||
|
|
|
|||
|
|
@ -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 ═══════════════════════╝
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\
|
||||
|
|
|
|||
|
|
@ -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 ═══════════════════════╝
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
15
test/.dockerignore
Normal 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/**
|
||||
|
|
@ -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.
|
|
@ -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=="],
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"value": null
|
||||
},
|
||||
{
|
||||
"name": "RewardsAgentOrigin",
|
||||
"name": "AgentOrigin",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
228
test/e2e/suites/storagehub.test.ts
Normal file
228
test/e2e/suites/storagehub.test.ts
Normal 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})`;
|
||||
});
|
||||
});
|
||||
|
|
@ -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}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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) };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint =>
|
|||
const DATAHAVEN_PARAM_NAMES = [
|
||||
"EthereumGatewayAddress",
|
||||
"RewardsUpdateSelector",
|
||||
"RewardsAgentOrigin",
|
||||
"AgentOrigin",
|
||||
"DatahavenServiceManagerAddress"
|
||||
] as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
export const COMMON_LAUNCH_ARGS = [
|
||||
"--unsafe-force-node-key-generation",
|
||||
"--tmp",
|
||||
"--chain",
|
||||
"local",
|
||||
"--validator",
|
||||
"--discover-local",
|
||||
"--no-prometheus",
|
||||
|
|
|
|||
Loading…
Reference in a new issue