datahaven/test/scripts/generate-contracts.ts

193 lines
6.2 KiB
TypeScript
Raw Permalink Normal View History

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 =
fix: 🩹 map validator address to operator address for rewards & slashes (#441) ## Summary Slashing and rewards submissions were submitted through the bridge with their **solochain address** , while EigenLayer expects the **ethereum operator address**, the addresses were not being translated, so the protocol was broken. This PR adds a **reverse mapping** (Solochain address → Eth address) and uses it in both the slashing and rewards paths so that: - `slashValidatorsOperator` accepts requests where `operator` is a Solochain address and translates it to the Eth operator before calling EigenLayer. - `submitRewards` translates each `operatorRewards[].operator` from Solochain to Eth before calling the RewardsCoordinator. - Unknown or unmapped solochain addresses cause a revert (`UnknownSolochainAddress`) instead of silently failing. ## What's changed ### DataHavenServiceManager - **Reverse mapping**: `mapping(address => address) public validatorSolochainAddressToEthAddress` (Solochain → Eth), with `__GAP` reduced by one slot for upgradeable layout. - **Helper**: `_ethOperatorFromSolochain(address)` – returns Eth operator for a Solochain address, reverts with `UnknownSolochainAddress()` if unmapped. - **Registration / lifecycle**: - `registerOperator`: populates both forward and reverse mappings; enforces uniqueness (one Solochain per operator) and clears old reverse entry when an operator re-registers with a new Solochain. - `deregisterOperator`: clears both forward and reverse entries. - `updateSolochainAddressForValidator`: updates both mappings, enforces uniqueness and clears the previous Solochain's reverse entry. - **Slashing**: `slashValidatorsOperator` uses `_ethOperatorFromSolochain(slashings[i].operator)` so requests keyed by Solochain address are translated before calling EigenLayer. - **Rewards**: `submitRewards` builds a translated copy of the submission with each `operatorRewards[].operator` set via `_ethOperatorFromSolochain(...)`; unmapped addresses revert. ### IDataHavenServiceManager - New getter: `validatorSolochainAddressToEthAddress(address solochain) external view returns (address)`. - New errors: `UnknownSolochainAddress()`, `SolochainAddressAlreadyAssigned()`. ### Storage and fixtures - Storage snapshot updated for the new state variable. - `DataHavenServiceManagerBadLayout.sol` updated (reverse mapping + gap) for layout negative tests. - Storage layout test extended to assert the reverse mapping is preserved across proxy upgrade. ### Tests - **Slashing.t.sol**: Slashing with Solochain address (translation and emit of Eth operator); negative test for unmapped Solochain reverting with `UnknownSolochainAddress()`. - **RewardsSubmitter.t.sol**: Rewards submission with Solochain addresses (translation to Eth in RewardsCoordinator calldata); negative test for unmapped Solochain. - **StorageLayout.t.sol**: Reverse mapping preserved after upgrade. - **OperatorAddressMappings.t.sol** (new): Uniqueness (Solochain already assigned to another operator), update/deregister clearing reverse mapping, and getter behaviour. ## Testing - **Unit tests**: `forge test` from `contracts/` (all existing and new tests pass). - **Storage**: - `./scripts/check-storage-layout.sh` - `./scripts/check-storage-layout-negative.sh` - **Coverage**: Slashing path (Solochain → Eth translation + revert), rewards path (translation + revert), registration/update/deregister (reverse mapping and uniqueness), and storage layout upgrade preservation. --------- Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2026-02-18 19:38:13 +00:00
await $`bun run biome format --files-max-size=4000000 --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();
}