-
-
Notifications
You must be signed in to change notification settings - Fork 410
chore(testing): initial Kurtosis integration for exploratory migration #8296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/kurtosis-sim-tests
Are you sure you want to change the base?
Changes from 1 commit
b33fb3c
41b9c35
2ec1b92
2ca92a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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") { | ||
| 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), | ||
|
||
| 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; | ||
|
||
| } | ||
|
|
||
| //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; | ||
| } | ||
| } | ||
| } | ||
| */ | ||
There was a problem hiding this comment.
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.