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
6 changes: 6 additions & 0 deletions containers/bootnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export const BOOTNODE_SECRET_KEY = "00000000000000000000000000000000000000000000
export const BOOTNODE_ID =
"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8";

// Deterministic p2p identity for the sequencer L2 EL (op-geth/op-reth), used only
// when op-rbuilder runs as a HOST PROCESS instead of a container.
export const EL_P2P_SECRET_KEY = "0000000000000000000000000000000000000000000000000000000000000002";
export const EL_P2P_ID =
"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a";

// p2p only — reached over pod DNS (peers hardcode host "bootnode" + port 30303),
// so nothing is host-published.
export const ports: Ports = {};
Expand Down
6 changes: 6 additions & 0 deletions containers/op-geth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ContainerDef, ContainerResult, Ctx, Ports } from "../utils/types.ts";
import { portNum } from "../utils/types.ts";
import { EL_P2P_SECRET_KEY } from "./bootnode.ts";

// op-geth is the L2 execution engine (the sequencer's EL). op-node drives it
// over the engine API (authrpc). Default ports sit clear of the L1 reth
Expand Down Expand Up @@ -28,6 +29,10 @@ export function buildContainer(def: ContainerDef, _ctx: Ctx): ContainerResult {
const discovery = bootnodeId
? `--maxpeers 25 --bootnodes enode://${bootnodeId}@$(getent hosts bootnode | awk '{print $1}'):30303 --discovery.v4`
: "--maxpeers 0 --nodiscover";
// Set only by recipes/opstack.ts when op-rbuilder runs as a host process: the
// recipe publishes this EL's p2p port to the host so a deterministic node key
// is needed.
const hostBuilderP2p = ps.p2p !== undefined;
// geth init seeds the datadir from the L2 genesis, then exec's the node.
const script = [
"geth init --datadir /data_opgeth --state.scheme hash /artifacts/l2-genesis.json",
Expand All @@ -49,6 +54,7 @@ export function buildContainer(def: ContainerDef, _ctx: Ctx): ContainerResult {
"--port 30303",
"--metrics --metrics.addr 0.0.0.0",
`--metrics.port ${portNum(ps.metrics)}`,
...(hostBuilderP2p ? [`--nodekeyhex ${EL_P2P_SECRET_KEY}`] : []),
].join(" ");

return {
Expand Down
74 changes: 71 additions & 3 deletions containers/op-rbuilder.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import type { ContainerDef, ContainerResult, Ctx, Ports } from "../utils/types.ts";
import type {
BinaryBuildSpec,
ContainerDef,
ContainerResult,
Ctx,
HostCtx,
Ports,
ProcessDef,
ProcessResult,
} from "../utils/types.ts";
import { portNum } from "../utils/types.ts";
import { BOOTNODE_ID } from "./bootnode.ts";
import { binaryBuildPath } from "../utils/binary-build.ts";
import { BOOTNODE_ID, EL_P2P_ID } from "./bootnode.ts";

// op-rbuilder is the external L2 block builder (Flashbots' reth-based OP builder).
// rollup-boost asks it for a block each slot. It syncs the canonical chain from
// the sequencer EL over L2 P2P via the bootnode, then builds on the head. Reads
// the same l2-genesis + jwtsecret as the sequencer EL.
//
// recipes/opstack.ts's `builderBinary` runs this as a HOST PROCESS instead
export const ports: Ports = {
rpc: 9645,
authrpc: 9651, // rollup-boost queries the engine API here
metrics: { port: 6062, service: false },
// p2p 30303 is container-internal.
};

function resolvedPorts(def: ContainerDef): Ports {
function resolvedPorts(def: ContainerDef | ProcessDef): Ports {
return { ...ports, ...((def.config?.ports as Ports | undefined) ?? {}) };
}

Expand Down Expand Up @@ -56,3 +68,59 @@ export function buildContainer(def: ContainerDef, _ctx: Ctx): ContainerResult {
],
};
}

// Build from source when no local binary is configured
const BUILD: BinaryBuildSpec = {
repo: "https://github.com/flashbots/op-rbuilder.git",
ref: "main",
cmd: "cargo build --release --bin op-rbuilder",
artifact: "target/release/op-rbuilder",
};

// op-rbuilder's own p2p listener when it runs as a host process. Distinct from
// the container-mode literal (30303) above: in this mode host port 30303 is
// already taken by the sequencer EL's published p2p port.
const PROCESS_P2P_PORT = 30313;

function refs(def: ProcessDef) {
const l2 = def.refs?.l2;
if (!l2) throw new Error(`op-rbuilder ${def.name}: missing refs.l2`);
return { l2 };
}

export function buildProcess(def: ProcessDef, ctx: HostCtx): ProcessResult {
const { l2 } = refs(def);
const ps = resolvedPorts(def);
const dataDir = ctx.dataPath(def.name, "data");
// The sequencer EL only publishes its p2p port to the host in this mode.
const elP2pPort = new URL(ctx.url(l2, "p2p")).port;

return {
process: {
command: [
def.binary ?? binaryBuildPath(BUILD),
"node",
"--authrpc.port", String(portNum(ps.authrpc)),
"--authrpc.addr", "0.0.0.0",
"--authrpc.jwtsecret", `${ctx.artifactsPath}/jwtsecret`,
"--http",
"--http.addr", "0.0.0.0",
"--http.port", String(portNum(ps.rpc)),
"--chain", `${ctx.artifactsPath}/l2-genesis.json`,
"--datadir", dataDir,
"--color", "never",
"--metrics", `0.0.0.0:${portNum(ps.metrics)}`,
"--port", String(PROCESS_P2P_PORT),
"--builder.enable-revert-protection",
// Builder signing key, same as container mode.
"--rollup.builder-secret-key", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
// 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
"--trusted-peers", `enode://${EL_P2P_ID}@127.0.0.1:${elP2pPort}`,
"--disable-discovery",
],
},
binaryBuild: def.binary ? undefined : BUILD,
};
}
4 changes: 4 additions & 0 deletions containers/op-reth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ContainerDef, ContainerResult, Ctx, Ports } from "../utils/types.ts";
import { portNum } from "../utils/types.ts";
import { EL_P2P_SECRET_KEY } from "./bootnode.ts";

// op-reth is the L2 execution engine for Karst and beyond: the Karst upgrade
// (OP Upgrade 19) ends op-geth support, so from Karst on the sequencer EL is
Expand Down Expand Up @@ -27,6 +28,8 @@ export function buildContainer(def: ContainerDef, _ctx: Ctx): ContainerResult {
const discovery = bootnodeId
? ["--bootnodes", `enode://${bootnodeId}@bootnode:30303`, "--rollup.discovery.v4"]
: ["--disable-discovery"];
// Set only by recipes/opstack.ts when op-rbuilder runs as a host process.
const hostBuilderP2p = ps.p2p !== undefined;
return {
container: {
image: "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-reth:v2.3.3",
Expand Down Expand Up @@ -58,6 +61,7 @@ export function buildContainer(def: ContainerDef, _ctx: Ctx): ContainerResult {
"--engine.persistence-threshold", "0",
"--engine.memory-block-buffer-target", "0",
...discovery,
...(hostBuilderP2p ? ["--p2p-secret-key-hex", EL_P2P_SECRET_KEY] : []),
],
ports: ps,
volumeMounts: [
Expand Down
2 changes: 2 additions & 0 deletions decker.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const project: DeckerProject = {
// l2Fork: "karst",
// l2BlockTime: 1,
// externalBuilder: "op-rbuilder",
// // Run op-rbuilder as a host process instead of the pinned container
// builderBinary: "../op-rbuilder/target/profiling/op-rbuilder",
// },
//
// Add or override the prototypes the recipe resolves by name — tweak a
Expand Down
52 changes: 46 additions & 6 deletions recipes/opstack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Pod, Recipe } from "../utils/types.ts";
import type { Pod, ProcessDef, Recipe } from "../utils/types.ts";
import { BOOTNODE_ID } from "../containers/bootnode.ts";

// An OP stack, ported from builder-playground's `opstack` recipe. The artifacts
Expand All @@ -17,6 +17,14 @@ export type OpstackOptions = {
// back to the local EL). Works with all forks — the local EL (op-geth or
// op-reth) joins the bootnode so the builder peers + gets mempool txs.
externalBuilder?: string | false; // (default off)
// Run op-rbuilder as a HOST PROCESS instead of the pinned container.
// A string as a path to a prebuilt op-rbuilder binary; `true` (or the CLI's
// `--opt builderBinary=true`) builds it from source on `up` instead.
// Moves op-rbuilder from `pods` to `recipe.processes`. The sequencer L2 EL
// gets a deterministic p2p identity + its p2p port published to the host so
// op-rbuilder can trusted-peer-dial it directly instead of going through the
// container-mode bootnode, which the host can't reach. (default off)
builderBinary?: string | boolean;
};

export function recipe(o: OpstackOptions = {}): Recipe {
Expand All @@ -25,6 +33,17 @@ 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.
const builderBinaryOpt = o.builderBinary;
const builderHostProcess = builderBinaryOpt !== undefined && builderBinaryOpt !== false && builderBinaryOpt !== "false";
const builderBinaryPath = builderHostProcess && builderBinaryOpt !== true && builderBinaryOpt !== "true"
? String(builderBinaryOpt)
: undefined;
if (builderHostProcess && externalBuilder !== "op-rbuilder") {
throw new Error(`opstack: builderBinary requires externalBuilder="op-rbuilder" (got ${externalBuilder || "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 All @@ -35,14 +54,24 @@ export function recipe(o: OpstackOptions = {}): Recipe {
// trace API, op-geth speaks debug_trace*.
const l2Variant = l2El === "op-reth" ? "erigon" : "geth";

// With an external builder, op-node drives rollup-boost instead of the EL, and
// the local EL joins the bootnode so op-rbuilder can peer + sync the chain.
// With an external builder, op-node drives rollup-boost instead of the EL.
// Container mode: the local EL joins the bootnode so op-rbuilder can peer +
// sync the chain over pod-network discovery. Process mode: discovery can't
// cross the host/pod-network boundary, so instead the EL gets a fixed p2p
// identity + host-published p2p port for op-rbuilder to trusted-peer-dial
// directly.
const l2Engine = externalBuilder ? "rollup-boost" : l2El;
const l2ElConfig = externalBuilder ? { bootnodeId: BOOTNODE_ID } : undefined;
const l2ElConfig = builderHostProcess
? { ports: { p2p: { port: 30303, service: false } } }
: (externalBuilder ? { bootnodeId: BOOTNODE_ID } : undefined);
const builderPods: Pod[] = externalBuilder
? [
{ name: "bootnode", containers: [{ name: "bootnode", prototype: "bootnode" }] },
{ name: "op-rbuilder", containers: [{ name: "op-rbuilder", prototype: "op-rbuilder" }] },
// The bootnode is only needed for container mode's discovery-based
// peering; a host-process op-rbuilder dials the EL directly (see above).
...(builderHostProcess ? [] : [
{ name: "bootnode", containers: [{ name: "bootnode", prototype: "bootnode" }] },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If I am reading correctly, we are turning on the bootnode here but not doing the same when we switch to the processes instead. Is this intentional?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes! In host-process mode builder can't cross container network boundary so the discovery is not possible via bootnode, so we discard it completely. Instead the sequencer EL has a deterministic p2p identity + p2p port so the builder can dials it directly.

{ name: "op-rbuilder", containers: [{ name: "op-rbuilder", prototype: "op-rbuilder" }] },
]),
{
name: "rollup-boost",
containers: [
Expand All @@ -51,6 +80,16 @@ export function recipe(o: OpstackOptions = {}): Recipe {
},
]
: [];
const builderProcesses: ProcessDef[] = builderHostProcess
? [
{
name: "op-rbuilder",
prototype: "op-rbuilder",
refs: { l2: l2El },
...(builderBinaryPath ? { binary: builderBinaryPath } : {}),
},
]
: [];

// 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,
Expand Down Expand Up @@ -166,5 +205,6 @@ export function recipe(o: OpstackOptions = {}): Recipe {
],
},
],
...(builderProcesses.length > 0 ? { processes: builderProcesses } : {}),
};
}
Loading