Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 0 additions & 4 deletions apps/cli/src/commands/clean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ function stopAllServices(): void {
console.log("[clean] Stopping Docker...");
composeDown({ composeFile: COMPOSE_FILE, projectName: "arbitrum-testnode" });
exec("docker", ["compose", "-f", COMPOSE_FILE, "-p", "arbitrum-testnode", "down", "-v"]);

console.log("[clean] Killing Anvil...");
exec("pkill", ["-f", "anvil.*--port.*8545"]);
exec("pkill", ["-f", "testnode-l1-heartbeat"]);
}

function removeConfigDirPreservingSnapshots(): void {
Expand Down
14 changes: 11 additions & 3 deletions apps/cli/src/commands/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resolve } from "node:path";
import { waitForRpc } from "@arbitrum/testnode-core/docker.js";
import {
startAnvilWithState,
startL1Container,
startNitroFromSnapshot,
stopRuntime,
} from "@arbitrum/testnode-core/runtime.js";
Expand Down Expand Up @@ -72,7 +72,11 @@ snapshotCli.command("build", {
configDir: CONFIG_DIR,
});
const manifest = captureSnapshot(CONFIG_DIR, COMPOSE_FILE, snapshotId);
startAnvilWithState(CONFIG_DIR);
startL1Container({
composeFile: COMPOSE_FILE,
projectName: "arbitrum-testnode",
configDir: CONFIG_DIR,
});
await waitForRpc(RPCS.l1);
await startNitroFromSnapshot(
{
Expand Down Expand Up @@ -102,7 +106,11 @@ snapshotCli.command("restore", {
configDir: CONFIG_DIR,
});
const manifest = restoreSnapshot(CONFIG_DIR, snapshotId);
startAnvilWithState(CONFIG_DIR);
startL1Container({
composeFile: COMPOSE_FILE,
projectName: "arbitrum-testnode",
configDir: CONFIG_DIR,
});
await waitForRpc(RPCS.l1);
await startNitroFromSnapshot(
{
Expand Down
7 changes: 1 addition & 6 deletions apps/cli/src/commands/status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { resolve } from "node:path";
import { isServiceRunning } from "@arbitrum/testnode-core/docker.js";
import { exec } from "@arbitrum/testnode-core/exec.js";
import {
isPidRunning,
loadCurrentRun,
Expand All @@ -27,12 +26,8 @@ export const statusCli = Cli.create("status", {
const state = loadState(CONFIG_DIR);
const run = loadCurrentRun(CONFIG_DIR);

// Check Anvil (L1) — it runs on host, not in Docker
const anvilCheck = exec("pgrep", ["-f", "anvil.*--port.*8545"]);
const anvilRunning = anvilCheck.exitCode === 0;

const services: Record<string, boolean> = {
anvil: anvilRunning,
anvil: isServiceRunning("l1", DOCKER_OPTS),
sequencer: isServiceRunning("sequencer", DOCKER_OPTS),
validator: isServiceRunning("validator", DOCKER_OPTS),
l3node: isServiceRunning("l3node", DOCKER_OPTS),
Expand Down
5 changes: 0 additions & 5 deletions apps/cli/src/commands/stop.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { resolve } from "node:path";
import { composeDown } from "@arbitrum/testnode-core/docker.js";
import { exec } from "@arbitrum/testnode-core/exec.js";
import { stopCurrentRun } from "@arbitrum/testnode-core/run-logger.js";
import { Cli } from "incur";
import { findProjectRoot } from "../project-root.js";
Expand All @@ -20,10 +19,6 @@ export const stopCli = Cli.create("stop", {
console.log("[stop] Stopping Docker services...");
composeDown(DOCKER_OPTS);

console.log("[stop] Killing Anvil...");
exec("pkill", ["-f", "anvil.*--port.*8545"]);
exec("pkill", ["-f", "testnode-l1-heartbeat"]);

console.log("[stop] Done.");
return { success: true };
},
Expand Down
24 changes: 24 additions & 0 deletions docker/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
services:
# L1 anvil chain. 8545 is published so both host tooling (127.0.0.1:8545) and
# Nitro containers (host.docker.internal:8545) reach it. State persists to the
# bind-mounted config/anvil-state so snapshots work.
l1:
image: ghcr.io/foundry-rs/foundry:v1.3.5
ports:
- "8545:8545"
entrypoint: ["anvil"]
command:
- --host=0.0.0.0
- --port=8545
- --block-time=1
- --accounts=10
- --balance=1000000
- --mnemonic=indoor dish desk flag debris potato excuse depart ticket judge file exit
- --chain-id=1337
- --state=/state
volumes:
- ../config/anvil-state:/state

sequencer:
image: offchainlabs/nitro-node:v3.9.5-66e42c4
ports:
Expand All @@ -23,6 +43,8 @@ services:
- --ws.port=8548
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- l1

l3node:
image: offchainlabs/nitro-node:v3.9.5-66e42c4
Expand Down Expand Up @@ -84,6 +106,7 @@ services:
- "host.docker.internal:host-gateway"
depends_on:
- sequencer
- l1

scripts:
image: nitro-testnode-scripts:latest
Expand All @@ -98,6 +121,7 @@ services:
dockerfile: tokenbridge.Dockerfile
depends_on:
- sequencer
- l1
environment:
- ARB_URL=http://sequencer:8547
- ETH_URL=http://host.docker.internal:8545
Expand Down
10 changes: 3 additions & 7 deletions packages/core/src/init/chain-steps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ChildProcess } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import type { Address } from "viem";
Expand All @@ -13,7 +12,7 @@ import { deployTestErc20 } from "../fee-token.js";
import { ZERO_ADDRESS } from "../init-helpers.js";
import { patchGeneratedL2NodeConfig, patchGeneratedL3NodeConfig } from "../node-config-patches.js";
import { inboxAbi, publicClient, rollupAbi, walletClient } from "../rpc.js";
import { startAnvilWithState } from "../runtime.js";
import { startL1Container } from "../runtime.js";
import { deployRollupViaSdk, prepareNodeConfigFromDeployment } from "../sdk-chain.js";
import { markStepDone } from "../state.js";
import {
Expand Down Expand Up @@ -46,7 +45,6 @@ const CONTRACT_DEPLOYER_POLLING_INTERVAL_MS = 100;
const CONTRACT_DEPLOYER_CREATE2_CONFIRMATIONS = 1;
const WASM_MODULE_ROOT = "0xdb698a2576298f25448bc092e52cf13b1e24141c997135d70f217d674bbeb69a";

let anvilProcess: ChildProcess | undefined;
let contractDeployerImageBuilt = false;

interface RollupCreatorDeployment {
Expand Down Expand Up @@ -178,10 +176,8 @@ async function deployRollupCreatorViaDocker(
function createL1Steps(runtime: InitRuntime): Record<string, StepRunner> {
return {
"start-l1": async (state) => {
anvilProcess = startAnvilWithState(runtime.configDir);
return markStepDone(state, "start-l1", {
...(anvilProcess?.pid ? { pid: anvilProcess.pid } : {}),
});
startL1Container(runtime);
return markStepDone(state, "start-l1");
},
"wait-l1": async (state) => {
await waitForRpc(L1_RPC);
Expand Down
14 changes: 3 additions & 11 deletions packages/core/src/init/runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ChildProcess } from "node:child_process";
import { waitForRpc } from "../docker.js";
import {
finishActiveRun,
Expand All @@ -8,12 +7,7 @@ import {
startRunLoggingFromEnv,
updateRunStep,
} from "../run-logger.js";
import {
resetRuntime,
startAnvilWithState,
startNitroFromSnapshot,
stopRuntime,
} from "../runtime.js";
import { resetRuntime, startL1Container, startNitroFromSnapshot, stopRuntime } from "../runtime.js";
import { installSnapshotRelease } from "../snapshot-release.js";
import {
DEFAULT_SNAPSHOT_ID,
Expand All @@ -33,8 +27,6 @@ const L1_RPC = "http://127.0.0.1:8545";
const L2_RPC = "http://127.0.0.1:8547";
const L3_RPC = "http://127.0.0.1:8549";

let _anvilProcess: ChildProcess | undefined;

async function runInitLoop(
runtime: InitRuntime,
feeTokenDecimals?: number,
Expand Down Expand Up @@ -243,7 +235,7 @@ async function runSnapshotRestoreFlow(
});
restoreSnapshot(runtime.configDir, snapshotId);
startRunLoggingFromEnv(runtime.configDir) ?? startInlineRunLogging(runtime.configDir, logArgs);
_anvilProcess = startAnvilWithState(runtime.configDir);
startL1Container(runtime);
await waitForRpc(L1_RPC);
await startNitroFromSnapshot(
{
Expand Down Expand Up @@ -281,7 +273,7 @@ async function finalizeFreshInit(runtime: InitRuntime, snapshotId: string, total
configDir: runtime.configDir,
});
const snapshot = captureSnapshot(runtime.configDir, runtime.composeFile, snapshotId);
_anvilProcess = startAnvilWithState(runtime.configDir);
startL1Container(runtime);
await waitForRpc(L1_RPC);
await startNitroFromSnapshot(
{
Expand Down
72 changes: 16 additions & 56 deletions packages/core/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { type ChildProcess, spawn } from "node:child_process";
import { existsSync, readdirSync, rmSync } from "node:fs";
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import process from "node:process";
import { MNEMONIC } from "./accounts.js";
import { composeUp, waitForRpc } from "./docker.js";
import { composeDown, composeUp, waitForRpc } from "./docker.js";
import { exec } from "./exec.js";
import { SNAPSHOTS_DIRNAME, getAnvilStateDir } from "./snapshot.js";

Expand All @@ -19,19 +16,14 @@ export interface RuntimeOptions {
configDir: string;
}

const L1_HEARTBEAT_MARKER = "testnode-l1-heartbeat";
const STATIC_CONFIG_FILES = new Set([SNAPSHOTS_DIRNAME, "testnodes.json"]);

export function stopRuntime(options: RuntimeOptions): void {
exec("docker", ["compose", "-f", options.composeFile, "-p", options.projectName, "down"]);
exec("pkill", ["-f", "anvil.*--port.*8545"]);
exec("pkill", ["-f", L1_HEARTBEAT_MARKER]);
composeDown({ composeFile: options.composeFile, projectName: options.projectName });
}

export function resetRuntime(options: RuntimeOptions): void {
exec("docker", ["compose", "-f", options.composeFile, "-p", options.projectName, "down", "-v"]);
exec("pkill", ["-f", "anvil.*--port.*8545"]);
exec("pkill", ["-f", L1_HEARTBEAT_MARKER]);

if (!existsSync(options.configDir)) {
return;
Expand All @@ -45,52 +37,20 @@ export function resetRuntime(options: RuntimeOptions): void {
}
}

export function startAnvilWithState(configDir: string): ChildProcess {
const anvilProcess = spawn(
"anvil",
[
"--host",
"0.0.0.0",
"--port",
"8545",
"--block-time",
"1",
"--accounts",
"10",
"--balance",
"1000000",
"--mnemonic",
MNEMONIC,
"--chain-id",
"1337",
"--state",
getAnvilStateDir(configDir),
],
{ stdio: "ignore", detached: true },
);
anvilProcess.unref();
const heartbeatScript = `
/* ${L1_HEARTBEAT_MARKER} */
const rpcUrl = "http://127.0.0.1:8545";
const payload = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "evm_mine", params: [] });
const tick = async () => {
try {
await fetch(rpcUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: payload,
});
} catch {}
};
setInterval(tick, 100);
tick();
`;
const heartbeat = spawn(process.execPath, ["-e", heartbeatScript], {
stdio: "ignore",
detached: true,
/**
* Start the L1 anvil chain as the `l1` Docker compose service. The host
* `config/anvil-state` dir is bind-mounted into the container, so it is created
* first to keep snapshot capture/restore working against the host fs.
*/
export function startL1Container(options: RuntimeOptions): void {
mkdirSync(getAnvilStateDir(options.configDir), { recursive: true });
const result = composeUp(["l1"], {
composeFile: options.composeFile,
projectName: options.projectName,
});
heartbeat.unref();
return anvilProcess;
if (result.exitCode !== 0) {
throw new Error(result.stderr.trim() || "failed to start l1 service");
}
}

export async function startNitroFromSnapshot(
Expand Down
Loading