diff --git a/containers/chain-monitor.ts b/containers/chain-monitor.ts new file mode 100644 index 0000000..d04acf9 --- /dev/null +++ b/containers/chain-monitor.ts @@ -0,0 +1,52 @@ +import type { ContainerDef, ContainerResult, Ctx, Ports } from "../utils/types.ts"; +import { portNum } from "../utils/types.ts"; +import { BUILDER_ADDRESS, FLASHBLOCKS_WS_PORT } from "./op-rbuilder.ts"; + +// chain-monitor watches L1/L2 for stalled/missed blocks and builder wallet +export const ports: Ports = { + http: 8087, +}; + +function resolvedPorts(def: ContainerDef): Ports { + return { ...ports, ...((def.config?.ports as Ports | undefined) ?? {}) }; +} + +const DEFAULT_L2_BLOCK_TIME_SECONDS = 2; + +function refs(def: ContainerDef) { + const l1 = def.refs?.l1; + const l2 = def.refs?.l2; + if (!l1) throw new Error(`chain-monitor ${def.name}: missing refs.l1`); + if (!l2) throw new Error(`chain-monitor ${def.name}: missing refs.l2`); + return { l1, l2 }; +} + +export function buildContainer(def: ContainerDef, ctx: Ctx): ContainerResult { + const { l1, l2 } = refs(def); + const ps = resolvedPorts(def); + const l2BlockTimeSeconds = (def.config?.l2BlockTimeSeconds as number | undefined) ?? DEFAULT_L2_BLOCK_TIME_SECONDS; + const flashblocks = def.config?.flashblocks; + const l2Rpc = ctx.url(l2, "rpc"); + + return { + container: { + image: "ghcr.io/flashbots/chain-monitor:v0.0.58-dev.7", + args: [ + "serve", + "--server-listen-address", `0.0.0.0:${portNum(ps.http)}`, + "--l1-rpc", ctx.url(l1, "rpc"), + "--l2-rpc", l2Rpc, + "--l2-block-time", `${l2BlockTimeSeconds}s`, + "--l2-monitor-builder-address", BUILDER_ADDRESS, + "--l2-monitor-wallet", `builder=${BUILDER_ADDRESS}`, + // op-rbuilder's static port table has no "flashblocks" entry, reuse the + // host resolved for --l2-rpc and pair it with the fixed ws port. + ...(flashblocks + ? ["--l2-monitor-flashblocks-private-stream", `builder=ws://${new URL(l2Rpc).hostname}:${FLASHBLOCKS_WS_PORT}`] + : []), + ], + ports: ps, + // chain-monitor has no startup retry + }, + }; +} diff --git a/containers/op-rbuilder.ts b/containers/op-rbuilder.ts index ed1cb7d..8400d7a 100644 --- a/containers/op-rbuilder.ts +++ b/containers/op-rbuilder.ts @@ -43,8 +43,9 @@ const FLASHBLOCKS_DEFAULTS: Required = { continuousBuild: true, }; -// op-rbuilder default for --flashblocks.port -const FLASHBLOCKS_WS_PORT = 1111; +// op-rbuilder default for --flashblocks.port. Exported so chain-monitor.ts can +// reuse it (the static `ports` table above has no "flashblocks" entry). +export const FLASHBLOCKS_WS_PORT = 1111; function flashblocksArgs(opt: boolean | FlashblocksConfig | undefined, wsPort: number): string[] { if (!opt) return []; @@ -82,7 +83,7 @@ export function buildContainer(def: ContainerDef, _ctx: Ctx): ContainerResult { "--port", "30303", "--builder.enable-revert-protection", // Builder signing key (1st hardhat account). - "--rollup.builder-secret-key", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "--rollup.builder-secret-key", BUILDER_SECRET_KEY, // Peer with the sequencer EL through the bootnode so it can sync the chain. "--bootnodes", `enode://${BOOTNODE_ID}@bootnode:30303`, "--nat", "none", @@ -115,6 +116,10 @@ const BUILD: BinaryBuildSpec = { // already taken by the sequencer EL's published p2p port. const PROCESS_P2P_PORT = 30313; +// Builder signing key (1st hardhat/anvil dev account) +export const BUILDER_SECRET_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; +export const BUILDER_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + function refs(def: ProcessDef) { const l2 = def.refs?.l2; if (!l2) throw new Error(`op-rbuilder ${def.name}: missing refs.l2`); @@ -148,7 +153,7 @@ export function buildProcess(def: ProcessDef, ctx: HostCtx): ProcessResult { "--port", String(PROCESS_P2P_PORT), "--builder.enable-revert-protection", // Builder signing key, same as container mode. - "--rollup.builder-secret-key", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "--rollup.builder-secret-key", BUILDER_SECRET_KEY, // Rootless podman pod IPs aren't reachable from the host and // discovery-advertised addresses are useless across that boundary, so // don't rely on discovery at all: dial EL directly by its fixed enode diff --git a/decker.example.ts b/decker.example.ts index d3fb215..028fc2c 100644 --- a/decker.example.ts +++ b/decker.example.ts @@ -56,6 +56,9 @@ export const project: DeckerProject = { // // Enable op-rbuilder's Flashblocks websocket stream (ws://:1111); // // pass an object instead of `true` to override individual fields. // flashblocks: true, + // // Add a chain-monitor sidecar watching L1 + L2 for stalled/missed blocks + // // and the builder's known wallet (+ its Flashblocks stream, if enabled). + // chainMonitor: true, // }, // // Add or override the prototypes the recipe resolves by name — tweak a diff --git a/recipes/opstack.ts b/recipes/opstack.ts index b86579f..6bccc21 100644 --- a/recipes/opstack.ts +++ b/recipes/opstack.ts @@ -32,6 +32,8 @@ export type OpstackOptions = { sendOffsetMs?: number; continuousBuild?: boolean; }; + // Add a chain-monitor sidecar that watches L1/L2 + builder wallet. + chainMonitor?: boolean | string; }; export function recipe(o: OpstackOptions = {}): Recipe { @@ -40,8 +42,10 @@ export function recipe(o: OpstackOptions = {}): Recipe { const l2BlockTime = Number(o.l2BlockTime ?? 2); const externalBuilder = o.externalBuilder && o.externalBuilder !== "false" ? String(o.externalBuilder) : false; - // "true" and CLI's "true" (`--opt builderBinary=true`) mean "auto-build"; - // any other string is a path to a prebuilt binary. + // "true" and CLI's "true" (`--opt builderBinary=true`) mean "default"; + // any other string is a custom config. + // For builderBinary/flashblocks/chainMonitor options. + const builderBinaryOpt = o.builderBinary; const builderHostProcess = builderBinaryOpt !== undefined && builderBinaryOpt !== false && builderBinaryOpt !== "false"; const builderBinaryPath = builderHostProcess && builderBinaryOpt !== true && builderBinaryOpt !== "true" @@ -51,7 +55,6 @@ export function recipe(o: OpstackOptions = {}): Recipe { throw new Error(`opstack: builderBinary requires externalBuilder="op-rbuilder" (got ${externalBuilder || "false"})`); } - // Same true/"true" vs. false/"false"/undefined as builderBinary const flashblocksOpt = o.flashblocks; const flashblocksEnabled = flashblocksOpt !== undefined && flashblocksOpt !== false && flashblocksOpt !== "false"; const flashblocks = flashblocksEnabled @@ -61,6 +64,9 @@ export function recipe(o: OpstackOptions = {}): Recipe { // container mode and host-process mode can't tune Flashblocks differently. const builderConfig = flashblocks !== undefined ? { flashblocks } : undefined; + const chainMonitorOpt = o.chainMonitor; + const chainMonitor = chainMonitorOpt !== undefined && chainMonitorOpt !== false && chainMonitorOpt !== "false"; + // L2 execution client selection. Karst (OP Upgrade 19) ends op-geth support, so // from Karst on the L2 EL is op-reth and op-node needs a newer release. Forks // up to jovian keep the op-geth client set they were verified with. @@ -109,6 +115,18 @@ export function recipe(o: OpstackOptions = {}): Recipe { ] : []; + const chainMonitorPods: Pod[] = chainMonitor + ? [{ + name: "chain-monitor", + containers: [{ + name: "chain-monitor", + prototype: "chain-monitor", + refs: { l1: "el-1", l2: externalBuilder ? "op-rbuilder" : l2El }, + config: { l2BlockTimeSeconds: l2BlockTime, flashblocks: externalBuilder ? flashblocks : undefined }, + }], + }] + : []; + // L1: reth (el-1) + lighthouse beacon/validator produce the settlement chain. // L2: the sequencer EL (op-geth/op-reth by fork) is driven by op-node (directly, // or via rollup-boost with an external builder); op-batcher posts L2 batches @@ -175,6 +193,8 @@ export function recipe(o: OpstackOptions = {}): Recipe { }, ], }, + // Watches L1 + L2 for stalled/missed blocks, only when opted in. + ...chainMonitorPods, // --- Block explorers: one Blockscout per layer. Each is a 4-container stack // (postgres + verifier + backend + frontend); the two stacks use disjoint // host ports. UI URLs print in the `up` summary.