Skip to content
Open
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
52 changes: 52 additions & 0 deletions containers/chain-monitor.ts
Original file line number Diff line number Diff line change
@@ -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
},
};
}
13 changes: 9 additions & 4 deletions containers/op-rbuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ const FLASHBLOCKS_DEFAULTS: Required<FlashblocksConfig> = {
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 [];
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions decker.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export const project: DeckerProject = {
// // Enable op-rbuilder's Flashblocks websocket stream (ws://<host>: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
Expand Down
26 changes: 23 additions & 3 deletions recipes/opstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading