Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
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
Expand Up @@ -24,11 +24,11 @@ export class KurtosisSDKRunner implements IRunner {
private enclaveContext?: EnclaveContext;

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

Expand Down Expand Up @@ -104,8 +104,8 @@ export class KurtosisSDKRunner implements IRunner {
// FIXME: check if fields are correct
id: serviceName,
serviceContext,
beaconApiUrl: ports.get("http") ? `http://localhost:${ports.get("http")?.number}` : undefined,
roles: this.inferRoles(serviceName),
apiUrl: ports.get("http") ? `http://localhost:${ports.get("http")?.number}` : undefined,
role: this.inferRole(serviceName),
metadata: {},
};

Expand All @@ -120,12 +120,12 @@ export class KurtosisSDKRunner implements IRunner {
}

// Helper function as services come back as generic ServiceContext object -> it derives the logical role
private inferRoles(serviceName: string): NodeService["roles"] {
return {
// FIXME: check if inferRoles is necessary
beacon: serviceName.includes("cl"), //|| serviceName.includes("beacon")
validator: serviceName.includes("vc"), //|| serviceName.includes("validator")
execution: serviceName.includes("el"), //|| serviceName.includes("execution")
};
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}`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,15 @@ export type KurtosisNetworkConfig = {
network_params: Record<string, string | number>;
};

// Optional enrichment for nodes
// FIXME: check if boolean is the best way to represent roles
// Proposed solution: Return a BeaconClient type (i.e. "BeaconClient.Lodestar")
export type NodeRoles = {
beacon?: boolean;
validator?: boolean;
execution?: boolean;
};
// Kurtosis services have only one role - mutually exclusive
export type NodeRole = "beacon" | "validator" | "execution";

// Service abstraction, intended to be adjusted with the correct metadata
// FIXME: verify which NodeService parameters are actually required vs optional
export type NodeService = {
id: string;
serviceContext: ServiceContext;
beaconApiUrl?: string;
roles?: NodeRoles; // TODO: check if required or optional
apiUrl?: string; // Generic API URL for beacon, execution, or validator
role: NodeRole; // Required - each service has exactly one role
metadata?: Record<string, string | number>;
};

Expand Down
Loading
Loading