mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
feat(contracts): ✨ add set up validator script and execute it when starting integration tests (#47)
This PR adds the `setup-validators` Typescript script that, given an already started up network, sets up a new validator set and sends it through Snowbridge's Gateway to the solochain. To accomplish that purpose, this PR: - Modifies the `DeployLocal` script to save in the `anvil.json` file not only the deployed strategies' addresses but also the owner of each strategy's underlying token. This owner is used as the source of funds to transfer tokens to other validators so they can register under that strategy. - Adds an `OPERATOR_SOLOCHAIN_ADDRESS` to the `Accounts` utility script contract. This address is the one used as the Solochain address when registering a new Operator. - Updates the `SignUpOperator` (which I believe is now deprecated since we have multiple Operator Sets) and `SignUpOperatorBase` scripts to adapt to both aforementioned changes. - Updates the `ELScriptStorage` script to save the new extra information of each deployed strategy (the creator of the underlying token) in storage. - Adds a `validator-set.json` file which contains the validators that should be registered in the AVS and sent to the Solochain network through the Snowbridge Gateway when starting any integration test. This is currently hardcoded but could be generated in any other way, giving us flexibility for testing. - Adds both a Markdown file and a Excalidraw diagram showcasing both how the setup of integration tests work and possible integration tests that will be added in a future PR. This list is not exahustive as there are many more scenarios we will want to test using integration tests. - Updates the `e2e-cli.ts` script to execute the validator setup when bootstrapping the network used for integration testing. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
This commit is contained in:
parent
6ba0476e3f
commit
80a0138f00
15 changed files with 1106 additions and 184 deletions
|
|
@ -67,6 +67,13 @@ struct ServiceManagerInitParams {
|
|||
address gateway;
|
||||
}
|
||||
|
||||
// Struct to store more detailed strategy information
|
||||
struct StrategyInfo {
|
||||
address address_;
|
||||
address underlyingToken;
|
||||
address tokenCreator;
|
||||
}
|
||||
|
||||
contract Deploy is Script, DeployParams, Accounts {
|
||||
// Progress indicator
|
||||
uint16 public deploymentStep = 0;
|
||||
|
|
@ -91,7 +98,7 @@ contract Deploy is Script, DeployParams, Accounts {
|
|||
UpgradeableBeacon public eigenPodBeacon;
|
||||
EigenPod public eigenPodImplementation;
|
||||
StrategyBaseTVLLimits public baseStrategyImplementation;
|
||||
StrategyBaseTVLLimits[] public deployedStrategies;
|
||||
StrategyInfo[] public deployedStrategies;
|
||||
IETHPOSDeposit public ethPOSDeposit;
|
||||
|
||||
// EigenLayer required semver
|
||||
|
|
@ -542,14 +549,21 @@ contract Deploy is Script, DeployParams, Accounts {
|
|||
)
|
||||
);
|
||||
|
||||
deployedStrategies.push(strategy);
|
||||
// Store the strategy with its token information
|
||||
deployedStrategies.push(
|
||||
StrategyInfo({
|
||||
address_: address(strategy),
|
||||
underlyingToken: testToken,
|
||||
tokenCreator: _operator
|
||||
})
|
||||
);
|
||||
Logging.logContractDeployed("Test Strategy", address(strategy));
|
||||
}
|
||||
|
||||
// Whitelist strategies in the strategy manager
|
||||
IStrategy[] memory strategies = new IStrategy[](deployedStrategies.length);
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
strategies[i] = IStrategy(deployedStrategies[i]);
|
||||
strategies[i] = IStrategy(deployedStrategies[i].address_);
|
||||
}
|
||||
vm.broadcast(_operationsMultisigPrivateKey);
|
||||
strategyManager.addStrategiesToDepositWhitelist(strategies);
|
||||
|
|
@ -640,7 +654,7 @@ contract Deploy is Script, DeployParams, Accounts {
|
|||
);
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
Logging.logContractDeployed(
|
||||
string.concat("DeployedStrategy", vm.toString(i)), address(deployedStrategies[i])
|
||||
string.concat("DeployedStrategy", vm.toString(i)), deployedStrategies[i].address_
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -695,12 +709,27 @@ contract Deploy is Script, DeployParams, Accounts {
|
|||
vm.toString(address(baseStrategyImplementation)),
|
||||
'"'
|
||||
);
|
||||
|
||||
// Add strategies with token information
|
||||
if (deployedStrategies.length > 0) {
|
||||
json = string.concat(json, ",");
|
||||
json = string.concat(json, '"DeployedStrategies": [');
|
||||
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
json = string.concat(json, '"', vm.toString(address(deployedStrategies[i])), '"');
|
||||
json = string.concat(json, "{");
|
||||
json = string.concat(
|
||||
json, '"address": "', vm.toString(deployedStrategies[i].address_), '",'
|
||||
);
|
||||
json = string.concat(
|
||||
json,
|
||||
'"underlyingToken": "',
|
||||
vm.toString(deployedStrategies[i].underlyingToken),
|
||||
'",'
|
||||
);
|
||||
json = string.concat(
|
||||
json, '"tokenCreator": "', vm.toString(deployedStrategies[i].tokenCreator), '"'
|
||||
);
|
||||
json = string.concat(json, "}");
|
||||
|
||||
// Add comma if not the last element
|
||||
if (i < deployedStrategies.length - 1) {
|
||||
|
|
@ -814,16 +843,16 @@ contract Deploy is Script, DeployParams, Accounts {
|
|||
|
||||
function _prepareStrategiesForServiceManager(
|
||||
AVSConfig memory config,
|
||||
StrategyBaseTVLLimits[] memory strategies
|
||||
StrategyInfo[] memory strategies
|
||||
) internal pure {
|
||||
if (config.validatorsStrategies.length == 0) {
|
||||
config.validatorsStrategies = new address[](strategies.length);
|
||||
config.bspsStrategies = new address[](strategies.length);
|
||||
config.mspsStrategies = new address[](strategies.length);
|
||||
for (uint256 i = 0; i < strategies.length; i++) {
|
||||
config.validatorsStrategies[i] = address(strategies[i]);
|
||||
config.bspsStrategies[i] = address(strategies[i]);
|
||||
config.mspsStrategies[i] = address(strategies[i]);
|
||||
config.validatorsStrategies[i] = strategies[i].address_;
|
||||
config.bspsStrategies[i] = strategies[i].address_;
|
||||
config.mspsStrategies[i] = strategies[i].address_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.27;
|
||||
|
||||
// Testing imports
|
||||
import {Script} from "forge-std/Script.sol";
|
||||
import {console} from "forge-std/console.sol";
|
||||
import {Logging} from "../utils/Logging.sol";
|
||||
import {ELScriptStorage} from "../utils/ELScriptStorage.s.sol";
|
||||
import {DHScriptStorage} from "../utils/DHScriptStorage.s.sol";
|
||||
import {Accounts} from "../utils/Accounts.sol";
|
||||
|
||||
// EigenLayer imports
|
||||
import {IAllocationManagerTypes} from
|
||||
"eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
|
||||
import {StrategyBase} from "eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol";
|
||||
|
||||
// OpenZeppelin imports
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
contract SignUpOperator is Script, ELScriptStorage, DHScriptStorage, Accounts {
|
||||
// Progress indicator
|
||||
uint16 public deploymentStep = 0;
|
||||
uint16 public totalSteps = 3; // Total major steps
|
||||
|
||||
function _logProgress() internal {
|
||||
deploymentStep++;
|
||||
Logging.logProgress(deploymentStep, totalSteps);
|
||||
}
|
||||
|
||||
function run() public {
|
||||
string memory network = vm.envOr("NETWORK", string("anvil"));
|
||||
Logging.logHeader("SIGN UP DATAHAVEN OPERATOR");
|
||||
console.log("| Network: %s", network);
|
||||
console.log("| Timestamp: %s", vm.toString(block.timestamp));
|
||||
Logging.logFooter();
|
||||
|
||||
// Read addresses of latest deployment of EigenLayer contracts, for the given network.
|
||||
_loadELContracts(network);
|
||||
Logging.logInfo(string.concat("Loaded EigenLayer contracts for network: ", network));
|
||||
|
||||
// Read addresses of latest deployment of DataHaven contracts, for the given network.
|
||||
_loadDHContracts(network);
|
||||
Logging.logInfo(string.concat("Loaded DataHaven contracts for network: ", network));
|
||||
|
||||
_logProgress();
|
||||
|
||||
// STEP 1: Stake tokens into strategies
|
||||
Logging.logSection("Staking Tokens into Strategies");
|
||||
|
||||
// Get the deployed strategies and deposit some of the operator's balance into them.
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
IERC20 linkedToken = StrategyBase(deployedStrategies[i]).underlyingToken();
|
||||
|
||||
// Check that the operator has a balance of the linked token.
|
||||
uint256 balance = linkedToken.balanceOf(_operator);
|
||||
Logging.logInfo(
|
||||
string.concat(
|
||||
"Strategy ",
|
||||
vm.toString(i),
|
||||
" underlying token: ",
|
||||
vm.toString(address(linkedToken)),
|
||||
" - Operator balance: ",
|
||||
vm.toString(balance)
|
||||
)
|
||||
);
|
||||
|
||||
require(balance > 0, "Operator does not have a balance of the linked token");
|
||||
|
||||
// Stake some of the operator's balance as stake for the strategy.
|
||||
vm.startBroadcast(_operatorPrivateKey);
|
||||
uint256 balanceToStake = balance / 10;
|
||||
IERC20(linkedToken).approve(address(strategyManager), balanceToStake);
|
||||
strategyManager.depositIntoStrategy(deployedStrategies[i], linkedToken, balanceToStake);
|
||||
vm.stopBroadcast();
|
||||
|
||||
Logging.logStep(
|
||||
string.concat(
|
||||
"Staked ", vm.toString(balanceToStake), " tokens for strategy ", vm.toString(i)
|
||||
)
|
||||
);
|
||||
}
|
||||
_logProgress();
|
||||
|
||||
// STEP 2: Register as an operator in EigenLayer
|
||||
Logging.logSection("Registering as EigenLayer Operator");
|
||||
|
||||
// Register the operator as an operator.
|
||||
// We don't set a delegation approver, so that there is no need to sign any messages.
|
||||
address initDelegationApprover = address(0);
|
||||
uint32 allocationDelay = 0;
|
||||
string memory metadataURI = "";
|
||||
vm.broadcast(_operatorPrivateKey);
|
||||
delegation.registerAsOperator(initDelegationApprover, allocationDelay, metadataURI);
|
||||
Logging.logStep(
|
||||
string.concat("Registered operator in EigenLayer: ", vm.toString(_operator))
|
||||
);
|
||||
|
||||
// Check the staked balance of the operator.
|
||||
Logging.logSection("Operator Shares Information");
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
uint256 operatorShares = delegation.operatorShares(_operator, deployedStrategies[i]);
|
||||
Logging.logInfo(
|
||||
string.concat(
|
||||
"Operator shares for strategy ",
|
||||
vm.toString(i),
|
||||
": ",
|
||||
vm.toString(operatorShares)
|
||||
)
|
||||
);
|
||||
}
|
||||
_logProgress();
|
||||
|
||||
// STEP 3: Register as a DataHaven operator
|
||||
Logging.logSection("Registering as DataHaven Operator");
|
||||
|
||||
// Register the operator as operator for the DataHaven service.
|
||||
IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes
|
||||
.RegisterParams({avs: address(serviceManager), operatorSetIds: new uint32[](1), data: ""});
|
||||
|
||||
vm.broadcast(_operatorPrivateKey);
|
||||
allocationManager.registerForOperatorSets(_operator, registerParams);
|
||||
Logging.logStep("Registered operator in DataHaven service");
|
||||
|
||||
// // STEP 4: Demonstrate deregistration (for testing purposes)
|
||||
// Logging.logSection("Deregistering from DataHaven (Demo)");
|
||||
|
||||
// IAllocationManagerTypes.DeregisterParams memory deregisterParams = IAllocationManagerTypes.DeregisterParams({
|
||||
// avs: address(serviceManager),
|
||||
// operator: _operator,
|
||||
// operatorSetIds: new uint32[](1)
|
||||
// });
|
||||
// vm.broadcast(_operatorPrivateKey);
|
||||
// allocationManager.deregisterFromOperatorSets(deregisterParams);
|
||||
// Logging.logStep("Deregistered operator from DataHaven service (for demonstration)");
|
||||
|
||||
Logging.logHeader("OPERATOR SETUP COMPLETE");
|
||||
Logging.logInfo(string.concat("Operator: ", vm.toString(_operator)));
|
||||
Logging.logInfo("Successfully configured operator for DataHaven");
|
||||
Logging.logFooter();
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ abstract contract SignUpOperatorBase is Script, ELScriptStorage, DHScriptStorage
|
|||
|
||||
// Get the deployed strategies and deposit some of the operator's balance into them.
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
IERC20 linkedToken = StrategyBase(deployedStrategies[i]).underlyingToken();
|
||||
IERC20 linkedToken = StrategyBase(deployedStrategies[i].strategy).underlyingToken();
|
||||
|
||||
// Check that the operator has a balance of the linked token.
|
||||
uint256 balance = linkedToken.balanceOf(_operator);
|
||||
|
|
@ -91,7 +91,9 @@ abstract contract SignUpOperatorBase is Script, ELScriptStorage, DHScriptStorage
|
|||
vm.startBroadcast(_operatorPrivateKey);
|
||||
uint256 balanceToStake = balance / 10;
|
||||
linkedToken.approve(address(strategyManager), balanceToStake);
|
||||
strategyManager.depositIntoStrategy(deployedStrategies[i], linkedToken, balanceToStake);
|
||||
strategyManager.depositIntoStrategy(
|
||||
deployedStrategies[i].strategy, linkedToken, balanceToStake
|
||||
);
|
||||
vm.stopBroadcast();
|
||||
|
||||
Logging.logStep(
|
||||
|
|
@ -119,7 +121,8 @@ abstract contract SignUpOperatorBase is Script, ELScriptStorage, DHScriptStorage
|
|||
// Check the staked balance of the operator.
|
||||
Logging.logSection("Operator Shares Information");
|
||||
for (uint256 i = 0; i < deployedStrategies.length; i++) {
|
||||
uint256 operatorShares = delegation.operatorShares(_operator, deployedStrategies[i]);
|
||||
uint256 operatorShares =
|
||||
delegation.operatorShares(_operator, deployedStrategies[i].strategy);
|
||||
Logging.logInfo(
|
||||
string.concat(
|
||||
"Operator shares for strategy ",
|
||||
|
|
@ -146,7 +149,11 @@ abstract contract SignUpOperatorBase is Script, ELScriptStorage, DHScriptStorage
|
|||
uint32[] memory operatorSetIds = new uint32[](1);
|
||||
operatorSetIds[0] = _getOperatorSetId();
|
||||
IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes
|
||||
.RegisterParams({avs: address(serviceManager), operatorSetIds: operatorSetIds, data: ""});
|
||||
.RegisterParams({
|
||||
avs: address(serviceManager),
|
||||
operatorSetIds: operatorSetIds,
|
||||
data: abi.encodePacked(_operatorSolochainAddress)
|
||||
});
|
||||
|
||||
vm.broadcast(_operatorPrivateKey);
|
||||
allocationManager.registerForOperatorSets(_operator, registerParams);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ contract Accounts is Script {
|
|||
uint256(0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d) // Second pre-funded account from Anvil
|
||||
);
|
||||
address internal _operator = vm.addr(_operatorPrivateKey);
|
||||
bytes32 internal _operatorSolochainAddress = vm.envOr(
|
||||
"OPERATOR_SOLOCHAIN_ADDRESS",
|
||||
bytes32(0x000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266) // Placeholder
|
||||
);
|
||||
|
||||
uint256 internal _executorMultisigPrivateKey = vm.envOr(
|
||||
"EXECUTOR_MULTISIG_PRIVATE_KEY",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,19 @@ import {StrategyBaseTVLLimits} from
|
|||
"eigenlayer-contracts/src/contracts/strategies/StrategyBaseTVLLimits.sol";
|
||||
import {IETHPOSDeposit} from "eigenlayer-contracts/src/contracts/interfaces/IETHPOSDeposit.sol";
|
||||
|
||||
// Struct used in the deployment JSON file to store detailed strategy information
|
||||
struct DeployedStrategyJson {
|
||||
address strategyAddress;
|
||||
address strategyUnderlyingToken;
|
||||
address strategyTokenCreator;
|
||||
}
|
||||
|
||||
// Struct used here to store strategy information
|
||||
struct DeployedStrategyInfo {
|
||||
StrategyBaseTVLLimits strategy;
|
||||
address strategyTokenCreator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title ELScriptStorage
|
||||
* @notice This contract is a utility for scripts that need to interact with EigenLayer contracts.
|
||||
|
|
@ -33,7 +46,7 @@ contract ELScriptStorage is Script {
|
|||
EigenPodManager public eigenPodManager;
|
||||
EigenPod public eigenPodBeacon;
|
||||
StrategyBaseTVLLimits public baseStrategy;
|
||||
StrategyBaseTVLLimits[] public deployedStrategies;
|
||||
DeployedStrategyInfo[] public deployedStrategies;
|
||||
IETHPOSDeposit public ethPOSDeposit;
|
||||
|
||||
// EigenLayer required semver
|
||||
|
|
@ -64,10 +77,17 @@ contract ELScriptStorage is Script {
|
|||
vm.parseJsonAddress(deploymentFile, ".BaseStrategyImplementation")
|
||||
);
|
||||
ethPOSDeposit = IETHPOSDeposit(vm.parseJsonAddress(deploymentFile, ".ETHPOSDeposit"));
|
||||
address[] memory deployedStrategiesAddresses =
|
||||
vm.parseJsonAddressArray(deploymentFile, ".DeployedStrategies");
|
||||
for (uint256 i = 0; i < deployedStrategiesAddresses.length; i++) {
|
||||
deployedStrategies.push(StrategyBaseTVLLimits(deployedStrategiesAddresses[i]));
|
||||
bytes memory deployedStrategiesArrayData =
|
||||
vm.parseJson(deploymentFile, ".DeployedStrategies");
|
||||
DeployedStrategyJson[] memory strategies =
|
||||
abi.decode(deployedStrategiesArrayData, (DeployedStrategyJson[]));
|
||||
for (uint256 i = 0; i < strategies.length; i++) {
|
||||
address strategyAddress = strategies[i].strategyAddress;
|
||||
address strategyTokenCreator = strategies[i].strategyTokenCreator;
|
||||
DeployedStrategyInfo memory strategyInfo;
|
||||
strategyInfo.strategy = StrategyBaseTVLLimits(strategyAddress);
|
||||
strategyInfo.strategyTokenCreator = strategyTokenCreator;
|
||||
deployedStrategies.push(strategyInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,21 +5,21 @@
|
|||
"name": "test",
|
||||
"dependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@dotenvx/dotenvx": "^1.39.0",
|
||||
"@types/dockerode": "^3.3.37",
|
||||
"@dotenvx/dotenvx": "^1.41.0",
|
||||
"@types/dockerode": "^3.3.38",
|
||||
"chalk": "^5.4.1",
|
||||
"dockerode": "^4.0.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"dockerode": "^4.0.6",
|
||||
"dotenv": "^16.5.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"viem": "^2.25.0",
|
||||
"viem": "^2.28.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
"typescript": "^5.8.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
|
||||
|
||||
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.39.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^16.4.5", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js", "git-dotenvx": "src/cli/dotenvx.js" } }, "sha512-qGfDpL/3S17MQYXpR3HkBS5xNQ7wiFlqLdpr+iIQzv17aMRcSlgL4EjMIsYFZ540Dq17J+y5FVElA1AkVoXiUA=="],
|
||||
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.41.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^16.4.5", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js", "git-dotenvx": "src/cli/dotenvx.js" } }, "sha512-lFZOSKLM2/Jm7FXYUIvnciUhMsuEatyxCgau4lnjDD59LaSYiaNLjyjnUL/aYpH1+iaDhD37+mPOzH9kBZlUJQ=="],
|
||||
|
||||
"@ecies/ciphers": ["@ecies/ciphers@0.2.3", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-tapn6XhOueMwht3E2UzY0ZZjYokdaw9XtL9kEyjhQ/Fb9vL9xTFbOaI+fV0AWvTpYu4BNloC6getKW6NtSg4mA=="],
|
||||
|
||||
|
|
@ -58,9 +58,9 @@
|
|||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
|
||||
"@noble/curves": ["@noble/curves@1.8.2", "", { "dependencies": { "@noble/hashes": "1.7.2" } }, "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
|
||||
"@noble/hashes": ["@noble/hashes@1.7.2", "", {}, "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
|
|
@ -88,18 +88,16 @@
|
|||
|
||||
"@scure/bip39": ["@scure/bip39@1.5.4", "", { "dependencies": { "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.4" } }, "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="],
|
||||
"@types/bun": ["@types/bun@1.2.10", "", { "dependencies": { "bun-types": "1.2.10" } }, "sha512-eilv6WFM3M0c9ztJt7/g80BDusK98z/FrFwseZgT4bXCq2vPhXD4z8R3oddmAn+R/Nmz9vBn4kweJKmGTZj+lg=="],
|
||||
|
||||
"@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="],
|
||||
|
||||
"@types/dockerode": ["@types/dockerode@3.3.37", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-r+IoKpE5MLKaeD8CvoEh39ckWMLHR/+WBMoRQxrkL+apJqEWLMhBHh+93KIfyPWGd6gK7Q21jpoULKgNoRI0YA=="],
|
||||
"@types/dockerode": ["@types/dockerode@3.3.38", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="],
|
||||
|
||||
"@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"abitype": ["abitype@1.0.8", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
|
@ -120,7 +118,7 @@
|
|||
|
||||
"buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="],
|
||||
"bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="],
|
||||
|
||||
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
|
|
@ -146,9 +144,9 @@
|
|||
|
||||
"docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="],
|
||||
|
||||
"dockerode": ["dockerode@4.0.5", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA=="],
|
||||
"dockerode": ["dockerode@4.0.6", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-FbVf3Z8fY/kALB9s+P9epCpWhfi/r0N2DgYYcYpsAUlaTxPjdsitsFobnltb+lyCgAIvf9C+4PSWlTnHlJMf1w=="],
|
||||
|
||||
"dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
|
||||
"dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="],
|
||||
|
||||
"eciesjs": ["eciesjs@0.4.14", "", { "dependencies": { "@ecies/ciphers": "^0.2.2", "@noble/ciphers": "^1.0.0", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0" } }, "sha512-eJAgf9pdv214Hn98FlUzclRMYWF7WfoLlkS9nWMTm1qcCwn6Ad4EGD9lr9HXMBfSrZhYQujRE+p0adPRkctC6A=="],
|
||||
|
||||
|
|
@ -300,7 +298,7 @@
|
|||
|
||||
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
||||
|
||||
"viem": ["viem@2.25.0", "", { "dependencies": { "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", "ox": "0.6.9", "ws": "8.18.1" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-TtFgfQkZOfb642s8+i+h27dRhBfZV//WWOkZ9saoS1Ml8kipj9RiOiDaSmAUly1rhq9kbnYhni1xVtb195XVGA=="],
|
||||
"viem": ["viem@2.28.0", "", { "dependencies": { "@noble/curves": "1.8.2", "@noble/hashes": "1.7.2", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", "ox": "0.6.9", "ws": "8.18.1" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-Z4W5O1pe+6pirYTFm451FcZmfGAUxUWt2L/eWC+YfTF28j/8rd7q6MBAi05lMN4KhLJjhN0s5YGIPB+kf1L20g=="],
|
||||
|
||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||
|
||||
|
|
@ -316,10 +314,24 @@
|
|||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"@scure/bip32/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
|
||||
|
||||
"@scure/bip32/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
|
||||
|
||||
"@scure/bip39/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
|
||||
|
||||
"@types/ssh2/@types/node": ["@types/node@18.19.86", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"eciesjs/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
|
||||
|
||||
"eciesjs/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
|
||||
|
||||
"ox/@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
|
||||
|
||||
"ox/@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
|
||||
|
||||
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
|
|
|||
BIN
test/bun.lockb
BIN
test/bun.lockb
Binary file not shown.
30
test/configs/validator-set.json
Normal file
30
test/configs/validator-set.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"validators": [
|
||||
{
|
||||
"publicKey": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||
"privateKey": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
|
||||
"solochainAddress": "0x000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
|
||||
},
|
||||
{
|
||||
"publicKey": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
||||
"privateKey": "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
|
||||
"solochainAddress": "0x00000000000000000000000070997970C51812dc3A010C7d01b50e0d17dc79C8"
|
||||
},
|
||||
{
|
||||
"publicKey": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
|
||||
"privateKey": "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
|
||||
"solochainAddress": "0x0000000000000000000000003C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
|
||||
},
|
||||
{
|
||||
"publicKey": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
|
||||
"privateKey": "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
|
||||
"solochainAddress": "0x00000000000000000000000090F79bf6EB2c4f870365E785982E1f101E93b906"
|
||||
},
|
||||
{
|
||||
"publicKey": "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
|
||||
"privateKey": "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
|
||||
"solochainAddress": "0x00000000000000000000000015d34AAf54267DB7D7c367839AAf71A00a2C6A65"
|
||||
}
|
||||
],
|
||||
"notes": "This is a placeholder validator set based on Anvil funded accounts. The solochainAddress fields are dummy placeholders derived from the Ethereum addresses and should be replaced with actual DataHaven chain addresses when full E2E flow is done."
|
||||
}
|
||||
|
|
@ -18,18 +18,18 @@
|
|||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@dotenvx/dotenvx": "^1.39.0",
|
||||
"@types/dockerode": "^3.3.37",
|
||||
"@dotenvx/dotenvx": "^1.41.0",
|
||||
"@types/dockerode": "^3.3.38",
|
||||
"chalk": "^5.4.1",
|
||||
"dockerode": "^4.0.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"dockerode": "^4.0.6",
|
||||
"dotenv": "^16.5.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"viem": "^2.25.0"
|
||||
"viem": "^2.28.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
241
test/resources/datahaven-integration-test-flow.md
Normal file
241
test/resources/datahaven-integration-test-flow.md
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
# DataHaven Integration Test Flow
|
||||
|
||||
This document provides a detailed explanation of the DataHaven integration test flow, complementing the visual diagram in [datahavenBasicTestFlow.png](./datahavenBasicTestFlow.png).
|
||||
|
||||
## Overview
|
||||
|
||||
The integration test flow is designed to be modular, with each step being independently executable. This allows for:
|
||||
|
||||
- Running specific steps without redoing the entire setup
|
||||
- Retrying failed steps without starting from scratch
|
||||
- Testing individual components in isolation
|
||||
- Debugging specific parts of the system
|
||||
|
||||
## 1. Infrastructure Bootstrap (Kurtosis)
|
||||
|
||||
The first step involves setting up the testing infrastructure using Kurtosis, a container orchestration platform for test environments.
|
||||
|
||||
### Components Launched
|
||||
|
||||
- **Ethereum Network**
|
||||
- Execution Layer (EL) clients: reth nodes
|
||||
- Consensus Layer (CL) clients: lighthouse nodes
|
||||
- Block explorer (Blockscout) for monitoring
|
||||
- **DataHaven Solochain**
|
||||
- Multiple validator nodes to form a test network
|
||||
- Based on Substrate
|
||||
- Genesis configuration with initial placeholder validators
|
||||
- **Snowbridge Relayer**
|
||||
- Bridge component connecting Ethereum and DataHaven
|
||||
- Configured with beefy-relay.json and beacon-relay.json
|
||||
|
||||
### Key Commands
|
||||
|
||||
```bash
|
||||
# Start the E2E CLI environment with the minimal configuration
|
||||
bun run scripts/e2e-cli.ts
|
||||
# Alternative
|
||||
bun start:e2e:minimal
|
||||
|
||||
# Start the E2E CLI environment with Blockscout and verified contracts
|
||||
bun start:e2e:verified
|
||||
|
||||
# Behind the scenes both commands run:
|
||||
bun run scripts/launch-kurtosis.ts
|
||||
# And then continue setting up the environment with the next steps.
|
||||
```
|
||||
|
||||
## 2. Ethereum-side Contract Deployment
|
||||
|
||||
After the infrastructure is set up, we deploy all the necessary smart contracts to the Ethereum network.
|
||||
|
||||
### Contracts Deployed
|
||||
|
||||
- **EigenLayer Core Contracts**
|
||||
- Strategy Manager, Delegation Manager, Permission Controller, etc.
|
||||
- **Snowbridge Contracts**
|
||||
- BeefyClient: Verifies BEEFY commitments from DataHaven chain
|
||||
- Gateway: Processes cross-chain messages
|
||||
- AgentExecutor: Executes messages on the destination chain
|
||||
- **DataHaven Contracts**
|
||||
- DataHavenServiceManager: Main contract for managing the DataHaven service
|
||||
- RewardsRegistry: Handles validator rewards
|
||||
- VetoableSlasher: Manages slashing with veto capabilities
|
||||
|
||||
### Initial Configuration
|
||||
|
||||
- Initialize contracts with test accounts
|
||||
- Configure DataHavenServiceManager with initial operator sets
|
||||
- Configure Gateway with appropriate parameters
|
||||
- Configure RewardsRegistry with initial values
|
||||
|
||||
### Key Commands
|
||||
|
||||
```bash
|
||||
# Build and deploy contracts (this is done automatically by the ``e2e-cli`` script if the `--deploy-contracts` flag is set)
|
||||
cd contracts
|
||||
forge build
|
||||
forge script script/deploy/DeployLocal.s.sol --rpc-url <RPC_URL> --broadcast --verify
|
||||
```
|
||||
|
||||
## 3. Validator Registration & Sync
|
||||
|
||||
In this phase, we register validators as operators in EigenLayer and sync the validator set to the DataHaven chain. This process is split into three distinct steps, each of which can be run independently:
|
||||
|
||||
### Steps
|
||||
|
||||
1. **Fund Validators with Tokens**
|
||||
- Use `fund-validators.ts` script to fund validators with necessary tokens
|
||||
- Transfers 5% of creator's tokens to each validator
|
||||
- Transfers 1% of creator's ETH to validators with zero balance
|
||||
- Ensures validators have sufficient funds for operations
|
||||
|
||||
2. **Register Operators in EigenLayer**
|
||||
- Use `setup-validators.ts` script to register validators
|
||||
- Deposits stake and registers for operator sets
|
||||
- Sets up the validator set in the Ethereum side
|
||||
- Configures validator addresses and permissions
|
||||
|
||||
3. **Sync Validator Set to DataHaven**
|
||||
- Use `update-validator-set.ts` script to sync validators
|
||||
- Calls `sendNewValidatorSet` function in the DataHavenServiceManager contract
|
||||
- Sends validator set through Snowbridge Gateway to DataHaven solochain
|
||||
- Updates validator set on the substrate chain
|
||||
|
||||
### Key Commands
|
||||
|
||||
Each script can be run independently and has its own configuration options. The scripts are designed to be idempotent, meaning they can be run multiple times safely.
|
||||
|
||||
```bash
|
||||
# Fund validators with tokens
|
||||
bun run test/scripts/fund-validators.ts --rpc-url <RPC_URL> [--config <CONFIG_PATH>] [--network <NETWORK_NAME>] [--deployment-path <DEPLOYMENT_PATH>]
|
||||
|
||||
# Register validators in EigenLayer
|
||||
bun run test/scripts/setup-validators.ts --rpc-url <RPC_URL> [--config <CONFIG_PATH>] [--network <NETWORK_NAME>] [--deployment-path <DEPLOYMENT_PATH>] [--signup] [--no-signup]
|
||||
|
||||
# Sync validator set to DataHaven
|
||||
bun run test/scripts/update-validator-set.ts --rpc-url <RPC_URL>
|
||||
```
|
||||
|
||||
### CLI Options
|
||||
|
||||
Each script supports various command-line options:
|
||||
|
||||
- `--rpc-url`: (Required) The RPC URL to connect to
|
||||
- `--config`: (Optional) Path to JSON config file with validator addresses
|
||||
- `--network`: (Optional) Network name for default deployment path (defaults to "anvil")
|
||||
- `--deployment-path`: (Optional) Custom deployment path
|
||||
- `--signup`/`--no-signup`: (Optional) For setup-validators.ts, explicitly enable/disable validator registration
|
||||
|
||||
If a step fails, you can simply rerun that specific script without needing to restart the entire process. The scripts are designed to handle partial completion and can be safely rerun.
|
||||
|
||||
## Specific integration tests (TODO)
|
||||
|
||||
### Rewards Epoch Processing
|
||||
|
||||
After validators are registered and synced, we run them for a test epoch to generate rewards.
|
||||
|
||||
#### Process
|
||||
|
||||
1. **Run DataHaven Validators**
|
||||
|
||||
- Validators produce blocks for a configurable epoch
|
||||
- Block production metrics are recorded
|
||||
- System measures performance (blocks produced, attestations, etc.)
|
||||
|
||||
2. **Generate Rewards Merkle Tree**
|
||||
|
||||
- Calculate rewards based on validator performance
|
||||
- Create a merkle tree with validator addresses and their earned points
|
||||
- Root of the tree is used for efficient verification
|
||||
|
||||
3. **Relay Rewards via Snowbridge**
|
||||
- BEEFY message contains the rewards merkle root
|
||||
- Message is sent from DataHaven to Ethereum
|
||||
- Relayer submits proof to the Ethereum network
|
||||
|
||||
#### Testing Aspects
|
||||
|
||||
- Short epochs are configured for testing purposes
|
||||
- Validator performance can be artificially adjusted for testing different scenarios
|
||||
- Reward distribution algorithms are tested for fairness and accuracy
|
||||
|
||||
### Rewards Claiming
|
||||
|
||||
After rewards data is relayed to Ethereum, validators can claim their rewards.
|
||||
|
||||
#### Claiming Process
|
||||
|
||||
1. **Update RewardsRegistry**
|
||||
|
||||
- RewardsRegistry contract receives the merkle root from Snowbridge
|
||||
- Only the authorized agent can update the root
|
||||
- Event `RewardsMerkleRootUpdated` is emitted
|
||||
|
||||
2. **Validators Claim Rewards**
|
||||
|
||||
- Each validator calls `claimOperatorRewards` on ServiceManager
|
||||
- Provides merkle proof of their reward amount
|
||||
- ServiceManager verifies proof against the stored root
|
||||
|
||||
3. **Rewards Distribution Verification**
|
||||
- Check that validators received the correct amount
|
||||
- Verify balances match expected rewards
|
||||
- Test edge cases (zero rewards, maximum rewards)
|
||||
|
||||
#### Key Tests
|
||||
|
||||
- Verify only valid proofs can claim rewards
|
||||
- Ensure rewards can't be double-claimed
|
||||
- Test that rewards distribution is accurate and fair
|
||||
- Verify wrong agents can't update the rewards merkle root
|
||||
|
||||
### Validator Operations Testing
|
||||
|
||||
Another testing scenario is testing the operational aspects of the validator set.
|
||||
|
||||
#### Key Operations Tested
|
||||
|
||||
1. **Adding Validators**
|
||||
|
||||
- Add new validators
|
||||
- Verify they appear in the next session
|
||||
- Ensure they can produce blocks after activation
|
||||
|
||||
2. **Removing Validators**
|
||||
|
||||
- Remove validators and verify they stop producing blocks
|
||||
- Test session transitions after removal
|
||||
- Verify proper cleanup of validator resources
|
||||
|
||||
3. **Slashing Mechanisms**
|
||||
|
||||
- Test slashing for various offenses
|
||||
- Verify VetoableSlasher functions correctly
|
||||
- Test veto committee mechanisms
|
||||
|
||||
4. **Operator Set Modifications**
|
||||
- Modify operator sets from Ethereum
|
||||
- Verify changes propagate to the DataHaven chain
|
||||
- Test stake changes and their effects
|
||||
|
||||
## Communication Patterns
|
||||
|
||||
The integration tests rely on several communication patterns:
|
||||
|
||||
1. **Ethereum to DataHaven**
|
||||
|
||||
- Validator set updates
|
||||
- Configuration changes
|
||||
- Administrative commands
|
||||
|
||||
2. **DataHaven to Ethereum**
|
||||
|
||||
- Rewards information
|
||||
- Validator performance metrics
|
||||
- Block finality information
|
||||
|
||||
3. **Bridge Mechanisms**
|
||||
- BEEFY commitments for security
|
||||
- Agent execution patterns for message delivery
|
||||
- Merkle proofs for data verification
|
||||
BIN
test/resources/datahavenBasicTestFlow.png
Normal file
BIN
test/resources/datahavenBasicTestFlow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 580 KiB |
|
|
@ -1,15 +1,21 @@
|
|||
import { $ } from "bun";
|
||||
import chalk from "chalk";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger, printDivider, printHeader } from "utils";
|
||||
import { logger, printDivider, printHeader, promptWithTimeout } from "utils";
|
||||
import { deployContracts } from "./deploy-contracts";
|
||||
import { fundValidators } from "./fund-validators";
|
||||
import { launchKurtosis } from "./launch-kurtosis";
|
||||
import sendTxn from "./send-txn";
|
||||
import { setupValidators } from "./setup-validators";
|
||||
import { updateValidatorSet } from "./update-validator-set";
|
||||
|
||||
interface ScriptOptions {
|
||||
verified: boolean;
|
||||
launchKurtosis?: boolean;
|
||||
deployContracts?: boolean;
|
||||
fundValidators?: boolean;
|
||||
setupValidators?: boolean;
|
||||
updateValidatorSet?: boolean;
|
||||
blockscout?: boolean;
|
||||
help?: boolean;
|
||||
}
|
||||
|
|
@ -22,6 +28,9 @@ async function main() {
|
|||
verified: args.includes("--verified"),
|
||||
launchKurtosis: parseFlag(args, "launchKurtosis"),
|
||||
deployContracts: parseFlag(args, "deploy-contracts"),
|
||||
fundValidators: parseFlag(args, "fund-validators"),
|
||||
setupValidators: parseFlag(args, "setup-validators"),
|
||||
updateValidatorSet: parseFlag(args, "update-validator-set"),
|
||||
blockscout: parseFlag(args, "blockscout"),
|
||||
help: args.includes("--help") || args.includes("-h")
|
||||
};
|
||||
|
|
@ -98,12 +107,89 @@ async function main() {
|
|||
);
|
||||
}
|
||||
|
||||
await deployContracts({
|
||||
const contractsDeployed = await deployContracts({
|
||||
rpcUrl: networkRpcUrl,
|
||||
verified: options.verified,
|
||||
blockscoutBackendUrl,
|
||||
deployContracts: options.deployContracts
|
||||
});
|
||||
|
||||
// Set up validators using the extracted function
|
||||
if (contractsDeployed) {
|
||||
let shouldFundValidators = options.fundValidators;
|
||||
let shouldSetupValidators = options.setupValidators;
|
||||
let shouldUpdateValidatorSet = options.updateValidatorSet;
|
||||
|
||||
// If not specified, prompt for funding
|
||||
if (shouldFundValidators === undefined) {
|
||||
shouldFundValidators = await promptWithTimeout(
|
||||
"Do you want to fund validators with tokens and ETH?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldFundValidators ? "will fund" : "will not fund"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
// If not specified, prompt for setup
|
||||
if (shouldSetupValidators === undefined) {
|
||||
shouldSetupValidators = await promptWithTimeout(
|
||||
"Do you want to register validators in EigenLayer?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldSetupValidators ? "will register" : "will not register"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
// If not specified, prompt for update
|
||||
if (shouldUpdateValidatorSet === undefined) {
|
||||
shouldUpdateValidatorSet = await promptWithTimeout(
|
||||
"Do you want to update the validator set on the substrate chain?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldUpdateValidatorSet ? "will update" : "will not update"} validator set`
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldFundValidators) {
|
||||
await fundValidators({
|
||||
rpcUrl: networkRpcUrl
|
||||
// Default values for other options
|
||||
});
|
||||
} else {
|
||||
logger.info("Skipping validator funding");
|
||||
}
|
||||
|
||||
if (shouldSetupValidators) {
|
||||
await setupValidators({
|
||||
rpcUrl: networkRpcUrl
|
||||
// Default values for other options
|
||||
});
|
||||
|
||||
if (shouldUpdateValidatorSet) {
|
||||
await updateValidatorSet({
|
||||
rpcUrl: networkRpcUrl
|
||||
// Default values for other options
|
||||
});
|
||||
} else {
|
||||
logger.info("Skipping validator set update");
|
||||
}
|
||||
} else {
|
||||
logger.info("Skipping validator setup");
|
||||
}
|
||||
} else if (options.setupValidators || options.fundValidators) {
|
||||
logger.warn(
|
||||
"⚠️ Validator operations requested but contracts were not deployed. Skipping validator operations."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check all dependencies at once
|
||||
|
|
@ -168,6 +254,12 @@ function getOptionsString(options: ScriptOptions): string {
|
|||
optionStrings.push(`launchKurtosis=${options.launchKurtosis}`);
|
||||
if (options.deployContracts !== undefined)
|
||||
optionStrings.push(`deployContracts=${options.deployContracts}`);
|
||||
if (options.fundValidators !== undefined)
|
||||
optionStrings.push(`fundValidators=${options.fundValidators}`);
|
||||
if (options.setupValidators !== undefined)
|
||||
optionStrings.push(`setupValidators=${options.setupValidators}`);
|
||||
if (options.updateValidatorSet !== undefined)
|
||||
optionStrings.push(`updateValidatorSet=${options.updateValidatorSet}`);
|
||||
if (options.blockscout !== undefined) optionStrings.push(`blockscout=${options.blockscout}`);
|
||||
return optionStrings.length ? optionStrings.join(", ") : "no options";
|
||||
}
|
||||
|
|
@ -184,6 +276,12 @@ ${chalk.green("--launchKurtosis")} Clean and launch Kurtosis enclave if
|
|||
${chalk.green("--no-launchKurtosis")} Keep existing Kurtosis enclave if already running
|
||||
${chalk.green("--deploy-contracts")} Deploy smart contracts after Kurtosis starts
|
||||
${chalk.green("--no-deploy-contracts")} Skip smart contract deployment
|
||||
${chalk.green("--fund-validators")} Fund validators with tokens and ETH for local testing
|
||||
${chalk.green("--no-fund-validators")} Skip funding validators
|
||||
${chalk.green("--setup-validators")} Set up validators after contracts are deployed
|
||||
${chalk.green("--no-setup-validators")} Skip validator setup
|
||||
${chalk.green("--update-validator-set")} Update validator set on substrate chain after setup
|
||||
${chalk.green("--no-update-validator-set")} Skip validator set update
|
||||
${chalk.green("--blockscout")} Launch Kurtosis with Blockscout services (uses minimal-with-bs.yaml)
|
||||
${chalk.green("--no-blockscout")} Launch Kurtosis without Blockscout services (uses minimal.yaml)
|
||||
${chalk.green("--help, -h")} Show this help menu
|
||||
|
|
@ -197,6 +295,12 @@ ${chalk.yellow("Examples:")}
|
|||
|
||||
${chalk.gray("# Start without deploying contracts")}
|
||||
bun run start-kurtosis --no-deploy-contracts
|
||||
|
||||
${chalk.gray("# Start without funding validators")}
|
||||
bun run start-kurtosis --no-fund-validators
|
||||
|
||||
${chalk.gray("# Start without updating validator set")}
|
||||
bun run start-kurtosis --no-update-validator-set
|
||||
`);
|
||||
}
|
||||
|
||||
|
|
|
|||
284
test/scripts/fund-validators.ts
Normal file
284
test/scripts/fund-validators.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
// Script to fund validators with tokens and ETH for local testing
|
||||
import { $ } from "bun";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger, printHeader } from "../utils/index";
|
||||
|
||||
interface FundValidatorsOptions {
|
||||
rpcUrl: string;
|
||||
validatorsConfig?: string; // Path to JSON config file with validator addresses
|
||||
networkName?: string; // Network name for default deployment path
|
||||
deploymentPath?: string; // Optional custom deployment path
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON structure for validator configuration
|
||||
*/
|
||||
interface ValidatorConfig {
|
||||
validators: {
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
solochainAddress?: string; // Optional substrate address
|
||||
}[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Structure for strategy information in the deployment file
|
||||
*/
|
||||
interface StrategyInfo {
|
||||
address: string;
|
||||
underlyingToken: string;
|
||||
tokenCreator: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deployment file structure with enhanced strategy information
|
||||
*/
|
||||
interface DeploymentInfo {
|
||||
network: string;
|
||||
DeployedStrategies: StrategyInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Funds validators with tokens and ETH for local testing
|
||||
*
|
||||
* @param options - Configuration options for funding
|
||||
* @param options.rpcUrl - The RPC URL to connect to
|
||||
* @param options.validatorsConfig - Path to JSON config file (uses default config if not provided)
|
||||
* @returns Promise resolving to true if validators were funded successfully
|
||||
*/
|
||||
export const fundValidators = async (options: FundValidatorsOptions): Promise<boolean> => {
|
||||
const { rpcUrl, validatorsConfig, networkName = "anvil", deploymentPath } = options;
|
||||
|
||||
printHeader("Funding DataHaven Validators for Local Testing");
|
||||
|
||||
// Validate RPC URL
|
||||
invariant(rpcUrl, "❌ RPC URL is required");
|
||||
|
||||
// Load validator configuration - use default path if not specified
|
||||
const configPath = validatorsConfig || path.resolve(__dirname, "../configs/validator-set.json");
|
||||
|
||||
// Ensure the configuration file exists
|
||||
if (!fs.existsSync(configPath)) {
|
||||
logger.error(`Validator configuration file not found: ${configPath}`);
|
||||
throw new Error("Validator configuration file is required");
|
||||
}
|
||||
|
||||
// Load and validate the validator configuration
|
||||
logger.debug(`Loading validator configuration from ${configPath}`);
|
||||
let config: ValidatorConfig;
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(configPath, "utf8");
|
||||
config = JSON.parse(fileContent);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to parse validator config file: ${error}`);
|
||||
throw new Error("Invalid JSON format in validator configuration file");
|
||||
}
|
||||
|
||||
// Validate the validators array
|
||||
if (!config.validators || !Array.isArray(config.validators) || config.validators.length === 0) {
|
||||
logger.error("Invalid validator configuration: 'validators' array is missing or empty");
|
||||
throw new Error("Validator configuration must contain a non-empty 'validators' array");
|
||||
}
|
||||
|
||||
// Validate each validator entry
|
||||
for (const [index, validator] of config.validators.entries()) {
|
||||
if (!validator.publicKey) {
|
||||
throw new Error(`Validator at index ${index} is missing 'publicKey'`);
|
||||
}
|
||||
if (!validator.privateKey) {
|
||||
throw new Error(`Validator at index ${index} is missing 'privateKey'`);
|
||||
}
|
||||
if (!validator.publicKey.startsWith("0x")) {
|
||||
throw new Error(`Validator publicKey at index ${index} must start with '0x'`);
|
||||
}
|
||||
if (!validator.privateKey.startsWith("0x")) {
|
||||
throw new Error(`Validator privateKey at index ${index} must start with '0x'`);
|
||||
}
|
||||
}
|
||||
|
||||
const validators = config.validators;
|
||||
logger.info(`Found ${validators.length} validators to fund`);
|
||||
|
||||
// Get cast path for transactions
|
||||
const { stdout: castPath } = await $`which cast`.quiet();
|
||||
const castExecutable = castPath.toString().trim();
|
||||
|
||||
// Get the deployment information to find the strategies
|
||||
const defaultDeploymentPath = path.resolve(`../contracts/deployments/${networkName}.json`);
|
||||
const finalDeploymentPath = deploymentPath || defaultDeploymentPath;
|
||||
|
||||
if (!fs.existsSync(finalDeploymentPath)) {
|
||||
logger.error(`Deployment file not found: ${finalDeploymentPath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const deployments: DeploymentInfo = JSON.parse(fs.readFileSync(finalDeploymentPath, "utf8"));
|
||||
|
||||
// Ensure there's at least one deployed strategy
|
||||
if (!deployments.DeployedStrategies || deployments.DeployedStrategies.length === 0) {
|
||||
logger.error("No strategies found in deployment file - cannot proceed");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug(`Found ${deployments.DeployedStrategies.length} strategies with token information`);
|
||||
|
||||
// We need to ensure all operators to be registered have the necessary tokens
|
||||
logger.info("Funding validators with tokens...");
|
||||
|
||||
// Iterate through the strategies, using the embedded token information to fund validators
|
||||
for (const strategy of deployments.DeployedStrategies) {
|
||||
const strategyAddress = strategy.address;
|
||||
const underlyingTokenAddress = strategy.underlyingToken;
|
||||
const tokenCreator = strategy.tokenCreator;
|
||||
|
||||
logger.debug(
|
||||
`Processing strategy ${strategyAddress} with token ${underlyingTokenAddress} created by ${tokenCreator}`
|
||||
);
|
||||
|
||||
// Find the token creator in our validator list
|
||||
const creatorValidator = validators.find((validator) => validator.publicKey === tokenCreator);
|
||||
if (!creatorValidator) {
|
||||
logger.error(`Token creator ${tokenCreator} not found in validators list`);
|
||||
logger.warn("Will try to continue with other strategies...");
|
||||
continue;
|
||||
}
|
||||
|
||||
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} balance --erc20 ${underlyingTokenAddress} ${tokenCreator} --rpc-url ${rpcUrl}`;
|
||||
const getEthBalanceCmd = `${castExecutable} balance ${tokenCreator} --rpc-url ${rpcUrl}`;
|
||||
const { stdout: erc20BalanceOutput } = await $`sh -c ${getErc20BalanceCmd}`.quiet();
|
||||
const { stdout: ethBalanceOutput } = await $`sh -c ${getEthBalanceCmd}`.quiet();
|
||||
const creatorErc20Balance = erc20BalanceOutput.toString().trim().split(" ")[0];
|
||||
const creatorEthBalance = ethBalanceOutput.toString().trim();
|
||||
logger.debug(`Token creator has ${creatorErc20Balance} tokens and ${creatorEthBalance} ETH`);
|
||||
|
||||
// Transfer 5% of the creator's tokens to each validator + 1% of the creator's ETH. ETH is transferred only if the receiving validator does not have any
|
||||
const erc20TransferAmount = BigInt(creatorErc20Balance) / BigInt(20); // 5% of the balance
|
||||
const ethTransferAmount = BigInt(creatorEthBalance) / BigInt(100); // 1% of the balance
|
||||
logger.debug(`Transferring ${erc20TransferAmount} tokens to each validator`);
|
||||
|
||||
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();
|
||||
if (transferExitCode !== 0) {
|
||||
logger.error(
|
||||
`Failed to transfer tokens to validator ${validator.publicKey}: ${transferStderr.toString()}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify the transfer was successful
|
||||
const validatorBalanceCmd = `${castExecutable} call ${underlyingTokenAddress} "balanceOf(address)(uint256)" ${validator.publicKey} --rpc-url ${rpcUrl}`;
|
||||
const { stdout: validatorBalanceOutput } = await $`sh -c ${validatorBalanceCmd}`.quiet();
|
||||
const validatorBalance = validatorBalanceOutput.toString().trim().split(" ")[0];
|
||||
|
||||
// Note: We shouldn't use strict equality here as other transactions might affect balances
|
||||
if (BigInt(validatorBalance) < erc20TransferAmount) {
|
||||
logger.warn(
|
||||
`Validator ${validator.publicKey} has less than expected balance (${validatorBalance} < ${erc20TransferAmount})`
|
||||
);
|
||||
} else {
|
||||
logger.success(`Successfully transferred tokens to validator ${validator.publicKey}`);
|
||||
}
|
||||
|
||||
// Check this validator's ETH balance
|
||||
const validatorEthBalanceCmd = `${castExecutable} balance ${validator.publicKey} --rpc-url ${rpcUrl}`;
|
||||
const { stdout: validatorEthBalanceOutput } =
|
||||
await $`sh -c ${validatorEthBalanceCmd}`.quiet();
|
||||
const validatorEthBalance = validatorEthBalanceOutput.toString().trim();
|
||||
logger.debug(`Validator ${validator.publicKey} has ${validatorEthBalance} ETH`);
|
||||
|
||||
// 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();
|
||||
if (ethTransferExitCode !== 0) {
|
||||
logger.error(
|
||||
`Failed to transfer ETH to validator ${validator.publicKey}: ${ethTransferStderr.toString()}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify the ETH transfer was successful
|
||||
const validatorEthBalanceAfterCmd = `${castExecutable} balance ${validator.publicKey} --rpc-url ${rpcUrl}`;
|
||||
const { stdout: validatorEthBalanceAfterOutput } =
|
||||
await $`sh -c ${validatorEthBalanceAfterCmd}`.quiet();
|
||||
const validatorEthBalanceAfter = validatorEthBalanceAfterOutput.toString().trim();
|
||||
if (BigInt(validatorEthBalanceAfter) < ethTransferAmount) {
|
||||
logger.warn(
|
||||
`Validator ${validator.publicKey} has less than expected ETH balance (${validatorEthBalanceAfter} < ${ethTransferAmount})`
|
||||
);
|
||||
} else {
|
||||
logger.success(`Successfully transferred ETH to validator ${validator.publicKey}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info("All validators have been funded with tokens");
|
||||
return true;
|
||||
};
|
||||
|
||||
// Allow script to be run directly with CLI arguments
|
||||
if (import.meta.main) {
|
||||
const args = process.argv.slice(2);
|
||||
const options: {
|
||||
rpcUrl?: string;
|
||||
validatorsConfig?: string;
|
||||
networkName?: string;
|
||||
deploymentPath?: string;
|
||||
} = {
|
||||
networkName: "anvil" // Default network name
|
||||
};
|
||||
|
||||
// Extract RPC URL
|
||||
const rpcUrlIndex = args.indexOf("--rpc-url");
|
||||
if (rpcUrlIndex !== -1 && rpcUrlIndex + 1 < args.length) {
|
||||
options.rpcUrl = args[rpcUrlIndex + 1];
|
||||
}
|
||||
|
||||
// Extract validators config path
|
||||
const configIndex = args.indexOf("--config");
|
||||
if (configIndex !== -1 && configIndex + 1 < args.length) {
|
||||
options.validatorsConfig = args[configIndex + 1];
|
||||
}
|
||||
|
||||
// Extract network name
|
||||
const networkIndex = args.indexOf("--network");
|
||||
if (networkIndex !== -1 && networkIndex + 1 < args.length) {
|
||||
options.networkName = args[networkIndex + 1];
|
||||
}
|
||||
|
||||
// Extract custom deployment path
|
||||
const deploymentPathIndex = args.indexOf("--deployment-path");
|
||||
if (deploymentPathIndex !== -1 && deploymentPathIndex + 1 < args.length) {
|
||||
options.deploymentPath = args[deploymentPathIndex + 1];
|
||||
}
|
||||
|
||||
// Check required parameters
|
||||
if (!options.rpcUrl) {
|
||||
console.error("Error: --rpc-url parameter is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Run funding
|
||||
fundValidators({
|
||||
rpcUrl: options.rpcUrl,
|
||||
validatorsConfig: options.validatorsConfig,
|
||||
networkName: options.networkName,
|
||||
deploymentPath: options.deploymentPath
|
||||
}).catch((error) => {
|
||||
console.error("Validator funding failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
217
test/scripts/setup-validators.ts
Normal file
217
test/scripts/setup-validators.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
// Setup of validators for DataHaven
|
||||
import { $ } from "bun";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger, printHeader, promptWithTimeout } from "../utils/index";
|
||||
|
||||
interface SetupValidatorsOptions {
|
||||
rpcUrl: string;
|
||||
validatorsConfig?: string; // Path to JSON config file with validator addresses
|
||||
executeSignup?: boolean;
|
||||
networkName?: string; // Network name for default deployment path
|
||||
deploymentPath?: string; // Optional custom deployment path
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON structure for validator configuration
|
||||
*/
|
||||
interface ValidatorConfig {
|
||||
validators: {
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
solochainAddress?: string; // Optional substrate address
|
||||
}[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers validators in EigenLayer
|
||||
*
|
||||
* @param options - Configuration options for setup
|
||||
* @param options.rpcUrl - The RPC URL to connect to
|
||||
* @param options.validatorsConfig - Path to JSON config file (uses default config if not provided)
|
||||
* @param options.executeSignup - Whether to run the SignUpValidator script
|
||||
* @returns Promise resolving to true if validators were set up successfully, false if skipped
|
||||
*/
|
||||
export const setupValidators = async (options: SetupValidatorsOptions): Promise<boolean> => {
|
||||
const {
|
||||
rpcUrl,
|
||||
validatorsConfig,
|
||||
executeSignup,
|
||||
networkName = "anvil",
|
||||
deploymentPath
|
||||
} = options;
|
||||
|
||||
// Check if executeSignup option was set via flags, or prompt if not
|
||||
let shouldExecuteSignup = executeSignup;
|
||||
if (shouldExecuteSignup === undefined) {
|
||||
shouldExecuteSignup = await promptWithTimeout(
|
||||
"Do you want to register validators in EigenLayer?",
|
||||
true,
|
||||
10
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Using flag option: ${shouldExecuteSignup ? "will register" : "will not register"} validators`
|
||||
);
|
||||
}
|
||||
|
||||
if (!shouldExecuteSignup) {
|
||||
logger.info("Skipping validator setup. Done!");
|
||||
return false;
|
||||
}
|
||||
|
||||
printHeader("Setting Up DataHaven Validators");
|
||||
|
||||
// Validate RPC URL
|
||||
invariant(rpcUrl, "❌ RPC URL is required");
|
||||
|
||||
// Load validator configuration - use default path if not specified
|
||||
const configPath = validatorsConfig || path.resolve(__dirname, "../configs/validator-set.json");
|
||||
|
||||
// Ensure the configuration file exists
|
||||
if (!fs.existsSync(configPath)) {
|
||||
logger.error(`Validator configuration file not found: ${configPath}`);
|
||||
throw new Error("Validator configuration file is required");
|
||||
}
|
||||
|
||||
// Load and validate the validator configuration
|
||||
logger.debug(`Loading validator configuration from ${configPath}`);
|
||||
let config: ValidatorConfig;
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(configPath, "utf8");
|
||||
config = JSON.parse(fileContent);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to parse validator config file: ${error}`);
|
||||
throw new Error("Invalid JSON format in validator configuration file");
|
||||
}
|
||||
|
||||
// Validate the validators array
|
||||
if (!config.validators || !Array.isArray(config.validators) || config.validators.length === 0) {
|
||||
logger.error("Invalid validator configuration: 'validators' array is missing or empty");
|
||||
throw new Error("Validator configuration must contain a non-empty 'validators' array");
|
||||
}
|
||||
|
||||
// Validate each validator entry
|
||||
for (const [index, validator] of config.validators.entries()) {
|
||||
if (!validator.publicKey) {
|
||||
throw new Error(`Validator at index ${index} is missing 'publicKey'`);
|
||||
}
|
||||
if (!validator.privateKey) {
|
||||
throw new Error(`Validator at index ${index} is missing 'privateKey'`);
|
||||
}
|
||||
if (!validator.publicKey.startsWith("0x")) {
|
||||
throw new Error(`Validator publicKey at index ${index} must start with '0x'`);
|
||||
}
|
||||
if (!validator.privateKey.startsWith("0x")) {
|
||||
throw new Error(`Validator privateKey at index ${index} must start with '0x'`);
|
||||
}
|
||||
}
|
||||
|
||||
const validators = config.validators;
|
||||
logger.info(`Found ${validators.length} validators to register`);
|
||||
|
||||
// Get forge path
|
||||
const { stdout: forgePath } = await $`which forge`.quiet();
|
||||
const forgeExecutable = forgePath.toString().trim();
|
||||
|
||||
// Iterate through all validators to register them
|
||||
for (let i = 0; i < validators.length; i++) {
|
||||
const validator = validators[i];
|
||||
logger.info(`Setting up validator ${i} (${validator.publicKey})`);
|
||||
|
||||
// Setting up the environment variables directly
|
||||
const env = {
|
||||
...process.env,
|
||||
NETWORK: networkName,
|
||||
// OPERATOR_PRIVATE_KEY is what the script reads to set the operator
|
||||
OPERATOR_PRIVATE_KEY: validator.privateKey,
|
||||
// OPERATOR_SOLOCHAIN_ADDRESS is the validator's address on the substrate chain
|
||||
OPERATOR_SOLOCHAIN_ADDRESS: validator.solochainAddress || ""
|
||||
};
|
||||
|
||||
// Prepare command to register validator
|
||||
const signupCommand = `${forgeExecutable} script script/transact/SignUpValidator.s.sol --rpc-url ${rpcUrl} --broadcast --no-rpc-rate-limit --non-interactive`;
|
||||
|
||||
logger.debug(`Running command: ${signupCommand}`);
|
||||
|
||||
// Run with environment variables directly passed to the environment
|
||||
const { exitCode, stderr } = await $`sh -c ${signupCommand}`
|
||||
.cwd("../contracts")
|
||||
.env(env)
|
||||
.nothrow();
|
||||
|
||||
if (exitCode !== 0) {
|
||||
logger.error(`Failed to register validator ${validator.publicKey}: ${stderr.toString()}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.success(`Successfully registered validator ${validator.publicKey}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Allow script to be run directly with CLI arguments
|
||||
if (import.meta.main) {
|
||||
const args = process.argv.slice(2);
|
||||
const options: {
|
||||
rpcUrl?: string;
|
||||
validatorsConfig?: string;
|
||||
executeSignup?: boolean;
|
||||
networkName?: string;
|
||||
deploymentPath?: string;
|
||||
} = {
|
||||
executeSignup: args.includes("--no-signup") ? false : undefined,
|
||||
networkName: "anvil" // Default network name
|
||||
};
|
||||
|
||||
// Extract RPC URL
|
||||
const rpcUrlIndex = args.indexOf("--rpc-url");
|
||||
if (rpcUrlIndex !== -1 && rpcUrlIndex + 1 < args.length) {
|
||||
options.rpcUrl = args[rpcUrlIndex + 1];
|
||||
}
|
||||
|
||||
// Extract validators config path
|
||||
const configIndex = args.indexOf("--config");
|
||||
if (configIndex !== -1 && configIndex + 1 < args.length) {
|
||||
options.validatorsConfig = args[configIndex + 1];
|
||||
}
|
||||
|
||||
// Extract network name
|
||||
const networkIndex = args.indexOf("--network");
|
||||
if (networkIndex !== -1 && networkIndex + 1 < args.length) {
|
||||
options.networkName = args[networkIndex + 1];
|
||||
}
|
||||
|
||||
// Extract custom deployment path
|
||||
const deploymentPathIndex = args.indexOf("--deployment-path");
|
||||
if (deploymentPathIndex !== -1 && deploymentPathIndex + 1 < args.length) {
|
||||
options.deploymentPath = args[deploymentPathIndex + 1];
|
||||
}
|
||||
|
||||
// Parse signup flag
|
||||
if (args.includes("--signup")) {
|
||||
options.executeSignup = true;
|
||||
}
|
||||
|
||||
// Check required parameters
|
||||
if (!options.rpcUrl) {
|
||||
console.error("Error: --rpc-url parameter is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Run setup
|
||||
setupValidators({
|
||||
rpcUrl: options.rpcUrl,
|
||||
validatorsConfig: options.validatorsConfig,
|
||||
executeSignup: options.executeSignup,
|
||||
networkName: options.networkName,
|
||||
deploymentPath: options.deploymentPath
|
||||
}).catch((error) => {
|
||||
console.error("Validator setup failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
115
test/scripts/update-validator-set.ts
Normal file
115
test/scripts/update-validator-set.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
// Update validator set on DataHaven substrate chain
|
||||
import { $ } from "bun";
|
||||
import invariant from "tiny-invariant";
|
||||
import { logger, printHeader } from "../utils/index";
|
||||
|
||||
interface UpdateValidatorSetOptions {
|
||||
rpcUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the validator set to the DataHaven chain through Snowbridge
|
||||
*
|
||||
* @param options - Configuration options for update
|
||||
* @param options.rpcUrl - The RPC URL to connect to
|
||||
* @returns Promise resolving to true if validator set was sent successfully, false if skipped
|
||||
*/
|
||||
export const updateValidatorSet = async (options: UpdateValidatorSetOptions): Promise<boolean> => {
|
||||
const { rpcUrl } = options;
|
||||
|
||||
printHeader("Updating DataHaven Validator Set");
|
||||
|
||||
// Validate RPC URL
|
||||
invariant(rpcUrl, "❌ RPC URL is required");
|
||||
|
||||
// Get cast path for transactions
|
||||
const { stdout: castPath } = await $`which cast`.quiet();
|
||||
const castExecutable = castPath.toString().trim();
|
||||
|
||||
// Get the owner's private key for transaction signing from the .env
|
||||
const ownerPrivateKey =
|
||||
process.env.AVS_OWNER_PRIVATE_KEY ||
|
||||
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e"; // Sixth pre-funded account from Anvil
|
||||
|
||||
// Get deployed contract addresses from the deployments file
|
||||
const deploymentPath = path.resolve("../contracts/deployments/anvil.json");
|
||||
|
||||
if (!fs.existsSync(deploymentPath)) {
|
||||
logger.error(`Deployment file not found: ${deploymentPath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const deployments = JSON.parse(fs.readFileSync(deploymentPath, "utf8"));
|
||||
|
||||
// Prepare command to send validator set
|
||||
const serviceManagerAddress = deployments.ServiceManager;
|
||||
invariant(serviceManagerAddress, "ServiceManager address not found in deployments");
|
||||
|
||||
// Using cast to send the transaction
|
||||
const executionFee = "100000000000000000"; // 0.1 ETH
|
||||
const relayerFee = "200000000000000000"; // 0.2 ETH
|
||||
const value = "300000000000000000"; // 0.3 ETH (sum of fees)
|
||||
|
||||
const sendCommand = `${castExecutable} send --private-key ${ownerPrivateKey} --value ${value} ${serviceManagerAddress} "sendNewValidatorSet(uint128,uint128)" ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
|
||||
|
||||
logger.debug(`Running command: ${sendCommand}`);
|
||||
|
||||
const { exitCode, stderr } = await $`sh -c ${sendCommand}`.nothrow();
|
||||
|
||||
if (exitCode !== 0) {
|
||||
logger.error(`Failed to send validator set: ${stderr.toString()}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.success("Validator set sent to Snowbridge Gateway");
|
||||
|
||||
// Check if the validator set has been queued on the substrate side (placeholder)
|
||||
logger.debug("Checking validator set on substrate chain (not implemented)");
|
||||
/*
|
||||
// PLACEHOLDER: Code to check if validator set has been queued on substrate
|
||||
// This requires a connection to the DataHaven substrate node which is not available yet
|
||||
|
||||
// Example of what this might look like:
|
||||
const substrateApi = await ApiPromise.create({ provider: new WsProvider('ws://localhost:9944') });
|
||||
const validatorSetModule = substrateApi.query.validatorSet;
|
||||
const queuedValidators = await validatorSetModule.queuedValidators();
|
||||
|
||||
if (queuedValidators.length === validators.length) {
|
||||
logger.success('Validator set successfully queued on substrate chain');
|
||||
} else {
|
||||
logger.warn('Validator set not properly queued on substrate chain');
|
||||
}
|
||||
*/
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Allow script to be run directly with CLI arguments
|
||||
if (import.meta.main) {
|
||||
const args = process.argv.slice(2);
|
||||
const options: {
|
||||
rpcUrl?: string;
|
||||
} = {};
|
||||
|
||||
// Extract RPC URL
|
||||
const rpcUrlIndex = args.indexOf("--rpc-url");
|
||||
if (rpcUrlIndex !== -1 && rpcUrlIndex + 1 < args.length) {
|
||||
options.rpcUrl = args[rpcUrlIndex + 1];
|
||||
}
|
||||
|
||||
// Check required parameters
|
||||
if (!options.rpcUrl) {
|
||||
console.error("Error: --rpc-url parameter is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Run update
|
||||
updateValidatorSet({
|
||||
rpcUrl: options.rpcUrl
|
||||
}).catch((error) => {
|
||||
console.error("Validator set update failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue