Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ packages/beacon-node/mainnet_pubkeys.csv
packages/api/oapi-schemas
test-logs/

# Kurtosis test files (keep locally, don't commit)
packages/cli/test/utils/crucible/kurtosis/test/runner-test.ts

Comment on lines +45 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would remove this line when this PR get ready.

# Autogenerated docs
packages/**/docs
packages/**/typedocs
Expand Down
77 changes: 6 additions & 71 deletions packages/cli/test/sim/multiFork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {mergeAssertion} from "../utils/crucible/assertions/mergeAssertion.js";
import {nodeAssertion} from "../utils/crucible/assertions/nodeAssertion.js";
import {createWithdrawalAssertions} from "../utils/crucible/assertions/withdrawalsAssertion.js";
import {BeaconClient, ExecutionClient, Match, ValidatorClient} from "../utils/crucible/interfaces.js";
import {Simulation} from "../utils/crucible/simulation.js";
//import {Simulation} from "../utils/crucible/simulation.js"; // TODO: Merge simulation-kurtosis.js logic into simulation.js
import {Simulation} from "../utils/crucible/kurtosis/simulation/simulation-kurtosis.js";
import {defineSimTestConfig, logFilesDir} from "../utils/crucible/utils/index.js";
import {connectAllNodes, waitForSlot} from "../utils/crucible/utils/network.js";
import {assertCheckpointSync, assertRangeSync, assertUnknownBlockSync} from "../utils/crucible/utils/syncing.js";
Expand All @@ -27,81 +28,15 @@ const {estimatedTimeoutMs, forkConfig} = defineSimTestConfig({
initialNodes: 5,
});

const env = await Simulation.initWithDefaults(

// Load configuration and create simulation (services not started yet)
const env = await Simulation.initWithKurtosisConfig(
{
id: "multi-fork",
logsDir: path.join(logFilesDir, "multi-fork"),
forkConfig,
},
[
{
id: "node-1",
beacon: BeaconClient.Lodestar,
validator: {
type: ValidatorClient.Lodestar,
options: {
// this will cause race in beacon but since builder is not attached will
// return with engine full block and publish via publishBlockV2
clientOptions: {
"builder.selection": "default",
},
},
},
execution: ExecutionClient.Geth,
keysCount: 32,
mining: true,
},
{
id: "node-2",
beacon: BeaconClient.Lodestar,
validator: {
type: ValidatorClient.Lodestar,
options: {
// this will make the beacon respond with blinded version of the local block as no
// builder is attached to beacon, and publish via publishBlindedBlockV2
clientOptions: {
"builder.selection": "default",
blindedLocal: true,
},
},
},
execution: ExecutionClient.Geth,
keysCount: 32,
remote: true,
},
{
id: "node-3",
beacon: BeaconClient.Lodestar,
validator: {
type: ValidatorClient.Lodestar,
options: {
// this builder selection will make it respond with full block
clientOptions: {
"builder.selection": "executiononly",
},
},
},
execution: ExecutionClient.Geth,
keysCount: 32,
},
{
id: "node-4",
beacon: BeaconClient.Lodestar,
validator: {
type: ValidatorClient.Lodestar,
options: {
// this builder selection will make it respond with blinded version
// of local block and subsequent publishing via publishBlindedBlockV2
clientOptions: {
"builder.selection": "default",
},
},
},
execution: ExecutionClient.Geth,
keysCount: 32,
},
{id: "node-5", beacon: BeaconClient.Lighthouse, execution: ExecutionClient.Geth, keysCount: 32},
]
"configs/multi-fork.yml" // Kurtosis network configuration
);

env.tracker.register({
Expand Down
73 changes: 29 additions & 44 deletions packages/cli/test/utils/crucible/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import {ChildProcess} from "node:child_process";
//import {ChildProcess} from "node:child_process";
import {SecretKey} from "@chainsafe/blst";
import {ApiClient} from "@lodestar/api";
import {ApiClient as KeyManagerApi} from "@lodestar/api/keymanager";
import {ChainForkConfig} from "@lodestar/config";
import {LogLevel, Logger} from "@lodestar/logger";
import {ForkName} from "@lodestar/params";
import {Epoch, SignedBeaconBlock, Slot} from "@lodestar/types";
import {ServiceContext} from "kurtosis-sdk";
import {Web3} from "web3";
import {BeaconArgs} from "../../../src/cmds/beacon/options.js";
import {IValidatorCliArgs} from "../../../src/cmds/validator/options.js";
import {GlobalArgs} from "../../../src/options/index.js";
import {EpochClock} from "./epochClock.js";
import { KurtosisNetworkConfig, KurtosisServicesMap } from "./kurtosis/runner/kurtosisTypes.js";

export type NodeId = string;

export type SimulationInitOptions = {
id: string;
logsDir: string;
forkConfig: ChainForkConfig;
trustedSetup?: boolean;
};

export type SimulationOptions = {
Expand All @@ -26,6 +29,7 @@ export type SimulationOptions = {
rootDir: string;
controller: AbortController;
genesisTime: number;
trustedSetup?: boolean;
logLevel?: LogLevel;
};

Expand All @@ -45,11 +49,6 @@ export enum ExecutionClient {
Nethermind = "execution-nethermind",
}

export enum ExecutionStartMode {
PreMerge = "pre-merge",
PostMerge = "post-merge",
}

export type BeaconClientsOptions = {
[BeaconClient.Lodestar]: Partial<BeaconArgs & GlobalArgs>;
[BeaconClient.Lighthouse]: Record<string, unknown>;
Expand Down Expand Up @@ -136,7 +135,7 @@ export interface ExecutionGenesisOptions<E extends ExecutionClient = ExecutionCl
export interface ExecutionGeneratorOptions<E extends ExecutionClient = ExecutionClient>
extends ExecutionGenesisOptions<E>,
GeneratorOptions {
mode: ExecutionStartMode;
//mode: ExecutionStartMode; //✅ REMOVED - ExecutionStartMode not needed anymore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes remove that line completely.

mining: boolean;
paths: ExecutionPaths;
clientOptions: ExecutionClientsOptions[E];
Expand Down Expand Up @@ -164,52 +163,37 @@ export type LighthouseAPI = Omit<ApiClient, "lodestar"> & {
};
};

// NEW - Kurtosis-specific BeaconNode
export interface BeaconNode<C extends BeaconClient = BeaconClient> {
readonly client: C;
readonly id: string;
/**
* Beacon Node Rest API URL accessible form the host machine if the process is running in private network inside docker
*/
readonly restPublicUrl: string;
/**
* Beacon Node Rest API URL accessible within private network
*/
readonly restPrivateUrl: string;
readonly restPublicUrl: string; //🔄 From Kurtosis?
readonly restPrivateUrl: string; //🔄 From Kurtosis?
readonly api: C extends BeaconClient.Lodestar ? LodestarAPI : LighthouseAPI;
readonly job: Job;
readonly serviceContext: ServiceContext; // ✅ NEW - Kurtosis-native
}

// NEW - Kurtosis-specific ValidatorNode
export interface ValidatorNode<C extends ValidatorClient = ValidatorClient> {
readonly client: C;
readonly id: string;
readonly keyManager: KeyManagerApi;
readonly keys: ValidatorClientKeys;
readonly job: Job;
readonly serviceContext: ServiceContext; // ✅ NEW - Kurtosis-native
}

// NEW - Kurtosis-specific executionNode
export interface ExecutionNode<E extends ExecutionClient = ExecutionClient> {
readonly client: E;
readonly id: string;
readonly ttd: bigint;
/**
* Engine URL accessible form the host machine if the process is running in private network inside docker
*/
readonly engineRpcPublicUrl: string;
/**
* Engine URL accessible within private network inside docker
*/
readonly engineRpcPrivateUrl: string;
/**
* RPC URL accessible form the host machine if the process is running in private network inside docker
*/
readonly ethRpcPublicUrl: string;
/**
* RPC URL accessible within private network inside docker
*/
readonly ethRpcPrivateUrl: string;
readonly engineRpcPublicUrl: string; //🔄 From Kurtosis?
readonly engineRpcPrivateUrl: string; //🔄 From Kurtosis?
readonly ethRpcPublicUrl: string; //🔄 From Kurtosis?
readonly ethRpcPrivateUrl: string; //🔄 From Kurtosis?
readonly jwtSecretHex: string;
readonly provider: E extends ExecutionClient.Mock ? null : Web3;
readonly job: Job;
readonly serviceContext: ServiceContext; // ✅ NEW - Kurtosis-native
}

export interface NodePair {
Expand Down Expand Up @@ -287,17 +271,19 @@ export type RunnerOptions = {
};
};

//✅ New Kurtosis Runner
export interface IRunner {
create: (jobOptions: JobOptions[]) => Job;
on(event: RunnerEvent, cb: (id: string) => void | Promise<void>): void;
start(): Promise<void>;
stop(): Promise<void>;
getNextIp(): string;
}
// Takes a structured config and instantiates the network
create: (config: KurtosisNetworkConfig) => Promise<KurtosisServicesMap>; // ✅ NEW - Kurtosis-native

// Starts the environment (e.g., enclave)
start: (enclaveName: string) => Promise<void>; // ✅ NEW - Kurtosis-native

export interface RunnerEnv<T extends RunnerType> {
type: T;
create: (jobOption: Omit<JobOptions<T>, "children">) => Job;
// Stops or tears down the environment
stop: () => Promise<void>;

// Attach listeners for events, such as service start, stop, crash, etc.
on(event: RunnerEvent, cb: (id: string) => void | Promise<void>): void;
}

export type RunnerEvent = "starting" | "started" | "stopping" | "stop";
Expand Down Expand Up @@ -383,7 +369,6 @@ export interface AssertionError {
message: string;
data?: Record<string, unknown>;
}
export type ChildProcessWithJobOptions = {jobOptions: JobOptions; childProcess: ChildProcess};

export type Eth1GenesisBlock = {
config: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* KurtosisSDKRunner - Ethereum Network Simulation Runner
* =====================================================
*
* An implementation of the KurtosisSDKRunner that leverages
* Kurtosis SDK for creating and managing Ethereum network simulations.
* It acts as a replacement for the current Runner.
*
* This runner provides:
* - Enclave lifecycle management (create, start, stop, destroy)
* - Service deployment using ethpandaops/ethereum-package
* - Automatic service role inference and metadata extraction
* - Resource cleanup and error handling
*
*/

import {EnclaveContext, KurtosisContext, StarlarkRunConfig} from "kurtosis-sdk";
import {IRunner, RunnerEvent} from "../simulation/interfaces-kurtosis.js";
import {KurtosisNetworkConfig, KurtosisServicesMap, NodeService} from "./kurtosisTypes.js";

export class KurtosisSDKRunner implements IRunner {
private enclaveName: string;
private kurtosisContext?: KurtosisContext;
private enclaveContext?: EnclaveContext;

/**
* Creates a new KurtosisSDKRunner instance
*
* @param enclaveName - Unique name for the Kurtosis enclave (required for isolation)
*/
constructor(enclaveName: string) {
this.enclaveName = enclaveName;
}

// This method establishes a connection to the local Kurtosis engine
// and creates an isolated environment for the Ethereum network simulation
async start(enclaveName: string): Promise<void> {
// Create engine context
const contextResult = await KurtosisContext.newKurtosisContextFromLocalEngine();
if (contextResult.isErr()) throw contextResult.error;
this.kurtosisContext = contextResult.value;

// Keep enclave identity in state
this.enclaveName = enclaveName;

// Create enclave and store its context
const enclaveResult = await this.kurtosisContext.createEnclave(this.enclaveName);
if (enclaveResult.isErr()) throw enclaveResult.error;
this.enclaveContext = enclaveResult.value;
}

async stop(): Promise<void> {
if (this.kurtosisContext && this.enclaveName) {
await this.kurtosisContext.destroyEnclave(this.enclaveName);
}
}

/**
* Creates and deploys the Ethereum network based on the provided configuration
*
* This method orchestrates the deployment of the entire Ethereum network
* using the ethpandaops/ethereum-package Starlark package. It creates
* consensus layer, execution layer, and validator services according to
* the configuration specification.
*
* config: Network configuration specifying participants and parameters
* Output: Promise resolving to a map of service names to NodeService objects
*
*/

async create(config: KurtosisNetworkConfig): Promise<KurtosisServicesMap> {
if (!this.enclaveContext) {
throw new Error("Enclave context not initialized. Did you call start()?");
}

const serializedParams = JSON.stringify(config);
const runConfig = new StarlarkRunConfig(
StarlarkRunConfig.WithSerializedParams(serializedParams),
StarlarkRunConfig.WithDryRun(false)
);

const pkg = "github.com/ethpandaops/ethereum-package";
const runResult = await this.enclaveContext.runStarlarkRemotePackageBlocking(pkg, runConfig);
if (runResult.isErr()) throw runResult.error;

const run = runResult.value;
if (run.executionError) {
throw new Error(`Package executionError: ${run.executionError}`);
}

const servicesResult = await this.enclaveContext.getServices();
if (servicesResult.isErr()) throw servicesResult.error;

const services: KurtosisServicesMap = new Map(); //Mapping Kurtosis services

for (const [serviceName] of servicesResult.value) {
const ctxResult = await this.enclaveContext.getServiceContext(serviceName);
if (ctxResult.isErr()) throw ctxResult.error;

const serviceContext = ctxResult.value;
const ports = serviceContext.getPublicPorts();

const node: NodeService = {
// FIXME: check if fields are correct
id: serviceName,
serviceContext,
apiUrl: ports.get("http") ? `http://localhost:${ports.get("http")?.number}` : undefined,
role: this.inferRole(serviceName),
metadata: {},
};

services.set(serviceName, node);
}

return services;
}

on(_event: RunnerEvent, _cb: (id: string) => void | Promise<void>): void {
// TODO: Event handling not implemented yet
}

// Helper function as services come back as generic ServiceContext object -> it derives the logical role
private inferRole(serviceName: string): NodeService["role"] {
if (serviceName.includes("cl")) return "beacon";
if (serviceName.includes("vc")) return "validator";
if (serviceName.includes("el")) return "execution";

// Default fallback - could be improved with better service naming detection
throw new Error(`Unable to infer role for service: ${serviceName}`);
}
}
Loading
Loading