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
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.
*
* enclaveName: Default name for the Kurtosis enclave - can be overridden
*/
constructor(enclaveName = "crucible-enclave") {
Copy link
Contributor

Choose a reason for hiding this comment

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

Make the name required, so the enclave name already be unique with the simulation running. And don't mixup the services among different simulations.

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,
beaconApiUrl: ports.get("http") ? `http://localhost:${ports.get("http")?.number}` : undefined,
roles: this.inferRoles(serviceName),
Copy link
Contributor

Choose a reason for hiding this comment

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

Can there be a service with multiple roles? I think not.

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 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")
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* KurtosisServiceMap returning a NodeService for more detailed metadata
*
* Enriched: {
"lodestar_1": {
serviceContext: ServiceContext,
beaconApiUrl: "http://localhost:PORT"
},
...
}
*/

import {ServiceContext} from "kurtosis-sdk";

// Core simulation config passed to Kurtosis runner
// Replicating network config from .YAML file
export type KurtosisNetworkConfig = {
participants: Array<{
el_type: string;
cl_type: string;
cl_image?: string;
count?: number;
cl_extra_params?: string[];
el_extra_params?: string[];
}>;
additional_services?: string[];
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;
};

// 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
metadata?: Record<string, string | number>;
};

// Map of all services (used by test runner and tracker)
export type KurtosisServicesMap = Map<string, NodeService>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Loader for YAML input into a KurtosisNetworkConfig
* Used with *.test.ts files and during the testing phase
*/

// Loader for YAML input into a KurtosisNetworkConfig
// Used with *.test.ts files and during the testing phase

import fs from "node:fs/promises";
import path from "node:path";
import {parse} from "yaml";
import type {KurtosisNetworkConfig} from "../runner/kurtosisTypes.js";

export async function loadKurtosisConfig(fileName: string, baseDir?: string): Promise<KurtosisNetworkConfig> {
let fullPath: string;

if (baseDir) {
// If baseDir is provided, treat fileName as relative to baseDir
fullPath = path.join(baseDir, fileName);
} else {
// If no baseDir, treat fileName as absolute path
fullPath = fileName;
}

const raw = await fs.readFile(fullPath, "utf8");
return parse(raw) as KurtosisNetworkConfig;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to validate the kurtosis config at this point? may be some helper function in their SDK.

Copy link
Author

Choose a reason for hiding this comment

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

The create() function calls runStarlarkRemotePackageBlocking(), which in turn invokes sanity_check() and input_parser() from the ethereum-package.

These already provide an initial validation of the Kurtosis config. Is this sufficient?

If sufficient but should occur earlier, one possible adjustment could be to move the validation (happening in simulation-kurtosis.ts) to run right after await env.runner.start(sim-${id}); and before const services = await env.runner.create(kurtosisConfig);, so that it happens before create() rather than inside it

}

//move the baseDir inside taking inspo from this code snippet?
/*
export class KurtosisConfigLoader {
private static readonly CONFIG_DIR = "./test/sim/configs";

static loadConfig(configName: string): KurtosisNetworkConfig {
const configPath = resolve(this.CONFIG_DIR, `${configName}.yml`);
const yamlContent = readFileSync(configPath, 'utf8');
return yamlLoad(yamlContent) as KurtosisNetworkConfig;
}
}
}
*/
Loading