mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## Add E2E validator-set update flow - feat: `test/utils/validators.ts` for on-demand validator orchestration. - feat: `test/suites/validator-set-update.test.ts` covering allowlist → register → update. - some minor launcher updates: avoid docker cache, add `--platform` when building datahaven image, avoid sending validator-set update on launch. - Helpers: ABI shortcut in `test/utils/contracts.ts`; config tweaks in `test/configs/validator-set.json`. - Minor cleanup/formatting across `test/launcher/*`, `test/scripts/setup-validators.ts`, and related tests. - added `keepAlive` flag to `BaseTestSuite`, in order to avoid tearing down the network while debugging. Defaults, obviously, to false. - added a `failOnTomeout` option on to waitForDataHavenEvents() so the test fails of the timeout is reached and no event was captured. ### Coverage - The test simulates an scenario in which we have two active authorities (alice and bob), which are running, and registered as operators, which is the normal state after the chain launches. Then: - It launches two more nodes (charlie and dave) - It add the nodes to allowlist and register them as operators - It sends the validator set update message - Checks that the validator update message was propagated through the gateway and arrived the external-validators pallet - Checks that the chain continues producing blocks ### Notes The last test case has a timeout of 10 minutes. This is to respect propagation times of the message through the relayers. We are testing that the external validators pallet actually updated the validator set. Locally, I could expect 5~6 minutes, I just wanted to be on the safe side. CI is passing showing that this was enough indeed. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
153 lines
5.8 KiB
TypeScript
153 lines
5.8 KiB
TypeScript
import * as generated from "contract-bindings";
|
|
import invariant from "tiny-invariant";
|
|
import { type Abi, erc20Abi, getContract, isAddress } from "viem";
|
|
import { z } from "zod";
|
|
import { logger } from "./logger";
|
|
import { createDefaultClient, type ViemClientInterface } from "./viem";
|
|
|
|
const ethAddressRegex = /^0x[a-fA-F0-9]{40}$/;
|
|
const ethAddress = z.string().regex(ethAddressRegex, "Invalid Ethereum address");
|
|
const ethAddressCustom = z.custom<`0x${string}`>(
|
|
(val) => typeof val === "string" && ethAddressRegex.test(val),
|
|
{ message: "Invalid Ethereum address" }
|
|
);
|
|
const ethBytes32Regex = /^0x[a-fA-F0-9]{64}$/;
|
|
const ethBytes32 = z.string().regex(ethBytes32Regex, "Invalid Ethereum bytes32");
|
|
const ethBytes4Regex = /^0x[a-fA-F0-9]{8}$/;
|
|
const ethBytes4 = z.string().regex(ethBytes4Regex, "Invalid Ethereum bytes4");
|
|
|
|
const DeployedStrategySchema = z.object({
|
|
address: ethAddress,
|
|
underlyingToken: ethAddress,
|
|
tokenCreator: ethAddress
|
|
});
|
|
|
|
const DeploymentsSchema = z.object({
|
|
network: z.string(),
|
|
BeefyClient: ethAddressCustom,
|
|
AgentExecutor: ethAddressCustom,
|
|
Gateway: ethAddressCustom,
|
|
ServiceManager: ethAddressCustom,
|
|
ServiceManagerImplementation: ethAddressCustom,
|
|
VetoableSlasher: ethAddressCustom,
|
|
RewardsRegistry: ethAddressCustom,
|
|
RewardsAgent: ethAddressCustom,
|
|
DelegationManager: ethAddressCustom,
|
|
StrategyManager: ethAddressCustom,
|
|
AVSDirectory: ethAddressCustom,
|
|
EigenPodManager: ethAddressCustom.optional(),
|
|
EigenPodBeacon: ethAddressCustom.optional(),
|
|
RewardsCoordinator: ethAddressCustom,
|
|
AllocationManager: ethAddressCustom,
|
|
PermissionController: ethAddressCustom,
|
|
ETHPOSDeposit: ethAddressCustom.optional(),
|
|
BaseStrategyImplementation: ethAddressCustom.optional(),
|
|
DeployedStrategies: z.array(DeployedStrategySchema).optional()
|
|
});
|
|
|
|
export type Deployments = z.infer<typeof DeploymentsSchema>;
|
|
|
|
const RewardsInfoSchema = z.object({
|
|
RewardsAgent: ethAddressCustom,
|
|
RewardsAgentOrigin: ethBytes32,
|
|
updateRewardsMerkleRootSelector: ethBytes4
|
|
});
|
|
|
|
export type RewardsInfo = z.infer<typeof RewardsInfoSchema>;
|
|
|
|
export const parseDeploymentsFile = async (network = "anvil"): Promise<Deployments> => {
|
|
const deploymentsPath = `../contracts/deployments/${network}.json`;
|
|
const deploymentsFile = Bun.file(deploymentsPath);
|
|
if (!(await deploymentsFile.exists())) {
|
|
logger.error(`File ${deploymentsPath} does not exist`);
|
|
throw new Error(`Error reading ${network} deployments file`);
|
|
}
|
|
const deploymentsJson = await deploymentsFile.json();
|
|
logger.info(`Deployments: ${JSON.stringify(deploymentsJson, null, 2)}`);
|
|
try {
|
|
const parsedDeployments = DeploymentsSchema.parse(deploymentsJson);
|
|
logger.debug(`Successfully parsed ${network} deployments file.`);
|
|
return parsedDeployments;
|
|
} catch (error) {
|
|
logger.error(`Failed to parse ${network} deployments file:`, error);
|
|
throw new Error(`Invalid ${network} deployments file format`);
|
|
}
|
|
};
|
|
|
|
export const parseRewardsInfoFile = async (network = "anvil"): Promise<RewardsInfo> => {
|
|
const rewardsInfoPath = `../contracts/deployments/${network}-rewards-info.json`;
|
|
const rewardsInfoFile = Bun.file(rewardsInfoPath);
|
|
if (!(await rewardsInfoFile.exists())) {
|
|
logger.error(`File ${rewardsInfoPath} does not exist`);
|
|
throw new Error(`Error reading ${network} rewards info file`);
|
|
}
|
|
const rewardsInfoJson = await rewardsInfoFile.json();
|
|
try {
|
|
const parsedRewardsInfo = RewardsInfoSchema.parse(rewardsInfoJson);
|
|
logger.debug(`Successfully parsed ${network} rewards info file.`);
|
|
return parsedRewardsInfo;
|
|
} catch (error) {
|
|
logger.error(`Failed to parse ${network} rewards info file:`, error);
|
|
throw new Error(`Invalid ${network} rewards info file format`);
|
|
}
|
|
};
|
|
|
|
// Add to this if we add any new contracts
|
|
const abiMap = {
|
|
BeefyClient: generated.beefyClientAbi,
|
|
AgentExecutor: generated.agentExecutorAbi,
|
|
Gateway: generated.gatewayAbi,
|
|
ServiceManager: generated.dataHavenServiceManagerAbi,
|
|
ServiceManagerImplementation: generated.dataHavenServiceManagerAbi,
|
|
VetoableSlasher: generated.vetoableSlasherAbi,
|
|
RewardsRegistry: generated.rewardsRegistryAbi,
|
|
RewardsAgent: generated.agentAbi,
|
|
DelegationManager: generated.delegationManagerAbi,
|
|
StrategyManager: generated.strategyManagerAbi,
|
|
AVSDirectory: generated.avsDirectoryAbi,
|
|
EigenPodManager: generated.eigenPodManagerAbi,
|
|
EigenPodBeacon: generated.eigenPodAbi,
|
|
RewardsCoordinator: generated.rewardsCoordinatorAbi,
|
|
AllocationManager: generated.allocationManagerAbi,
|
|
PermissionController: generated.permissionControllerAbi,
|
|
ETHPOSDeposit: generated.iethposDepositAbi,
|
|
BaseStrategyImplementation: generated.strategyBaseTvlLimitsAbi,
|
|
DeployedStrategies: erc20Abi
|
|
} as const satisfies Record<keyof Omit<Deployments, "network" | "RewardsAgentOrigin">, Abi>;
|
|
|
|
type ContractName = keyof typeof abiMap;
|
|
type AbiFor<C extends ContractName> = (typeof abiMap)[C];
|
|
export type ContractInstance<C extends ContractName> = Awaited<
|
|
ReturnType<typeof getContractInstance<C>>
|
|
>;
|
|
|
|
// TODO: make this work with DeployedStrategies
|
|
export const getContractInstance = async <C extends ContractName>(
|
|
contract: C,
|
|
viemClient?: ViemClientInterface,
|
|
network = "anvil"
|
|
) => {
|
|
const deployments = await parseDeploymentsFile(network);
|
|
const contractAddress = deployments[contract];
|
|
logger.debug(`Contract ${contract} deployed to ${contractAddress}`);
|
|
|
|
const client = viemClient ?? (await createDefaultClient());
|
|
invariant(
|
|
typeof contractAddress === "string" && isAddress(contractAddress),
|
|
`Contract address for ${contract} is not a valid address`
|
|
);
|
|
|
|
const abi: AbiFor<C> = abiMap[contract];
|
|
invariant(abi, `ABI for contract ${contract} not found`);
|
|
|
|
return getContract({
|
|
address: contractAddress,
|
|
abi,
|
|
client
|
|
});
|
|
};
|
|
|
|
export const getAbi = async (contract: string) => {
|
|
const contractInstance = await getContractInstance(contract as ContractName);
|
|
return contractInstance.abi;
|
|
};
|