Skip to content
Merged
186 changes: 121 additions & 65 deletions custom-recipes/buildernet/mkosi/README.md
Original file line number Diff line number Diff line change
@@ -1,106 +1,162 @@
# BuilderNet VM Scripts
# BuilderNet VM for Playground

Temporary bash scripts for running BuilderNet VM alongside builder-playground.
Feel free to translate them to Go and integrate into the CLI.
Run a BuilderNet VM alongside builder-playground's L1 network. The playground manages the L1 stack (Reth, Lighthouse, mev-boost-relay, builder-hub) in Docker, while a BuilderNet VM runs in QEMU and proposes blocks to the network.

## Information:
> **Important:** The VM image must be built with the `playground` mkosi profile. Production images are hardened and will not work with this setup.

Changes are made in two repos:
- [flashbots-images](https://github.com/flashbots/flashbots-images/tree/fryd/mkosi-playground) on `fryd/mkosi-playground` branch,
- [builder-playground](https://github.com/flashbots/builder-playground/tree/fryd/mkosi-playground) on `fryd/mkosi-playground` branch,
## Prerequisites

There are no plans of using [buildernet-playground](https://github.com/flashbots/buildernet-playground) repo.
- **Linux with KVM** (macOS is not supported)
- **Docker** (for the L1 network stack)
- **QEMU** with KVM support (`qemu-system-x86_64`)
- **UEFI firmware** (`edk2-ovmf` or equivalent)
- **socat** (for VM console access)
- **jq**, **curl**

## Start playground

Start playground with: `--bind-external` (expose ports to the VM) and `--contender` (create transactions)
Verify KVM is available:

```bash
builder-playground \
start buildernet \
--bind-external \
--output ".playground-home" \
--contender \
--detached fryd-vm-builder
ls /dev/kvm
```

## First Time Setup
## Quick Start

```bash
./sync.sh # Clone / fetch flashbots-images repo
./build.sh # Build VM image with mkosi
./prepare.sh # Extract image + create data disk
# 1. Install builder-playground
curl -sSfL https://raw.githubusercontent.com/flashbots/builder-playground/main/install.sh | bash

# 2. Create project dir and generate recipe files
mkdir buildernet-dev && cd buildernet-dev
builder-playground generate buildernet/mkosi

# 3. Point to the VM image binary
# - alternatively you can edit playground.yaml
# - you can point to local disk or URL
export BUILDERNET_IMAGE=flashbots-images/path/buildernet-playground.qcow2

# 4. Start
builder-playground start playground.yaml --bind-external --detached

# 5. Verify by sending transaction
builder-playground test --rpc http://localhost:18645 --el-rpc http://localhost:8545
```

`sync.sh` clones/updates the `fryd/mkosi-playground` branch of flashbots-images.
If the test transaction is included in a block, the full pipeline is working: transaction reaches rbuilder inside the VM, rbuilder builds a block, and it lands on the L1 chain.

## Run VM
## Using Your Own Image

```bash
./start.sh # Start VM (background)
./console.sh # Open console to the VM
./ssh.sh # SSH into running VM (requires SSH key setup)
./stop.sh # Stop VM
For developers working on [flashbots-images](https://github.com/flashbots/flashbots-images) who want to test VM changes against a local network.

Build the image in your flashbots-images checkout using the **playground** mkosi profile, then point `BUILDERNET_IMAGE` to it:

```yaml
# In playground.yaml, under builder > env:
env:
BUILDERNET_IMAGE: "/path/to/buildernet-qemu_latest.qcow2"
```

## Builder Hub
Or override via environment variable:

```bash
./builderhub-configure.sh # Register VM with builder-hub and update config for the VM
./builderhub-get-config.sh # Get configuration for the VM
export BUILDERNET_IMAGE=/path/to/buildernet-qemu_latest.qcow2
```

## Operator API
See the [flashbots-images](https://github.com/flashbots/flashbots-images) repository for build environment setup, available profiles, and customization options.

Scripts to interact with the operator-api service running inside the VM.
## VM Management

> **Note:** Actions and File Uploads could potentially be used for various things, like injecting genesis config instead of BuilderHub - still exploring this functionality.
### Lifecycle

```bash
./operator-api-health.sh # Check if operator-api is healthy
./operator-api-logs.sh # Get event logs
./operator-api-action.sh <action> # Execute an action
./scripts/stop.sh # Stop the VM (Docker L1 stack keeps running)
./scripts/prepare.sh <path-or-url> # Reset VM to fresh state
./scripts/start.sh # Start the VM
```

Available actions:
- `reboot` - Reboot the system
- `rbuilder_restart` - Restart rbuilder-operator service
- `rbuilder_stop` - Stop rbuilder-operator service
- `fetch_config` - Fetch config from BuilderHub
- `rbuilder_bidding_restart` - Restart rbuilder-bidding service
- `ssh_stop` - Stop SSH service
- `ssh_start` - Start SSH service
- `haproxy_restart` - Restart HAProxy service
### Access

### File Uploads
```bash
./scripts/console.sh # Serial console (exit: Ctrl+])
```

Upload files to predefined paths. Only whitelisted names from `[file_uploads]` config are allowed:
### Environment Variables

```toml
[file_uploads]
rbuilder_blocklist = "/var/lib/persistent/rbuilder-operator/rbuilder.blocklist.json"
```
| Variable | Default | Description |
|----------|---------|-------------|
| `BUILDERNET_IMAGE` | *(set in playground.yaml)* | Path or URL to the VM qcow2 image |
| `QEMU_CPU` | `8` | Number of CPU cores |
| `QEMU_RAM` | `32G` | Memory allocation |
| `QEMU_ACCEL` | `kvm` | Acceleration (`kvm` or `tcg`) |

Environment variables override values defined in `playground.yaml`.

## Operator API

The operator-api runs inside the VM and is exposed on port 13535:

```bash
# Stores local blocklist.json content to the configured remote path
curl -k --data-binary "@blocklist.json" https://localhost:13535/api/v1/file-upload/rbuilder_blocklist
./scripts/operator-api-health.sh # Health check
./scripts/operator-api-logs.sh # Event logs
./scripts/operator-api-action.sh <action> # Execute an action
```

### Customization

To add more actions or file uploads, modify the config template:
https://github.com/flashbots/flashbots-images/blob/fryd/mkosi-playground/mkosi.profiles/playground/mkosi.extra/usr/lib/mustache-templates/etc/operator-api/config.toml.mustache
Available actions: `reboot`, `rbuilder_restart`, `rbuilder_stop`, `fetch_config`, `rbuilder_bidding_restart`, `ssh_stop`, `ssh_start`, `haproxy_restart`.

## Maintenance
## Stopping

```bash
./sync.sh # Update flashbots-images to latest
./clean.sh # Clean build artifacts + runtime files
# Stop just the VM (Docker keeps running)
./scripts/stop.sh

# Stop everything
builder-playground stop <session-name>

# Full cleanup
builder-playground clean <session-name>
./scripts/clean.sh
```

Use `builder-playground list` to find the session name.

## Ports

| Port | Service |
|------|---------|
| 2222 | SSH (maps to VM:40192) |
| 13535 | Operator API (maps to VM:3535) |
### VM (QEMU host forwarding)

| Host Port | VM Port | Service |
|-----------|---------|---------|
| 2222 | 40192 | SSH |
| 13535 | 3535 | Operator API |
| 18645 | 8645 | rbuilder JSON-RPC |
| 10080 | 80 | HAProxy HTTP |
| 10443 | 443 | HAProxy HTTPS |

### Playground (Docker)

Use `builder-playground port <service> <port-name>` to look up host ports. Common services: `el` (Reth), `beacon` (Lighthouse), `mev-boost-relay`.

## File Structure

After `builder-playground generate buildernet/mkosi`:

```
buildernet-dev/
├── playground.yaml # Recipe (L1 stack + VM lifecycle hooks)
├── config/ # configs used by the BuilderNet setup
└── scripts/
├── prepare.sh # Download/copy image + create data disk
├── start.sh # Start the QEMU VM
├── stop.sh # Stop the QEMU VM
├── console.sh # Serial console
├── clean.sh # Remove runtime files
└── operator-api-*.sh # Operator API helpers
```

## Troubleshooting

**"KVM not available"** — Ensure `/dev/kvm` exists. You may need: `sudo usermod -aG kvm $USER`

**"OVMF not found"** — Install UEFI firmware: `sudo apt install ovmf` (Debian/Ubuntu) or `sudo dnf install edk2-ovmf` (Fedora)

**VM boots but doesn't connect** — Ensure playground was started with `--bind-external` so the VM can reach Docker services via `10.0.2.2` (QEMU user-mode networking gateway).

**Console shows login prompt** — The image was not built with the `playground` mkosi profile. Rebuild with the profile enabled.
3 changes: 2 additions & 1 deletion custom-recipes/buildernet/mkosi/playground.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ recipe:

builder:
lifecycle_hooks: true
env:
BUILDERNET_IMAGE: ".flashbots-images/mkosi.output/buildernet-qemu_latest.qcow2"
init:
- rm -rf .runtime/buildernet-vm.qcow2 || true
- ./scripts/prepare.sh
start: ./scripts/start.sh
stop:
Expand Down
44 changes: 35 additions & 9 deletions custom-recipes/buildernet/mkosi/scripts/prepare.sh
Original file line number Diff line number Diff line change
@@ -1,31 +1,57 @@
#!/usr/bin/env bash
# Extract VM image and create data disk
#
# Usage:
# ./prepare.sh /path/to/image.qcow2 # Use local image
# ./prepare.sh https://example.com/img.qcow2 # Download from URL
#
# Environment:
# BUILDERNET_IMAGE - Path or URL to VM image (overridden by $1 argument)
set -eu -o pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="${SCRIPT_DIR}/.."

FLASHBOTS_IMAGES_DIR="${PROJECT_DIR}/.flashbots-images"
RUNTIME_DIR="${PROJECT_DIR}/.runtime"

QEMU_QCOW2="${FLASHBOTS_IMAGES_DIR}/mkosi.output/buildernet-qemu_latest.qcow2"

VM_IMAGE="${RUNTIME_DIR}/buildernet-vm.qcow2"
VM_DATA_DISK="${RUNTIME_DIR}/persistent.raw"

if [[ ! -f "${QEMU_QCOW2}" ]]; then
echo "Error: QEMU qcow2 image not found: ${QEMU_QCOW2}"
echo "Run ./scripts/build.sh first."
# Determine source image: $1 > $BUILDERNET_IMAGE > error
SOURCE="${1:-${BUILDERNET_IMAGE:-}}"

if [[ -z "${SOURCE}" ]]; then
echo "Error: no VM image specified."
echo "Set BUILDERNET_IMAGE or pass a path/URL as argument."
echo "Usage: ./scripts/prepare.sh [/path/to/image.qcow2 | https://url/to/image.qcow2]"
exit 1
fi

echo "prepare.sh: PROJECT_DIR=${PROJECT_DIR}"
echo "prepare.sh: RUNTIME_DIR=${RUNTIME_DIR}"
echo "prepare.sh: SOURCE=${SOURCE}"

# Ensure the VM is stopped before replacing runtime files (PID file, disk images).
"${SCRIPT_DIR}/stop.sh"

rm -rf "${RUNTIME_DIR}"
mkdir -p "${RUNTIME_DIR}"

rm -f "${VM_IMAGE}"
cp --sparse=always "${QEMU_QCOW2}" "${VM_IMAGE}"
if [[ "${SOURCE}" =~ ^https?:// ]]; then
echo "prepare.sh: downloading from URL..."
TMP_IMAGE="${VM_IMAGE}.tmp"
curl -fSL -o "${TMP_IMAGE}" "${SOURCE}"
mv "${TMP_IMAGE}" "${VM_IMAGE}"
elif [[ -f "${SOURCE}" ]]; then
echo "prepare.sh: copying local file ($(du -h "${SOURCE}" | cut -f1))..."
cp --sparse=always "${SOURCE}" "${VM_IMAGE}"
else
echo "Error: VM image not found: ${SOURCE}"
exit 1
fi

echo "prepare.sh: creating data disk..."
qemu-img create -f raw "${VM_DATA_DISK}" 100G

echo "Runtime ready: ${RUNTIME_DIR}"
echo "prepare.sh: runtime ready"
ls -lah "${RUNTIME_DIR}"
Loading