2026-01-06 11:27:50 +00:00
|
|
|
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();
|
2026-01-06 11:27:50 +00:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|