From ab1de45acc05cb7929d1aa51a8a861c7b641409d Mon Sep 17 00:00:00 2001 From: Teddy Knox Date: Tue, 2 Dec 2025 16:35:20 -0500 Subject: [PATCH 1/5] Add op-rbuilder submodule --- .gitmodules | 3 +++ op-rbuilder | 1 + 2 files changed, 4 insertions(+) create mode 160000 op-rbuilder diff --git a/.gitmodules b/.gitmodules index 1591d79b2a8af..3b6bfc735bff2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,3 +32,6 @@ [submodule "packages/contracts-bedrock/lib/superchain-registry"] path = packages/contracts-bedrock/lib/superchain-registry url = https://github.com/ethereum-optimism/superchain-registry +[submodule "op-rbuilder"] + path = op-rbuilder + url = git@github.com:flashbots/op-rbuilder.git diff --git a/op-rbuilder b/op-rbuilder new file mode 160000 index 0000000000000..272d462d980a4 --- /dev/null +++ b/op-rbuilder @@ -0,0 +1 @@ +Subproject commit 272d462d980a43e7caf568df0fbbc0c2e0066207 From 08f9a9475a0c5c2421759d1015889ffe16a9e4d7 Mon Sep 17 00:00:00 2001 From: Teddy Knox Date: Tue, 2 Dec 2025 16:36:30 -0500 Subject: [PATCH 2/5] Add rollup-boost submodule --- .gitmodules | 3 +++ rollup-boost | 1 + 2 files changed, 4 insertions(+) create mode 160000 rollup-boost diff --git a/.gitmodules b/.gitmodules index 3b6bfc735bff2..9e17b97c78653 100644 --- a/.gitmodules +++ b/.gitmodules @@ -35,3 +35,6 @@ [submodule "op-rbuilder"] path = op-rbuilder url = git@github.com:flashbots/op-rbuilder.git +[submodule "rollup-boost"] + path = rollup-boost + url = git@github.com:flashbots/rollup-boost.git diff --git a/rollup-boost b/rollup-boost new file mode 160000 index 0000000000000..196237bab2a02 --- /dev/null +++ b/rollup-boost @@ -0,0 +1 @@ +Subproject commit 196237bab2a02298de994b439e0455abb1ac512f From d5272986c012d7914716d309630bc6fa29f00763 Mon Sep 17 00:00:00 2001 From: Teddy Knox Date: Tue, 2 Dec 2025 16:37:25 -0500 Subject: [PATCH 3/5] Move kona => kona-proofs --- .circleci/config.yml | 22 ++++++++++----------- Makefile | 2 +- {kona => kona-proofs}/.gitignore | 0 {kona => kona-proofs}/justfile | 0 {kona => kona-proofs}/version.json | 0 op-devstack/shared/challenger/challenger.go | 4 ++-- op-devstack/sysgo/superroot.go | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) rename {kona => kona-proofs}/.gitignore (100%) rename {kona => kona-proofs}/justfile (100%) rename {kona => kona-proofs}/version.json (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index db70f0527b332..33c95effbc57f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -806,7 +806,7 @@ jobs: fi # Let them cook! - KONA_VERSION=$(jq -r .version kona/version.json) \ + KONA_VERSION=$(jq -r .version kona-proofs/version.json) \ docker buildx bake \ --progress plain \ --builder=buildx-build \ @@ -2030,20 +2030,20 @@ jobs: checkout-method: blobless - restore_cache: name: Restore kona cache - key: kona-prestate-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} + key: kona-prestate-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} - run: name: Build kona prestates command: just build-prestates - working_directory: kona + working_directory: kona-proofs - save_cache: - key: kona-prestate-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} + key: kona-prestate-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} name: Save Kona to cache paths: - - "kona/prestates/" + - "kona-proofs/prestates/" - persist_to_workspace: root: . paths: - - "kona/prestates/*" + - "kona-proofs/prestates/*" cannon-kona-host: docker: @@ -2053,7 +2053,7 @@ jobs: checkout-method: blobless - restore_cache: name: Restore kona host cache - key: kona-host-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} + key: kona-host-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} - run: # The mise plugin for clang is broken so install via apt-get name: Install clang command: | @@ -2062,16 +2062,16 @@ jobs: - run: name: Build kona host command: just build-kona-host - working_directory: kona + working_directory: kona-proofs - save_cache: - key: kona-host-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} + key: kona-host-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} name: Save Kona host to cache paths: - - "kona/bin/kona-host" + - "kona-proofs/bin/kona-host" - persist_to_workspace: root: . paths: - - "kona/bin/kona-host" + - "kona-proofs/bin/kona-host" publish-cannon-prestates: resource_class: medium diff --git a/Makefile b/Makefile index b5a7f61d89059..621c0bad0ae61 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ golang-docker: ## Builds Docker images for Go components using buildx GIT_COMMIT=$$(git rev-parse HEAD) \ GIT_DATE=$$(git show -s --format='%ct') \ IMAGE_TAGS=$$(git rev-parse HEAD),latest \ - KONA_VERSION=$$(jq -r .version kona/version.json) \ + KONA_VERSION=$$(jq -r .version kona-proofs/version.json) \ docker buildx bake \ --progress plain \ --load \ diff --git a/kona/.gitignore b/kona-proofs/.gitignore similarity index 100% rename from kona/.gitignore rename to kona-proofs/.gitignore diff --git a/kona/justfile b/kona-proofs/justfile similarity index 100% rename from kona/justfile rename to kona-proofs/justfile diff --git a/kona/version.json b/kona-proofs/version.json similarity index 100% rename from kona/version.json rename to kona-proofs/version.json diff --git a/op-devstack/shared/challenger/challenger.go b/op-devstack/shared/challenger/challenger.go index 2b0fa928834eb..68ea35f2376d6 100644 --- a/op-devstack/shared/challenger/challenger.go +++ b/op-devstack/shared/challenger/challenger.go @@ -79,12 +79,12 @@ func applyCannonKonaConfig(c *config.Config, rollupCfgs []*rollup.Config, l1Gene if err := applyVmConfig(root, &c.CannonKona, c.Datadir, rollupCfgs, l1Genesis, l2Geneses); err != nil { return err } - c.CannonKona.Server = root + "kona/bin/kona-host" + c.CannonKona.Server = root + "kona-proofs/bin/kona-host" absRoot, err := filepath.Abs(root) if err != nil { return fmt.Errorf("failed to get absolute path to prestate dir: %w", err) } - c.CannonKonaAbsolutePreStateBaseURL, err = url.Parse("file:" + absRoot + "/kona/prestates") + c.CannonKonaAbsolutePreStateBaseURL, err = url.Parse("file:" + absRoot + "/kona-proofs/prestates") if err != nil { return fmt.Errorf("failed to create kona prestates url: %w", err) } diff --git a/op-devstack/sysgo/superroot.go b/op-devstack/sysgo/superroot.go index 3b358ec770420..7e5d48e6b1909 100644 --- a/op-devstack/sysgo/superroot.go +++ b/op-devstack/sysgo/superroot.go @@ -241,7 +241,7 @@ func getCannonKonaAbsolutePrestate(t devtest.CommonT) common.Hash { } func loadKonaVersions(t devtest.CommonT) konaVersions { - konaVersionPath := "kona/version.json" + konaVersionPath := "kona-proofs/version.json" root, err := findMonorepoRoot(konaVersionPath) t.Require().NoError(err) p := path.Join(root, konaVersionPath) From d7e518b54e0a1a1f65867f7b6738ad3f4432b5a0 Mon Sep 17 00:00:00 2001 From: Teddy Knox Date: Tue, 2 Dec 2025 16:39:09 -0500 Subject: [PATCH 4/5] Add kona submodule --- .gitmodules | 3 +++ kona | 1 + 2 files changed, 4 insertions(+) create mode 160000 kona diff --git a/.gitmodules b/.gitmodules index 9e17b97c78653..3f1e08b2426a4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -38,3 +38,6 @@ [submodule "rollup-boost"] path = rollup-boost url = git@github.com:flashbots/rollup-boost.git +[submodule "kona"] + path = kona + url = git@github.com:op-rs/kona.git diff --git a/kona b/kona new file mode 160000 index 0000000000000..be9d6734effed --- /dev/null +++ b/kona @@ -0,0 +1 @@ +Subproject commit be9d6734effed58a906577b5198201f8c4cd3b4f From 05321c27bb0cc30d8c7dfb49102262f94de0e962 Mon Sep 17 00:00:00 2001 From: Teddy Knox Date: Tue, 2 Dec 2025 17:04:40 -0500 Subject: [PATCH 5/5] Update CI to build rust binaries ahead of acceptance test time --- .circleci/config.yml | 185 +++++++++++++++++- justfile | 6 + op-acceptance-tests/justfile | 6 +- .../flashblocks/flashblocks_stream_test.go | 3 - op-devstack/sysgo/l2_cl_kona.go | 11 +- op-devstack/sysgo/op_rbuilder.go | 11 +- op-devstack/sysgo/rollup_boost.go | 12 +- op-devstack/sysgo/rust_binary.go | 96 +++++++++ op-devstack/sysgo/supervisor_kona.go | 11 +- op-devstack/sysgo/system.go | 19 -- op-devstack/sysgo/util.go | 39 +++- 11 files changed, 351 insertions(+), 48 deletions(-) create mode 100644 op-devstack/sysgo/rust_binary.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 33c95effbc57f..da16eb3bd274f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -212,7 +212,7 @@ commands: command: | # Manually craft the submodule update command in order to take advantage # of the -j parameter, which speeds it up a lot. - git submodule update --init --recursive --force -j 8 + git submodule update --init --recursive --single-branch --force -j 8 working_directory: packages/contracts-bedrock # Notifies us on Slack a build fails on develop @@ -1690,6 +1690,15 @@ jobs: checkout-method: blobless - attach_workspace: at: . + - run: + name: Configure Rust binary paths (sysgo) + command: | + ROOT_DIR="$(pwd)" + BIN_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" + echo "export RUST_BINARY_PATH_KONA_NODE=$BIN_DIR/kona-node" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_KONA_SUPERVISOR=$BIN_DIR/kona-supervisor" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_OP_RBUILDER=$BIN_DIR/op-rbuilder" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_ROLLUP_BOOST=$BIN_DIR/rollup-boost" >> "$BASH_ENV" # Restore cached Go modules - restore_cache: keys: @@ -1782,6 +1791,17 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + - attach_workspace: + at: . + - run: + name: Configure Rust binary paths (sysgo) + command: | + ROOT_DIR="$(pwd)" + BIN_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" + echo "export RUST_BINARY_PATH_KONA_NODE=$BIN_DIR/kona-node" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_KONA_SUPERVISOR=$BIN_DIR/kona-supervisor" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_OP_RBUILDER=$BIN_DIR/op-rbuilder" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_ROLLUP_BOOST=$BIN_DIR/rollup-boost" >> "$BASH_ENV" - restore_cache: keys: - go-mod-v1-{{ checksum "go.sum" }} @@ -2073,6 +2093,111 @@ jobs: paths: - "kona-proofs/bin/kona-host" + # Build a single Rust binary for sysgo tests (parameterized). + rust-binary-build: + docker: + - image: <> + resource_class: xlarge + parameters: + directory: + description: "Directory containing the Cargo workspace" + type: string + binaries: + description: "Space-separated list of binary names to copy from target/release into the cache" + type: string + build_command: + description: "Cargo build command to run" + type: string + needs_clang: + description: "Whether to install clang (needed by bindgen for reth-mdbx-sys)" + type: boolean + default: false + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - run: + name: Get << parameters.directory >> submodule HASH + command: | + mkdir -p .circleci-cache + # Verify the path is a git submodule (gitlink). For submodules, ls-tree prints: + # 160000 commit + MODE_AND_TYPE="$(git ls-tree HEAD << parameters.directory >> | awk '{print $1 " " $2}')" + if [ "$MODE_AND_TYPE" != "160000 commit" ]; then + echo "ERROR: << parameters.directory >> is not a submodule path (expected '160000 commit', got '$MODE_AND_TYPE')" >&2 + git ls-tree HEAD << parameters.directory >> >&2 || true + exit 1 + fi + + git ls-tree HEAD << parameters.directory >> | awk '{print $3}' > .circleci-cache/expected-submod-sha.txt + echo "Expected submodule HASH: $(cat .circleci-cache/expected-submod-sha.txt)" + echo "<< parameters.binaries >>" | tr ' ' '\n' | awk 'NF' | sort -u > .circleci-cache/expected-binaries.txt + echo "Expected binaries: $(tr '\n' ' ' < .circleci-cache/expected-binaries.txt)" + - restore_cache: + name: Restore << parameters.directory >> cache + keys: + - rust-<< parameters.directory >>-v8-{{ checksum ".circleci-cache/expected-submod-sha.txt" }}-{{ checksum ".circleci-cache/expected-binaries.txt" }} + - run: + name: Build << parameters.directory >> submodule (if needed) + command: | + ROOT_DIR="$(pwd)" + BIN_CACHE_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" + + HIT=true + for bin in << parameters.binaries >>; do + if [ ! -f "$BIN_CACHE_DIR/$bin" ]; then + HIT=false + break + fi + done + if [ "$HIT" = "true" ]; then + echo "Cache hit - binaries exist" + exit 0 + fi + + echo "Cache miss - will build" + # Only run on cache miss. We intentionally do not cache into the submodule directory so that cache + # restore cannot make the submodule path non-empty (which breaks `git submodule update --init`). + git submodule update --init --recursive --depth 1 -j 8 --single-branch << parameters.directory >> + + if [ "<< parameters.needs_clang >>" = "true" ]; then + sudo apt-get update + sudo apt-get install -y --no-install-recommends clang + fi + + cd << parameters.directory >> && << parameters.build_command >> + + mkdir -p "$BIN_CACHE_DIR" + for bin in << parameters.binaries >>; do + SRC="$ROOT_DIR/<< parameters.directory >>/target/release/$bin" + DST="$BIN_CACHE_DIR/$bin" + if [ ! -f "$SRC" ]; then + echo "ERROR: expected built binary not found at $SRC" >&2 + exit 1 + fi + cp "$SRC" "$DST" + chmod +x "$DST" || true + done + no_output_timeout: 30m + - save_cache: + key: rust-<< parameters.directory >>-v8-{{ checksum ".circleci-cache/expected-submod-sha.txt" }}-{{ checksum ".circleci-cache/expected-binaries.txt" }} + name: Save << parameters.directory >> cache + paths: + - ".circleci-cache/rust-binaries" + - persist_to_workspace: + root: . + paths: + - ".circleci-cache/rust-binaries" + + # Aggregator job - allows downstream jobs to depend on a single job instead of listing all build jobs. + rust-binaries-for-sysgo: + docker: + - image: <> + resource_class: small + steps: + - run: + name: All Rust binaries ready + command: echo "All Rust binaries built and persisted to workspace" + publish-cannon-prestates: resource_class: medium docker: @@ -3169,12 +3294,41 @@ workflows: - cannon-prestate-quick: context: - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-kona + directory: kona + binaries: "kona-node kona-supervisor" + build_command: cargo build --release --bin kona-node --bin kona-supervisor + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-op-rbuilder + directory: op-rbuilder + binaries: "op-rbuilder" + build_command: cargo build --release -p op-rbuilder --bin op-rbuilder + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-rollup-boost + directory: rollup-boost + binaries: "rollup-boost" + build_command: cargo build --release -p rollup-boost --bin rollup-boost + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binaries-for-sysgo: + requires: + - rust-build-kona + - rust-build-op-rbuilder + - rust-build-rollup-boost - op-acceptance-tests-flake-shake: context: - circleci-repo-readonly-authenticated-github-token requires: - contracts-bedrock-build - cannon-prestate-quick + - rust-binaries-for-sysgo - op-acceptance-tests-flake-shake-report: requires: - op-acceptance-tests-flake-shake @@ -3280,6 +3434,34 @@ workflows: - cannon-kona-host: # needed for sysgo tests (if any package is in-memory) context: - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-kona + directory: kona + binaries: "kona-node kona-supervisor" + build_command: cargo build --release --bin kona-node --bin kona-supervisor + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-op-rbuilder + directory: op-rbuilder + binaries: "op-rbuilder" + build_command: cargo build --release -p op-rbuilder --bin op-rbuilder + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-rollup-boost + directory: rollup-boost + binaries: "rollup-boost" + build_command: cargo build --release -p rollup-boost --bin rollup-boost + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binaries-for-sysgo: + requires: + - rust-build-kona + - rust-build-op-rbuilder + - rust-build-rollup-boost # IN-MEMORY (all) - op-acceptance-tests: name: memory-all @@ -3294,6 +3476,7 @@ workflows: - cannon-prestate-quick - cannon-kona-prestate - cannon-kona-host + - rust-binaries-for-sysgo # Generate flaky test report - generate-flaky-report: name: generate-flaky-tests-report diff --git a/justfile b/justfile index 14665b5f726a7..ccc0c0e8d0a82 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,9 @@ +# Build all Rust binaries (release) for sysgo tests. +build-rust-release: + cd kona && cargo build --release --bin kona-node --bin kona-supervisor + cd op-rbuilder && cargo build --release -p op-rbuilder --bin op-rbuilder + cd rollup-boost && cargo build --release -p rollup-boost --bin rollup-boost + # Checks that TODO comments have corresponding issues. todo-checker: ./ops/scripts/todo-checker.sh diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index a13688a492452..c5649fa627320 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -46,7 +46,7 @@ acceptance-test devnet="" gate="base": echo "Building contracts (local build)..." cd {{REPO_ROOT}} echo " - Updating submodules..." - git submodule update --init --recursive + git submodule update --init --recursive --single-branch -j 8 echo " - Installing mise..." mise install cd packages/contracts-bedrock @@ -62,6 +62,10 @@ acceptance-test devnet="" gate="base": cd {{REPO_ROOT}} make cannon-prestates fi + + echo "Building Rust binaries (kona-node, kona-supervisor, op-rbuilder, rollup-boost)..." + cd {{REPO_ROOT}} + just build-rust-debug fi cd {{REPO_ROOT}}/op-acceptance-tests diff --git a/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go b/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go index 323ffe5175977..377e201d68943 100644 --- a/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go +++ b/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go @@ -1,6 +1,3 @@ -//go:build !ci - -// use a tag prefixed with "!". Such tag ensures that the default behaviour of this test would be to be built/run even when the go toolchain (go test) doesn't specify any tag filter. package flashblocks import ( diff --git a/op-devstack/sysgo/l2_cl_kona.go b/op-devstack/sysgo/l2_cl_kona.go index b9dfe61c32770..dce2891ed6e03 100644 --- a/op-devstack/sysgo/l2_cl_kona.go +++ b/op-devstack/sysgo/l2_cl_kona.go @@ -254,10 +254,13 @@ func WithKonaNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack ) } - execPath := os.Getenv("KONA_NODE_EXEC_PATH") - p.Require().NotEmpty(execPath, "KONA_NODE_EXEC_PATH environment variable must be set") - _, err = os.Stat(execPath) - p.Require().NotErrorIs(err, os.ErrNotExist, "executable must exist") + execPath, err := EnsureRustBinary(p, RustBinarySpec{ + SrcDir: "kona", + Package: "kona-node", + Binary: "kona-node", + }) + p.Require().NoError(err, "prepare kona-node binary") + p.Require().NotEmpty(execPath, "kona-node binary path resolved") k := &KonaNode{ id: l2CLID, diff --git a/op-devstack/sysgo/op_rbuilder.go b/op-devstack/sysgo/op_rbuilder.go index fc92a5aba2dc3..93d4235ffa907 100644 --- a/op-devstack/sysgo/op_rbuilder.go +++ b/op-devstack/sysgo/op_rbuilder.go @@ -378,10 +378,15 @@ func (b *OPRBuilderNode) Start() { b.sub = NewSubProcess(b.p, stdOut, stdErr) - exec := os.Getenv("OP_RBUILDER_EXEC_PATH") - b.p.Require().NotEmpty(exec, "OP_RBUILDER_EXEC_PATH must be set") + execPath, err := EnsureRustBinary(b.p, RustBinarySpec{ + SrcDir: "op-rbuilder", + Package: "op-rbuilder", + Binary: "op-rbuilder", + }) + b.p.Require().NoError(err, "prepare op-rbuilder binary") + b.p.Require().NotEmpty(execPath, "op-rbuilder binary path resolved") - err := b.sub.Start(exec, args, env) + err = b.sub.Start(execPath, args, env) b.p.Require().NoError(err, "start OPRBuilderNode") const readinessTimeout = 15 * time.Second diff --git a/op-devstack/sysgo/rollup_boost.go b/op-devstack/sysgo/rollup_boost.go index ece089e00e40e..f7bffc40273df 100644 --- a/op-devstack/sysgo/rollup_boost.go +++ b/op-devstack/sysgo/rollup_boost.go @@ -3,7 +3,6 @@ package sysgo import ( "net" "net/http" - "os" "strconv" "strings" "sync" @@ -112,10 +111,15 @@ func (r *RollupBoostNode) Start() { r.sub = NewSubProcess(r.p, stdOut, stdErr) - exec := os.Getenv("ROLLUP_BOOST_EXEC_PATH") - r.p.Require().NotEmpty(exec, "ROLLUP_BOOST_EXEC_PATH must be set") + execPath, err := EnsureRustBinary(r.p, RustBinarySpec{ + SrcDir: "rollup-boost", + Package: "rollup-boost", + Binary: "rollup-boost", + }) + r.p.Require().NoError(err, "prepare rollup-boost binary") + r.p.Require().NotEmpty(execPath, "rollup-boost binary path resolved") - err := r.sub.Start(exec, args, env) + err = r.sub.Start(execPath, args, env) r.p.Require().NoError(err, "start rollup-boost") rpcUpstreamURL := "http://" + cfg.RPCHost + ":" + strconv.Itoa(int(cfg.RPCPort)) diff --git a/op-devstack/sysgo/rust_binary.go b/op-devstack/sysgo/rust_binary.go new file mode 100644 index 0000000000000..cd74a11e897c9 --- /dev/null +++ b/op-devstack/sysgo/rust_binary.go @@ -0,0 +1,96 @@ +package sysgo + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + opservice "github.com/ethereum-optimism/optimism/op-service" +) + +// RustBinarySpec describes a Rust binary to be built and located. +type RustBinarySpec struct { + SrcDir string // directory name relative to monorepo root, e.g. "rollup-boost" + Package string // cargo package name, e.g. "rollup-boost" + Binary string // binary name, e.g. "rollup-boost" +} + +// EnsureRustBinary locates or builds a Rust binary as needed. +// +// Env var overrides (suffix derived from binary name, e.g. "rollup-boost" -> "ROLLUP_BOOST"): +// - RUST_BINARY_PATH_: absolute path to pre-built binary (skips build, must exist) +// - RUST_SRC_DIR_: overrides SrcDir (absolute path to cargo project root) +// +// Build behavior: +// - RUST_JIT_BUILD=1: runs cargo build --release (letting cargo handle rebuild detection) +// - Otherwise: only checks binary exists, errors if missing +func EnsureRustBinary(p devtest.P, spec RustBinarySpec) (string, error) { + envSuffix := toEnvVarSuffix(spec.Binary) + + // Check for explicit binary path override + if pathOverride := os.Getenv("RUST_BINARY_PATH_" + envSuffix); pathOverride != "" { + if _, err := os.Stat(pathOverride); os.IsNotExist(err) { + return "", fmt.Errorf("%s binary not found at overridden path %s", spec.Binary, pathOverride) + } + p.Logger().Info("Using overridden binary path", "binary", spec.Binary, "path", pathOverride) + return pathOverride, nil + } + + // Determine source root + srcRoot, err := resolveSrcRoot(spec.SrcDir, envSuffix) + if err != nil { + return "", err + } + + binaryPath := filepath.Join(srcRoot, "target", "release", spec.Binary) + jitBuild := os.Getenv("RUST_JIT_BUILD") != "" + + if jitBuild { + p.Logger().Info("Building Rust binary (JIT)", "binary", spec.Binary, "dir", srcRoot) + if err := buildRustBinary(p.Ctx(), srcRoot, spec.Package, spec.Binary); err != nil { + return "", err + } + } else { + if _, err := os.Stat(binaryPath); os.IsNotExist(err) { + return "", fmt.Errorf("%s binary not found at %s; "+ + "run 'just build-rust-debug' before the test or set RUST_JIT_BUILD=1", spec.Binary, binaryPath) + } + } + + return binaryPath, nil +} + +// resolveSrcRoot determines the cargo project root, checking for env var override first. +func resolveSrcRoot(defaultSrcDir, envSuffix string) (string, error) { + if srcOverride := os.Getenv("RUST_SRC_DIR_" + envSuffix); srcOverride != "" { + return srcOverride, nil + } + + rootDir, err := os.Getwd() + if err != nil { + return "", err + } + monorepoRoot, err := opservice.FindMonorepoRoot(rootDir) + if err != nil { + return "", err + } + return filepath.Join(monorepoRoot, defaultSrcDir), nil +} + +// toEnvVarSuffix converts a binary name to an env var suffix. +// e.g. "rollup-boost" -> "ROLLUP_BOOST" +func toEnvVarSuffix(binary string) string { + return strings.ToUpper(strings.ReplaceAll(binary, "-", "_")) +} + +func buildRustBinary(ctx context.Context, root, pkg, bin string) error { + cmd := exec.CommandContext(ctx, "cargo", "build", "--release", "-p", pkg, "--bin", bin) + cmd.Dir = root + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/op-devstack/sysgo/supervisor_kona.go b/op-devstack/sysgo/supervisor_kona.go index 8d4b925b57022..926c87b225528 100644 --- a/op-devstack/sysgo/supervisor_kona.go +++ b/op-devstack/sysgo/supervisor_kona.go @@ -158,10 +158,13 @@ func WithKonaSupervisor(supervisorID stack.SupervisorID, clusterID stack.Cluster "KONA_LOG_STDOUT_FORMAT=json", } - execPath := os.Getenv("KONA_SUPERVISOR_EXEC_PATH") - p.Require().NotEmpty(execPath, "KONA_SUPERVISOR_EXEC_PATH environment variable must be set") - _, err = os.Stat(execPath) - p.Require().NotErrorIs(err, os.ErrNotExist, "executable must exist") + execPath, err := EnsureRustBinary(p, RustBinarySpec{ + SrcDir: "kona", + Package: "kona-supervisor", + Binary: "kona-supervisor", + }) + p.Require().NoError(err, "prepare kona-supervisor binary") + p.Require().NotEmpty(execPath, "kona-supervisor binary path resolved") konaSupervisor := &KonaSupervisor{ id: supervisorID, diff --git a/op-devstack/sysgo/system.go b/op-devstack/sysgo/system.go index a925d4703e2ab..2f7741c2509c0 100644 --- a/op-devstack/sysgo/system.go +++ b/op-devstack/sysgo/system.go @@ -1,9 +1,6 @@ package sysgo import ( - "os" - "strings" - "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" "github.com/ethereum-optimism/optimism/op-devstack/stack" @@ -625,22 +622,6 @@ func singleChainSystemWithFlashblocksOpts(ids *SingleChainSystemWithFlashblocksI seqID := NewELNodeIdentity("127.0.0.1", 0) builderID := NewELNodeIdentity("127.0.0.1", 0) // allocate dynamic port for builder - var missingEnv []string - if os.Getenv("OP_RBUILDER_EXEC_PATH") == "" { - missingEnv = append(missingEnv, "OP_RBUILDER_EXEC_PATH") - } - if os.Getenv("ROLLUP_BOOST_EXEC_PATH") == "" { - missingEnv = append(missingEnv, "ROLLUP_BOOST_EXEC_PATH") - } - if len(missingEnv) > 0 { - missing := strings.Join(missingEnv, ", ") - opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { - o.P().Logger().Warn("Skipping single-chain flashblocks system; missing executables", "missing_env", missing) - o.P().SkipNow() - })) - return opt - } - opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { o.P().Logger().Info("Setting up") })) diff --git a/op-devstack/sysgo/util.go b/op-devstack/sysgo/util.go index 893d9f1705161..b3903c26d6900 100644 --- a/op-devstack/sysgo/util.go +++ b/op-devstack/sysgo/util.go @@ -37,24 +37,45 @@ func propagateEnvVarOrDefault(envVarName string, defaultValue string) string { } var availableLocalPortMutex sync.Mutex +var recentlyAllocatedPorts = make(map[int]struct{}) // getAvailableLocalPort searches for and returns a currently unused local port. +// Tracks recently allocated ports to avoid returning the same port twice +// (the OS may recycle a port immediately after we release it). // Note: this function is threadsafe. func getAvailableLocalPort() (string, error) { availableLocalPortMutex.Lock() defer availableLocalPortMutex.Unlock() - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return "", fmt.Errorf("could not listen on ephemeral port: %w", err) - } - defer ln.Close() + // Keep listeners open while looping so the OS won't return the same port twice + var heldListeners []net.Listener + defer func() { + for _, ln := range heldListeners { + ln.Close() + } + }() + + const maxAttempts = 100 + for range maxAttempts { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return "", fmt.Errorf("could not listen on ephemeral port: %w", err) + } + heldListeners = append(heldListeners, ln) - addr, ok := ln.Addr().(*net.TCPAddr) - if !ok { - return "", errors.New("listener did not return a TCP addr") + addr, ok := ln.Addr().(*net.TCPAddr) + if !ok { + return "", errors.New("listener did not return a TCP addr") + } + port := addr.Port + + if _, used := recentlyAllocatedPorts[port]; used { + continue + } + recentlyAllocatedPorts[port] = struct{}{} + return strconv.Itoa(port), nil } - return strconv.Itoa(addr.Port), nil + return "", errors.New("failed to allocate unique port after max attempts") } // waitTCPReady parses a URL and waits for its TCP endpoint to become ready using EventuallyWithT.