test : improve contract injection (#326)

## Summary

This PR improve the generating state workflow. It will also check for
outdated state-diff.json and add a practical script to easily generate a
new one.

The way we generate state has also been changed to make it work with
macOS M1 system. We don't run the tool in the container anymore but
instead directly on the machine.

## What changes

* A check-generated-state.js script was added to quickly look for
outdated test
* The check was added in the CI
* A generate-contracts.ts script was added to easily generate the new
state with the new instructions to run on MacOS

---------

Co-authored-by: Gonza Montiel <gon.montiel@gmail.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Co-authored-by: Gonza Montiel <gonzamontiel@users.noreply.github.com>
This commit is contained in:
undercover-cactus 2026-01-06 12:27:50 +01:00 committed by GitHub
parent 4cdd2a91d9
commit 42ec577f15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 20125 additions and 16 deletions

View file

@ -51,6 +51,8 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: test/.bun-version
- name: Check for outdated state-diff.json
run: bun ./scripts/check-generated-state.ts
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Pull Kurtosis images

View file

@ -15,8 +15,10 @@
"!**/contract-bindings/**/*",
"!**/html/**/*",
"!**/datahaven/contracts/out/**/*",
"!**/contracts/out/**/*"
]
"!**/contracts/out/**/*",
"!**/contracts/deployments/state-diff.checksum"
],
"maxSize": 3000000
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"formatter": {

View file

@ -1 +1 @@
{"network": "anvil","BeefyClient": "0x99bbA657f2BbC93c02D617f8bA121cB8Fc104Acf","AgentExecutor": "0x0E801D84Fa97b50751Dbf25036d067dCf18858bF","Gateway": "0x9d4454B023096f34B160D6B654540c56A1F81688","ServiceManager": "0x809d550fca64d94Bd9F66E60752A544199cfAC3D","ServiceManagerImplementation": "0x36C02dA8a0983159322a80FFE9F24b1acfF8B570","RewardsRegistry": "0x4c5859f0F772848b2D91F1D83E2Fe57935348029","RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","DelegationManager": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82","StrategyManager": "0x9A676e781A523b5d0C0e43731313A708CB607508","AVSDirectory": "0x0B306BF915C4d645ff596e518fAf3F9669b97016","EigenPodManager": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1","EigenPodBeacon": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1","RewardsCoordinator": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE","AllocationManager": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed","PermissionController": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c","ETHPOSDeposit": "0xC7f2Cf4845C6db0e1a1e91ED41Bcd0FcC1b0E141","BaseStrategyImplementation": "0xf5059a5D33d5853360D16C683c16e67980206f36","DeployedStrategies": [{"address": "0x998abeb3E57409262aE5b751f60747921B33613E","underlyingToken": "0x95401dc811bb5740090279Ba06cfA8fcF6113778","tokenCreator": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"}]}
{"network": "anvil","BeefyClient": "0x99bbA657f2BbC93c02D617f8bA121cB8Fc104Acf","AgentExecutor": "0x0E801D84Fa97b50751Dbf25036d067dCf18858bF","Gateway": "0x9d4454B023096f34B160D6B654540c56A1F81688","ServiceManager": "0x809d550fca64d94Bd9F66E60752A544199cfAC3D","ServiceManagerImplementation": "0x36C02dA8a0983159322a80FFE9F24b1acfF8B570","RewardsRegistry": "0x4c5859f0F772848b2D91F1D83E2Fe57935348029","RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","DelegationManager": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82","StrategyManager": "0x9A676e781A523b5d0C0e43731313A708CB607508","AVSDirectory": "0x0B306BF915C4d645ff596e518fAf3F9669b97016","EigenPodManager": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1","EigenPodBeacon": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1","RewardsCoordinator": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE","AllocationManager": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed","PermissionController": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c","ETHPOSDeposit": "0xC7f2Cf4845C6db0e1a1e91ED41Bcd0FcC1b0E141","BaseStrategyImplementation": "0xf5059a5D33d5853360D16C683c16e67980206f36","DeployedStrategies": [{"address": "0x998abeb3E57409262aE5b751f60747921B33613E","underlyingToken": "0x95401dc811bb5740090279Ba06cfA8fcF6113778","tokenCreator": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"}]}

View file

@ -0,0 +1 @@
f2f144097486bce2697989c88e774916eb6e681a

File diff suppressed because one or more lines are too long

View file

@ -1,8 +0,0 @@
version := v0.1.2
generate-ethereum-state:
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/${version}/chaos-linux-amd64-${version}.tar.gz"
docker exec $(shell docker ps -aq --filter name='el-1-reth') bash -c "tar -xzvf chaos-linux-amd64-${version}.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

@ -4,6 +4,7 @@ import { DEFAULT_SUBSTRATE_WS_PORT } from "utils/constants";
import { createParameterCollection } from "utils/parameters";
import { getBlockscoutUrl } from "../../../launcher/kurtosis";
import { LaunchedNetwork } from "../../../launcher/types/launchedNetwork";
import { updateParameters } from "../../../scripts/deploy-contracts";
import { checkBaseDependencies } from "../common/checks";
import { deployContracts } from "./contracts";
import { launchDataHavenSolochain } from "./datahaven";
@ -47,6 +48,7 @@ export interface LaunchOptions {
relayerImageTag: string;
storagehub?: boolean;
cleanNetwork?: boolean;
injectContracts?: boolean;
}
// ===== Launch Handler Functions =====
@ -64,7 +66,10 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
await launchDataHavenSolochain(options, launchedNetwork);
await launchKurtosis(options, launchedNetwork);
// Default injectContracts to true if not specified
const injectContracts = options.injectContracts !== undefined ? options.injectContracts : true;
await launchKurtosis({ ...options, injectContracts }, launchedNetwork);
logger.trace("Checking if Blockscout is enabled...");
let blockscoutBackendUrl: string | undefined;
@ -78,7 +83,8 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
);
}
if (options.deployContracts) {
// skip deploying contracts if we have injected it
if (options.deployContracts && !options.injectContracts) {
const contractsDeployed = await deployContracts({
rpcUrl: launchedNetwork.elRpcUrl,
verified: options.verified,
@ -88,6 +94,9 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
});
await performValidatorOperations(options, launchedNetwork.elRpcUrl, contractsDeployed);
} else {
// We are injecting contracts but we still need the addresses
await updateParameters(parameterCollection);
}
await setParametersFromCollection({

View file

@ -80,7 +80,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

@ -144,6 +144,15 @@ 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 pre-deployed contracts from state-diff.json into Kurtosis network",
true
)
.option(
"--nic, --no-inject-contracts",
"Deploy contracts instead of injecting from state-diff.json"
)
.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

@ -61,7 +61,7 @@ export const launchKurtosisNetwork = async (
await pullMacOSImages();
}
await runKurtosisEnclave(options, configFilePath);
await runKurtosisEnclave({ ...options }, configFilePath);
await registerServices(launchedNetwork, options.kurtosisEnclaveName);
logger.success("Kurtosis network launched successfully");
@ -352,6 +352,7 @@ export const runKurtosisEnclave = async (
blockscout?: boolean;
slotTime?: number;
kurtosisNetworkArgs?: string;
injectContracts?: boolean;
},
configFilePath: string
): Promise<void> => {

18891
test/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@
"build:docker:operator": "docker build --no-cache --platform linux/amd64 -t datahavenxyz/datahaven:local -f ../docker/datahaven-dev.Dockerfile ../.",
"generate:wagmi": "wagmi generate",
"generate:snowbridge-cfgs": "bun -e \"import {generateSnowbridgeConfigs} from './scripts/gen-snowbridge-cfgs.ts'; await generateSnowbridgeConfigs()\"",
"generate:contracts": "bun -e \"import {generateContracts} from './scripts/generate-contracts.ts'; await generateContracts()\"",
"generate:types": "(cd ../operator && cargo build --release) && bun x papi add --wasm \"../operator/target/release/wbuild/datahaven-stagenet-runtime/datahaven_stagenet_runtime.wasm\" datahaven",
"generate:types:fast": "(cd ../operator && cargo build --release --features fast-runtime) && bun x papi add --wasm \"../operator/target/release/wbuild/datahaven-stagenet-runtime/datahaven_stagenet_runtime.wasm\" datahaven",
"start:e2e:verified": "bun cli launch --verified --blockscout --deploy-contracts --setup-validators --update-validator-set --fund-validators",

View file

@ -0,0 +1,17 @@
import { readFileSync } from "node:fs";
import { generateContractsChecksum } from "./contracts-checksum.ts";
// Read the previously stored checksum
const originalHash = readFileSync("../contracts/deployments/state-diff.checksum", "utf-8").trim();
// Root directory for the contracts; all files under this tree are included in the checksum
const contractsPath = "../contracts/src";
// Recompute checksum over all files under contractsPath (including nested directories)
const currentHash = generateContractsChecksum(contractsPath);
if (currentHash !== originalHash) {
throw new Error(
"State generated file is outdated. From the repository root, run `cd test && bun generate:contracts` to regenerate it."
);
}

View file

@ -0,0 +1,39 @@
// @ts-nocheck
import { createHash } from "node:crypto";
import type { Dirent } from "node:fs";
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
/**
* Recursively walks a directory and feeds all file contents into a SHA1 hash.
* This ensures that any change in nested contract files is reflected
* in the resulting checksum.
*/
export function generateContractsChecksum(contractsPath: string): string {
const root = path.resolve(contractsPath);
const hash = createHash("sha1");
const visit = (dir: string) => {
const entries: Dirent[] = readdirSync(dir, { withFileTypes: true });
// Ensure deterministic ordering across platforms
entries
.slice()
.sort((a: Dirent, b: Dirent) => a.name.localeCompare(b.name))
.forEach((entry: Dirent) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
visit(fullPath);
} else if (entry.isFile()) {
const data = readFileSync(fullPath);
hash.update(data);
}
});
};
visit(root);
return hash.digest("hex");
}

View file

@ -0,0 +1,192 @@
import { existsSync, writeFileSync } from "node:fs";
import { platform } from "node:process";
import { $ } from "bun";
import { logger } from "../utils/logger.ts";
import { generateContractsChecksum } from "./contracts-checksum.ts";
const CHAOS_VERSION = "v0.1.2";
const CHAOS_RELEASE_URL = `https://github.com/undercover-cactus/Chaos/releases/download/${CHAOS_VERSION}/`;
const STATE_DIFF_PATH = "../contracts/deployments/state-diff.json";
const STATE_DIFF_CHECKSUM_PATH = "../contracts/deployments/state-diff.checksum";
const HOST_DB_PATH = "/tmp/db";
/**
* Finds the Reth container by name pattern and verifies contracts are deployed
*/
async function findRethContainer(): Promise<string> {
const { stdout } = await $`docker ps --format "{{.Names}}" --filter name=el-1-reth`.quiet();
const containerName = stdout.toString().trim();
if (!containerName) {
const setupCommand =
"bun cli launch --launch-kurtosis --deploy-contracts --no-inject-contracts --no-datahaven --no-relayer --no-set-parameters --no-setup-validators --no-fund-validators";
throw new Error(
"❌ Could not find Reth container with contracts deployed.\n\n" +
"To generate state-diff.json, you need a running Kurtosis network with contracts deployed.\n\n" +
"Run this command to launch the network and deploy contracts:\n\n" +
` ${setupCommand}\n\n` +
"Note: The --no-inject-contracts flag ensures contracts are actually deployed\n" +
"instead of being injected from state-diff.json.\n\n" +
`If you already have a Kurtosis network running, you'll need to deploy contracts\n` +
"using the launch command with --no-launch-kurtosis --no-inject-contracts flags."
);
}
logger.info(`📦 Found Reth container: ${containerName}`);
return containerName;
}
async function copyDatabaseFromContainer(containerName: string): Promise<void> {
logger.info("📋 Copying database from container...");
// Copy database in the host machine
logger.info(`Import the database into ${HOST_DB_PATH} from the container`);
await $`rm -rf ${HOST_DB_PATH}`.quiet();
const result = await $`docker cp ${containerName}:/data/reth/execution-data/db ${HOST_DB_PATH}`;
if (result.exitCode !== 0) {
throw new Error("Fail to copy the reth database into the /tmp folder.");
}
logger.info("✅ Database copied");
}
/**
* Downloads and extracts Chaos tool inside the container
*/
async function setupChaos(): Promise<void> {
logger.info("📥 Downloading Chaos tool...");
// Check host platform
let tarName: string;
if (platform === "darwin") {
tarName = `chaos-macos-amd64-${CHAOS_VERSION}`;
} else if (platform === "linux") {
tarName = `chaos-linux-amd64-${CHAOS_VERSION}`;
} else {
throw new Error(
`Unsupported platform : ${platform}. Chaos tool doesn't have a build for your system yet.`
);
}
const resultWget = await $`wget ${CHAOS_RELEASE_URL}/${tarName}.tar.gz -O /tmp/chaos.tar.gz`;
if (resultWget.exitCode !== 0) {
throw new Error("Fail to download binary. Verify if 'wget' is installed on your machine.");
}
// Untar binary
logger.info("📦 Extracting Chaos tool...");
const resultTar = await $`tar -xzvf /tmp/chaos.tar.gz -C /tmp/`;
if (resultTar.exitCode !== 0) {
throw new Error("Fail to unpack binary. Verify if 'wget' is installed on your machine.");
}
logger.info("✅ Chaos tool ready");
}
/**
* Runs Chaos to generate state-diff.json
*/
async function runChaos(): Promise<void> {
logger.info("🔍 Running Chaos to extract contract state...");
const result = await $`/tmp/target/release/chaos --database-path ${HOST_DB_PATH}`;
if (result.exitCode !== 0) {
throw new Error("Fail to generate state.");
}
logger.info("✅ State extraction complete");
}
/**
* Copies state.json from container to host
*/
async function copyStateFile(): Promise<void> {
logger.info("📋 Copying state.json to our repo");
const stateFile = "state.json";
if (!existsSync(stateFile)) {
throw new Error("❌ Failed to copy state.json from our temp folder");
}
// Move to final location
await $`mv ${stateFile} ${STATE_DIFF_PATH}`.quiet();
logger.info(`✅ State file saved to ${STATE_DIFF_PATH}`);
}
/**
* Formats the state-diff.json file using biome
*/
async function formatStateDiff(): Promise<void> {
logger.info("🎨 Formatting state-diff.json...");
// Use a higher max size (3MB) to handle the large state-diff.json file
const result =
await $`bun run biome format --files-max-size=3000000 --write ${STATE_DIFF_PATH}`.quiet();
if (result.exitCode !== 0) {
logger.warn("⚠️ Biome formatting had issues, but continuing...");
logger.debug(result.stderr.toString());
}
logger.info("✅ Formatting complete");
}
/**
* Saves the checksum to a file
*/
function saveChecksum(checksum: string): void {
writeFileSync(STATE_DIFF_CHECKSUM_PATH, checksum, "utf-8");
logger.info(`✅ Checksum saved to ${STATE_DIFF_CHECKSUM_PATH}`);
}
/**
* Main function to generate contracts state-diff
*/
export async function generateContracts(): Promise<void> {
logger.info("🚀 Starting contract state-diff generation...");
try {
// 1. Find Reth container
const containerName = await findRethContainer();
// 2. Copy database
await copyDatabaseFromContainer(containerName);
// 3. Setup Chaos tool
await setupChaos();
// 4. Run Chaos to extract state
await runChaos();
// 5. Copy state.json to host
await copyStateFile();
// 6. Format the JSON file
await formatStateDiff();
// 7. Generate checksum
logger.info("🔐 Generating checksum...");
const checksum = generateContractsChecksum("../contracts/src");
logger.info(`📝 Checksum: ${checksum}`);
// 7. Save checksum
saveChecksum(checksum);
logger.info("✅ Contract state-diff generation complete!");
logger.info(` - State file: ${STATE_DIFF_PATH}`);
logger.info(` - Checksum: ${STATE_DIFF_CHECKSUM_PATH}`);
logger.info(` - Run 'bun run ./scripts/check-generated-state.ts' to validate`);
} catch (error) {
logger.error("❌ Failed to generate contract state-diff:", error);
throw error;
}
}
// Run if called directly
if (import.meta.main) {
await generateContracts();
}