Merge branch 'main' into feat/add-validator-submitter-ci-job

This commit is contained in:
Ahmad Kaouk 2026-03-03 16:09:03 +01:00 committed by GitHub
commit 9c14c2fcdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 6688 additions and 19578 deletions

View file

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

@ -0,0 +1 @@
0.20.0

View file

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

View file

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

View file

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

View file

@ -1 +1 @@
74018d5581304551932388025aebb9508b907e22
d7d30510de741750e5b2069228eb2b037f20cc22

File diff suppressed because one or more lines are too long

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -197,7 +197,8 @@ contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
rewardsInitiator,
emptyStrategies,
address(snowbridgeGatewayMock),
address(0)
address(0),
"v-test"
)
)
)

View file

@ -249,7 +249,8 @@ contract AVSDeployer is Test {
rewardsInitiator,
defaultStrategyAndMultipliers,
address(snowbridgeGatewayMock),
avsOwner
avsOwner,
"v-mock"
)
)
)

22
operator/Cargo.lock generated
View file

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

View file

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

View 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"] }

View 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(())
}
}

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

View file

@ -24,7 +24,8 @@ export default defineConfig({
"DelegationManager.sol/**",
"PermissionController.sol/**",
"IETHPOSDeposit.sol/**",
"StrategyBaseTVLLimits.sol/**"
"StrategyBaseTVLLimits.sol/**",
"ProxyAdmin.sol/**"
]
})
]