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:
Tobi Demeco 2025-04-22 16:49:51 -03:00 committed by GitHub
parent 6ba0476e3f
commit 80a0138f00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1106 additions and 184 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

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

View file

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

View file

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

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

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

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