diff --git a/.gitignore b/.gitignore index f01ed3a0a..414c524a1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ genesis.json /op-reth .env +# decker (local devnet) clone + generated manifests/runtime state +/.decker + # editors .code .idea diff --git a/README.md b/README.md index c658584bc..d8714b85b 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ This runs both unit tests and integration tests. ## Local Devnet -Local development environment setup and configuration files can be found in [`local-testing/README.md`](./local-testing/README.md). +Local devnet setup via [decker](https://github.com/flashbots/decker) is documented in [`local-testing/README.md`](./local-testing/README.md). ## Running GitHub actions locally diff --git a/decker.ts b/decker.ts new file mode 100644 index 000000000..9500236e2 --- /dev/null +++ b/decker.ts @@ -0,0 +1,42 @@ +// decker (https://github.com/flashbots/decker) manifest for op-rbuilder's local +// devnet. See local-testing/README.md for quickstart. + +import { recipe as opstackRecipe, type OpstackOptions } from "./.decker/recipes/opstack.ts"; +import type { DeckerProject } from "./.decker/utils/manifest.ts"; +import type { Recipe } from "./.decker/utils/types.ts"; +import { obsPods } from "./local-testing/decker/obs.ts"; + +const options: OpstackOptions = { + // Run op-rbuilder behind rollup-boost + externalBuilder: "op-rbuilder", + // Host process instead of pinned op-rbuilder container + // Use `just devnet` or `make op-rbuilder-profiling` to compile binary. + builderBinary: "./target/profiling/op-rbuilder", + // Karst needs 1s+ blocks for local iteration. + l2BlockTime: 1, + flashblocks: { blockTimeMs: 200, endBufferMs: 75, sendOffsetMs: -30, continuousBuild: true }, + chainMonitor: true, +}; + +// Builder's RPC port: decker's op-rbuilder container/process +// prototype hardcodes rpc=9645 (authrpc=9651, metrics=6062) with no options +// surface (recipe or prototype-override) to repin it to builder-playground's +// old :2222 without risking a mismatch between the port it actually binds and +// what other services resolve by name so :9645 is the port to use everywhere +// for this devnet. +const opstack = opstackRecipe(options); + +// Prometheus + Grafana appended on top of the opstack recipe's own pods +// (see local-testing/decker/obs.ts). +const recipe: Recipe = { ...opstack, pods: [...opstack.pods, ...obsPods(opstack)] }; + +export const project: DeckerProject = { + decker: { + source: "https://github.com/flashbots/decker.git", + ref: "main", + into: ".decker", + }, + recipe, + // Docker instead of podman (no rootless podman socket available): + // target: { pods: "docker-compose" }, +}; diff --git a/justfile b/justfile index 23c05d476..9f2affab0 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,9 @@ +# Build profiling op-rbuilder binary and start the decker local devnet +# (see local-testing/README.md; requires `decker` CLI on PATH) +devnet: + make op-rbuilder-profiling FEATURES=telemetry,loki + decker start + # Build and run op-rbuilder in playground mode for testing run-playground: cargo build --bin op-rbuilder -p op-rbuilder diff --git a/local-testing/README.md b/local-testing/README.md index 0e640518b..9b97c37b0 100644 --- a/local-testing/README.md +++ b/local-testing/README.md @@ -1,14 +1,106 @@ # Local Dev Environment -This directory contains local development environment configuration for `op-rbuilder`. +Local devnet configuration for `op-rbuilder`, powered by [decker](https://github.com/flashbots/decker). [builder-playground](https://github.com/flashbots/builder-playground) (`local-testing/playground.yaml`) is kept working as a legacy fallback, see [Legacy appendix](#legacy-builder-playground-deprecated). ## Prerequisites +- **podman**, with its (rootless) API socket running. For docker instead, uncomment the `target: { pods: "docker-compose" }` line at the bottom of `decker.ts`. +- **decker** CLI: + ```bash + curl -sSfL https://raw.githubusercontent.com/flashbots/decker/main/install.sh | bash + ``` +- **process-compose**: `op-rbuilder` runs as a host process here, which decker drives through its `process-compose` renderer. +- [flashbots/contender](https://github.com/flashbots/contender), for traffic generation. + +## Quickstart + +**First time only**: clone decker into `.decker/` before anything else: + +```bash +git clone https://github.com/flashbots/decker.git .decker +``` + +From the repository root: + +```bash +just devnet +``` + +This builds the profiling `op-rbuilder` binary and runs `decker start` against the `.decker/` clone above, bringing the whole stack up. + +Or alternatively: + +```bash +make op-rbuilder-profiling FEATURES=telemetry,loki +decker start # fg, Ctrl+C to stop; use `decker up` + `decker down` to detach +``` + +See `decker.ts` for the exact recipe options (fork, L2 block time, flashblocks config, chain-monitor, observability). + +## Generate Local Traffic + +```bash +contender spam --tps 50 -r http://localhost:9645 --optimism --min-balance 0.14eth --forever +``` + +You should start to see blocks being built and landed on-chain with `contender` transactions. + +## Send a Test Transaction + +```bash +decker test --rpc 9645 +``` + +(`--rpc` accepts a bare port as shorthand for `http://localhost:`, or a full URL.) + +## Services + +| Service | Port | Notes | +|---|---|---| +| op-rbuilder RPC | `9645` | decker's fixed port | +| op-rbuilder authrpc | `9651` | rollup-boost engine-API | +| op-rbuilder metrics | `6062` | | +| op-rbuilder flashblocks (ws) | `1111` | `ws://localhost:1111` | +| chain-monitor | `8087` | `http://localhost:8087/metrics` | +| Prometheus | `9009` | scrapes op-rbuilder + chain-monitor metrics | +| Grafana | `3000` | `http://localhost:3000`, anonymous Admin (no login) | +| L1 EL (`el-1`) RPC | `8545` | | +| L2 sequencer EL RPC | `9545` | fallback target rollup-boost | +| L1 explorer (Blockscout) | `3002` | `http://localhost:3002` | +| L2 explorer (Blockscout) | `3003` | `http://localhost:3003` | +| Pod logs (Dozzle) | `18080` | podman only; under `docker-compose` use `decker attach` for logs | + +## Observability + +Provisioned by `local-testing/decker/obs.ts`: + +- **Prometheus** (`:9009`) scrapes `op-rbuilder`'s metrics. +- **Grafana** (`:3000`) provisioned with op-rbuilder local dashboard. + +**Deferred**: Loki, Tempo and Alloy. decker has no container prototypes for them yet. + +## Builder-Only Restart Loop + +To iterate on `op-rbuilder` itself without tearing down the rest of the devnet: stop decker's own builder process (it holds `:9645`/`:9651`), rebuild, then run the binary directly from the repository root; it auto-detects the running devnet's artifacts: + +```bash +make op-rbuilder-profiling FEATURES=telemetry,loki +./target/profiling/op-rbuilder node --builder.playground +``` + +A bare `--builder.playground` resolves `./.decker/runtime/artifacts` first, falling back to the legacy `$HOME/.local/state/builder-playground/devnet` if that doesn't exist. + +## Karst + +The devnet defaults to the `karst` L2 fork, which requires `op-reth` v2.3.x dependency. + +## Legacy: builder-playground (deprecated) + +Kept working during the migration to decker above; do not add anything new here. + 1. [flashbots/builder-playground](https://github.com/flashbots/builder-playground). 1. [flashbots/contender](https://github.com/flashbots/contender). -## Start Local Devnet - From the repository root: ```bash @@ -31,20 +123,16 @@ Services: Logs can be found at `$HOME/.local/state/builder-playground/sessions/latest/logs/` -## Generate Local Traffic - -Run `contender`: +Generate local traffic with `contender`: ```bash contender spam --tps 50 -r http://localhost:2222 --optimism --min-balance 0.14eth --forever ``` -You should start to see blocks being built and landed on-chain with `contender` transactions. - -Alternatively, send a single test transaction: +Send a single test transaction (note the `--rpc` flag — a bare target URL is silently ignored): ```bash -builder-playground test http://localhost:2222 --timeout 30s --retries 10 +builder-playground test --rpc http://localhost:2222 --timeout 30s --retries 10 ``` `op-rbuilder` automatically detects settings and ports from the running playground via `--builder.playground`. diff --git a/local-testing/decker/obs.ts b/local-testing/decker/obs.ts new file mode 100644 index 000000000..0065e34c3 --- /dev/null +++ b/local-testing/decker/obs.ts @@ -0,0 +1,83 @@ +// Observability overlay for decker.ts: Prometheus scraping op-rbuilder (+ +// chain-monitor, when enabled) and Grafana provisioning. +// +// Loki/Tempo/Alloy are deferred and will be wired as follow up. +import type { ContainerDef, ContainerResult, Ctx, Pod, Prototype, Recipe } from "../../.decker/utils/types.ts"; + +// Resolved against this module's file. +const GRAFANA_DIR = new URL("../grafana/", import.meta.url); + +function readLocal(relPath: string): string { + return Deno.readTextFileSync(new URL(relPath, GRAFANA_DIR)); +} + +// Read once at module load instead of per-render. +const DASHBOARD_PROVIDER_YML = readLocal("dashboards.yaml"); +const OP_RBUILDER_DASHBOARD_JSON = readLocal("dashboards/op-rbuilder.json"); + +const GRAFANA_HTTP_PORT = 3000; + +function buildGrafanaContainer(def: ContainerDef, ctx: Ctx): ContainerResult { + const prometheusRef = def.refs?.prometheus; + if (!prometheusRef) throw new Error(`grafana ${def.name}: missing refs.prometheus`); + + const datasourcesYml = `apiVersion: 1 +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: ${ctx.url(prometheusRef, "http")} + uid: prometheus + isDefault: true + editable: false + jsonData: + timeInterval: "5s" + httpMethod: POST +`; + + return { + container: { + image: "docker.io/grafana/grafana:13.0.1", + env: { + // Passwordless dev access + GF_AUTH_ANONYMOUS_ENABLED: "true", + GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin", + GF_AUTH_DISABLE_LOGIN_FORM: "true", + GF_AUTH_BASIC_ENABLED: "false", + GF_USERS_DEFAULT_THEME: "dark", + GF_LOG_LEVEL: "warn", + }, + ports: { http: GRAFANA_HTTP_PORT }, + }, + configs: [ + { filename: "datasources.yml", content: datasourcesYml, mountPath: "/etc/grafana/provisioning/datasources/datasources.yml" }, + { filename: "dashboards.yml", content: DASHBOARD_PROVIDER_YML, mountPath: "/etc/grafana/provisioning/dashboards/dashboards.yml" }, + { filename: "op-rbuilder.json", content: OP_RBUILDER_DASHBOARD_JSON, mountPath: "/var/lib/grafana/dashboards/op-rbuilder.json" }, + ], + }; +} + +const grafanaPrototype: Prototype = { + ports: { http: GRAFANA_HTTP_PORT }, + buildContainer: buildGrafanaContainer, + webui: { label: "Grafana (op-rbuilder dashboard)" }, +}; + +// Appended to the opstack recipe `pods`. +export function obsPods(base: Recipe): Pod[] { + const hasChainMonitor = base.pods.some((p) => p.name === "chain-monitor"); + const scrape = [ + { job: "op-rbuilder", ref: "op-rbuilder", port: "metrics" }, + ...(hasChainMonitor ? [{ job: "chain-monitor", ref: "chain-monitor", port: "http", path: "/metrics" }] : []), + ]; + return [ + { + name: "prometheus", + containers: [{ name: "prometheus", prototype: "prometheus", config: { scrape } }], + }, + { + name: "grafana", + containers: [{ name: "grafana", prototype: grafanaPrototype, refs: { prometheus: "prometheus" } }], + }, + ]; +}