mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
Merge branch 'main' into feat/add-validator-submitter-ci-job
This commit is contained in:
commit
9c14c2fcdc
58 changed files with 6688 additions and 19578 deletions
2
.github/workflows/task-e2e.yml
vendored
2
.github/workflows/task-e2e.yml
vendored
|
|
@ -60,6 +60,8 @@ jobs:
|
|||
run: bun ./scripts/check-generated-state.ts
|
||||
- name: Install Foundry
|
||||
uses: foundry-rs/foundry-toolchain@v1
|
||||
with:
|
||||
version: v1.4.3
|
||||
- name: Pull Kurtosis images
|
||||
run: |
|
||||
docker pull ${{ env.KURTOSIS_CORE_IMAGE }}:${{ env.KURTOSIS_VERSION }}
|
||||
|
|
|
|||
1
contracts/VERSION
Normal file
1
contracts/VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.20.0
|
||||
|
|
@ -1 +1,27 @@
|
|||
{"network": "anvil","BeefyClient": "0x99bbA657f2BbC93c02D617f8bA121cB8Fc104Acf","AgentExecutor": "0x0E801D84Fa97b50751Dbf25036d067dCf18858bF","Gateway": "0x9d4454B023096f34B160D6B654540c56A1F81688","ServiceManager": "0x809d550fca64d94Bd9F66E60752A544199cfAC3D","ServiceManagerImplementation": "0x36C02dA8a0983159322a80FFE9F24b1acfF8B570","RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","DelegationManager": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82","StrategyManager": "0x9A676e781A523b5d0C0e43731313A708CB607508","AVSDirectory": "0x0B306BF915C4d645ff596e518fAf3F9669b97016","EigenPodManager": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1","EigenPodBeacon": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1","RewardsCoordinator": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE","AllocationManager": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed","PermissionController": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c","ETHPOSDeposit": "0xC7f2Cf4845C6db0e1a1e91ED41Bcd0FcC1b0E141","BaseStrategyImplementation": "0xf5059a5D33d5853360D16C683c16e67980206f36","DeployedStrategies": [{"address": "0x998abeb3E57409262aE5b751f60747921B33613E","underlyingToken": "0x95401dc811bb5740090279Ba06cfA8fcF6113778","tokenCreator": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"}]}
|
||||
{
|
||||
"network": "anvil",
|
||||
"BeefyClient": "0x99bbA657f2BbC93c02D617f8bA121cB8Fc104Acf",
|
||||
"AgentExecutor": "0x0E801D84Fa97b50751Dbf25036d067dCf18858bF",
|
||||
"Gateway": "0x9d4454B023096f34B160D6B654540c56A1F81688",
|
||||
"ServiceManager": "0x809d550fca64d94Bd9F66E60752A544199cfAC3D",
|
||||
"ServiceManagerImplementation": "0x36C02dA8a0983159322a80FFE9F24b1acfF8B570",
|
||||
"ProxyAdmin": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
|
||||
"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7",
|
||||
"DelegationManager": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
|
||||
"StrategyManager": "0x9A676e781A523b5d0C0e43731313A708CB607508",
|
||||
"AVSDirectory": "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
|
||||
"EigenPodManager": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
|
||||
"EigenPodBeacon": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1",
|
||||
"RewardsCoordinator": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
|
||||
"AllocationManager": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
|
||||
"PermissionController": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
|
||||
"ETHPOSDeposit": "0xC7f2Cf4845C6db0e1a1e91ED41Bcd0FcC1b0E141",
|
||||
"BaseStrategyImplementation": "0xf5059a5D33d5853360D16C683c16e67980206f36",
|
||||
"DeployedStrategies": [
|
||||
{
|
||||
"address": "0x998abeb3E57409262aE5b751f60747921B33613E",
|
||||
"underlyingToken": "0x95401dc811bb5740090279Ba06cfA8fcF6113778",
|
||||
"tokenCreator": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,15 @@
|
|||
{"network": "hoodi","BeefyClient": "0x109F9D0064D68639552d9aE037D67186EC870a1f","AgentExecutor": "0xfd44dC7B88d1C5186f5b60A0576245055F9dBEeB","Gateway": "0x0B13aAD3f9bD6bEFB9a4B678E6804b172f320C25","ServiceManager": "0xd69a0181D5d89827648E681cA6a4Cd517dEE8f1B","ServiceManagerImplementation": "0x9F4Fbc2A95d21d58BE029C8F6a656856E16833D6","VetoableSlasher": "0x66E9b408A45C6b7532fa6F7a992719aBAE1039D8","RewardsRegistry": "0x0a9C6901A3a23756BC97d40F44BfA611241a70D5","RewardsAgent": "0xeAd1BB0eA0e203f88d6D332F19910dcdF4A3B1A8","DelegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d","StrategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41","AVSDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926","RewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7","AllocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0","PermissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2"}
|
||||
{
|
||||
"network": "hoodi",
|
||||
"BeefyClient": "0x109F9D0064D68639552d9aE037D67186EC870a1f",
|
||||
"AgentExecutor": "0xfd44dC7B88d1C5186f5b60A0576245055F9dBEeB",
|
||||
"Gateway": "0x0B13aAD3f9bD6bEFB9a4B678E6804b172f320C25",
|
||||
"ServiceManager": "0xd69a0181D5d89827648E681cA6a4Cd517dEE8f1B",
|
||||
"ServiceManagerImplementation": "0x9F4Fbc2A95d21d58BE029C8F6a656856E16833D6",
|
||||
"RewardsAgent": "0xeAd1BB0eA0e203f88d6D332F19910dcdF4A3B1A8",
|
||||
"DelegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d",
|
||||
"StrategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41",
|
||||
"AVSDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926",
|
||||
"RewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7",
|
||||
"AllocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0",
|
||||
"PermissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2"
|
||||
}
|
||||
|
|
@ -1 +1,16 @@
|
|||
{"network": "stagenet-hoodi","BeefyClient": "0xE65dc4eCA2Fd428361076e1f204731224CeB4292","AgentExecutor": "0x35d3FdCB19A246a1763421168dF69dA3dE207063","Gateway": "0xE9352f1488F12bFEd722c133C129ca5F467463d1","ServiceManager": "0xED73cCaF067cebC706B2B3a6cf2b9af2c696c6d3","ServiceManagerImplementation": "0x5E1DA2eE025Dac2F8c391Ac86ebA20bd34c32465","RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","DelegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d","StrategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41","AVSDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926","RewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7","AllocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0","PermissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2"}
|
||||
{
|
||||
"network": "stagenet-hoodi",
|
||||
"BeefyClient": "0xE65dc4eCA2Fd428361076e1f204731224CeB4292",
|
||||
"AgentExecutor": "0x35d3FdCB19A246a1763421168dF69dA3dE207063",
|
||||
"Gateway": "0xE9352f1488F12bFEd722c133C129ca5F467463d1",
|
||||
"ServiceManager": "0xED73cCaF067cebC706B2B3a6cf2b9af2c696c6d3",
|
||||
"ServiceManagerImplementation": "0x5E1DA2eE025Dac2F8c391Ac86ebA20bd34c32465",
|
||||
"ProxyAdmin": "0xeb1a705e1aa96e6a6329d8a8eb0f5ec38eb7b69d",
|
||||
"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7",
|
||||
"DelegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d",
|
||||
"StrategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41",
|
||||
"AVSDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926",
|
||||
"RewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7",
|
||||
"AllocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0",
|
||||
"PermissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
74018d5581304551932388025aebb9508b907e22
|
||||
d7d30510de741750e5b2069228eb2b037f20cc22
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -48,6 +48,7 @@ struct ServiceManagerInitParams {
|
|||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] validatorsStrategiesAndMultipliers;
|
||||
address gateway;
|
||||
address validatorSetSubmitter;
|
||||
string initialVersion;
|
||||
}
|
||||
|
||||
// Struct to store more detailed strategy information
|
||||
|
|
@ -149,7 +150,8 @@ abstract contract DeployBase is Script, DeployParams, Accounts {
|
|||
gateway,
|
||||
serviceManager,
|
||||
serviceManagerImplementation,
|
||||
rewardsAgentAddress
|
||||
rewardsAgentAddress,
|
||||
proxyAdmin
|
||||
);
|
||||
|
||||
_outputRewardsAgentInfo(rewardsAgentAddress, snowbridgeConfig.rewardsMessageOrigin);
|
||||
|
|
@ -260,13 +262,18 @@ abstract contract DeployBase is Script, DeployParams, Accounts {
|
|||
});
|
||||
}
|
||||
|
||||
// Read version from environment variable (passed by TypeScript wrapper)
|
||||
string memory version = vm.envOr("DATAHAVEN_VERSION", string("0.1.0"));
|
||||
console.log("| Version: %s", version);
|
||||
|
||||
// Create service manager initialisation parameters struct
|
||||
ServiceManagerInitParams memory initParams = ServiceManagerInitParams({
|
||||
avsOwner: avsConfig.avsOwner,
|
||||
rewardsInitiator: avsConfig.rewardsInitiator,
|
||||
validatorsStrategiesAndMultipliers: strategiesAndMultipliers,
|
||||
gateway: address(gateway),
|
||||
validatorSetSubmitter: avsConfig.validatorSetSubmitter
|
||||
validatorSetSubmitter: avsConfig.validatorSetSubmitter,
|
||||
initialVersion: version
|
||||
});
|
||||
|
||||
// Create the service manager proxy (different logic for local vs testnet)
|
||||
|
|
@ -306,7 +313,8 @@ abstract contract DeployBase is Script, DeployParams, Accounts {
|
|||
IGatewayV2 gateway,
|
||||
DataHavenServiceManager serviceManager,
|
||||
DataHavenServiceManager serviceManagerImplementation,
|
||||
address rewardsAgent
|
||||
address rewardsAgent,
|
||||
ProxyAdmin proxyAdmin
|
||||
) internal virtual;
|
||||
|
||||
/**
|
||||
|
|
|
|||
89
contracts/script/deploy/DeployImplementation.s.sol
Normal file
89
contracts/script/deploy/DeployImplementation.s.sol
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.27;
|
||||
|
||||
import {Script} from "forge-std/Script.sol";
|
||||
import {console} from "forge-std/console.sol";
|
||||
|
||||
// DataHaven imports
|
||||
import {DataHavenServiceManager} from "../../src/DataHavenServiceManager.sol";
|
||||
|
||||
// EigenLayer imports
|
||||
import {RewardsCoordinator} from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.sol";
|
||||
import {AllocationManager} from "eigenlayer-contracts/src/contracts/core/AllocationManager.sol";
|
||||
|
||||
// OpenZeppelin imports
|
||||
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
|
||||
import {
|
||||
ITransparentUpgradeableProxy
|
||||
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
|
||||
|
||||
/**
|
||||
* @title DeployImplementation
|
||||
* @notice Script for deploying implementation contracts and upgrading proxies
|
||||
*/
|
||||
contract DeployImplementation is Script {
|
||||
function run() public {
|
||||
// This script is designed to be called with specific function selectors
|
||||
// Use forge script with --sig "functionName()" to call specific functions
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Deploy new ServiceManager implementation
|
||||
*/
|
||||
function deployServiceManagerImpl() public {
|
||||
console.log("Deploying ServiceManager Implementation...");
|
||||
|
||||
// Get constructor parameters from environment variables
|
||||
address rewardsCoordinator = vm.envAddress("REWARDS_COORDINATOR");
|
||||
address allocationManager = vm.envAddress("ALLOCATION_MANAGER");
|
||||
|
||||
require(rewardsCoordinator != address(0), "REWARDS_COORDINATOR not set");
|
||||
require(allocationManager != address(0), "ALLOCATION_MANAGER not set");
|
||||
|
||||
uint256 deployerPrivateKey = uint256(vm.envBytes32("PRIVATE_KEY"));
|
||||
vm.broadcast(deployerPrivateKey);
|
||||
DataHavenServiceManager serviceManagerImpl = new DataHavenServiceManager(
|
||||
RewardsCoordinator(rewardsCoordinator), AllocationManager(allocationManager)
|
||||
);
|
||||
|
||||
console.log("ServiceManager Implementation deployed at:", address(serviceManagerImpl));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update ServiceManager proxy and set version in one transaction.
|
||||
* @dev Uses upgradeAndCall so the version update is atomically bundled with the upgrade.
|
||||
* updateVersion is gated by onlyProxyAdmin, and upgradeAndCall executes the calldata
|
||||
* with msg.sender = ProxyAdmin — satisfying that check.
|
||||
* The AVS owner owns the ProxyAdmin, so the trust chain is: AVS owner → ProxyAdmin → updateVersion.
|
||||
*/
|
||||
function updateServiceManagerProxyWithVersion() public {
|
||||
console.log("Updating ServiceManager proxy with version...");
|
||||
|
||||
// Get addresses and version from environment variables
|
||||
address serviceManager = vm.envAddress("SERVICE_MANAGER");
|
||||
address newImplementation = vm.envAddress("SERVICE_MANAGER_IMPL");
|
||||
address proxyAdmin = vm.envAddress("PROXY_ADMIN");
|
||||
string memory newVersion = vm.envString("NEW_VERSION");
|
||||
|
||||
require(serviceManager != address(0), "SERVICE_MANAGER not set");
|
||||
require(newImplementation != address(0), "SERVICE_MANAGER_IMPL not set");
|
||||
require(newImplementation.code.length > 0, "SERVICE_MANAGER_IMPL is not a contract");
|
||||
require(proxyAdmin != address(0), "PROXY_ADMIN not set");
|
||||
require(bytes(newVersion).length > 0, "NEW_VERSION not set");
|
||||
|
||||
// Encode the updateVersion call — gated by onlyProxyAdmin, satisfied since
|
||||
// upgradeAndCall executes this calldata with msg.sender = ProxyAdmin
|
||||
bytes memory data = abi.encodeWithSignature("updateVersion(string)", newVersion);
|
||||
|
||||
// AVS owner owns the ProxyAdmin (transferred during deployment)
|
||||
uint256 avsOwnerPrivateKey = uint256(vm.envBytes32("AVS_OWNER_PRIVATE_KEY"));
|
||||
vm.broadcast(avsOwnerPrivateKey);
|
||||
ProxyAdmin(proxyAdmin)
|
||||
.upgradeAndCall(
|
||||
ITransparentUpgradeableProxy(payable(serviceManager)), newImplementation, data
|
||||
);
|
||||
|
||||
console.log("ServiceManager proxy updated to:", newImplementation);
|
||||
console.log("Version updated to:", newVersion);
|
||||
}
|
||||
}
|
||||
|
|
@ -115,6 +115,11 @@ contract DeployLive is DeployBase {
|
|||
ProxyAdmin proxyAdmin = new ProxyAdmin();
|
||||
Logging.logContractDeployed("ProxyAdmin", address(proxyAdmin));
|
||||
|
||||
// Transfer ProxyAdmin ownership to AVS owner so upgrades can only be performed by AVS owner
|
||||
vm.broadcast(_deployerPrivateKey);
|
||||
proxyAdmin.transferOwnership(_avsOwner);
|
||||
Logging.logStep("ProxyAdmin ownership transferred to AVS owner");
|
||||
|
||||
vm.broadcast(_deployerPrivateKey);
|
||||
bytes memory initData = abi.encodeWithSelector(
|
||||
DataHavenServiceManager.initialize.selector,
|
||||
|
|
@ -122,7 +127,8 @@ contract DeployLive is DeployBase {
|
|||
params.rewardsInitiator,
|
||||
params.validatorsStrategiesAndMultipliers,
|
||||
params.gateway,
|
||||
params.validatorSetSubmitter
|
||||
params.validatorSetSubmitter,
|
||||
params.initialVersion
|
||||
);
|
||||
|
||||
TransparentUpgradeableProxy proxy =
|
||||
|
|
@ -137,7 +143,8 @@ contract DeployLive is DeployBase {
|
|||
IGatewayV2 gateway,
|
||||
DataHavenServiceManager serviceManager,
|
||||
DataHavenServiceManager serviceManagerImplementation,
|
||||
address rewardsAgent
|
||||
address rewardsAgent,
|
||||
ProxyAdmin proxyAdmin
|
||||
) internal override {
|
||||
Logging.logHeader("DEPLOYMENT SUMMARY");
|
||||
|
||||
|
|
@ -186,6 +193,7 @@ contract DeployLive is DeployBase {
|
|||
vm.toString(address(serviceManagerImplementation)),
|
||||
'",'
|
||||
);
|
||||
json = string.concat(json, '"ProxyAdmin": "', vm.toString(address(proxyAdmin)), '",');
|
||||
json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",');
|
||||
|
||||
// EigenLayer contracts (existing on live network)
|
||||
|
|
|
|||
|
|
@ -182,12 +182,12 @@ contract DeployLocal is DeployBase {
|
|||
_deployStrategies(pauserRegistry, proxyAdmin);
|
||||
Logging.logStep("Strategy contracts deployed successfully");
|
||||
|
||||
// Transfer ownership of core contracts
|
||||
// Transfer ownership of core contracts to AVS owner so upgrades can be performed by AVS owner
|
||||
vm.broadcast(_deployerPrivateKey);
|
||||
proxyAdmin.transferOwnership(eigenLayerConfig.executorMultisig);
|
||||
proxyAdmin.transferOwnership(_avsOwner);
|
||||
vm.broadcast(_deployerPrivateKey);
|
||||
eigenPodBeacon.transferOwnership(eigenLayerConfig.executorMultisig);
|
||||
Logging.logStep("Ownership transferred to multisig");
|
||||
Logging.logStep("ProxyAdmin ownership transferred to AVS owner");
|
||||
|
||||
Logging.logFooter();
|
||||
return proxyAdmin;
|
||||
|
|
@ -208,7 +208,8 @@ contract DeployLocal is DeployBase {
|
|||
params.rewardsInitiator,
|
||||
params.validatorsStrategiesAndMultipliers,
|
||||
params.gateway,
|
||||
params.validatorSetSubmitter
|
||||
params.validatorSetSubmitter,
|
||||
params.initialVersion
|
||||
);
|
||||
|
||||
TransparentUpgradeableProxy proxy =
|
||||
|
|
@ -223,7 +224,8 @@ contract DeployLocal is DeployBase {
|
|||
IGatewayV2 gateway,
|
||||
DataHavenServiceManager serviceManager,
|
||||
DataHavenServiceManager serviceManagerImplementation,
|
||||
address rewardsAgent
|
||||
address rewardsAgent,
|
||||
ProxyAdmin proxyAdmin
|
||||
) internal override {
|
||||
Logging.logHeader("DEPLOYMENT SUMMARY");
|
||||
|
||||
|
|
@ -284,6 +286,7 @@ contract DeployLocal is DeployBase {
|
|||
vm.toString(address(serviceManagerImplementation)),
|
||||
'",'
|
||||
);
|
||||
json = string.concat(json, '"ProxyAdmin": "', vm.toString(address(proxyAdmin)), '",');
|
||||
json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",');
|
||||
|
||||
// EigenLayer contracts
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pragma solidity ^0.8.27;
|
|||
|
||||
// OpenZeppelin imports
|
||||
import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
|
||||
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ import {IDataHavenServiceManager} from "./interfaces/IDataHavenServiceManager.so
|
|||
/**
|
||||
* @title DataHaven ServiceManager contract
|
||||
* @notice Manages validators in the DataHaven network and submits rewards to EigenLayer
|
||||
* @dev This contract is upgradeable and integrates with EigenLayer's AllocationManager
|
||||
*/
|
||||
contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHavenServiceManager {
|
||||
using SafeERC20 for IERC20;
|
||||
|
|
@ -75,9 +77,15 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
/// @inheritdoc IDataHavenServiceManager
|
||||
mapping(IStrategy => uint96) public strategiesAndMultipliers;
|
||||
|
||||
/// @notice Semantic version of the deployed DataHaven AVS stack.
|
||||
/// Set during initialization based on deployment chain.
|
||||
/// This should match the `version` field in the corresponding
|
||||
/// `contracts/deployments/<chain>.json`.
|
||||
string private _version;
|
||||
|
||||
/// @notice Storage gap for upgradeability (must be at end of state variables)
|
||||
// solhint-disable-next-line var-name-mixedcase
|
||||
uint256[43] private __GAP;
|
||||
uint256[42] private __GAP;
|
||||
|
||||
// ============ Modifiers ============
|
||||
|
||||
|
|
@ -105,6 +113,22 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
_;
|
||||
}
|
||||
|
||||
/// @notice Restricts function to the ProxyAdmin contract.
|
||||
/// @dev Version updates must come through the ProxyAdmin so they are always
|
||||
/// bundled with an actual proxy upgrade (via upgradeAndCall). The ProxyAdmin
|
||||
/// is owned by the AVS owner, so the trust chain is: AVS owner → ProxyAdmin → updateVersion.
|
||||
modifier onlyProxyAdmin() {
|
||||
_checkProxyAdmin();
|
||||
_;
|
||||
}
|
||||
|
||||
function _checkProxyAdmin() internal view {
|
||||
// EIP-1967 admin slot: keccak256("eip1967.proxy.admin") - 1
|
||||
bytes32 adminSlot = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
|
||||
address proxyAdmin = StorageSlot.getAddressSlot(adminSlot).value;
|
||||
require(msg.sender == proxyAdmin, NotProxyAdmin());
|
||||
}
|
||||
|
||||
function _checkRewardsInitiator() internal view {
|
||||
require(msg.sender == rewardsInitiator, OnlyRewardsInitiator());
|
||||
}
|
||||
|
|
@ -145,17 +169,22 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
address _rewardsInitiator,
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory validatorsStrategiesAndMultipliers,
|
||||
address _snowbridgeGatewayAddress,
|
||||
address _validatorSetSubmitter
|
||||
address _validatorSetSubmitter,
|
||||
string memory initialVersion
|
||||
) public virtual initializer {
|
||||
require(initialOwner != address(0), ZeroAddress());
|
||||
require(_rewardsInitiator != address(0), ZeroAddress());
|
||||
require(_snowbridgeGatewayAddress != address(0), ZeroAddress());
|
||||
require(bytes(initialVersion).length > 0, EmptyVersion());
|
||||
|
||||
__Ownable_init();
|
||||
_transferOwnership(initialOwner);
|
||||
rewardsInitiator = _rewardsInitiator;
|
||||
emit RewardsInitiatorSet(address(0), _rewardsInitiator);
|
||||
|
||||
// Set version from parameter (allows flexibility per deployment environment)
|
||||
_version = initialVersion;
|
||||
|
||||
// Register the DataHaven service in the AllocationManager.
|
||||
_ALLOCATION_MANAGER.updateAVSMetadataURI(address(this), DATAHAVEN_AVS_METADATA);
|
||||
|
||||
|
|
@ -186,6 +215,16 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
}
|
||||
}
|
||||
|
||||
// ============ View Functions ============
|
||||
|
||||
/// @notice Returns the semantic version of the deployed DataHaven AVS stack
|
||||
/// @return The version string (e.g., "1.0.0")
|
||||
function DATAHAVEN_VERSION() external view returns (string memory) {
|
||||
return _version;
|
||||
}
|
||||
|
||||
// ============ External Functions ============
|
||||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
function setValidatorSetSubmitter(
|
||||
address newSubmitter
|
||||
|
|
@ -585,6 +624,22 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
emit SlashingComplete();
|
||||
}
|
||||
|
||||
// ============ Version Management ============
|
||||
|
||||
/// @notice Updates the contract version (typically called after upgrades)
|
||||
/// @param newVersion The new version string (e.g., "1.1.0")
|
||||
/// @dev Only callable by the ProxyAdmin, ensuring version changes are always
|
||||
/// bundled with a proxy upgrade via upgradeAndCall. The AVS owner controls
|
||||
/// the ProxyAdmin, maintaining the trust chain: AVS owner → ProxyAdmin → updateVersion.
|
||||
function updateVersion(
|
||||
string memory newVersion
|
||||
) external onlyProxyAdmin {
|
||||
require(bytes(newVersion).length > 0, "Version cannot be empty");
|
||||
string memory oldVersion = _version;
|
||||
_version = newVersion;
|
||||
emit VersionUpdated(oldVersion, newVersion);
|
||||
}
|
||||
|
||||
// ============ Internal Functions ============
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ interface IDataHavenServiceManagerErrors {
|
|||
|
||||
/// @notice Thrown when an operation requires the operator to be registered but it is not
|
||||
error OperatorNotRegistered();
|
||||
|
||||
/// @notice Thrown when an empty version string is provided
|
||||
error EmptyVersion();
|
||||
|
||||
/// @notice Thrown when the caller is not the ProxyAdmin
|
||||
error NotProxyAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -107,6 +113,11 @@ interface IDataHavenServiceManagerEvents {
|
|||
/// @param newSubmitter The new validator set submitter address
|
||||
event ValidatorSetSubmitterUpdated(address indexed oldSubmitter, address indexed newSubmitter);
|
||||
|
||||
/// @notice Emitted when the contract version is updated
|
||||
/// @param oldVersion The previous version string
|
||||
/// @param newVersion The new version string
|
||||
event VersionUpdated(string oldVersion, string newVersion);
|
||||
|
||||
/// @notice Emitted when a validator set message is submitted for a target era
|
||||
/// @param targetEra The target era for the validator set
|
||||
/// @param payloadHash The keccak256 hash of the encoded message payload
|
||||
|
|
@ -177,19 +188,21 @@ interface IDataHavenServiceManager is
|
|||
|
||||
/**
|
||||
* @notice Initializes the DataHaven Service Manager
|
||||
* @param initialOwner Address of the initial owner
|
||||
* @param initialOwner Address of the initial owner (AVS owner)
|
||||
* @param rewardsInitiator Address authorized to initiate rewards
|
||||
* @param validatorsStrategiesAndMultipliers Array of strategy-multiplier pairs for the validators
|
||||
* operator set. Each multiplier must be non-zero.
|
||||
* @param _snowbridgeGatewayAddress Address of the Snowbridge Gateway
|
||||
* @param _validatorSetSubmitter Address authorized to submit validator set messages
|
||||
* @param initialVersion The initial semantic version string (e.g., "1.0.0")
|
||||
*/
|
||||
function initialize(
|
||||
address initialOwner,
|
||||
address rewardsInitiator,
|
||||
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory validatorsStrategiesAndMultipliers,
|
||||
address _snowbridgeGatewayAddress,
|
||||
address _validatorSetSubmitter
|
||||
address _validatorSetSubmitter,
|
||||
string memory initialVersion
|
||||
) external;
|
||||
|
||||
/**
|
||||
|
|
@ -359,4 +372,22 @@ interface IDataHavenServiceManager is
|
|||
address operator,
|
||||
uint32[] calldata operatorSetIds
|
||||
) external;
|
||||
|
||||
// ============ Version Management ============
|
||||
|
||||
/**
|
||||
* @notice Returns the semantic version of the deployed DataHaven AVS stack
|
||||
* @return The version string (e.g., "1.0.0")
|
||||
*/
|
||||
function DATAHAVEN_VERSION() external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @notice Updates the contract version (typically called after upgrades)
|
||||
* @param newVersion The new version string (e.g., "1.1.0")
|
||||
* @dev Only callable by the ProxyAdmin. Version changes are always bundled with
|
||||
* a proxy upgrade via upgradeAndCall. Trust chain: AVS owner → ProxyAdmin → updateVersion.
|
||||
*/
|
||||
function updateVersion(
|
||||
string memory newVersion
|
||||
) external;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
"type": "t_array(t_uint256)49_storage"
|
||||
},
|
||||
{
|
||||
"astId": 23775,
|
||||
"astId": 23887,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "rewardsInitiator",
|
||||
"offset": 0,
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
"type": "t_address"
|
||||
},
|
||||
{
|
||||
"astId": 23780,
|
||||
"astId": 23892,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "validatorsAllowlist",
|
||||
"offset": 0,
|
||||
|
|
@ -57,15 +57,15 @@
|
|||
"type": "t_mapping(t_address,t_bool)"
|
||||
},
|
||||
{
|
||||
"astId": 23784,
|
||||
"astId": 23896,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "_snowbridgeGateway",
|
||||
"offset": 0,
|
||||
"slot": "103",
|
||||
"type": "t_contract(IGatewayV2)23481"
|
||||
"type": "t_contract(IGatewayV2)23591"
|
||||
},
|
||||
{
|
||||
"astId": 23789,
|
||||
"astId": 23901,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "validatorEthAddressToSolochainAddress",
|
||||
"offset": 0,
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
"type": "t_mapping(t_address,t_address)"
|
||||
},
|
||||
{
|
||||
"astId": 23793,
|
||||
"astId": 23905,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "validatorSolochainAddressToEthAddress",
|
||||
"offset": 0,
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
"type": "t_mapping(t_address,t_address)"
|
||||
},
|
||||
{
|
||||
"astId": 23796,
|
||||
"astId": 23908,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "validatorSetSubmitter",
|
||||
"offset": 0,
|
||||
|
|
@ -89,20 +89,28 @@
|
|||
"type": "t_address"
|
||||
},
|
||||
{
|
||||
"astId": 23802,
|
||||
"astId": 23914,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "strategiesAndMultipliers",
|
||||
"offset": 0,
|
||||
"slot": "107",
|
||||
"type": "t_mapping(t_contract(IStrategy)7361,t_uint96)"
|
||||
"type": "t_mapping(t_contract(IStrategy)7471,t_uint96)"
|
||||
},
|
||||
{
|
||||
"astId": 23807,
|
||||
"astId": 23917,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "_version",
|
||||
"offset": 0,
|
||||
"slot": "108",
|
||||
"type": "t_string_storage"
|
||||
},
|
||||
{
|
||||
"astId": 23922,
|
||||
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
|
||||
"label": "__GAP",
|
||||
"offset": 0,
|
||||
"slot": "108",
|
||||
"type": "t_array(t_uint256)43_storage"
|
||||
"slot": "109",
|
||||
"type": "t_array(t_uint256)42_storage"
|
||||
}
|
||||
],
|
||||
"types": {
|
||||
|
|
@ -111,10 +119,10 @@
|
|||
"label": "address",
|
||||
"numberOfBytes": "20"
|
||||
},
|
||||
"t_array(t_uint256)43_storage": {
|
||||
"t_array(t_uint256)42_storage": {
|
||||
"encoding": "inplace",
|
||||
"label": "uint256[43]",
|
||||
"numberOfBytes": "1376",
|
||||
"label": "uint256[42]",
|
||||
"numberOfBytes": "1344",
|
||||
"base": "t_uint256"
|
||||
},
|
||||
"t_array(t_uint256)49_storage": {
|
||||
|
|
@ -134,12 +142,12 @@
|
|||
"label": "bool",
|
||||
"numberOfBytes": "1"
|
||||
},
|
||||
"t_contract(IGatewayV2)23481": {
|
||||
"t_contract(IGatewayV2)23591": {
|
||||
"encoding": "inplace",
|
||||
"label": "contract IGatewayV2",
|
||||
"numberOfBytes": "20"
|
||||
},
|
||||
"t_contract(IStrategy)7361": {
|
||||
"t_contract(IStrategy)7471": {
|
||||
"encoding": "inplace",
|
||||
"label": "contract IStrategy",
|
||||
"numberOfBytes": "20"
|
||||
|
|
@ -158,13 +166,18 @@
|
|||
"numberOfBytes": "32",
|
||||
"value": "t_bool"
|
||||
},
|
||||
"t_mapping(t_contract(IStrategy)7361,t_uint96)": {
|
||||
"t_mapping(t_contract(IStrategy)7471,t_uint96)": {
|
||||
"encoding": "mapping",
|
||||
"key": "t_contract(IStrategy)7361",
|
||||
"key": "t_contract(IStrategy)7471",
|
||||
"label": "mapping(contract IStrategy => uint96)",
|
||||
"numberOfBytes": "32",
|
||||
"value": "t_uint96"
|
||||
},
|
||||
"t_string_storage": {
|
||||
"encoding": "bytes",
|
||||
"label": "string",
|
||||
"numberOfBytes": "32"
|
||||
},
|
||||
"t_uint256": {
|
||||
"encoding": "inplace",
|
||||
"label": "uint256",
|
||||
|
|
|
|||
|
|
@ -197,7 +197,8 @@ contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
|
|||
rewardsInitiator,
|
||||
emptyStrategies,
|
||||
address(snowbridgeGatewayMock),
|
||||
address(0)
|
||||
address(0),
|
||||
"v-test"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -249,7 +249,8 @@ contract AVSDeployer is Test {
|
|||
rewardsInitiator,
|
||||
defaultStrategyAndMultipliers,
|
||||
address(snowbridgeGatewayMock),
|
||||
avsOwner
|
||||
avsOwner,
|
||||
"v-mock"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
22
operator/Cargo.lock
generated
22
operator/Cargo.lock
generated
|
|
@ -2669,6 +2669,7 @@ dependencies = [
|
|||
"pallet-file-system",
|
||||
"pallet-file-system-runtime-api",
|
||||
"pallet-grandpa",
|
||||
"pallet-grandpa-benchmarking",
|
||||
"pallet-identity",
|
||||
"pallet-im-online",
|
||||
"pallet-message-queue",
|
||||
|
|
@ -2972,6 +2973,7 @@ dependencies = [
|
|||
"pallet-file-system",
|
||||
"pallet-file-system-runtime-api",
|
||||
"pallet-grandpa",
|
||||
"pallet-grandpa-benchmarking",
|
||||
"pallet-identity",
|
||||
"pallet-im-online",
|
||||
"pallet-message-queue",
|
||||
|
|
@ -3128,6 +3130,7 @@ dependencies = [
|
|||
"pallet-file-system",
|
||||
"pallet-file-system-runtime-api",
|
||||
"pallet-grandpa",
|
||||
"pallet-grandpa-benchmarking",
|
||||
"pallet-identity",
|
||||
"pallet-im-online",
|
||||
"pallet-message-queue",
|
||||
|
|
@ -9282,6 +9285,25 @@ dependencies = [
|
|||
"sp-staking",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-grandpa-benchmarking"
|
||||
version = "0.25.0"
|
||||
dependencies = [
|
||||
"finality-grandpa",
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"pallet-grandpa",
|
||||
"pallet-session",
|
||||
"parity-scale-codec",
|
||||
"sp-application-crypto",
|
||||
"sp-consensus-grandpa",
|
||||
"sp-core",
|
||||
"sp-runtime",
|
||||
"sp-session",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-identity"
|
||||
version = "39.1.0"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ pallet-external-validators = { path = "./pallets/external-validators", default-f
|
|||
pallet-external-validators-rewards = { path = "./pallets/external-validators-rewards", default-features = false }
|
||||
pallet-outbound-commitment-store = { path = "./pallets/outbound-commitment-store", default-features = false }
|
||||
pallet-proxy-genesis-companion = { path = "./pallets/proxy-genesis-companion", default-features = false }
|
||||
pallet-grandpa-benchmarking = { path = "./pallets/grandpa-benchmarking", default-features = false }
|
||||
pallet-session-benchmarking = { path = "./pallets/session-benchmarking", default-features = false }
|
||||
|
||||
# Crates.io (wasm)
|
||||
|
|
|
|||
56
operator/pallets/grandpa-benchmarking/Cargo.toml
Normal file
56
operator/pallets/grandpa-benchmarking/Cargo.toml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
[package]
|
||||
authors = { workspace = true }
|
||||
description = "Benchmarking helpers for pallet-grandpa in DataHaven runtimes."
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
name = "pallet-grandpa-benchmarking"
|
||||
version = { workspace = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true }
|
||||
frame-benchmarking = { workspace = true, optional = true }
|
||||
frame-support = { workspace = true }
|
||||
frame-system = { workspace = true }
|
||||
finality-grandpa = { version = "0.16.3", default-features = false }
|
||||
pallet-grandpa = { workspace = true }
|
||||
pallet-session = { workspace = true, features = ["historical"] }
|
||||
sp-consensus-grandpa = { workspace = true }
|
||||
sp-application-crypto = { workspace = true }
|
||||
sp-core = { workspace = true }
|
||||
sp-runtime = { workspace = true }
|
||||
sp-session = { workspace = true }
|
||||
sp-std = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"frame-benchmarking?/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"finality-grandpa/std",
|
||||
"pallet-grandpa/std",
|
||||
"pallet-session/std",
|
||||
"sp-consensus-grandpa/std",
|
||||
"sp-application-crypto/std",
|
||||
"sp-core/std",
|
||||
"sp-runtime/std",
|
||||
"sp-session/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"pallet-grandpa/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
sp-core = { workspace = true, features = ["full_crypto"] }
|
||||
190
operator/pallets/grandpa-benchmarking/src/benchmarking.rs
Normal file
190
operator/pallets/grandpa-benchmarking/src/benchmarking.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
//! Benchmarks for `pallet_grandpa` covering both extrinsics in `pallet_grandpa::WeightInfo`:
|
||||
//! `report_equivocation` and `note_stalled`.
|
||||
//!
|
||||
//! Upstream `pallet-grandpa` benchmarks `check_equivocation_proof` (a raw crypto cost proxy) and
|
||||
//! `note_stalled`, but does not benchmark the actual `report_equivocation` extrinsic dispatch.
|
||||
//! This crate fills that gap so the node's benchmark command can generate a complete `WeightInfo`
|
||||
//! impl from real measurements against the DataHaven runtime.
|
||||
//!
|
||||
//! The equivocation proof is pre-encoded (see `PREENCODED_EQUIVOCATION_PROOF`) and was generated
|
||||
//! with the same ed25519 seed used in `setup_equivocation`, so the authority ID embedded in the
|
||||
//! proof matches the key registered in the session.
|
||||
//! Regenerate with: `cargo test -p pallet-grandpa-benchmarking --features std -- test_generate_equivocation_blob --nocapture`
|
||||
//!
|
||||
//! `frame-omni-bencher` will fail with `InvalidEquivocationProof` because its WASM host does not
|
||||
//! provide a real ed25519 verifier. Use the node's `benchmark pallet` subcommand instead.
|
||||
|
||||
use alloc::{boxed::Box, vec};
|
||||
|
||||
use codec::Decode;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::traits::{KeyOwnerProofSystem, OnInitialize};
|
||||
use frame_system::RawOrigin;
|
||||
use sp_application_crypto::{RuntimeAppPublic, UncheckedFrom};
|
||||
use sp_runtime::traits::Convert;
|
||||
|
||||
use crate::{Config, Pallet};
|
||||
|
||||
type GrandpaId = sp_consensus_grandpa::AuthorityId;
|
||||
type GrandpaEquivocationProof<T> = sp_consensus_grandpa::EquivocationProof<
|
||||
<T as frame_system::Config>::Hash,
|
||||
frame_system::pallet_prelude::BlockNumberFor<T>,
|
||||
>;
|
||||
|
||||
/// Pre-encoded equivocation proof (set_id=0, round=1) signed with the test vector key.
|
||||
/// Generated by `test_generate_equivocation_blob` in `tests` module of `lib.rs`.
|
||||
const PREENCODED_EQUIVOCATION_PROOF: [u8; 249] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 215, 90, 152, 1, 130, 177, 10, 183, 213, 75,
|
||||
254, 211, 201, 100, 7, 58, 14, 225, 114, 243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81,
|
||||
26, 225, 44, 34, 212, 241, 98, 217, 160, 18, 201, 49, 146, 51, 218, 93, 62, 146, 60, 197, 225,
|
||||
2, 155, 143, 144, 228, 114, 73, 201, 171, 37, 107, 53, 1, 0, 0, 0, 94, 182, 72, 53, 215, 108,
|
||||
169, 70, 216, 243, 227, 8, 163, 172, 0, 93, 157, 90, 110, 18, 72, 38, 48, 16, 57, 74, 178, 17,
|
||||
106, 150, 24, 107, 195, 175, 45, 40, 156, 45, 67, 202, 120, 13, 87, 252, 21, 17, 62, 155, 246,
|
||||
219, 28, 34, 255, 230, 191, 85, 75, 147, 164, 14, 131, 146, 99, 2, 123, 10, 161, 115, 94, 91,
|
||||
165, 141, 50, 54, 49, 108, 103, 31, 228, 240, 14, 211, 102, 238, 114, 65, 124, 158, 208, 42,
|
||||
83, 168, 1, 158, 133, 184, 2, 0, 0, 0, 151, 111, 43, 192, 22, 148, 165, 193, 112, 145, 172, 94,
|
||||
236, 197, 151, 102, 5, 97, 64, 30, 160, 179, 79, 79, 150, 102, 200, 105, 32, 233, 249, 185,
|
||||
118, 73, 110, 32, 193, 87, 150, 41, 254, 155, 104, 77, 236, 36, 48, 202, 161, 26, 247, 61, 181,
|
||||
109, 221, 114, 165, 70, 43, 146, 198, 158, 253, 1,
|
||||
];
|
||||
|
||||
/// Constructs a dummy, unique, deterministic grandpa id
|
||||
fn grandpa_id_for_validator(i: u32) -> GrandpaId {
|
||||
let mut raw = [0u8; 32];
|
||||
raw[..4].copy_from_slice(&i.to_le_bytes());
|
||||
raw[4] = 0xff;
|
||||
GrandpaId::unchecked_from(raw)
|
||||
}
|
||||
|
||||
fn setup_equivocation<T: Config>(
|
||||
extra_validators: u32,
|
||||
) -> Result<
|
||||
(
|
||||
Box<GrandpaEquivocationProof<T>>,
|
||||
<T as pallet_grandpa::Config>::KeyOwnerProof,
|
||||
<T as frame_system::Config>::AccountId,
|
||||
),
|
||||
BenchmarkError,
|
||||
> {
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use frame_system::Pallet as System;
|
||||
|
||||
let reporter: T::AccountId = whitelisted_caller();
|
||||
frame_system::Pallet::<T>::inc_providers(&reporter);
|
||||
|
||||
// Ensure we are at a sane block number and that session is initialized.
|
||||
System::<T>::set_block_number(1u32.into());
|
||||
<pallet_session::Pallet<T> as OnInitialize<BlockNumberFor<T>>>::on_initialize(1u32.into());
|
||||
|
||||
// Use the same seed as in test_generate_equivocation_blob so the key matches the
|
||||
// pre-encoded proof and the host keystore can store it for KeyOwnerProof.
|
||||
let seed = b"0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60".to_vec();
|
||||
let grandpa_id: GrandpaId = GrandpaId::generate_pair(Some(seed));
|
||||
|
||||
// Install session keys for the reporter account. The runtime provides the concrete
|
||||
// `T::Keys` type construction via `Config::benchmark_session_keys`.
|
||||
let keys = T::benchmark_session_keys(grandpa_id.clone());
|
||||
pallet_session::Pallet::<T>::set_keys(
|
||||
RawOrigin::Signed(reporter.clone()).into(),
|
||||
keys.clone(),
|
||||
vec![0, 1, 2, 3],
|
||||
)
|
||||
.map_err(|_| BenchmarkError::Stop("set_keys failed"))?;
|
||||
|
||||
// ValidatorId = AccountId in all DataHaven runtimes; the reporter went through set_keys
|
||||
// successfully so the conversion is guaranteed to succeed.
|
||||
let reporter_validator_id: T::ValidatorId = T::ValidatorIdOf::convert(reporter.clone())
|
||||
.ok_or_else(|| BenchmarkError::Stop("could not convert reporter to ValidatorId"))?;
|
||||
|
||||
// Build the full validator + queued-keys lists: the real offender first, then
|
||||
// `extra_validators` background validators. Each gets a unique GRANDPA key so the session
|
||||
// trie has `extra_validators + 1` distinct leaves. The resulting trie depth (and therefore
|
||||
// `key_owner_proof.validator_count()`) scales with `v`, giving the linear regression a
|
||||
// real signal to measure.
|
||||
let mut validators = vec![reporter_validator_id.clone()];
|
||||
let mut queued_keys = vec![(reporter_validator_id, keys)];
|
||||
|
||||
for i in 0..extra_validators {
|
||||
let validator_id: T::ValidatorId = account("validator", i, i);
|
||||
let validator_keys = T::benchmark_session_keys(grandpa_id_for_validator(i));
|
||||
pallet_session::NextKeys::<T>::insert(&validator_id, validator_keys.clone());
|
||||
validators.push(validator_id.clone());
|
||||
queued_keys.push((validator_id, validator_keys));
|
||||
}
|
||||
|
||||
// Overwrite session storage so the full set (offender + background validators) is active.
|
||||
pallet_session::Validators::<T>::put(validators);
|
||||
pallet_session::QueuedKeys::<T>::put(queued_keys);
|
||||
|
||||
// Initialize session again after overwriting validator state.
|
||||
<pallet_session::Pallet<T> as OnInitialize<BlockNumberFor<T>>>::on_initialize(1u32.into());
|
||||
|
||||
// Generate a fresh KeyOwnerProof via Historical for the GRANDPA key registered above.
|
||||
// The authority ID in this proof must match the one embedded in PREENCODED_EQUIVOCATION_PROOF,
|
||||
// which is guaranteed because both use the same seed. The proof's `validator_count` field
|
||||
// will now equal `extra_validators + 1`, matching what the weight function receives at
|
||||
// dispatch time in production.
|
||||
let key_owner_proof = pallet_session::historical::Pallet::<T>::prove((
|
||||
sp_consensus_grandpa::KEY_TYPE,
|
||||
grandpa_id.clone(),
|
||||
))
|
||||
.ok_or_else(|| BenchmarkError::Stop("Historical::prove returned None".into()))?;
|
||||
|
||||
// Decode the pre-encoded equivocation proof (set_id=0, round=1). The pallet will only
|
||||
// accept this if the on-chain current_set_id is also 0, which holds in a fresh benchmark
|
||||
// environment since no set rotation has occurred.
|
||||
let proof: GrandpaEquivocationProof<T> =
|
||||
Decode::decode(&mut &PREENCODED_EQUIVOCATION_PROOF[..])
|
||||
.map_err(|_| BenchmarkError::Stop("failed to decode pre-encoded equivocation proof"))?;
|
||||
|
||||
Ok((Box::new(proof), key_owner_proof, reporter))
|
||||
}
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
use pallet_grandpa::Call;
|
||||
|
||||
#[benchmark]
|
||||
fn note_stalled() -> Result<(), BenchmarkError> {
|
||||
let delay = 1000u32.into();
|
||||
let best_finalized_block_number = 1u32.into();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, delay, best_finalized_block_number);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Benchmarks the full `report_equivocation` dispatch cost.
|
||||
///
|
||||
/// The upstream `WeightInfo::report_equivocation` signature takes
|
||||
/// `(validator_count: u32, max_nominators_per_validator: u32)` as linear components.
|
||||
/// We expose the same parameters here so the generated weight function matches the
|
||||
/// trait exactly and the linear terms are populated from real measurements.
|
||||
/// `v` = validator_count, `n` = max_nominators_per_validator (matches upstream component names).
|
||||
///
|
||||
/// `v` background validators are registered in the session alongside the real offender, so
|
||||
/// the session trie has `v + 1` leaves and `key_owner_proof.validator_count()` equals `v + 1`.
|
||||
/// This means the trie verification path actually grows with `v`, giving the linear regression
|
||||
/// a real signal and producing a meaningful slope coefficient in the generated weight function.
|
||||
///
|
||||
/// # Disclaimer: setup over-counts `v` cost
|
||||
///
|
||||
/// The `v` slope in the generated weights is dominated by `Session::NextKeys` reads, because
|
||||
/// `Historical::prove` (called during setup) reads all `v` validators to build the full trie.
|
||||
/// In production, `Historical::check_proof` (called during dispatch) only traverses the O(log v)
|
||||
/// Merkle path nodes — it does not read all validators. The generated weights therefore
|
||||
/// over-count the per-validator cost and are conservative/safe (they overcharge rather than
|
||||
/// undercharge), but this is a known tension inherent to benchmarking session historical
|
||||
/// proofs: the setup must call `prove`, which is more expensive than `check_proof`.
|
||||
#[benchmark]
|
||||
fn report_equivocation(v: Linear<0, 1000>, n: Linear<0, 1>) -> Result<(), BenchmarkError> {
|
||||
let (proof, key_owner_proof, reporter) = setup_equivocation::<T>(v)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(reporter), proof, key_owner_proof);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
83
operator/pallets/grandpa-benchmarking/src/lib.rs
Normal file
83
operator/pallets/grandpa-benchmarking/src/lib.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub struct Pallet<T: Config>(pallet_grandpa::Pallet<T>);
|
||||
|
||||
/// Benchmarking configuration for `pallet-grandpa` in DataHaven.
|
||||
///
|
||||
/// This is a small wrapper crate (similar to `pallet-session-benchmarking`) that provides
|
||||
/// benchmarks for GRANDPA extrinsics that upstream `pallet-grandpa` does not expose in its
|
||||
/// own benchmarking suite. Run via the node's `benchmark pallet` subcommand, not
|
||||
/// `frame-omni-bencher`, as the latter lacks a real ed25519 verifier.
|
||||
pub trait Config:
|
||||
pallet_grandpa::Config<
|
||||
KeyOwnerProof = <pallet_session::historical::Pallet<Self> as frame_support::traits::KeyOwnerProofSystem<(
|
||||
sp_core::crypto::KeyTypeId,
|
||||
sp_consensus_grandpa::AuthorityId,
|
||||
)>>::Proof,
|
||||
> + pallet_session::Config
|
||||
+ pallet_session::historical::Config
|
||||
{
|
||||
/// Construct a full `Self::Keys` value for benchmarking, filling all slots except GRANDPA
|
||||
/// with dummy values and placing `grandpa` in the GRANDPA slot.
|
||||
fn benchmark_session_keys(grandpa: sp_consensus_grandpa::AuthorityId) -> Self::Keys;
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
#[cfg(all(test, feature = "std"))]
|
||||
mod tests {
|
||||
use codec::Encode;
|
||||
use sp_consensus_grandpa;
|
||||
use sp_core::{ed25519, Pair as PairTrait};
|
||||
use sp_runtime::traits::{BlakeTwo256, Hash as HashT};
|
||||
|
||||
/// Run with: cargo test -p pallet-grandpa-benchmarking --features std -- test_generate_equivocation_blob --nocapture
|
||||
/// Then paste the printed bytes into PREENCODED_EQUIVOCATION_PROOF in benchmarking.rs.
|
||||
#[test]
|
||||
fn test_generate_equivocation_blob() {
|
||||
let seed = "0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60";
|
||||
let pair = ed25519::Pair::from_string(seed, None).expect("valid seed");
|
||||
let set_id: u64 = 0;
|
||||
let round: u64 = 1;
|
||||
|
||||
let h1 = BlakeTwo256::hash_of(&1u32);
|
||||
let h2 = BlakeTwo256::hash_of(&2u32);
|
||||
let prevote1 = finality_grandpa::Prevote {
|
||||
target_hash: h1,
|
||||
target_number: 1u32,
|
||||
};
|
||||
let prevote2 = finality_grandpa::Prevote {
|
||||
target_hash: h2,
|
||||
target_number: 2u32,
|
||||
};
|
||||
|
||||
let msg1 = finality_grandpa::Message::Prevote(prevote1.clone());
|
||||
let msg2 = finality_grandpa::Message::Prevote(prevote2.clone());
|
||||
let payload1 = sp_consensus_grandpa::localized_payload(round, set_id, &msg1);
|
||||
let payload2 = sp_consensus_grandpa::localized_payload(round, set_id, &msg2);
|
||||
|
||||
let sig1 = pair.sign(&payload1);
|
||||
let sig2 = pair.sign(&payload2);
|
||||
|
||||
let equivocation = finality_grandpa::Equivocation {
|
||||
round_number: round,
|
||||
identity: sp_consensus_grandpa::AuthorityId::from(pair.public()),
|
||||
first: (prevote1, sig1.into()),
|
||||
second: (prevote2, sig2.into()),
|
||||
};
|
||||
|
||||
let proof = sp_consensus_grandpa::EquivocationProof::<sp_core::H256, u32>::new(
|
||||
set_id,
|
||||
equivocation.into(),
|
||||
);
|
||||
let encoded = proof.encode();
|
||||
println!(
|
||||
"PREENCODED_EQUIVOCATION_PROOF (len={}): {:?}",
|
||||
encoded.len(),
|
||||
encoded
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,7 @@ pallet-referenda = { workspace = true }
|
|||
pallet-safe-mode = { workspace = true }
|
||||
pallet-scheduler = { workspace = true }
|
||||
pallet-session = { workspace = true }
|
||||
pallet-grandpa-benchmarking = { workspace = true, optional = true }
|
||||
pallet-session-benchmarking = { workspace = true, optional = true }
|
||||
pallet-sudo = { workspace = true }
|
||||
pallet-timestamp = { workspace = true }
|
||||
|
|
@ -196,6 +197,7 @@ std = [
|
|||
"frame-metadata-hash-extension/std",
|
||||
"frame-support/std",
|
||||
"frame-system-benchmarking?/std",
|
||||
"pallet-grandpa-benchmarking?/std",
|
||||
"pallet-session-benchmarking?/std",
|
||||
"frame-system-rpc-runtime-api/std",
|
||||
"frame-system/std",
|
||||
|
|
@ -342,6 +344,7 @@ runtime-benchmarks = [
|
|||
"pallet-referenda/runtime-benchmarks",
|
||||
"pallet-proxy/runtime-benchmarks",
|
||||
"pallet-scheduler/runtime-benchmarks",
|
||||
"pallet-grandpa-benchmarking/runtime-benchmarks",
|
||||
"pallet-session-benchmarking/runtime-benchmarks",
|
||||
"pallet-sudo/runtime-benchmarks",
|
||||
"pallet-timestamp/runtime-benchmarks",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ frame_benchmarking::define_benchmarks!(
|
|||
[pallet_mmr, Mmr]
|
||||
[pallet_beefy_mmr, BeefyMmrLeaf]
|
||||
[pallet_babe, Babe]
|
||||
[pallet_grandpa, Grandpa]
|
||||
[pallet_grandpa, pallet_grandpa_benchmarking::Pallet::<Runtime>]
|
||||
[pallet_randomness, Randomness]
|
||||
|
||||
// Substrate pallets
|
||||
|
|
|
|||
|
|
@ -973,6 +973,18 @@ impl_runtime_apis! {
|
|||
|
||||
impl frame_system_benchmarking::Config for Runtime {}
|
||||
impl pallet_session_benchmarking::Config for Runtime {}
|
||||
impl pallet_grandpa_benchmarking::Config for Runtime {
|
||||
fn benchmark_session_keys(grandpa: GrandpaId) -> Self::Keys {
|
||||
use sp_core::crypto::UncheckedFrom;
|
||||
|
||||
SessionKeys {
|
||||
babe: sp_consensus_babe::AuthorityId::unchecked_from([1u8; 32]),
|
||||
grandpa,
|
||||
im_online: pallet_im_online::sr25519::AuthorityId::unchecked_from([1u8; 32]),
|
||||
beefy: sp_consensus_beefy::ecdsa_crypto::AuthorityId::unchecked_from([1u8; 33]),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl baseline::Config for Runtime {}
|
||||
|
||||
use frame_support::traits::WhitelistedStorageKeys;
|
||||
|
|
|
|||
|
|
@ -17,33 +17,34 @@
|
|||
|
||||
//! Autogenerated weights for `pallet_grandpa`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 51.0.0
|
||||
//! DATE: 2026-01-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 46.2.0
|
||||
//! DATE: 2026-03-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `ip-10-0-0-176`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz`
|
||||
//! WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
|
||||
|
||||
// Executed Command:
|
||||
// frame-omni-bencher
|
||||
// v1
|
||||
// ./target/production/datahaven-node
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/production/wbuild/datahaven-mainnet-runtime/datahaven_mainnet_runtime.compact.compressed.wasm
|
||||
// --genesis-builder
|
||||
// runtime
|
||||
// --pallet
|
||||
// pallet_grandpa
|
||||
// --extrinsic
|
||||
//
|
||||
// *
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --header
|
||||
// ../file_header.txt
|
||||
// --template
|
||||
// benchmarking/frame-weight-template.hbs
|
||||
// --output
|
||||
// runtime/mainnet/src/weights/pallet_grandpa.rs
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
|
|
@ -55,24 +56,52 @@ use sp_std::marker::PhantomData;
|
|||
/// Weights for `pallet_grandpa`.
|
||||
pub struct WeightInfo<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> pallet_grandpa::WeightInfo for WeightInfo<T> {
|
||||
/// The range of component `x` is `[0, 1]`.
|
||||
fn report_equivocation(prev: u32, _equivocations: u32) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 78_547_000 picoseconds.
|
||||
Weight::from_parts(78_982_114, 0)
|
||||
// Standard Error: 28_584
|
||||
.saturating_add(Weight::from_parts(129_485, 0).saturating_mul(prev.into()))
|
||||
}
|
||||
/// Storage: `Grandpa::Stalled` (r:0 w:1)
|
||||
/// Proof: `Grandpa::Stalled` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
|
||||
fn note_stalled() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 3_815_000 picoseconds.
|
||||
Weight::from_parts(3_947_000, 0)
|
||||
// Minimum execution time: 2_506_000 picoseconds.
|
||||
Weight::from_parts(2_591_000, 0)
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Session::CurrentIndex` (r:1 w:0)
|
||||
/// Proof: `Session::CurrentIndex` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Session::Validators` (r:1 w:0)
|
||||
/// Proof: `Session::Validators` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Session::NextKeys` (r:1001 w:0)
|
||||
/// Proof: `Session::NextKeys` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Grandpa::SetIdSession` (r:1 w:0)
|
||||
/// Proof: `Grandpa::SetIdSession` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Offences::ConcurrentReportsIndex` (r:1 w:1)
|
||||
/// Proof: `Offences::ConcurrentReportsIndex` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Offences::Reports` (r:1 w:1)
|
||||
/// Proof: `Offences::Reports` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `ExternalValidatorsSlashes::SlashingMode` (r:1 w:0)
|
||||
/// Proof: `ExternalValidatorsSlashes::SlashingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::ActiveEra` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::ErasStartSessionIndex` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::ErasStartSessionIndex` (`max_values`: None, `max_size`: Some(16), added: 2491, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(2002), added: 2497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidatorsSlashes::NextSlashId` (r:1 w:1)
|
||||
/// Proof: `ExternalValidatorsSlashes::NextSlashId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// The range of component `v` is `[0, 1000]`.
|
||||
/// The range of component `n` is `[0, 1]`.
|
||||
fn report_equivocation(v: u32, n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1202 + v * (184 ±0)`
|
||||
// Estimated: `4604 + n * (84 ±3) + v * (2660 ±0)`
|
||||
// Minimum execution time: 159_361_000 picoseconds.
|
||||
Weight::from_parts(161_997_000, 4604)
|
||||
// Standard Error: 6_387
|
||||
.saturating_add(Weight::from_parts(12_204_491, 0).saturating_mul(v.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(11_u64))
|
||||
.saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(v.into())))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
.saturating_add(Weight::from_parts(0, 84).saturating_mul(n.into()))
|
||||
.saturating_add(Weight::from_parts(0, 2660).saturating_mul(v.into()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ pallet-referenda = { workspace = true }
|
|||
pallet-safe-mode = { workspace = true }
|
||||
pallet-scheduler = { workspace = true }
|
||||
pallet-session = { workspace = true }
|
||||
pallet-grandpa-benchmarking = { workspace = true, optional = true }
|
||||
pallet-session-benchmarking = { workspace = true, optional = true }
|
||||
pallet-sudo = { workspace = true }
|
||||
pallet-timestamp = { workspace = true }
|
||||
|
|
@ -197,6 +198,7 @@ std = [
|
|||
"frame-metadata-hash-extension/std",
|
||||
"frame-support/std",
|
||||
"frame-system-benchmarking?/std",
|
||||
"pallet-grandpa-benchmarking?/std",
|
||||
"pallet-session-benchmarking?/std",
|
||||
"frame-system-rpc-runtime-api/std",
|
||||
"frame-system/std",
|
||||
|
|
@ -344,6 +346,7 @@ runtime-benchmarks = [
|
|||
"pallet-referenda/runtime-benchmarks",
|
||||
"pallet-proxy/runtime-benchmarks",
|
||||
"pallet-scheduler/runtime-benchmarks",
|
||||
"pallet-grandpa-benchmarking/runtime-benchmarks",
|
||||
"pallet-session-benchmarking/runtime-benchmarks",
|
||||
"pallet-sudo/runtime-benchmarks",
|
||||
"pallet-timestamp/runtime-benchmarks",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ frame_benchmarking::define_benchmarks!(
|
|||
[pallet_mmr, Mmr]
|
||||
[pallet_beefy_mmr, BeefyMmrLeaf]
|
||||
[pallet_babe, Babe]
|
||||
[pallet_grandpa, Grandpa]
|
||||
[pallet_grandpa, pallet_grandpa_benchmarking::Pallet::<Runtime>]
|
||||
[pallet_randomness, Randomness]
|
||||
|
||||
// Substrate pallets
|
||||
|
|
|
|||
|
|
@ -975,6 +975,18 @@ impl_runtime_apis! {
|
|||
|
||||
impl frame_system_benchmarking::Config for Runtime {}
|
||||
impl pallet_session_benchmarking::Config for Runtime {}
|
||||
impl pallet_grandpa_benchmarking::Config for Runtime {
|
||||
fn benchmark_session_keys(grandpa: GrandpaId) -> Self::Keys {
|
||||
use sp_core::crypto::UncheckedFrom;
|
||||
|
||||
SessionKeys {
|
||||
babe: sp_consensus_babe::AuthorityId::unchecked_from([1u8; 32]),
|
||||
grandpa,
|
||||
im_online: pallet_im_online::sr25519::AuthorityId::unchecked_from([1u8; 32]),
|
||||
beefy: sp_consensus_beefy::ecdsa_crypto::AuthorityId::unchecked_from([1u8; 33]),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl baseline::Config for Runtime {}
|
||||
|
||||
use frame_support::traits::WhitelistedStorageKeys;
|
||||
|
|
|
|||
|
|
@ -17,33 +17,34 @@
|
|||
|
||||
//! Autogenerated weights for `pallet_grandpa`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 51.0.0
|
||||
//! DATE: 2025-12-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 46.2.0
|
||||
//! DATE: 2026-03-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `ip-10-0-0-176`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz`
|
||||
//! WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
|
||||
|
||||
// Executed Command:
|
||||
// frame-omni-bencher
|
||||
// v1
|
||||
// ./target/production/datahaven-node
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/production/wbuild/datahaven-stagenet-runtime/datahaven_stagenet_runtime.compact.compressed.wasm
|
||||
// --genesis-builder
|
||||
// runtime
|
||||
// --pallet
|
||||
// pallet_grandpa
|
||||
// --extrinsic
|
||||
//
|
||||
// *
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --header
|
||||
// ../file_header.txt
|
||||
// --template
|
||||
// benchmarking/frame-weight-template.hbs
|
||||
// --output
|
||||
// runtime/stagenet/src/weights/pallet_grandpa.rs
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
|
|
@ -55,24 +56,52 @@ use sp_std::marker::PhantomData;
|
|||
/// Weights for `pallet_grandpa`.
|
||||
pub struct WeightInfo<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> pallet_grandpa::WeightInfo for WeightInfo<T> {
|
||||
/// The range of component `x` is `[0, 1]`.
|
||||
fn report_equivocation(prev: u32, _equivocations: u32) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 78_547_000 picoseconds.
|
||||
Weight::from_parts(78_982_114, 0)
|
||||
// Standard Error: 28_584
|
||||
.saturating_add(Weight::from_parts(129_485, 0).saturating_mul(prev.into()))
|
||||
}
|
||||
/// Storage: `Grandpa::Stalled` (r:0 w:1)
|
||||
/// Proof: `Grandpa::Stalled` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
|
||||
fn note_stalled() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 3_815_000 picoseconds.
|
||||
Weight::from_parts(3_947_000, 0)
|
||||
// Minimum execution time: 2_453_000 picoseconds.
|
||||
Weight::from_parts(2_554_000, 0)
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Session::CurrentIndex` (r:1 w:0)
|
||||
/// Proof: `Session::CurrentIndex` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Session::Validators` (r:1 w:0)
|
||||
/// Proof: `Session::Validators` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Session::NextKeys` (r:1001 w:0)
|
||||
/// Proof: `Session::NextKeys` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Grandpa::SetIdSession` (r:1 w:0)
|
||||
/// Proof: `Grandpa::SetIdSession` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Offences::ConcurrentReportsIndex` (r:1 w:1)
|
||||
/// Proof: `Offences::ConcurrentReportsIndex` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Offences::Reports` (r:1 w:1)
|
||||
/// Proof: `Offences::Reports` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `ExternalValidatorsSlashes::SlashingMode` (r:1 w:0)
|
||||
/// Proof: `ExternalValidatorsSlashes::SlashingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::ActiveEra` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::ErasStartSessionIndex` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::ErasStartSessionIndex` (`max_values`: None, `max_size`: Some(16), added: 2491, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(2002), added: 2497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidatorsSlashes::NextSlashId` (r:1 w:1)
|
||||
/// Proof: `ExternalValidatorsSlashes::NextSlashId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// The range of component `v` is `[0, 1000]`.
|
||||
/// The range of component `n` is `[0, 1]`.
|
||||
fn report_equivocation(v: u32, n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1202 + v * (184 ±0)`
|
||||
// Estimated: `4604 + n * (84 ±3) + v * (2660 ±0)`
|
||||
// Minimum execution time: 183_867_000 picoseconds.
|
||||
Weight::from_parts(186_282_000, 4604)
|
||||
// Standard Error: 5_669
|
||||
.saturating_add(Weight::from_parts(12_384_539, 0).saturating_mul(v.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(11_u64))
|
||||
.saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(v.into())))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
.saturating_add(Weight::from_parts(0, 84).saturating_mul(n.into()))
|
||||
.saturating_add(Weight::from_parts(0, 2660).saturating_mul(v.into()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ pallet-referenda = { workspace = true }
|
|||
pallet-safe-mode = { workspace = true }
|
||||
pallet-scheduler = { workspace = true }
|
||||
pallet-session = { workspace = true }
|
||||
pallet-grandpa-benchmarking = { workspace = true, optional = true }
|
||||
pallet-session-benchmarking = { workspace = true, optional = true }
|
||||
pallet-sudo = { workspace = true }
|
||||
pallet-timestamp = { workspace = true }
|
||||
|
|
@ -197,6 +198,7 @@ std = [
|
|||
"frame-metadata-hash-extension/std",
|
||||
"frame-support/std",
|
||||
"frame-system-benchmarking?/std",
|
||||
"pallet-grandpa-benchmarking?/std",
|
||||
"pallet-session-benchmarking?/std",
|
||||
"frame-system-rpc-runtime-api/std",
|
||||
"frame-system/std",
|
||||
|
|
@ -340,6 +342,7 @@ runtime-benchmarks = [
|
|||
"pallet-referenda/runtime-benchmarks",
|
||||
"pallet-proxy/runtime-benchmarks",
|
||||
"pallet-scheduler/runtime-benchmarks",
|
||||
"pallet-grandpa-benchmarking/runtime-benchmarks",
|
||||
"pallet-session-benchmarking/runtime-benchmarks",
|
||||
"pallet-sudo/runtime-benchmarks",
|
||||
"pallet-timestamp/runtime-benchmarks",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ frame_benchmarking::define_benchmarks!(
|
|||
[pallet_mmr, Mmr]
|
||||
[pallet_beefy_mmr, BeefyMmrLeaf]
|
||||
[pallet_babe, Babe]
|
||||
[pallet_grandpa, Grandpa]
|
||||
[pallet_grandpa, pallet_grandpa_benchmarking::Pallet::<Runtime>]
|
||||
[pallet_randomness, Randomness]
|
||||
|
||||
// Substrate pallets
|
||||
|
|
|
|||
|
|
@ -973,6 +973,18 @@ impl_runtime_apis! {
|
|||
|
||||
impl frame_system_benchmarking::Config for Runtime {}
|
||||
impl pallet_session_benchmarking::Config for Runtime {}
|
||||
impl pallet_grandpa_benchmarking::Config for Runtime {
|
||||
fn benchmark_session_keys(grandpa: GrandpaId) -> Self::Keys {
|
||||
use sp_core::crypto::UncheckedFrom;
|
||||
|
||||
SessionKeys {
|
||||
babe: sp_consensus_babe::AuthorityId::unchecked_from([1u8; 32]),
|
||||
grandpa,
|
||||
im_online: pallet_im_online::sr25519::AuthorityId::unchecked_from([1u8; 32]),
|
||||
beefy: sp_consensus_beefy::ecdsa_crypto::AuthorityId::unchecked_from([1u8; 33]),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl baseline::Config for Runtime {}
|
||||
|
||||
use frame_support::traits::WhitelistedStorageKeys;
|
||||
|
|
|
|||
|
|
@ -17,33 +17,34 @@
|
|||
|
||||
//! Autogenerated weights for `pallet_grandpa`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 51.0.0
|
||||
//! DATE: 2026-01-07, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 46.2.0
|
||||
//! DATE: 2026-03-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `ip-10-0-0-176`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz`
|
||||
//! WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
|
||||
|
||||
// Executed Command:
|
||||
// frame-omni-bencher
|
||||
// v1
|
||||
// ./target/production/datahaven-node
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/production/wbuild/datahaven-testnet-runtime/datahaven_testnet_runtime.compact.compressed.wasm
|
||||
// --genesis-builder
|
||||
// runtime
|
||||
// --pallet
|
||||
// pallet_grandpa
|
||||
// --extrinsic
|
||||
//
|
||||
// *
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --header
|
||||
// ../file_header.txt
|
||||
// --template
|
||||
// benchmarking/frame-weight-template.hbs
|
||||
// --output
|
||||
// runtime/testnet/src/weights/pallet_grandpa.rs
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
|
|
@ -55,24 +56,52 @@ use sp_std::marker::PhantomData;
|
|||
/// Weights for `pallet_grandpa`.
|
||||
pub struct WeightInfo<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> pallet_grandpa::WeightInfo for WeightInfo<T> {
|
||||
/// The range of component `x` is `[0, 1]`.
|
||||
fn report_equivocation(prev: u32, _equivocations: u32) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 78_547_000 picoseconds.
|
||||
Weight::from_parts(78_982_114, 0)
|
||||
// Standard Error: 28_584
|
||||
.saturating_add(Weight::from_parts(129_485, 0).saturating_mul(prev.into()))
|
||||
}
|
||||
/// Storage: `Grandpa::Stalled` (r:0 w:1)
|
||||
/// Proof: `Grandpa::Stalled` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
|
||||
fn note_stalled() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 3_815_000 picoseconds.
|
||||
Weight::from_parts(3_947_000, 0)
|
||||
// Minimum execution time: 2_418_000 picoseconds.
|
||||
Weight::from_parts(2_662_000, 0)
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Session::CurrentIndex` (r:1 w:0)
|
||||
/// Proof: `Session::CurrentIndex` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Session::Validators` (r:1 w:0)
|
||||
/// Proof: `Session::Validators` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Session::NextKeys` (r:1001 w:0)
|
||||
/// Proof: `Session::NextKeys` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Grandpa::SetIdSession` (r:1 w:0)
|
||||
/// Proof: `Grandpa::SetIdSession` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Offences::ConcurrentReportsIndex` (r:1 w:1)
|
||||
/// Proof: `Offences::ConcurrentReportsIndex` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `Offences::Reports` (r:1 w:1)
|
||||
/// Proof: `Offences::Reports` (`max_values`: None, `max_size`: None, mode: `Measured`)
|
||||
/// Storage: `ExternalValidatorsSlashes::SlashingMode` (r:1 w:0)
|
||||
/// Proof: `ExternalValidatorsSlashes::SlashingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::ActiveEra` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::ErasStartSessionIndex` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::ErasStartSessionIndex` (`max_values`: None, `max_size`: Some(16), added: 2491, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:0)
|
||||
/// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(2002), added: 2497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `ExternalValidatorsSlashes::NextSlashId` (r:1 w:1)
|
||||
/// Proof: `ExternalValidatorsSlashes::NextSlashId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// The range of component `v` is `[0, 1000]`.
|
||||
/// The range of component `n` is `[0, 1]`.
|
||||
fn report_equivocation(v: u32, n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1202 + v * (184 ±0)`
|
||||
// Estimated: `4604 + n * (84 ±3) + v * (2660 ±0)`
|
||||
// Minimum execution time: 186_789_000 picoseconds.
|
||||
Weight::from_parts(190_096_000, 4604)
|
||||
// Standard Error: 5_639
|
||||
.saturating_add(Weight::from_parts(12_403_947, 0).saturating_mul(v.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(11_u64))
|
||||
.saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(v.into())))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
.saturating_add(Weight::from_parts(0, 84).saturating_mul(n.into()))
|
||||
.saturating_add(Weight::from_parts(0, 2660).saturating_mul(v.into()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#!/bin/bash
|
||||
# DataHaven Benchmarking Script using frame-omni-bencher
|
||||
# Automatically discovers and benchmarks all pallets in the runtime
|
||||
# DataHaven Benchmarking Script
|
||||
# Uses frame-omni-bencher for most pallets.
|
||||
# Pallets listed in NODE_PALLETS are benchmarked via the native node binary instead,
|
||||
# because frame-omni-bencher's WASM host lacks the crypto primitives they require
|
||||
# (e.g. pallet_grandpa needs a real ed25519 verifier for report_equivocation).
|
||||
|
||||
set -e
|
||||
|
||||
|
|
@ -10,6 +13,11 @@ STEPS=${2:-50}
|
|||
REPEAT=${3:-20}
|
||||
FEATURES="runtime-benchmarks"
|
||||
|
||||
# Pallets that must be benchmarked via the native node binary instead of frame-omni-bencher.
|
||||
# Add pallet names here (space-separated) when their benchmarks require crypto or host
|
||||
# functions that the WASM execution environment cannot provide.
|
||||
NODE_PALLETS=("pallet_grandpa")
|
||||
|
||||
# Color codes for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
|
|
@ -52,9 +60,17 @@ if [ ! -f "$TEMPLATE_PATH" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Build the runtime WASM
|
||||
echo -e "${YELLOW}Building runtime $RUNTIME (production profile) with features: $FEATURES${NC}"
|
||||
cargo build --profile production --features "$FEATURES" -p datahaven-$RUNTIME-runtime
|
||||
# Build the runtime WASM and the node binary
|
||||
echo -e "${YELLOW}Building runtime $RUNTIME and node (production profile) with features: $FEATURES${NC}"
|
||||
cargo build --profile production --features "$FEATURES" \
|
||||
-p datahaven-$RUNTIME-runtime \
|
||||
-p datahaven-node
|
||||
|
||||
NODE_BIN="target/production/datahaven-node"
|
||||
if [ ! -f "$NODE_BIN" ]; then
|
||||
echo -e "${RED}Error: Node binary not found at $NODE_BIN${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the WASM path
|
||||
WASM_PATH="target/production/wbuild/datahaven-$RUNTIME-runtime/datahaven_${RUNTIME}_runtime.compact.compressed.wasm"
|
||||
|
|
@ -97,6 +113,48 @@ mkdir -p "$WEIGHTS_DIR"
|
|||
# Run benchmarks for each pallet using frame-omni-bencher
|
||||
echo -e "${GREEN}Starting benchmarks...${NC}\n"
|
||||
|
||||
# Returns 0 if the given pallet should be benchmarked via the node binary, 1 otherwise.
|
||||
requires_node_benchmark() {
|
||||
local PALLET=$1
|
||||
for node_pallet in "${NODE_PALLETS[@]}"; do
|
||||
if [ "$node_pallet" == "$PALLET" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Benchmark a pallet via the native node binary.
|
||||
# Used for pallets whose benchmarks require host functions unavailable in WASM
|
||||
# (e.g. real ed25519 verification for pallet_grandpa::report_equivocation).
|
||||
benchmark_pallet_via_node() {
|
||||
local PALLET=$1
|
||||
local OUTPUT_FILE=$2
|
||||
|
||||
echo -e "${YELLOW}Benchmarking $PALLET (via node binary)...${NC}"
|
||||
|
||||
"$NODE_BIN" benchmark pallet \
|
||||
--runtime "$WASM_PATH" \
|
||||
--genesis-builder runtime \
|
||||
--pallet "$PALLET" \
|
||||
--extrinsic "*" \
|
||||
--header ../file_header.txt \
|
||||
--template "$TEMPLATE_PATH" \
|
||||
--output "$WEIGHTS_DIR/$OUTPUT_FILE.rs" \
|
||||
--steps "$STEPS" \
|
||||
--repeat "$REPEAT" 2>&1 | tee "benchmark_${PALLET}.log"
|
||||
|
||||
local exit_code=${PIPESTATUS[0]}
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ $PALLET benchmarked successfully (node)${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ Error benchmarking $PALLET (node)${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to run benchmark for a pallet
|
||||
benchmark_pallet() {
|
||||
local PALLET=$1
|
||||
|
|
@ -131,10 +189,18 @@ benchmark_pallet() {
|
|||
for PALLET in "${PALLETS[@]}"; do
|
||||
# Use the pallet name directly as the output file name
|
||||
OUTPUT_FILE="$PALLET"
|
||||
if benchmark_pallet "$PALLET" "$OUTPUT_FILE"; then
|
||||
RESULTS[$PALLET]="SUCCESS"
|
||||
if requires_node_benchmark "$PALLET"; then
|
||||
if benchmark_pallet_via_node "$PALLET" "$OUTPUT_FILE"; then
|
||||
RESULTS[$PALLET]="SUCCESS"
|
||||
else
|
||||
RESULTS[$PALLET]="FAILED"
|
||||
fi
|
||||
else
|
||||
RESULTS[$PALLET]="FAILED"
|
||||
if benchmark_pallet "$PALLET" "$OUTPUT_FILE"; then
|
||||
RESULTS[$PALLET]="SUCCESS"
|
||||
else
|
||||
RESULTS[$PALLET]="FAILED"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
|
|
|||
22295
test/bun.lock
22295
test/bun.lock
File diff suppressed because it is too large
Load diff
64
test/cli/handlers/contracts/checks.ts
Normal file
64
test/cli/handlers/contracts/checks.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { logger, printDivider, printHeader } from "utils";
|
||||
import { checkContractVersions, validateVersionFormats } from "utils/contracts/versioning";
|
||||
|
||||
type ContractsFlightOptions = {
|
||||
chain: string;
|
||||
rpcUrl?: string;
|
||||
};
|
||||
|
||||
export const versioningPreChecks = async ({ chain, rpcUrl }: ContractsFlightOptions) => {
|
||||
logger.info("🧪 Running contracts version checks...");
|
||||
|
||||
const contractResult = await checkContractVersions(chain, rpcUrl);
|
||||
if (contractResult.skipped) {
|
||||
logger.warn("⚠️ On-chain contract version checks were skipped due to unavailable infra.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contractResult.ok) {
|
||||
throw new Error(`Contract version check failed for chain '${chain}'`);
|
||||
}
|
||||
};
|
||||
|
||||
export const versioningPostChecks = async ({ chain, rpcUrl }: ContractsFlightOptions) => {
|
||||
logger.info("🧪 Running contracts version checks after changes...");
|
||||
|
||||
const contractResult = await checkContractVersions(chain, rpcUrl);
|
||||
if (contractResult.skipped) {
|
||||
logger.warn("⚠️ On-chain contract version checks were skipped due to unavailable infra.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contractResult.ok) {
|
||||
throw new Error(`Contract version check failed for chain '${chain}'`);
|
||||
}
|
||||
};
|
||||
|
||||
export const contractsChecks = async (options: any, command: any) => {
|
||||
let chain = options.chain;
|
||||
if (!chain && command.parent) {
|
||||
chain = command.parent.getOptionValue("chain");
|
||||
}
|
||||
if (!chain) {
|
||||
chain = command.getOptionValue("chain");
|
||||
}
|
||||
|
||||
printHeader(`Contracts Checks on ${chain}`);
|
||||
|
||||
try {
|
||||
// Validate version formats
|
||||
logger.info("🔍 Validating version formats...");
|
||||
const formatOk = await validateVersionFormats();
|
||||
if (!formatOk) {
|
||||
throw new Error("Version format validation failed");
|
||||
}
|
||||
|
||||
// Run existing version checks
|
||||
await versioningPreChecks({ chain, rpcUrl: options.rpcUrl });
|
||||
printDivider();
|
||||
logger.success("Contract checks passed");
|
||||
} catch (error) {
|
||||
logger.error(`❌ Contract checks failed: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { logger, printDivider, printHeader } from "utils";
|
||||
import { deployContracts } from "../../../scripts/deploy-contracts";
|
||||
import { versioningPostChecks, versioningPreChecks } from "./checks";
|
||||
import { showDeploymentPlanAndStatus } from "./status";
|
||||
import { verifyContracts } from "./verify";
|
||||
|
||||
|
|
@ -44,21 +45,36 @@ export const contractsDeploy = async (options: any, command: any) => {
|
|||
if (environment) {
|
||||
logger.info(`📡 Using environment: ${environment}`);
|
||||
}
|
||||
if (options.rpcUrl) {
|
||||
logger.info(`📡 Using RPC URL: ${options.rpcUrl}`);
|
||||
|
||||
// For anvil, auto-detect RPC URL from Kurtosis if not provided
|
||||
let rpcUrl = options.rpcUrl;
|
||||
if (chain === "anvil" && !rpcUrl) {
|
||||
const { getAnvilRpcUrl } = await import("../../../utils/anvil");
|
||||
rpcUrl = await getAnvilRpcUrl();
|
||||
logger.info(`📡 Auto-detected Anvil RPC URL: ${rpcUrl}`);
|
||||
} else if (rpcUrl) {
|
||||
logger.info(`📡 Using RPC URL: ${rpcUrl}`);
|
||||
}
|
||||
|
||||
// Override options with detected RPC URL
|
||||
const deployOptions = { ...options, rpcUrl };
|
||||
|
||||
// Chain is guaranteed to be defined by preAction hook validation
|
||||
await versioningPreChecks({ chain: chain!, rpcUrl: deployOptions.rpcUrl });
|
||||
|
||||
// Chain is guaranteed to be defined by preAction hook validation
|
||||
await deployContracts({
|
||||
chain: chain!,
|
||||
environment: environment,
|
||||
rpcUrl: options.rpcUrl,
|
||||
privateKey: options.privateKey,
|
||||
avsOwnerKey: options.avsOwnerKey,
|
||||
avsOwnerAddress: options.avsOwnerAddress,
|
||||
rpcUrl: deployOptions.rpcUrl,
|
||||
privateKey: deployOptions.privateKey,
|
||||
avsOwnerKey: deployOptions.avsOwnerKey,
|
||||
avsOwnerAddress: deployOptions.avsOwnerAddress,
|
||||
txExecution: txExecutionOverride
|
||||
});
|
||||
|
||||
await versioningPostChecks({ chain: chain!, rpcUrl: deployOptions.rpcUrl });
|
||||
|
||||
printDivider();
|
||||
} catch (error) {
|
||||
logger.error(`❌ Deployment failed: ${error}`);
|
||||
|
|
@ -120,6 +136,8 @@ export const SUPPORTED_NETWORKS = [
|
|||
] as const;
|
||||
|
||||
export const contractsPreActionHook = async (thisCommand: any) => {
|
||||
const args = thisCommand.args || [];
|
||||
const subcommand = args[0];
|
||||
let chain = thisCommand.getOptionValue("chain");
|
||||
let environment = thisCommand.getOptionValue("environment");
|
||||
|
||||
|
|
@ -163,9 +181,25 @@ export const contractsPreActionHook = async (thisCommand: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (!privateKey && !process.env.DEPLOYER_PRIVATE_KEY) {
|
||||
logger.warn(
|
||||
"⚠️ Private key not provided. Will use DEPLOYER_PRIVATE_KEY environment variable if set, or default Anvil key."
|
||||
);
|
||||
// Context-aware private key warnings
|
||||
if (!privateKey) {
|
||||
if (subcommand === "upgrade") {
|
||||
// Upgrades require DEPLOYER_PRIVATE_KEY (ProxyAdmin owner + versionUpdater)
|
||||
if (!process.env.DEPLOYER_PRIVATE_KEY) {
|
||||
logger.warn(
|
||||
"⚠️ DEPLOYER_PRIVATE_KEY not set. Upgrades require the deployer's private key (ProxyAdmin owner)."
|
||||
);
|
||||
}
|
||||
} else if (subcommand === "deploy") {
|
||||
// Deployments use DEPLOYER_PRIVATE_KEY for deployment, AVS_OWNER_PRIVATE_KEY for AVS ownership
|
||||
if (!process.env.DEPLOYER_PRIVATE_KEY) {
|
||||
logger.warn("⚠️ DEPLOYER_PRIVATE_KEY not set. Will use default Anvil key for deployment.");
|
||||
}
|
||||
if (!process.env.AVS_OWNER_PRIVATE_KEY) {
|
||||
logger.warn(
|
||||
"⚠️ AVS_OWNER_PRIVATE_KEY not set. ServiceManager will be owned by deployer account."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
export * from "./beefy-checkpoint";
|
||||
export * from "./checks";
|
||||
export * from "./deploy";
|
||||
export * from "./rewards-origin";
|
||||
export * from "./status";
|
||||
export * from "./update-metadata";
|
||||
export * from "./upgrade";
|
||||
export * from "./verify";
|
||||
|
|
|
|||
504
test/cli/handlers/contracts/upgrade.ts
Normal file
504
test/cli/handlers/contracts/upgrade.ts
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { logger, printDivider } from "utils";
|
||||
import { type Deployments, parseDeploymentsFile } from "utils/contracts";
|
||||
import { encodeFunctionData } from "viem";
|
||||
import { CHAIN_CONFIGS } from "../../../configs/contracts/config";
|
||||
import { buildContracts } from "../../../scripts/deploy-contracts";
|
||||
import { verifyContracts } from "./verify";
|
||||
|
||||
interface ContractsUpgradeOptions {
|
||||
chain: string;
|
||||
rpcUrl?: string;
|
||||
privateKeyFile?: string;
|
||||
verify?: boolean;
|
||||
version?: string; // Explicit version to upgrade to (writes to VERSION file); omit to read from VERSION file
|
||||
execute?: boolean; // When false (default), output calldata for multisig; when true, broadcast on-chain
|
||||
}
|
||||
|
||||
const resolveUpgradeContext = (options: ContractsUpgradeOptions) => {
|
||||
const chainConfig = CHAIN_CONFIGS[options.chain as keyof typeof CHAIN_CONFIGS];
|
||||
if (!chainConfig) {
|
||||
throw new Error(`Unsupported chain: ${options.chain}`);
|
||||
}
|
||||
|
||||
const rpcUrl = options.rpcUrl || chainConfig.RPC_URL;
|
||||
|
||||
// Key used to deploy new implementation contracts (any funded account works)
|
||||
let deployerKey: string;
|
||||
if (options.privateKeyFile) {
|
||||
deployerKey = readPrivateKeyFromFile(options.privateKeyFile);
|
||||
} else if (process.env.DEPLOYER_PRIVATE_KEY) {
|
||||
deployerKey = process.env.DEPLOYER_PRIVATE_KEY;
|
||||
} else if (process.env.PRIVATE_KEY) {
|
||||
deployerKey = process.env.PRIVATE_KEY;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Deployer key is required. Provide either --private-key-file or set DEPLOYER_PRIVATE_KEY/PRIVATE_KEY environment variable"
|
||||
);
|
||||
}
|
||||
|
||||
// AVS owner key — owns the ProxyAdmin and the ServiceManager contract.
|
||||
// Required only when --execute is set; in dry-run mode the calldata is printed for manual multisig execution.
|
||||
const avsOwnerKey = process.env.AVS_OWNER_PRIVATE_KEY;
|
||||
if (options.execute && !avsOwnerKey) {
|
||||
throw new Error(
|
||||
"AVS_OWNER_PRIVATE_KEY environment variable is required when using --execute to perform upgrades"
|
||||
);
|
||||
}
|
||||
|
||||
return { chainConfig, rpcUrl, deployerKey, avsOwnerKey };
|
||||
};
|
||||
|
||||
const readPrivateKeyFromFile = (filePath: string): string => {
|
||||
const privateKey = readFileSync(filePath, "utf8").trim();
|
||||
if (!privateKey) {
|
||||
throw new Error("Private key file is empty");
|
||||
}
|
||||
return privateKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a command safely using spawn to prevent command injection
|
||||
*/
|
||||
const executeCommand = async (
|
||||
command: string,
|
||||
args: string[],
|
||||
env: Record<string, string>,
|
||||
cwd: string
|
||||
): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
env,
|
||||
cwd,
|
||||
stdio: "pipe"
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout?.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
reject(new Error(`Command failed with code ${code}: ${stderr}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
reject(new Error(`Command execution failed: ${error.message}`));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles contract upgrade by deploying only implementation contracts
|
||||
* and updating proxy contracts to point to new implementations.
|
||||
*
|
||||
* Dry-run mode (default, --execute not set):
|
||||
* Deploys the new implementation, then prints the ProxyAdmin.upgradeAndCall
|
||||
* calldata so the multisig team can execute it manually. No AVS owner key needed.
|
||||
*
|
||||
* Execute mode (--execute):
|
||||
* Full on-chain upgrade — deploys the implementation and broadcasts the proxy
|
||||
* upgrade + version update transaction signed by the AVS owner.
|
||||
*/
|
||||
export const contractsUpgrade = async (options: ContractsUpgradeOptions) => {
|
||||
const isDryRun = !options.execute;
|
||||
|
||||
try {
|
||||
logger.info("🔄 Starting contract upgrade...");
|
||||
logger.info(`📡 Using chain: ${options.chain}`);
|
||||
if (isDryRun) {
|
||||
logger.info(
|
||||
"ℹ️ Dry-run mode: the proxy upgrade transaction will NOT be broadcast. Calldata will be printed for manual multisig execution."
|
||||
);
|
||||
logger.info(" Pass --execute to broadcast the upgrade on-chain.");
|
||||
}
|
||||
|
||||
// For anvil, auto-detect RPC URL from Kurtosis if not provided
|
||||
let resolvedRpcUrl = options.rpcUrl;
|
||||
if (options.chain === "anvil" && !resolvedRpcUrl) {
|
||||
const { getAnvilRpcUrl } = await import("../../../utils/anvil");
|
||||
resolvedRpcUrl = await getAnvilRpcUrl();
|
||||
logger.info(`📡 Auto-detected Anvil RPC URL: ${resolvedRpcUrl}`);
|
||||
} else if (resolvedRpcUrl) {
|
||||
logger.info(`📡 Using RPC URL: ${resolvedRpcUrl}`);
|
||||
}
|
||||
|
||||
const upgradeOptions = { ...options, rpcUrl: resolvedRpcUrl };
|
||||
const { rpcUrl, deployerKey, avsOwnerKey } = resolveUpgradeContext(upgradeOptions);
|
||||
|
||||
// Resolve target version:
|
||||
// - If an explicit version is provided, write it to contracts/VERSION and use it.
|
||||
// - Otherwise read the current version from contracts/VERSION.
|
||||
const targetVersion = await resolveTargetVersion(options.version);
|
||||
logger.info(`🎯 Target version: ${targetVersion}`);
|
||||
|
||||
// Build contracts first
|
||||
await buildContracts();
|
||||
|
||||
// Deploy new implementation contracts (signed by deployer — any funded account)
|
||||
const serviceManagerImplAddress = await deployImplementationContracts(
|
||||
options.chain,
|
||||
rpcUrl,
|
||||
deployerKey
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
// Print the calldata for the proxy upgrade so the multisig team can execute it
|
||||
await printProxyUpgradeCalldata(options.chain, serviceManagerImplAddress, targetVersion);
|
||||
} else {
|
||||
// Update proxy contracts to point to new implementations AND update version in one transaction.
|
||||
// Must be signed by the AVS owner, who owns both the ProxyAdmin and the ServiceManager.
|
||||
await updateProxyContracts(
|
||||
options.chain,
|
||||
rpcUrl,
|
||||
avsOwnerKey as string,
|
||||
serviceManagerImplAddress,
|
||||
targetVersion
|
||||
);
|
||||
|
||||
// Verify contracts if requested
|
||||
if (options.verify) {
|
||||
logger.info("🔍 Verifying upgraded contracts...");
|
||||
await verifyContracts({
|
||||
chain: options.chain,
|
||||
rpcUrl,
|
||||
skipVerification: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
printDivider();
|
||||
logger.success(
|
||||
isDryRun
|
||||
? "Dry-run complete. Submit the transaction above via your multisig to finalize the upgrade."
|
||||
: "Contract upgrade completed successfully"
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`❌ Contract upgrade failed: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deploys only the implementation contracts
|
||||
*/
|
||||
const deployImplementationContracts = async (
|
||||
chain: string,
|
||||
rpcUrl: string,
|
||||
privateKey: string
|
||||
): Promise<string> => {
|
||||
logger.info("🚀 Deploying new implementation contracts...");
|
||||
|
||||
// Deploy new ServiceManager implementation
|
||||
const serviceManagerImplAddress = await deployServiceManagerImplementation(
|
||||
chain,
|
||||
rpcUrl,
|
||||
privateKey
|
||||
);
|
||||
logger.success(`ServiceManager Implementation deployed: ${serviceManagerImplAddress}`);
|
||||
|
||||
// Persist the new implementation address so it becomes the source-of-truth for subsequent steps.
|
||||
const deploymentPath = `../contracts/deployments/${chain}.json`;
|
||||
const currentDeployments = await parseDeploymentsFile(chain);
|
||||
const updatedDeployments = {
|
||||
...currentDeployments,
|
||||
ServiceManagerImplementation: serviceManagerImplAddress as `0x${string}`
|
||||
};
|
||||
writeFileSync(deploymentPath, JSON.stringify(updatedDeployments, null, 2));
|
||||
logger.info(`📝 Updated ${deploymentPath} with new ServiceManagerImplementation`);
|
||||
|
||||
return serviceManagerImplAddress;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deploys new ServiceManager implementation contract
|
||||
*/
|
||||
const deployServiceManagerImplementation = async (
|
||||
chain: string,
|
||||
rpcUrl: string,
|
||||
privateKey: string
|
||||
): Promise<string> => {
|
||||
logger.info("📦 Deploying ServiceManager implementation...");
|
||||
|
||||
const actualDeployments = await parseDeploymentsFile(chain);
|
||||
|
||||
// Note: Private key is passed via PRIVATE_KEY environment variable (not command-line)
|
||||
// to prevent it from appearing in system process lists (security best practice)
|
||||
const env = {
|
||||
...process.env,
|
||||
PRIVATE_KEY: privateKey,
|
||||
RPC_URL: rpcUrl,
|
||||
REWARDS_COORDINATOR: actualDeployments.RewardsCoordinator,
|
||||
PERMISSION_CONTROLLER: actualDeployments.PermissionController,
|
||||
ALLOCATION_MANAGER: actualDeployments.AllocationManager,
|
||||
ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY
|
||||
};
|
||||
|
||||
const { privateKeyToAccount } = await import("viem/accounts");
|
||||
const normalizedKey = (
|
||||
privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`
|
||||
) as `0x${string}`;
|
||||
const deployerAddress = privateKeyToAccount(normalizedKey).address;
|
||||
|
||||
const deployArgs = [
|
||||
"script",
|
||||
"script/deploy/DeployImplementation.s.sol:DeployImplementation",
|
||||
"--sig",
|
||||
"deployServiceManagerImpl()",
|
||||
"--rpc-url",
|
||||
rpcUrl,
|
||||
"--sender",
|
||||
deployerAddress,
|
||||
"--broadcast",
|
||||
"--non-interactive"
|
||||
];
|
||||
|
||||
try {
|
||||
const result = await executeCommand(
|
||||
"forge",
|
||||
deployArgs,
|
||||
env as Record<string, string>,
|
||||
"../contracts"
|
||||
);
|
||||
|
||||
// Extract the deployed address from the output
|
||||
const addressMatch = result.match(
|
||||
/ServiceManager Implementation deployed at: (0x[a-fA-F0-9]{40})/
|
||||
);
|
||||
if (addressMatch) {
|
||||
return addressMatch[1];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Failed to extract ServiceManager implementation address from deployment output"
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to deploy ServiceManager implementation: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Minimal ABI for ProxyAdmin.upgradeAndCall — the only function needed to upgrade a
|
||||
* TransparentUpgradeableProxy and call an initializer in a single transaction.
|
||||
*/
|
||||
const PROXY_ADMIN_ABI = [
|
||||
{
|
||||
name: "upgradeAndCall",
|
||||
type: "function",
|
||||
stateMutability: "payable",
|
||||
inputs: [
|
||||
{ name: "proxy", type: "address" },
|
||||
{ name: "implementation", type: "address" },
|
||||
{ name: "data", type: "bytes" }
|
||||
],
|
||||
outputs: []
|
||||
}
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Prints the ProxyAdmin.upgradeAndCall calldata for manual multisig execution (dry-run).
|
||||
*
|
||||
* The upgrader team should submit this transaction from the multisig that owns the ProxyAdmin.
|
||||
* The call combines the proxy upgrade and the version update in one atomic transaction.
|
||||
*/
|
||||
const printProxyUpgradeCalldata = async (
|
||||
chain: string,
|
||||
serviceManagerImplAddress: string,
|
||||
version: string
|
||||
) => {
|
||||
const deployments = await parseDeploymentsFile(chain);
|
||||
|
||||
const proxyAdmin = deployments.ProxyAdmin ?? process.env.PROXY_ADMIN;
|
||||
if (!proxyAdmin) {
|
||||
throw new Error(
|
||||
"ProxyAdmin address is required to generate upgrade calldata. Add `ProxyAdmin` to the deployments file or set the PROXY_ADMIN environment variable."
|
||||
);
|
||||
}
|
||||
|
||||
const serviceManager = deployments.ServiceManager;
|
||||
if (!serviceManager) {
|
||||
throw new Error("ServiceManager address not found in deployments file");
|
||||
}
|
||||
|
||||
// Encode the updateVersion(string) call that will be passed as the `data` argument
|
||||
// to upgradeAndCall, so the version is set atomically with the proxy upgrade.
|
||||
const updateVersionData = encodeFunctionData({
|
||||
abi: [
|
||||
{
|
||||
name: "updateVersion",
|
||||
type: "function",
|
||||
stateMutability: "nonpayable",
|
||||
inputs: [{ name: "newVersion", type: "string" }],
|
||||
outputs: []
|
||||
}
|
||||
] as const,
|
||||
functionName: "updateVersion",
|
||||
args: [version]
|
||||
});
|
||||
|
||||
const calldata = encodeFunctionData({
|
||||
abi: PROXY_ADMIN_ABI,
|
||||
functionName: "upgradeAndCall",
|
||||
args: [
|
||||
serviceManager as `0x${string}`,
|
||||
serviceManagerImplAddress as `0x${string}`,
|
||||
updateVersionData
|
||||
]
|
||||
});
|
||||
|
||||
logger.info("");
|
||||
logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
logger.info("🔐 PROXY UPGRADE TRANSACTION (submit via multisig)");
|
||||
logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
const payload = {
|
||||
to: proxyAdmin,
|
||||
value: "0",
|
||||
data: calldata,
|
||||
description: `Upgrade ServiceManager proxy to ${serviceManagerImplAddress} and set version to ${version}`
|
||||
};
|
||||
logger.info(JSON.stringify(payload, null, 2));
|
||||
logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
logger.info("");
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates proxy contracts to point to new implementations and sets version
|
||||
*/
|
||||
const updateProxyContracts = async (
|
||||
chain: string,
|
||||
rpcUrl: string,
|
||||
avsOwnerKey: string,
|
||||
serviceManagerImplAddress: string,
|
||||
version: string
|
||||
) => {
|
||||
logger.info("🔄 Updating proxy contracts and version...");
|
||||
|
||||
const deployments = await parseDeploymentsFile(chain);
|
||||
|
||||
// Update ServiceManager proxy to point to new implementation and update version in one transaction
|
||||
await updateServiceManagerProxyWithVersion(
|
||||
deployments,
|
||||
rpcUrl,
|
||||
avsOwnerKey,
|
||||
serviceManagerImplAddress,
|
||||
version
|
||||
);
|
||||
|
||||
logger.success("Proxy contracts updated and version set successfully");
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates ServiceManager proxy to point to new implementation and updates version in one transaction.
|
||||
* Signed by the AVS owner, who owns both the ProxyAdmin and the ServiceManager.
|
||||
*/
|
||||
const updateServiceManagerProxyWithVersion = async (
|
||||
deployments: Deployments,
|
||||
rpcUrl: string,
|
||||
avsOwnerKey: string,
|
||||
serviceManagerImplAddress: string,
|
||||
version: string
|
||||
) => {
|
||||
logger.info(`🔄 Updating ServiceManager proxy and setting version to ${version}...`);
|
||||
|
||||
const proxyAdmin = deployments.ProxyAdmin ?? process.env.PROXY_ADMIN;
|
||||
if (!proxyAdmin) {
|
||||
throw new Error(
|
||||
"ProxyAdmin address is required for proxy updates. Add `ProxyAdmin` to the deployments file or set the PROXY_ADMIN environment variable."
|
||||
);
|
||||
}
|
||||
|
||||
// AVS_OWNER_PRIVATE_KEY is passed via environment variable (not command-line)
|
||||
// to prevent it from appearing in system process lists (security best practice)
|
||||
const env = {
|
||||
...process.env,
|
||||
AVS_OWNER_PRIVATE_KEY: avsOwnerKey,
|
||||
RPC_URL: rpcUrl,
|
||||
SERVICE_MANAGER: deployments.ServiceManager,
|
||||
SERVICE_MANAGER_IMPL: serviceManagerImplAddress,
|
||||
PROXY_ADMIN: proxyAdmin,
|
||||
NEW_VERSION: version
|
||||
};
|
||||
|
||||
// Derive the sender address from the AVS owner key so forge doesn't complain
|
||||
// about using the default sender when vm.broadcast is called with a key loaded
|
||||
// from an environment variable rather than --private-key.
|
||||
const { privateKeyToAccount } = await import("viem/accounts");
|
||||
const avsOwnerAddress = privateKeyToAccount(avsOwnerKey as `0x${string}`).address;
|
||||
|
||||
const updateArgs = [
|
||||
"script",
|
||||
"script/deploy/DeployImplementation.s.sol:DeployImplementation",
|
||||
"--sig",
|
||||
"updateServiceManagerProxyWithVersion()",
|
||||
"--rpc-url",
|
||||
rpcUrl,
|
||||
"--sender",
|
||||
avsOwnerAddress,
|
||||
"--broadcast",
|
||||
"--non-interactive"
|
||||
];
|
||||
|
||||
try {
|
||||
const result = await executeCommand("forge", updateArgs, env, "../contracts");
|
||||
|
||||
logger.success(`ServiceManager proxy updated and version set to ${version}`);
|
||||
logger.debug(result);
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to update ServiceManager proxy: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves the target version for upgrade.
|
||||
*
|
||||
* - If an explicit semver string is provided via --target, it is validated and used as-is.
|
||||
* contracts/VERSION is NOT written — it is a source-controlled file that must be bumped
|
||||
* in a commit, not mutated as a side-effect of targeting a specific chain.
|
||||
* - If undefined/empty, the current value of contracts/VERSION is read and returned.
|
||||
*
|
||||
* When --target differs from contracts/VERSION a warning is emitted so the operator
|
||||
* knows the file and the on-chain state will diverge until the file is updated in source control.
|
||||
*/
|
||||
const resolveTargetVersion = async (versionSpec: string | undefined): Promise<string> => {
|
||||
const cwd = process.cwd();
|
||||
const repoRoot = path.basename(cwd) === "test" ? path.join(cwd, "..") : cwd;
|
||||
const versionFile = path.join(repoRoot, "contracts", "VERSION");
|
||||
|
||||
const fileVersion = readFileSync(versionFile, "utf8").trim();
|
||||
|
||||
if (!versionSpec) {
|
||||
logger.info(`📖 Reading version from contracts/VERSION: ${fileVersion}`);
|
||||
return fileVersion;
|
||||
}
|
||||
|
||||
const semverRegex = /^\d+\.\d+\.\d+$/;
|
||||
if (!semverRegex.test(versionSpec)) {
|
||||
throw new Error(`Invalid version format: ${versionSpec}. Expected X.Y.Z`);
|
||||
}
|
||||
|
||||
if (versionSpec !== fileVersion) {
|
||||
logger.warn(
|
||||
`⚠️ --target ${versionSpec} differs from contracts/VERSION (${fileVersion}). ` +
|
||||
"The on-chain version will be set to the --target value. " +
|
||||
"Remember to update contracts/VERSION in source control to keep it in sync."
|
||||
);
|
||||
}
|
||||
|
||||
return versionSpec;
|
||||
};
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
#!/usr/bin/env bun
|
||||
import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
|
||||
import type { DeployEnvironment } from "utils";
|
||||
import { logger, printHeader } from "utils";
|
||||
import {
|
||||
contractsCheck,
|
||||
contractsChecks,
|
||||
contractsDeploy,
|
||||
contractsPreActionHook,
|
||||
contractsUpdateBeefyCheckpoint,
|
||||
contractsUpdateRewardsOrigin,
|
||||
contractsUpgrade,
|
||||
contractsVerify,
|
||||
deploy,
|
||||
deployPreActionHook,
|
||||
|
|
@ -14,7 +17,8 @@ import {
|
|||
launchPreActionHook,
|
||||
stop,
|
||||
stopPreActionHook,
|
||||
updateAVSMetadataURI
|
||||
updateAVSMetadataURI,
|
||||
versioningPostChecks
|
||||
} from "./handlers";
|
||||
|
||||
// Function to parse integer
|
||||
|
|
@ -205,6 +209,7 @@ const contractsCommand = program
|
|||
Commands:
|
||||
- status: Show deployment plan, configuration, and status (default)
|
||||
- deploy: Deploy contracts to specified chain
|
||||
- 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
|
||||
|
|
@ -218,6 +223,19 @@ const contractsCommand = program
|
|||
--rpc-url: Chain RPC URL (optional, defaults based on chain)
|
||||
--private-key: Private key for deployment
|
||||
--skip-verification: Skip contract verification
|
||||
|
||||
Versioning:
|
||||
- contracts/VERSION is the single source of truth for the code version and must be updated in source control.
|
||||
- bun cli contracts upgrade --target X.Y.Z upgrades on-chain to that version WITHOUT writing to contracts/VERSION.
|
||||
- Omit --target to use the current contracts/VERSION value (the common case after bumping the file in a commit).
|
||||
|
||||
Upgrade dry-run (production default):
|
||||
- bun cli contracts upgrade --chain hoodi --target X.Y.Z
|
||||
Deploys the new implementation, then prints the ProxyAdmin.upgradeAndCall calldata
|
||||
for the multisig team to execute manually. No AVS owner key required.
|
||||
- bun cli contracts upgrade --chain hoodi --target X.Y.Z --execute
|
||||
Full on-chain upgrade: deploys the implementation AND broadcasts the proxy upgrade
|
||||
+ version update transaction. Requires AVS_OWNER_PRIVATE_KEY.
|
||||
`
|
||||
)
|
||||
.description("Deploy and manage DataHaven AVS contracts on supported chains");
|
||||
|
|
@ -266,6 +284,66 @@ contractsCommand
|
|||
.hook("preAction", contractsPreActionHook)
|
||||
.action(contractsDeploy);
|
||||
|
||||
// Contracts Upgrade
|
||||
contractsCommand
|
||||
.command("upgrade")
|
||||
.description("Upgrade DataHaven AVS contracts by deploying new implementations")
|
||||
.option("--chain <value>", "Target chain (hoodi, mainnet, anvil)")
|
||||
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
|
||||
.option("--private-key-file <value>", "Path to file containing private key for deployment")
|
||||
.option("--verify", "Verify upgraded contracts on block explorer", false)
|
||||
.option(
|
||||
"--target <value>",
|
||||
"Version to upgrade to (X.Y.Z). Omit to use the current contracts/VERSION value. Does NOT write to contracts/VERSION — update that file in source control separately."
|
||||
)
|
||||
.option(
|
||||
"--execute",
|
||||
"Execute the proxy upgrade transaction on-chain. Without this flag the command outputs the calldata for manual multisig execution (dry-run mode).",
|
||||
false
|
||||
)
|
||||
.hook("preAction", contractsPreActionHook)
|
||||
.action(async (options: any, command: any) => {
|
||||
// Try to get chain from options or command
|
||||
let chain = options.chain;
|
||||
if (!chain && command.parent) {
|
||||
chain = command.parent.getOptionValue("chain");
|
||||
}
|
||||
if (!chain) {
|
||||
chain = command.getOptionValue("chain");
|
||||
}
|
||||
|
||||
printHeader(`Upgrading DataHaven Contracts on ${chain}`);
|
||||
|
||||
try {
|
||||
await contractsUpgrade({
|
||||
chain: chain,
|
||||
rpcUrl: options.rpcUrl,
|
||||
privateKeyFile: options.privateKeyFile,
|
||||
verify: options.verify,
|
||||
version: options.target,
|
||||
execute: options.execute
|
||||
});
|
||||
|
||||
if (options.execute) {
|
||||
await versioningPostChecks({
|
||||
chain,
|
||||
rpcUrl: options.rpcUrl
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ Upgrade failed: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Contracts Version Check
|
||||
contractsCommand
|
||||
.command("version-check")
|
||||
.description("Run contract version checks")
|
||||
.option("--chain <value>", "Target chain (hoodi, mainnet, anvil)")
|
||||
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
|
||||
.hook("preAction", contractsPreActionHook)
|
||||
.action(contractsChecks);
|
||||
|
||||
// Contracts Verify
|
||||
contractsCommand
|
||||
.command("verify")
|
||||
|
|
|
|||
|
|
@ -2047,6 +2047,13 @@ export const dataHavenServiceManagerAbi = [
|
|||
outputs: [{ name: '', internalType: 'string', type: 'string' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [],
|
||||
name: 'DATAHAVEN_VERSION',
|
||||
outputs: [{ name: '', internalType: 'string', type: 'string' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [],
|
||||
|
|
@ -2166,6 +2173,7 @@ export const dataHavenServiceManagerAbi = [
|
|||
internalType: 'address',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'initialVersion', internalType: 'string', type: 'string' },
|
||||
],
|
||||
name: 'initialize',
|
||||
outputs: [],
|
||||
|
|
@ -2397,6 +2405,13 @@ export const dataHavenServiceManagerAbi = [
|
|||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [{ name: 'newVersion', internalType: 'string', type: 'string' }],
|
||||
name: 'updateVersion',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
|
|
@ -2661,13 +2676,34 @@ export const dataHavenServiceManagerAbi = [
|
|||
],
|
||||
name: 'ValidatorSetSubmitterUpdated',
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'oldVersion',
|
||||
internalType: 'string',
|
||||
type: 'string',
|
||||
indexed: false,
|
||||
},
|
||||
{
|
||||
name: 'newVersion',
|
||||
internalType: 'string',
|
||||
type: 'string',
|
||||
indexed: false,
|
||||
},
|
||||
],
|
||||
name: 'VersionUpdated',
|
||||
},
|
||||
{ type: 'error', inputs: [], name: 'CallerIsNotValidator' },
|
||||
{ type: 'error', inputs: [], name: 'CantDeregisterFromMultipleOperatorSets' },
|
||||
{ type: 'error', inputs: [], name: 'CantRegisterToMultipleOperatorSets' },
|
||||
{ type: 'error', inputs: [], name: 'EmptyValidatorSet' },
|
||||
{ type: 'error', inputs: [], name: 'EmptyVersion' },
|
||||
{ type: 'error', inputs: [], name: 'IncorrectAVSAddress' },
|
||||
{ type: 'error', inputs: [], name: 'InvalidOperatorSetId' },
|
||||
{ type: 'error', inputs: [], name: 'InvalidSolochainAddressLength' },
|
||||
{ type: 'error', inputs: [], name: 'NotProxyAdmin' },
|
||||
{ type: 'error', inputs: [], name: 'OnlyAllocationManager' },
|
||||
{ type: 'error', inputs: [], name: 'OnlyRewardsInitiator' },
|
||||
{ type: 'error', inputs: [], name: 'OnlyValidatorSetSubmitter' },
|
||||
|
|
@ -6352,6 +6388,122 @@ export const permissionControllerAbi = [
|
|||
},
|
||||
] as const
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ProxyAdmin
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export const proxyAdminAbi = [
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
{
|
||||
name: 'proxy',
|
||||
internalType: 'contract ITransparentUpgradeableProxy',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'newAdmin', internalType: 'address', type: 'address' },
|
||||
],
|
||||
name: 'changeProxyAdmin',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
{
|
||||
name: 'proxy',
|
||||
internalType: 'contract ITransparentUpgradeableProxy',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'getProxyAdmin',
|
||||
outputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
{
|
||||
name: 'proxy',
|
||||
internalType: 'contract ITransparentUpgradeableProxy',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'getProxyImplementation',
|
||||
outputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ name: '', internalType: 'address', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [],
|
||||
name: 'renounceOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
{
|
||||
name: 'proxy',
|
||||
internalType: 'contract ITransparentUpgradeableProxy',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'implementation', internalType: 'address', type: 'address' },
|
||||
],
|
||||
name: 'upgrade',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
inputs: [
|
||||
{
|
||||
name: 'proxy',
|
||||
internalType: 'contract ITransparentUpgradeableProxy',
|
||||
type: 'address',
|
||||
},
|
||||
{ name: 'implementation', internalType: 'address', type: 'address' },
|
||||
{ name: 'data', internalType: 'bytes', type: 'bytes' },
|
||||
],
|
||||
name: 'upgradeAndCall',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'previousOwner',
|
||||
internalType: 'address',
|
||||
type: 'address',
|
||||
indexed: true,
|
||||
},
|
||||
{
|
||||
name: 'newOwner',
|
||||
internalType: 'address',
|
||||
type: 'address',
|
||||
indexed: true,
|
||||
},
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
},
|
||||
] as const
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RewardsCoordinator
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -10815,6 +10967,15 @@ export const readDataHavenServiceManagerDatahavenAvsMetadata =
|
|||
functionName: 'DATAHAVEN_AVS_METADATA',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"DATAHAVEN_VERSION"`
|
||||
*/
|
||||
export const readDataHavenServiceManagerDatahavenVersion =
|
||||
/*#__PURE__*/ createReadContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'DATAHAVEN_VERSION',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"MAX_ACTIVE_VALIDATORS"`
|
||||
*/
|
||||
|
|
@ -11119,6 +11280,15 @@ export const writeDataHavenServiceManagerUpdateSolochainAddressForValidator =
|
|||
functionName: 'updateSolochainAddressForValidator',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"updateVersion"`
|
||||
*/
|
||||
export const writeDataHavenServiceManagerUpdateVersion =
|
||||
/*#__PURE__*/ createWriteContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'updateVersion',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__
|
||||
*/
|
||||
|
|
@ -11296,6 +11466,15 @@ export const simulateDataHavenServiceManagerUpdateSolochainAddressForValidator =
|
|||
functionName: 'updateSolochainAddressForValidator',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"updateVersion"`
|
||||
*/
|
||||
export const simulateDataHavenServiceManagerUpdateVersion =
|
||||
/*#__PURE__*/ createSimulateContract({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: 'updateVersion',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__
|
||||
*/
|
||||
|
|
@ -11428,6 +11607,15 @@ export const watchDataHavenServiceManagerValidatorSetSubmitterUpdatedEvent =
|
|||
eventName: 'ValidatorSetSubmitterUpdated',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"VersionUpdated"`
|
||||
*/
|
||||
export const watchDataHavenServiceManagerVersionUpdatedEvent =
|
||||
/*#__PURE__*/ createWatchContractEvent({
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
eventName: 'VersionUpdated',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link delegationManagerAbi}__
|
||||
*/
|
||||
|
|
@ -14335,6 +14523,155 @@ export const watchPermissionControllerPendingAdminRemovedEvent =
|
|||
eventName: 'PendingAdminRemoved',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link proxyAdminAbi}__
|
||||
*/
|
||||
export const readProxyAdmin = /*#__PURE__*/ createReadContract({
|
||||
abi: proxyAdminAbi,
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"getProxyAdmin"`
|
||||
*/
|
||||
export const readProxyAdminGetProxyAdmin = /*#__PURE__*/ createReadContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'getProxyAdmin',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"getProxyImplementation"`
|
||||
*/
|
||||
export const readProxyAdminGetProxyImplementation =
|
||||
/*#__PURE__*/ createReadContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'getProxyImplementation',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"owner"`
|
||||
*/
|
||||
export const readProxyAdminOwner = /*#__PURE__*/ createReadContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'owner',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link proxyAdminAbi}__
|
||||
*/
|
||||
export const writeProxyAdmin = /*#__PURE__*/ createWriteContract({
|
||||
abi: proxyAdminAbi,
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"changeProxyAdmin"`
|
||||
*/
|
||||
export const writeProxyAdminChangeProxyAdmin =
|
||||
/*#__PURE__*/ createWriteContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'changeProxyAdmin',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"renounceOwnership"`
|
||||
*/
|
||||
export const writeProxyAdminRenounceOwnership =
|
||||
/*#__PURE__*/ createWriteContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'renounceOwnership',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"transferOwnership"`
|
||||
*/
|
||||
export const writeProxyAdminTransferOwnership =
|
||||
/*#__PURE__*/ createWriteContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'transferOwnership',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"upgrade"`
|
||||
*/
|
||||
export const writeProxyAdminUpgrade = /*#__PURE__*/ createWriteContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'upgrade',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link writeContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"upgradeAndCall"`
|
||||
*/
|
||||
export const writeProxyAdminUpgradeAndCall = /*#__PURE__*/ createWriteContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'upgradeAndCall',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link proxyAdminAbi}__
|
||||
*/
|
||||
export const simulateProxyAdmin = /*#__PURE__*/ createSimulateContract({
|
||||
abi: proxyAdminAbi,
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"changeProxyAdmin"`
|
||||
*/
|
||||
export const simulateProxyAdminChangeProxyAdmin =
|
||||
/*#__PURE__*/ createSimulateContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'changeProxyAdmin',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"renounceOwnership"`
|
||||
*/
|
||||
export const simulateProxyAdminRenounceOwnership =
|
||||
/*#__PURE__*/ createSimulateContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'renounceOwnership',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"transferOwnership"`
|
||||
*/
|
||||
export const simulateProxyAdminTransferOwnership =
|
||||
/*#__PURE__*/ createSimulateContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'transferOwnership',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"upgrade"`
|
||||
*/
|
||||
export const simulateProxyAdminUpgrade = /*#__PURE__*/ createSimulateContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'upgrade',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link simulateContract}__ with `abi` set to __{@link proxyAdminAbi}__ and `functionName` set to `"upgradeAndCall"`
|
||||
*/
|
||||
export const simulateProxyAdminUpgradeAndCall =
|
||||
/*#__PURE__*/ createSimulateContract({
|
||||
abi: proxyAdminAbi,
|
||||
functionName: 'upgradeAndCall',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link proxyAdminAbi}__
|
||||
*/
|
||||
export const watchProxyAdminEvent = /*#__PURE__*/ createWatchContractEvent({
|
||||
abi: proxyAdminAbi,
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link proxyAdminAbi}__ and `eventName` set to `"OwnershipTransferred"`
|
||||
*/
|
||||
export const watchProxyAdminOwnershipTransferredEvent =
|
||||
/*#__PURE__*/ createWatchContractEvent({
|
||||
abi: proxyAdminAbi,
|
||||
eventName: 'OwnershipTransferred',
|
||||
})
|
||||
|
||||
/**
|
||||
* Wraps __{@link readContract}__ with `abi` set to __{@link rewardsCoordinatorAbi}__
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"polkadot-api": "^1.15.1",
|
||||
"prom-client": "^15.1.0",
|
||||
"solc": "^0.8.30",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"viem": "^2.31.3",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { $ } from "bun";
|
||||
import { CHAIN_CONFIGS, loadChainConfig } from "configs/contracts/config";
|
||||
import invariant from "tiny-invariant";
|
||||
|
|
@ -116,6 +118,26 @@ export const executeDeployment = async (
|
|||
logger.success("Contracts deployed successfully");
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current code version from contracts/VERSION file
|
||||
* This is the single source of truth for the code version
|
||||
*/
|
||||
export const getCurrentVersion = async (): Promise<string> => {
|
||||
const cwd = process.cwd();
|
||||
const repoRoot = path.basename(cwd) === "test" ? path.join(cwd, "..") : cwd;
|
||||
const versionFile = path.join(repoRoot, "contracts", "VERSION");
|
||||
|
||||
try {
|
||||
const version = readFileSync(versionFile, "utf8").trim();
|
||||
if (!version) {
|
||||
throw new Error("VERSION file is empty");
|
||||
}
|
||||
return version;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read contracts/VERSION: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the parameters from the deployed contracts and add it to the collection.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -142,9 +142,8 @@ export const fundValidators = async (options: FundValidatorsOptions): Promise<bo
|
|||
logger.warn("Will try to continue with other strategies...");
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(`Found token creator address ${tokenCreator} in validators list`);
|
||||
const creatorPrivateKey = creatorValidator.privateKey;
|
||||
logger.debug(`Found token creator's private key for address ${tokenCreator}`);
|
||||
|
||||
// Get the ERC20 balance of the token creator and its ETH balance as well
|
||||
const getErc20BalanceCmd = `${castExecutable} call ${underlyingTokenAddress} "balanceOf(address)(uint256)" ${tokenCreator} --rpc-url ${rpcUrl}`;
|
||||
|
|
@ -160,15 +159,18 @@ export const fundValidators = async (options: FundValidatorsOptions): Promise<bo
|
|||
const ethTransferAmount = BigInt(creatorEthBalance) / BigInt(100); // 1% of the balance
|
||||
logger.debug(`Transferring ${erc20TransferAmount} tokens to each validator`);
|
||||
|
||||
// Tests use locally-signed transactions so funding works in CI (no TTY, no unlocked accounts).
|
||||
for (const validator of validators) {
|
||||
if (validator.publicKey !== tokenCreator) {
|
||||
const transferCmd = `${castExecutable} send --private-key ${creatorPrivateKey} ${underlyingTokenAddress} "transfer(address,uint256)" ${validator.publicKey} ${erc20TransferAmount} --rpc-url ${rpcUrl}`;
|
||||
const { exitCode: transferExitCode, stderr: transferStderr } = await $`sh -c ${transferCmd}`
|
||||
.nothrow()
|
||||
.quiet();
|
||||
if (transferExitCode !== 0) {
|
||||
const transferCmdSigned = `${castExecutable} send --private-key $PRIVATE_KEY ${underlyingTokenAddress} "transfer(address,uint256)" ${validator.publicKey} ${erc20TransferAmount} --rpc-url ${rpcUrl}`;
|
||||
const { exitCode: transferExitCodeSigned, stderr: transferStderrSigned } =
|
||||
await $`sh -c ${transferCmdSigned}`
|
||||
.env({ ...process.env, PRIVATE_KEY: creatorPrivateKey })
|
||||
.nothrow()
|
||||
.quiet();
|
||||
if (transferExitCodeSigned !== 0) {
|
||||
logger.error(
|
||||
`Failed to transfer tokens to validator ${validator.publicKey}: ${transferStderr.toString()}`
|
||||
`Failed to transfer tokens to validator ${validator.publicKey}: ${transferStderrSigned.toString()}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -196,12 +198,15 @@ export const fundValidators = async (options: FundValidatorsOptions): Promise<bo
|
|||
|
||||
// Transfer ETH only if the validator has no ETH
|
||||
if (BigInt(validatorEthBalance) === BigInt(0)) {
|
||||
const ethTransferCmd = `${castExecutable} send --private-key ${creatorPrivateKey} ${validator.publicKey} --value ${ethTransferAmount} --rpc-url ${rpcUrl}`;
|
||||
const { exitCode: ethTransferExitCode, stderr: ethTransferStderr } =
|
||||
await $`sh -c ${ethTransferCmd}`.nothrow().quiet();
|
||||
if (ethTransferExitCode !== 0) {
|
||||
const ethTransferCmdSigned = `${castExecutable} send --private-key $PRIVATE_KEY ${validator.publicKey} --value ${ethTransferAmount} --rpc-url ${rpcUrl}`;
|
||||
const { exitCode: ethExitCodeSigned, stderr: ethStderrSigned } =
|
||||
await $`sh -c ${ethTransferCmdSigned}`
|
||||
.env({ ...process.env, PRIVATE_KEY: creatorPrivateKey })
|
||||
.nothrow()
|
||||
.quiet();
|
||||
if (ethExitCodeSigned !== 0) {
|
||||
logger.error(
|
||||
`Failed to transfer ETH to validator ${validator.publicKey}: ${ethTransferStderr.toString()}`
|
||||
`Failed to transfer ETH to validator ${validator.publicKey}: ${ethStderrSigned.toString()}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const setDataHavenParameters = async (
|
|||
dhApi.tx.Parameters.set_parameter({
|
||||
key_value: {
|
||||
type: "RuntimeConfig",
|
||||
value: { type: p.name, value: [p.value] }
|
||||
value: { type: p.name as any, value: [p.value] }
|
||||
}
|
||||
}).decodedCall
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export const updateValidatorSet = async (options: UpdateValidatorSetOptions): Pr
|
|||
const serviceManagerAddress = deployments.ServiceManager;
|
||||
invariant(serviceManagerAddress, "ServiceManager address not found in deployments");
|
||||
|
||||
// Security Note: Private key is passed via PRIVATE_KEY env var (not in argv) to avoid exposure in process lists.
|
||||
// Using cast to send the transaction
|
||||
const executionFee = "100000000000000000"; // 0.1 ETH
|
||||
const relayerFee = "200000000000000000"; // 0.2 ETH
|
||||
|
|
@ -58,11 +59,14 @@ export const updateValidatorSet = async (options: UpdateValidatorSetOptions): Pr
|
|||
);
|
||||
}
|
||||
|
||||
const sendCommand = `${castExecutable} send --private-key ${ownerPrivateKey} --value ${value} ${serviceManagerAddress} "sendNewValidatorSetForEra(uint64,uint128,uint128)" ${targetEra} ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
|
||||
const sendCommand = `${castExecutable} send --private-key $PRIVATE_KEY --value ${value} ${serviceManagerAddress} "sendNewValidatorSetForEra(uint64,uint128,uint128)" ${targetEra} ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
|
||||
|
||||
logger.debug(`Running command: ${sendCommand}`);
|
||||
|
||||
const { exitCode, stderr } = await $`sh -c ${sendCommand}`.nothrow().quiet();
|
||||
const { exitCode, stderr } = await $`sh -c ${sendCommand}`
|
||||
.env({ ...process.env, PRIVATE_KEY: ownerPrivateKey })
|
||||
.nothrow()
|
||||
.quiet();
|
||||
|
||||
if (exitCode !== 0) {
|
||||
logger.error(`Failed to send validator set: ${stderr.toString()}`);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ COPY test/utils/ ./utils/
|
|||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
USER submitter
|
||||
|
||||
ENTRYPOINT ["bun", "run", "tools/validator-set-submitter/main.ts", "run"]
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ network_id: "anvil"
|
|||
# Fees (in ETH, sent as msg.value to cover Snowbridge relay costs)
|
||||
execution_fee: "0.1"
|
||||
relayer_fee: "0.2"
|
||||
|
||||
# Optional metrics port (default: 8080)
|
||||
# metrics_port: 8080
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
|
@ -64,6 +67,24 @@ bun tools/validator-set-submitter/main.ts run --dry-run
|
|||
|
||||
Private key precedence is: `--submitter-private-key` > `SUBMITTER_PRIVATE_KEY` > `submitter_private_key` in config file.
|
||||
|
||||
## Observability
|
||||
|
||||
The submitter exposes a Prometheus metrics server on `metrics_port` (default `8080`):
|
||||
|
||||
- `GET /metrics` — Prometheus metrics
|
||||
- `GET /healthz` — liveness
|
||||
- `GET /readyz` — readiness (`200` once startup checks pass and watcher is running)
|
||||
|
||||
Key metrics:
|
||||
|
||||
- `validator_set_submitter_submissions_total{outcome="success|failed|dry_run"}`
|
||||
- `validator_set_submitter_ticks_total{result="submitted_success|submitted_failed|skipped_*"}`
|
||||
- `validator_set_submitter_errors_total{type="tick_error|subscription_error"}`
|
||||
- `validator_set_submitter_missed_eras_total`
|
||||
- `validator_set_submitter_consecutive_missed_eras`
|
||||
- `validator_set_submitter_up`
|
||||
- `validator_set_submitter_ready`
|
||||
|
||||
## Docker
|
||||
|
||||
Build the image from the repository root:
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ export interface SubmitterConfig {
|
|||
executionFee: bigint;
|
||||
relayerFee: bigint;
|
||||
dryRun: boolean;
|
||||
metricsPort: number;
|
||||
}
|
||||
|
||||
interface CliOverrides {
|
||||
dryRun?: boolean;
|
||||
submitterPrivateKey?: string;
|
||||
metricsPort?: string;
|
||||
}
|
||||
|
||||
export async function loadConfig(
|
||||
|
|
@ -42,6 +44,8 @@ export async function loadConfig(
|
|||
const executionFee = parseEther(optionalString(raw, "execution_fee") ?? "0.1");
|
||||
const relayerFee = parseEther(optionalString(raw, "relayer_fee") ?? "0.2");
|
||||
|
||||
const metricsPort = resolveMetricsPort(raw, cli.metricsPort);
|
||||
|
||||
return {
|
||||
ethereumRpcUrl,
|
||||
datahavenWsUrl,
|
||||
|
|
@ -50,7 +54,8 @@ export async function loadConfig(
|
|||
networkId,
|
||||
executionFee,
|
||||
relayerFee,
|
||||
dryRun: cli.dryRun ?? false
|
||||
dryRun: cli.dryRun ?? false,
|
||||
metricsPort
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -99,3 +104,14 @@ function optionalHexString(raw: Record<string, unknown>, key: string): `0x${stri
|
|||
}
|
||||
return val as `0x${string}`;
|
||||
}
|
||||
|
||||
function resolveMetricsPort(raw: Record<string, unknown>, cliPort?: string): number {
|
||||
const portValue = cliPort ?? process.env.METRICS_PORT ?? optionalString(raw, "metrics_port");
|
||||
const port = portValue !== undefined ? Number(portValue) : 8080;
|
||||
|
||||
if (!Number.isFinite(port) || !Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error(`Invalid metrics port: ${port}. Must be an integer between 1 and 65535.`);
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,3 +19,6 @@ network_id: "anvil"
|
|||
# Fees (in ETH, sent as msg.value to cover Snowbridge relay costs)
|
||||
execution_fee: "0.1"
|
||||
relayer_fee: "0.2"
|
||||
|
||||
# Prometheus metrics server port (default: 8080)
|
||||
# metrics_port: 8080
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { logger } from "utils/logger";
|
|||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { getOnChainSubmitter } from "./chain";
|
||||
import { loadConfig } from "./config";
|
||||
import { createMetricsServer } from "./metrics";
|
||||
import { createClients, startSubmitter } from "./submitter";
|
||||
|
||||
const program = new Command()
|
||||
|
|
@ -21,13 +22,19 @@ program
|
|||
"--submitter-private-key <key>",
|
||||
"Override submitter private key (or use SUBMITTER_PRIVATE_KEY env var)"
|
||||
)
|
||||
.option("--metrics-port <port>", "Override metrics server port (or use METRICS_PORT env var)")
|
||||
.option("--dry-run", "Log what would be submitted without sending transactions", false)
|
||||
.action(async (opts) => {
|
||||
const config = await loadConfig(opts.config, {
|
||||
dryRun: opts.dryRun,
|
||||
submitterPrivateKey: opts.submitterPrivateKey
|
||||
submitterPrivateKey: opts.submitterPrivateKey,
|
||||
metricsPort: opts.metricsPort
|
||||
});
|
||||
|
||||
// Start metrics server early so /healthz is available during init
|
||||
const metricsServer = createMetricsServer(config.metricsPort);
|
||||
logger.info(`Metrics server listening on :${config.metricsPort}`);
|
||||
|
||||
logger.info("Validator Set Submitter starting...");
|
||||
logger.info(`Ethereum RPC: ${config.ethereumRpcUrl}`);
|
||||
logger.info(`DataHaven WS: ${config.datahavenWsUrl}`);
|
||||
|
|
@ -84,6 +91,7 @@ program
|
|||
try {
|
||||
await startSubmitter(clients, config, ac.signal);
|
||||
} finally {
|
||||
metricsServer.stop();
|
||||
clients.papiClient.destroy();
|
||||
logger.info("Submitter stopped, PAPI client destroyed");
|
||||
}
|
||||
|
|
|
|||
134
test/tools/validator-set-submitter/metrics.ts
Normal file
134
test/tools/validator-set-submitter/metrics.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { Counter, Gauge, Histogram, Registry } from "prom-client";
|
||||
|
||||
const PREFIX = "validator_set_submitter_";
|
||||
|
||||
export const registry = new Registry();
|
||||
|
||||
// --- Counters ---
|
||||
|
||||
export const submissionsTotal = new Counter({
|
||||
name: `${PREFIX}submissions_total`,
|
||||
help: "Total submission attempts and results",
|
||||
labelNames: ["outcome"] as const,
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const ticksTotal = new Counter({
|
||||
name: `${PREFIX}ticks_total`,
|
||||
help: "Total tick evaluations",
|
||||
labelNames: ["result"] as const,
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const errorsTotal = new Counter({
|
||||
name: `${PREFIX}errors_total`,
|
||||
help: "Non-submission errors",
|
||||
labelNames: ["type"] as const,
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const missedErasTotal = new Counter({
|
||||
name: `${PREFIX}missed_eras_total`,
|
||||
help: "Total eras where a submission attempt failed",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
// --- Gauges ---
|
||||
|
||||
export const activeEra = new Gauge({
|
||||
name: `${PREFIX}active_era`,
|
||||
help: "Current active era on DataHaven",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const targetEra = new Gauge({
|
||||
name: `${PREFIX}target_era`,
|
||||
help: "Target era for next submission",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const externalIndex = new Gauge({
|
||||
name: `${PREFIX}external_index`,
|
||||
help: "Latest confirmed era on-chain",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const currentSession = new Gauge({
|
||||
name: `${PREFIX}current_session`,
|
||||
help: "Current session number",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const lastSubmittedEra = new Gauge({
|
||||
name: `${PREFIX}last_submitted_era`,
|
||||
help: "Last era successfully submitted",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const consecutiveMissedEras = new Gauge({
|
||||
name: `${PREFIX}consecutive_missed_eras`,
|
||||
help: "Consecutive eras missed (resets to 0 on success)",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const up = new Gauge({
|
||||
name: `${PREFIX}up`,
|
||||
help: "1 if watcher is running, 0 if stopped",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const ready = new Gauge({
|
||||
name: `${PREFIX}ready`,
|
||||
help: "1 if startup checks passed and watcher running, 0 otherwise",
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
// --- Histograms ---
|
||||
|
||||
export const submissionDuration = new Histogram({
|
||||
name: `${PREFIX}submission_duration_seconds`,
|
||||
help: "Time from tx send to receipt",
|
||||
buckets: [1, 5, 10, 30, 60, 120, 300],
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
export const tickDuration = new Histogram({
|
||||
name: `${PREFIX}tick_duration_seconds`,
|
||||
help: "Time to process one tick",
|
||||
buckets: [0.1, 0.5, 1, 2, 5, 10, 30],
|
||||
registers: [registry]
|
||||
});
|
||||
|
||||
// --- HTTP Server ---
|
||||
|
||||
export function createMetricsServer(port: number) {
|
||||
const server = Bun.serve({
|
||||
port,
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (url.pathname === "/metrics") {
|
||||
const metrics = await registry.metrics();
|
||||
return new Response(metrics, {
|
||||
headers: { "Content-Type": registry.contentType }
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname === "/healthz") {
|
||||
return new Response("ok\n", { status: 200 });
|
||||
}
|
||||
|
||||
if (url.pathname === "/readyz") {
|
||||
const isReady = (await ready.get()).values[0]?.value === 1;
|
||||
if (isReady) {
|
||||
return new Response("ready\n", { status: 200 });
|
||||
}
|
||||
return new Response("not ready\n", { status: 503 });
|
||||
}
|
||||
|
||||
return new Response("Not Found\n", { status: 404 });
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import { privateKeyToAccount } from "viem/accounts";
|
|||
import { dataHavenServiceManagerAbi, gatewayAbi } from "../../contract-bindings";
|
||||
import { computeTargetEra, getActiveEra, getExternalIndex, isLastSessionOfEra } from "./chain";
|
||||
import type { SubmitterConfig } from "./config";
|
||||
import * as metrics from "./metrics";
|
||||
|
||||
interface SubmitterClients {
|
||||
publicClient: PublicClient;
|
||||
|
|
@ -71,32 +72,70 @@ async function waitForReceiptWithAbort(
|
|||
function createTicker(clients: SubmitterClients, config: SubmitterConfig, signal: AbortSignal) {
|
||||
let submittedEra: bigint | undefined;
|
||||
|
||||
return async (currentSession: number): Promise<void> => {
|
||||
const { dhApi } = clients;
|
||||
return async (currentSessionValue: number): Promise<void> => {
|
||||
const endTimer = metrics.tickDuration.startTimer();
|
||||
try {
|
||||
const { dhApi } = clients;
|
||||
|
||||
const activeEra = await getActiveEra(dhApi);
|
||||
if (!activeEra) {
|
||||
logger.warn("ActiveEra not set yet");
|
||||
return;
|
||||
metrics.currentSession.set(currentSessionValue);
|
||||
|
||||
const activeEra = await getActiveEra(dhApi);
|
||||
if (!activeEra) {
|
||||
logger.warn("ActiveEra not set yet");
|
||||
metrics.ticksTotal.inc({ result: "skipped_no_active_era" });
|
||||
return;
|
||||
}
|
||||
|
||||
metrics.activeEra.set(activeEra.index);
|
||||
|
||||
const targetEraValue = computeTargetEra(activeEra.index);
|
||||
metrics.targetEra.set(Number(targetEraValue));
|
||||
|
||||
if (submittedEra === targetEraValue) {
|
||||
logger.debug(`Tick skipped: era ${targetEraValue} already submitted locally`);
|
||||
metrics.ticksTotal.inc({ result: "skipped_already_submitted" });
|
||||
return;
|
||||
}
|
||||
|
||||
const externalIndexValue = await getExternalIndex(dhApi);
|
||||
metrics.externalIndex.set(Number(externalIndexValue));
|
||||
|
||||
if (externalIndexValue >= targetEraValue) {
|
||||
logger.debug(
|
||||
`Tick skipped: ExternalIndex=${externalIndexValue} >= TargetEra=${targetEraValue}, already confirmed`
|
||||
);
|
||||
submittedEra = targetEraValue;
|
||||
metrics.ticksTotal.inc({ result: "skipped_already_confirmed" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await isLastSessionOfEra(dhApi))) {
|
||||
logger.debug("Tick skipped: not last session of era");
|
||||
metrics.ticksTotal.inc({ result: "skipped_not_last_session" });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Session=${currentSessionValue} ActiveEra=${activeEra.index} TargetEra=${targetEraValue} ExternalIndex=${externalIndexValue}`
|
||||
);
|
||||
|
||||
const succeeded = await submitForEra(clients, config, targetEraValue, signal);
|
||||
if (succeeded) {
|
||||
submittedEra = targetEraValue;
|
||||
metrics.consecutiveMissedEras.set(0);
|
||||
metrics.lastSubmittedEra.set(Number(targetEraValue));
|
||||
metrics.ticksTotal.inc({ result: "submitted_success" });
|
||||
} else {
|
||||
if (!signal.aborted) {
|
||||
logger.warn(`Submission failed for target era ${targetEraValue}; era will be missed`);
|
||||
}
|
||||
metrics.missedErasTotal.inc();
|
||||
metrics.consecutiveMissedEras.inc();
|
||||
metrics.ticksTotal.inc({ result: "submitted_failed" });
|
||||
}
|
||||
} finally {
|
||||
endTimer();
|
||||
}
|
||||
|
||||
const targetEra = computeTargetEra(activeEra.index);
|
||||
if (submittedEra === targetEra) return;
|
||||
|
||||
const externalIndex = await getExternalIndex(dhApi);
|
||||
if (externalIndex >= targetEra) {
|
||||
submittedEra = targetEra;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await isLastSessionOfEra(dhApi))) return;
|
||||
|
||||
logger.info(
|
||||
`Session=${currentSession} ActiveEra=${activeEra.index} TargetEra=${targetEra} ExternalIndex=${externalIndex}`
|
||||
);
|
||||
|
||||
const succeeded = await submitForEra(clients, config, targetEra, signal);
|
||||
if (succeeded) submittedEra = targetEra;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -112,20 +151,28 @@ export async function startSubmitter(
|
|||
const { dhApi } = clients;
|
||||
const tick = createTicker(clients, config, signal);
|
||||
|
||||
metrics.up.set(1);
|
||||
metrics.ready.set(1);
|
||||
logger.info("Submitter started — watching session changes");
|
||||
|
||||
const sub = dhApi.query.Session.CurrentIndex.watchValue("finalized")
|
||||
.pipe(
|
||||
exhaustMap((currentSession) => {
|
||||
exhaustMap((currentSessionValue) => {
|
||||
if (signal.aborted) return EMPTY;
|
||||
return tick(currentSession).catch((err) => {
|
||||
if (!signal.aborted) logger.error(`Tick error: ${err}`);
|
||||
return tick(currentSessionValue).catch((err) => {
|
||||
if (!signal.aborted) {
|
||||
logger.error(`Tick error: ${err}`);
|
||||
metrics.errorsTotal.inc({ type: "tick_error" });
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
if (!signal.aborted) logger.error(`Session subscription error: ${err}`);
|
||||
if (!signal.aborted) {
|
||||
logger.error(`Session subscription error: ${err}`);
|
||||
metrics.errorsTotal.inc({ type: "subscription_error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -133,6 +180,8 @@ export async function startSubmitter(
|
|||
await Promise.race([onAbort(signal), done]);
|
||||
sub.unsubscribe();
|
||||
|
||||
metrics.up.set(0);
|
||||
metrics.ready.set(0);
|
||||
logger.info("Submitter stopped");
|
||||
}
|
||||
|
||||
|
|
@ -143,14 +192,14 @@ export async function startSubmitter(
|
|||
async function submitForEra(
|
||||
clients: SubmitterClients,
|
||||
config: SubmitterConfig,
|
||||
targetEra: bigint,
|
||||
targetEraValue: bigint,
|
||||
signal: AbortSignal
|
||||
): Promise<boolean> {
|
||||
const { publicClient, walletClient } = clients;
|
||||
|
||||
const totalFee = config.executionFee + config.relayerFee;
|
||||
logger.info(
|
||||
`Submitting era ${targetEra} (execFee=${config.executionFee} relayerFee=${config.relayerFee})`
|
||||
`Submitting era ${targetEraValue} (execFee=${config.executionFee} relayerFee=${config.relayerFee})`
|
||||
);
|
||||
|
||||
if (config.dryRun) {
|
||||
|
|
@ -158,18 +207,20 @@ async function submitForEra(
|
|||
address: config.serviceManagerAddress,
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: "buildNewValidatorSetMessageForEra",
|
||||
args: [targetEra]
|
||||
args: [targetEraValue]
|
||||
});
|
||||
logger.info(`[DRY RUN] Would send message: ${message}`);
|
||||
metrics.submissionsTotal.inc({ outcome: "dry_run" });
|
||||
return true;
|
||||
}
|
||||
|
||||
const endTimer = metrics.submissionDuration.startTimer();
|
||||
try {
|
||||
const hash = await walletClient.writeContract({
|
||||
address: config.serviceManagerAddress,
|
||||
abi: dataHavenServiceManagerAbi,
|
||||
functionName: "sendNewValidatorSetForEra",
|
||||
args: [targetEra, config.executionFee, config.relayerFee],
|
||||
args: [targetEraValue, config.executionFee, config.relayerFee],
|
||||
value: totalFee,
|
||||
chain: null
|
||||
});
|
||||
|
|
@ -178,6 +229,7 @@ async function submitForEra(
|
|||
const receipt = await waitForReceiptWithAbort(publicClient, hash, signal);
|
||||
if (receipt.status !== "success") {
|
||||
logger.error(`Transaction reverted: ${hash}`);
|
||||
metrics.submissionsTotal.inc({ outcome: "failed" });
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -196,14 +248,19 @@ async function submitForEra(
|
|||
|
||||
if (!hasOutbound) {
|
||||
logger.warn("Transaction succeeded but no OutboundMessageAccepted event found");
|
||||
metrics.submissionsTotal.inc({ outcome: "failed" });
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info("OutboundMessageAccepted confirmed");
|
||||
metrics.submissionsTotal.inc({ outcome: "success" });
|
||||
return true;
|
||||
} catch (err: unknown) {
|
||||
if (signal.aborted) return false;
|
||||
logger.error(`Submission attempt failed: ${err}`);
|
||||
metrics.submissionsTotal.inc({ outcome: "failed" });
|
||||
return false;
|
||||
} finally {
|
||||
endTimer();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
test/utils/anvil.ts
Normal file
48
test/utils/anvil.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { getPortFromKurtosis } from "./kurtosis";
|
||||
import { logger } from "./logger";
|
||||
|
||||
/**
|
||||
* Gets the RPC URL for the Anvil (local Ethereum) node running in Kurtosis
|
||||
* @param enclaveName - The name of the Kurtosis enclave (default: "datahaven-ethereum")
|
||||
* @returns The HTTP RPC URL for the Ethereum node
|
||||
*/
|
||||
export const getAnvilRpcUrl = async (enclaveName = "datahaven-ethereum"): Promise<string> => {
|
||||
try {
|
||||
logger.debug("Getting Anvil RPC URL from Kurtosis...");
|
||||
|
||||
// Get the RPC port from the EL (Execution Layer) service
|
||||
const rpcPort = await getPortFromKurtosis("el-1-reth-lodestar", "rpc", enclaveName);
|
||||
|
||||
const rpcUrl = `http://127.0.0.1:${rpcPort}`;
|
||||
logger.debug(`Anvil RPC URL: ${rpcUrl}`);
|
||||
|
||||
return rpcUrl;
|
||||
} catch (error) {
|
||||
logger.warn(`⚠️ Failed to get Anvil RPC URL from Kurtosis: ${error}`);
|
||||
logger.warn(" Falling back to default http://localhost:8545");
|
||||
return "http://localhost:8545";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the WebSocket URL for the Anvil (local Ethereum) node running in Kurtosis
|
||||
* @param enclaveName - The name of the Kurtosis enclave (default: "datahaven-ethereum")
|
||||
* @returns The WebSocket URL for the Ethereum node
|
||||
*/
|
||||
export const getAnvilWsUrl = async (enclaveName = "datahaven-ethereum"): Promise<string> => {
|
||||
try {
|
||||
logger.debug("Getting Anvil WebSocket URL from Kurtosis...");
|
||||
|
||||
// Get the WS port from the EL (Execution Layer) service
|
||||
const wsPort = await getPortFromKurtosis("el-1-reth-lodestar", "ws", enclaveName);
|
||||
|
||||
const wsUrl = `ws://127.0.0.1:${wsPort}`;
|
||||
logger.debug(`Anvil WebSocket URL: ${wsUrl}`);
|
||||
|
||||
return wsUrl;
|
||||
} catch (error) {
|
||||
logger.warn(`⚠️ Failed to get Anvil WebSocket URL from Kurtosis: ${error}`);
|
||||
logger.warn(" Falling back to default ws://localhost:8546");
|
||||
return "ws://localhost:8546";
|
||||
}
|
||||
};
|
||||
|
|
@ -11,6 +11,7 @@ const ethAddressCustom = z.custom<`0x${string}`>(
|
|||
(val) => typeof val === "string" && ethAddressRegex.test(val),
|
||||
{ message: "Invalid Ethereum address" }
|
||||
);
|
||||
|
||||
const DeployedStrategySchema = z.object({
|
||||
address: ethAddress,
|
||||
underlyingToken: ethAddress,
|
||||
|
|
@ -24,6 +25,7 @@ const DeploymentsSchema = z.object({
|
|||
Gateway: ethAddressCustom,
|
||||
ServiceManager: ethAddressCustom,
|
||||
ServiceManagerImplementation: ethAddressCustom,
|
||||
RewardsAgent: ethAddressCustom,
|
||||
DelegationManager: ethAddressCustom,
|
||||
StrategyManager: ethAddressCustom,
|
||||
AVSDirectory: ethAddressCustom,
|
||||
|
|
@ -34,6 +36,25 @@ const DeploymentsSchema = z.object({
|
|||
PermissionController: ethAddressCustom,
|
||||
ETHPOSDeposit: ethAddressCustom.optional(),
|
||||
BaseStrategyImplementation: ethAddressCustom.optional(),
|
||||
ProxyAdmin: ethAddressCustom.optional(),
|
||||
// Version tag for this set of deployed contracts (optional for backwards compatibility)
|
||||
version: z.string().optional(),
|
||||
deps: z
|
||||
.object({
|
||||
eigenlayer: z
|
||||
.object({
|
||||
release: z.string().optional(),
|
||||
gitCommit: z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
snowbridge: z
|
||||
.object({
|
||||
release: z.string().optional(),
|
||||
gitCommit: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional(),
|
||||
DeployedStrategies: z.array(DeployedStrategySchema).optional()
|
||||
});
|
||||
|
||||
|
|
@ -80,8 +101,12 @@ const abiMap = {
|
|||
PermissionController: generated.permissionControllerAbi,
|
||||
ETHPOSDeposit: generated.iethposDepositAbi,
|
||||
BaseStrategyImplementation: generated.strategyBaseTvlLimitsAbi,
|
||||
ProxyAdmin: generated.proxyAdminAbi,
|
||||
DeployedStrategies: erc20Abi
|
||||
} as const satisfies Record<keyof Omit<Deployments, "network">, Abi>;
|
||||
} as const satisfies Record<
|
||||
keyof Omit<Deployments, "network" | "version" | "deps" | "RewardsAgent">,
|
||||
Abi
|
||||
>;
|
||||
|
||||
type ContractName = keyof typeof abiMap;
|
||||
type AbiFor<C extends ContractName> = (typeof abiMap)[C];
|
||||
|
|
|
|||
180
test/utils/contracts/versioning.ts
Normal file
180
test/utils/contracts/versioning.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CHAIN_CONFIGS } from "configs/contracts/config";
|
||||
import { logger } from "utils";
|
||||
import { getContractInstance } from "utils/contracts";
|
||||
import type { ViemClientInterface } from "utils/viem";
|
||||
import { createWalletClient, defineChain, http, publicActions } from "viem";
|
||||
|
||||
export interface ContractVersionCheckResult {
|
||||
ok: boolean;
|
||||
skipped: boolean;
|
||||
}
|
||||
|
||||
const assertValidChain = (chain: string) => {
|
||||
const supportedChains = ["hoodi", "ethereum", "anvil"];
|
||||
if (!supportedChains.includes(chain)) {
|
||||
throw new Error(`Unsupported chain: ${chain}. Supported chains: ${supportedChains.join(", ")}`);
|
||||
}
|
||||
};
|
||||
|
||||
const isInfraUnavailableError = (error: unknown): boolean => {
|
||||
const message =
|
||||
error instanceof Error ? error.message : typeof error === "string" ? error : String(error);
|
||||
|
||||
return (
|
||||
message.includes("Failed to connect to Docker daemon") ||
|
||||
(message.includes("container") &&
|
||||
message.includes("cannot be found in running container list")) ||
|
||||
message.includes("ECONNREFUSED") ||
|
||||
message.includes("ECONNRESET") ||
|
||||
message.includes("ENOTFOUND") ||
|
||||
message.includes("EHOSTUNREACH") ||
|
||||
message.includes("Was there a typo in the url or port?")
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the expected version from contracts/VERSION file.
|
||||
* Returns undefined if the file cannot be read.
|
||||
*/
|
||||
const readVersionFile = (): string | undefined => {
|
||||
try {
|
||||
const cwd = process.cwd();
|
||||
const repoRoot = path.basename(cwd) === "test" ? path.join(cwd, "..") : cwd;
|
||||
const versionFile = path.join(repoRoot, "contracts", "VERSION");
|
||||
return readFileSync(versionFile, "utf8").trim();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkContractVersions = async (
|
||||
chain: string,
|
||||
rpcUrl?: string
|
||||
): Promise<ContractVersionCheckResult> => {
|
||||
assertValidChain(chain);
|
||||
logger.info(`🔍 Checking contract versions for chain '${chain}'`);
|
||||
|
||||
// Read expected version from contracts/VERSION file
|
||||
const version = readVersionFile();
|
||||
if (!version) {
|
||||
logger.info(
|
||||
"ℹ️ Could not read contracts/VERSION - skipping version check (probably fresh deployment)"
|
||||
);
|
||||
return { ok: true, skipped: true };
|
||||
}
|
||||
|
||||
let viemClient: ViemClientInterface | undefined;
|
||||
const chainConfig = CHAIN_CONFIGS[chain as keyof typeof CHAIN_CONFIGS];
|
||||
if (chainConfig && chain !== "anvil") {
|
||||
const chainDef = defineChain({
|
||||
id: chainConfig.CHAIN_ID,
|
||||
name: chainConfig.NETWORK_NAME,
|
||||
nativeCurrency: {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: {
|
||||
default: {
|
||||
http: [rpcUrl ?? chainConfig.RPC_URL]
|
||||
}
|
||||
},
|
||||
blockExplorers: chainConfig.BLOCK_EXPLORER
|
||||
? {
|
||||
default: { name: "Explorer", url: chainConfig.BLOCK_EXPLORER }
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
viemClient = createWalletClient({
|
||||
chain: chainDef,
|
||||
transport: http()
|
||||
}).extend(publicActions) as unknown as ViemClientInterface;
|
||||
}
|
||||
|
||||
let ok = true;
|
||||
|
||||
try {
|
||||
const serviceManager: any = await getContractInstance("ServiceManager", viemClient, chain);
|
||||
const smVersion: string = await serviceManager.read.DATAHAVEN_VERSION();
|
||||
|
||||
if (smVersion !== version) {
|
||||
logger.error(
|
||||
`❌ DataHavenServiceManager DATAHAVEN_VERSION=${smVersion} does not match contracts/VERSION=${version} for chain='${chain}'.`
|
||||
);
|
||||
ok = false;
|
||||
} else {
|
||||
logger.info(
|
||||
`✅ DataHavenServiceManager version matches contracts/VERSION (${version}) for chain='${chain}'.`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isInfraUnavailableError(error)) {
|
||||
logger.warn(
|
||||
`⚠️ Skipping on-chain version checks for chain='${chain}': no local Ethereum node or containers detected (${error}).`
|
||||
);
|
||||
return { ok: true, skipped: true };
|
||||
}
|
||||
const errorMsg = String(error);
|
||||
|
||||
// Check if function doesn't exist (old deployment without version tracking)
|
||||
if (
|
||||
errorMsg.includes("DATAHAVEN_VERSION") &&
|
||||
(errorMsg.includes("returned no data") || errorMsg.includes("does not have the function"))
|
||||
) {
|
||||
logger.warn(
|
||||
`⚠️ ServiceManager at ${chain} does not have DATAHAVEN_VERSION() function yet (old deployment). Skipping on-chain version check.`
|
||||
);
|
||||
return { ok: true, skipped: true };
|
||||
}
|
||||
|
||||
if (
|
||||
errorMsg.includes("DATAHAVEN_VERSION") &&
|
||||
(errorMsg.includes("reverted") || errorMsg.includes("missing revert data"))
|
||||
) {
|
||||
throw new Error(
|
||||
`ServiceManager at ${chain} does not expose DATAHAVEN_VERSION() yet. ` +
|
||||
"This usually means the on-chain implementation is older than the versioning update. " +
|
||||
"Upgrade the ServiceManager implementation, then re-run the check."
|
||||
);
|
||||
}
|
||||
throw new Error(`Failed to read version from DataHavenServiceManager: ${error}`);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
return { ok: false, skipped: false };
|
||||
}
|
||||
|
||||
logger.info(`✅ All checked contract versions match contracts/VERSION=${version} on '${chain}'.`);
|
||||
return { ok: true, skipped: false };
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that a version string follows semantic versioning (X.Y.Z)
|
||||
*/
|
||||
export const isValidSemver = (version: string): boolean => {
|
||||
const semverRegex = /^\d+\.\d+\.\d+$/;
|
||||
return semverRegex.test(version);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that contracts/VERSION contains a valid semver string
|
||||
*/
|
||||
export const validateVersionFormats = async (): Promise<boolean> => {
|
||||
const version = readVersionFile();
|
||||
|
||||
if (!version) {
|
||||
logger.warn("⚠️ Could not read contracts/VERSION");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidSemver(version)) {
|
||||
logger.error(`❌ Invalid version format in contracts/VERSION: ${version}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info(`✅ contracts/VERSION is valid semver: ${version}`);
|
||||
return true;
|
||||
};
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
export * from "./anvil";
|
||||
export * from "./blockscout";
|
||||
export * from "./constants";
|
||||
export * from "./contracts";
|
||||
export * from "./contracts/versioning";
|
||||
export * from "./docker";
|
||||
export * from "./events";
|
||||
export * from "./input";
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ export default defineConfig({
|
|||
"DelegationManager.sol/**",
|
||||
"PermissionController.sol/**",
|
||||
"IETHPOSDeposit.sol/**",
|
||||
"StrategyBaseTVLLimits.sol/**"
|
||||
"StrategyBaseTVLLimits.sol/**",
|
||||
"ProxyAdmin.sol/**"
|
||||
]
|
||||
})
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue