datahaven/test/scripts/generate-contracts.ts
Gonza Montiel ddbc9bdd8b
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 21:38:13 +02:00

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();
}