mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
feat: inject already deployed contract in Ethereum state to speed up e2e tests
This commit is contained in:
parent
f040682d93
commit
c8ef0d9f4c
11 changed files with 248 additions and 13 deletions
1
contracts/deployments/state-diff.json
Normal file
1
contracts/deployments/state-diff.json
Normal file
File diff suppressed because one or more lines are too long
6
test/Makefile
Normal file
6
test/Makefile
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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...");
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
Loading…
Reference in a new issue