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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ genesis.json
/op-reth
.env

# decker (local devnet) clone + generated manifests/runtime state
/.decker

# editors
.code
.idea
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 42 additions & 0 deletions decker.ts
Original file line number Diff line number Diff line change
@@ -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",
},
Comment on lines +34 to +38
recipe,
// Docker instead of podman (no rootless podman socket available):
// target: { pods: "docker-compose" },
};
6 changes: 6 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
108 changes: 98 additions & 10 deletions local-testing/README.md
Original file line number Diff line number Diff line change
@@ -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
```
Comment on lines +9 to +11
- **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:<port>`, 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
Expand All @@ -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`.
83 changes: 83 additions & 0 deletions local-testing/decker/obs.ts
Original file line number Diff line number Diff line change
@@ -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" } }],
},
];
}
Loading