feat: inject already deployed contract in Ethereum state to speed up e2e tests

This commit is contained in:
undercover-cactus 2025-10-06 17:29:53 +02:00
parent f040682d93
commit c8ef0d9f4c
11 changed files with 248 additions and 13 deletions

File diff suppressed because one or more lines are too long

6
test/Makefile Normal file
View file

@ -0,0 +1,6 @@
generate:
docker exec $(shell docker ps -aq --filter name='el-1-reth') bash -c "apt update && apt install -y wget"
docker exec $(shell docker ps -aq --filter name='el-1-reth') bash -c "wget https://github.com/undercover-cactus/Chaos/releases/download/v0.1.1/chaos-linux-amd64-v0.1.1.tar.gz"
docker exec $(shell docker ps -aq --filter name='el-1-reth') bash -c "tar -xzvf chaos-linux-amd64-v0.1.1.tar.gz"
docker exec $(shell docker ps -aq --filter name='el-1-reth') bash -c "./target/release/chaos --database-path /data/reth/execution-data/db"
docker cp $(shell docker ps -aq --filter name='el-1-reth'):state.json ../contracts/deployments/state-diff.json

View file

@ -100,6 +100,20 @@ Follow these steps to set up and interact with your local network:
- Block Explorer: [http://127.0.0.1:3000](http://127.0.0.1:3000).
- Kurtosis Dashboard: Run `kurtosis web` to access. From it you can see all the services running in the network, as well as their ports, status and logs.
## Generating Ethereum state
To avoid deploying contracts everytime for each tests, you can generate and then inject state in the Ethereum client.
### Generate state
```
$ bun cli launch --all
$ make generate
$ bun cli stop --all
```
## Troubleshooting
### E2E Network Launch doesn't work

View file

@ -9,6 +9,7 @@ interface DeployContractsOptions {
blockscoutBackendUrl?: string;
deployContracts?: boolean;
parameterCollection?: ParameterCollection;
injectContracts?: boolean;
}
/**
@ -20,11 +21,19 @@ interface DeployContractsOptions {
* @param options.blockscoutBackendUrl - URL for the Blockscout API (required if verified is true)
* @param options.deployContracts - Flag to control deployment (if undefined, will prompt)
* @param options.parameterCollection - Collection of parameters to update in the DataHaven runtime
* @param options.injectContracts - If true, skips contract deployment entirely
* @returns Promise resolving to true if contracts were deployed successfully, false if skipped
*/
export const deployContracts = async (options: DeployContractsOptions): Promise<boolean> => {
printHeader("Deploying Smart Contracts");
const { deployContracts } = options;
const { deployContracts, injectContracts } = options;
// Skip deployment if injectContracts is set
if (injectContracts) {
logger.info("💉 Inject contracts is enabled. Skipping contract deployment.");
printDivider();
return false;
}
// Check if deployContracts option was set via flags, or prompt if not
let shouldDeployContracts = deployContracts;

View file

@ -45,6 +45,7 @@ export interface LaunchOptions {
relayer?: boolean;
relayerImageTag: string;
cleanNetwork?: boolean;
injectContracts?: boolean;
}
// ===== Launch Handler Functions =====
@ -81,10 +82,70 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
verified: options.verified,
blockscoutBackendUrl,
deployContracts: options.deployContracts,
parameterCollection
parameterCollection,
injectContracts: options.injectContracts
});
await performValidatorOperations(options, launchedNetwork.elRpcUrl, contractsDeployed);
// If we're injecting contracts instead of deploying, still read the Gateway address
if (options.injectContracts && !contractsDeployed) {
try {
const { parseDeploymentsFile, parseRewardsInfoFile } = await import("utils/contracts");
const deployments = await parseDeploymentsFile();
const gatewayAddress = deployments.Gateway;
const rewardsRegistryAddress = deployments.RewardsRegistry;
const rewardsInfo = await parseRewardsInfoFile();
const rewardsAgentOrigin = rewardsInfo.RewardsAgentOrigin;
const updateRewardsMerkleRootSelector = rewardsInfo.updateRewardsMerkleRootSelector;
if (gatewayAddress) {
logger.debug(
`📝 Reading EthereumGatewayAddress from existing deployment: ${gatewayAddress}`
);
parameterCollection.addParameter({
name: "EthereumGatewayAddress",
value: gatewayAddress
});
}
if (rewardsRegistryAddress) {
logger.debug(`📝 Adding RewardsRegistryAddress parameter: ${rewardsRegistryAddress}`);
parameterCollection.addParameter({
name: "RewardsRegistryAddress",
value: rewardsRegistryAddress
});
} else {
logger.warn("⚠️ RewardsRegistry address not found in deployments file");
}
if (updateRewardsMerkleRootSelector) {
logger.debug(
`📝 Adding RewardsUpdateSelector parameter: ${updateRewardsMerkleRootSelector}`
);
parameterCollection.addParameter({
name: "RewardsUpdateSelector",
value: updateRewardsMerkleRootSelector
});
} else {
logger.warn("⚠️ updateRewardsMerkleRootSelector not found in rewards info file");
}
if (rewardsAgentOrigin) {
logger.debug(`📝 Adding RewardsAgentOrigin parameter: ${rewardsAgentOrigin}`);
parameterCollection.addParameter({
name: "RewardsAgentOrigin",
value: rewardsAgentOrigin
});
} else {
logger.warn("⚠️ RewardsAgentOrigin not found in deployments file");
}
} catch (error) {
logger.error(`Failed to read Gateway address from deployments: ${error}`);
}
}
const isDeployed = options.injectContracts || contractsDeployed;
await performValidatorOperations(options, launchedNetwork.elRpcUrl, isDeployed);
await setParametersFromCollection({
launchedNetwork,
@ -119,7 +180,8 @@ export const launchPreActionHook = (
buildDatahaven,
launchKurtosis,
relayer,
setParameters
setParameters,
injectContracts
} = thisCmd.opts();
// Check for conflicts with --all flag
@ -161,4 +223,10 @@ export const launchPreActionHook = (
if (deployContracts === false && fundValidators) {
thisCmd.error("--fundValidators requires --deployContracts to be set");
}
if (injectContracts && !deployContracts && !all) {
// If we have `--all` argument then `deployContracts` is technically true
thisCmd.error("--inject-contracts requires --deploy-contracts to be set");
}
};

View file

@ -35,6 +35,10 @@ export const launchKurtosis = async (
logger.info("👍 Skipping Kurtosis Ethereum network launch. Done!");
await registerServices(launchedNetwork, options.kurtosisEnclaveName);
if (options.kurtosisEnclaveName) {
logger.debug(`Registering ${options.kurtosisEnclaveName}`);
await registerServices(launchedNetwork, options.kurtosisEnclaveName);
}
printDivider();
return;
}
@ -81,7 +85,8 @@ export const launchKurtosis = async (
kurtosisEnclaveName: options.kurtosisEnclaveName,
blockscout: options.blockscout,
slotTime: options.slotTime,
kurtosisNetworkArgs: options.kurtosisNetworkArgs
kurtosisNetworkArgs: options.kurtosisNetworkArgs,
injectContracts: options.injectContracts
},
launchedNetwork
);

View file

@ -134,7 +134,7 @@ const removeDataHavenNetworks = async (options: StopOptions) => {
}
};
const stopAllEnclaves = async (options: StopOptions) => {
export const stopAllEnclaves = async (options: StopOptions) => {
logger.info("🔎 Checking for running Kurtosis enclaves...");
let shouldStopEnclave = options.all || options.enclave;

View file

@ -137,6 +137,7 @@ program
.option("--b, --blockscout", "Enable Blockscout")
.option("--slot-time <number>", "Set slot time in seconds", parseIntValue)
.option("--cn, --clean-network", "Always clean Kurtosis enclave and Docker containers")
.option("--ic, --inject-contracts", "Inject smart contracts from saved state diff")
.option(
"--datahaven-build-extra-args <value>",
"Extra args for DataHaven node Cargo build (the plain command is `cargo build --release` for linux, `cargo zigbuild --target x86_64-unknown-linux-gnu --release` for mac)",

View file

@ -9,6 +9,7 @@ import {
} from "utils";
import { parse, stringify } from "yaml";
import type { LaunchedNetwork } from "./types/launchedNetwork";
import { z } from "zod";
/**
* Configuration options for Kurtosis-related operations.
@ -18,6 +19,7 @@ export interface KurtosisOptions {
blockscout?: boolean;
slotTime?: number;
kurtosisNetworkArgs?: string;
injectContracts?: boolean;
}
/**
@ -192,6 +194,7 @@ export const modifyConfig = async (
slotTime?: number;
kurtosisNetworkArgs?: string;
kurtosisEnclaveName?: string;
injectContracts?: boolean;
},
configFile: string
): Promise<string> => {
@ -225,6 +228,42 @@ export const modifyConfig = async (
}
}
// Load and validate pre-deployed contracts
// TODO: Replace with CLI option
if (options.injectContracts) {
try {
const preDeployedFile = Bun.file("../contracts/deployments/state-diff.json");
if (await preDeployedFile.exists()) {
logger.debug(`Pre-deployed contracts file: ${preDeployedFile.name}`);
const preDeployedRaw = await preDeployedFile.text();
logger.trace(`Raw pre-deployed contracts data: ${preDeployedRaw}`);
const preDeployedData = JSON.parse(preDeployedRaw);
const validatedContracts = preDeployedContractsSchema.parse(preDeployedData);
logger.trace(`Validated contracts: ${JSON.stringify(validatedContracts, null, 2)}`);
const kurtosisFormattedContracts = transformToKurtosisFormat(validatedContracts);
logger.trace(
`Kurtosis formatted contracts: ${JSON.stringify(kurtosisFormattedContracts, null, 2)}`
);
parsedConfig.network_params.additional_preloaded_contracts = JSON.stringify(
kurtosisFormattedContracts,
null,
0
);
logger.debug("Pre-deployed contracts loaded and validated successfully");
} else {
logger.warn("Pre-deployed contracts file not found, skipping");
}
} catch (error) {
logger.error(`Failed to load pre-deployed contracts: ${error}`);
throw new Error("❌ Invalid pre-deployed contracts configuration");
}
}
logger.trace(parsedConfig);
// Use a unique filename based on the enclave name to avoid conflicts in parallel execution
const configFileName = options.kurtosisEnclaveName
@ -344,3 +383,34 @@ export const getBlockscoutUrl = async (enclaveName: string): Promise<string> =>
invariant(blockscoutPort, "❌ Could not find Blockscout service port");
return `http://127.0.0.1:${blockscoutPort}`;
};
const preDeployedContractsSchema = z.record(
z.string(),
z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address"),
code: z.string().regex(/^0x[a-fA-F0-9]*$/, "Invalid hex code"),
storage: z.union([
z.record(z.string(), z.string()),
z.string() // Allow empty string for contracts with no storage
])
})
);
const transformToKurtosisFormat = (contracts: z.infer<typeof preDeployedContractsSchema>) => {
const transformed: Record<string, any> = {};
for (const [_name, contract] of Object.entries(contracts)) {
// Handle storage - convert empty string to empty object
const storage =
typeof contract.storage === "string" && contract.storage === "" ? {} : contract.storage;
transformed[contract.address] = {
balance: "0ETH",
code: contract.code,
storage: storage,
nonce: "0x0" // Default nonce to 0
};
}
return transformed;
};

View file

@ -186,7 +186,8 @@ export const launchNetwork = async (
kurtosisEnclaveName: kurtosisEnclaveName,
blockscout: options.blockscout ?? false,
slotTime: options.slotTime || 2,
kurtosisNetworkArgs: options.kurtosisNetworkArgs
kurtosisNetworkArgs: options.kurtosisNetworkArgs,
injectContracts: true // Forcing it to be true to run e2e tests
},
launchedNetwork
);
@ -207,7 +208,9 @@ export const launchNetwork = async (
rpcUrl: launchedNetwork.elRpcUrl,
verified: options.verified ?? false,
blockscoutBackendUrl,
parameterCollection
parameterCollection,
deployContracts: false,
injectContracts: true // Because we are injecting contracts in kurtosis deployment
});
// 4. Fund validators
@ -216,11 +219,67 @@ export const launchNetwork = async (
rpcUrl: launchedNetwork.elRpcUrl
});
// 5. Setup validators
logger.info("🔐 Setting up validators...");
await setupValidators({
rpcUrl: launchedNetwork.elRpcUrl
});
// We are injecting contracts with already registers validator state
// // 5. Setup validators
// logger.info("🔐 Setting up validators...");
// await setupValidators({
// rpcUrl: launchedNetwork.elRpcUrl
// });
// We are injecting contracts but we still need the address
try {
const { parseDeploymentsFile, parseRewardsInfoFile } = await import("utils/contracts");
const deployments = await parseDeploymentsFile();
const gatewayAddress = deployments.Gateway;
const rewardsRegistryAddress = deployments.RewardsRegistry;
const rewardsInfo = await parseRewardsInfoFile();
const rewardsAgentOrigin = rewardsInfo.RewardsAgentOrigin;
const updateRewardsMerkleRootSelector = rewardsInfo.updateRewardsMerkleRootSelector;
if (gatewayAddress) {
logger.debug(
`📝 Reading EthereumGatewayAddress from existing deployment: ${gatewayAddress}`
);
parameterCollection.addParameter({
name: "EthereumGatewayAddress",
value: gatewayAddress
});
}
if (rewardsRegistryAddress) {
logger.debug(`📝 Adding RewardsRegistryAddress parameter: ${rewardsRegistryAddress}`);
parameterCollection.addParameter({
name: "RewardsRegistryAddress",
value: rewardsRegistryAddress
});
} else {
logger.warn("⚠️ RewardsRegistry address not found in deployments file");
}
if (updateRewardsMerkleRootSelector) {
logger.debug(
`📝 Adding RewardsUpdateSelector parameter: ${updateRewardsMerkleRootSelector}`
);
parameterCollection.addParameter({
name: "RewardsUpdateSelector",
value: updateRewardsMerkleRootSelector
});
} else {
logger.warn("⚠️ updateRewardsMerkleRootSelector not found in rewards info file");
}
if (rewardsAgentOrigin) {
logger.debug(`📝 Adding RewardsAgentOrigin parameter: ${rewardsAgentOrigin}`);
parameterCollection.addParameter({
name: "RewardsAgentOrigin",
value: rewardsAgentOrigin
});
} else {
logger.warn("⚠️ RewardsAgentOrigin not found in deployments file");
}
} catch (error) {
logger.error(`Failed to read Gateway address from deployments: ${error}`);
}
// 6. Set DataHaven runtime parameters
logger.info("⚙️ Setting DataHaven parameters...");

View file

@ -40,6 +40,8 @@ export const buildContracts = async () => {
stdout: buildStdout
} = await $`forge build`.cwd("../contracts").nothrow().quiet();
await $`bun run biome format --files-max-size=2000000 --write ../contracts/deployments/state-diff.json`;
if (buildExitCode !== 0) {
logger.error(buildStderr.toString());
throw Error("❌ Contracts have failed to build properly.");