mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## 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>
192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
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=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();
|
|
}
|