Skip to content

op-devstack: run lokahi as the supernode - #22602

Open
claude[bot] wants to merge 13 commits into
claude/lokahi-22532-nchain-compositionfrom
claude/lokahi-22533-devstack-component
Open

op-devstack: run lokahi as the supernode#22602
claude[bot] wants to merge 13 commits into
claude/lokahi-22532-nchain-compositionfrom
claude/lokahi-22533-devstack-component

Conversation

@claude

@claude claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Requested by Sebastian Stammler · Slack thread

Part of #22533, under the #22521 epic.

Runs lokahi as a real out-of-process supernode in the devstack, hosting two chains, and gates it on a test that both chains actually derive.

Before: selecting DEVSTACK_SUPERNODE_KIND=lokahi failed fast with a config dump, because nothing launched. Separately, lokahi could not start a chain at all — every chain died in gossipsub construction — and the release job never built the binary, so there would have been nothing to launch.

After: the seam launches the binary, waits for it to say it is listening, and hands out one endpoint per chain. A two-chain acceptance test asserts both chains derive a safe head from L1, that each endpoint answers for its own chain, and that taking one chain's execution layer away stalls that chain alone.

How

  • lokahi gains a process-wide admin RPC (lokahi_chains, lokahi_version). It may bind port 0 and logs the address it got, so an out-of-process launch waits for one line rather than one per chain, and the chain set it ended up with is read back to catch a mis-generated config.
  • The devstack component mirrors startMixedKonaNode: rustbin locates or builds the binary, logs are piped into the test logger, and startup blocks on the log line. The configuration is a generated file rather than an environment, because with N chains a flag per chain per setting is not a usable interface.
  • Two gaps stood in the way, both now handled. NetworkConfig::new left gossip_config at libp2p's default, whose ValidationMode::Strict the gossip behaviour rejects outright; and lokahi is not a workspace default member, so the release job's package list, its persisted kona-* glob, and the acceptance job's RUST_BINARY_PATH_* exports all skipped it.

The shared-supernode presets still turn lokahi away, and now say why: they drive the supernode through stack.SupernodeTestControl, whose InteropActivity() returns an in-process *interop.Interop pointer that no out-of-process supernode can provide. Replacing it with something RPC-shaped is separate work; until then the component is covered by the preset built for it.

Based on #22599.

claude and others added 9 commits August 21, 2026 09:11
The single-chain presets can already swap their per-chain CL between op-node
and the Rust kona-node via DEVSTACK_L2CL_KIND, but the multi-chain and interop
presets silently ignore it: their CL is a supernode hosting every chain behind
one RPC, and there is no per-chain node to swap. Selecting the supernode
implementation needs its own knob, because a run can legitimately want
kona-node for the single-chain presets and lokahi for the multi-chain ones.

Add DEVSTACK_SUPERNODE_KIND (op-supernode | lokahi), defaulting to the
in-process Go op-supernode so an unset environment behaves exactly as before,
and dispatch on it in both shared-supernode start paths before anything is
started. An unrecognized value fails rather than being ignored.

lokahi cannot host a chain yet -- rust/lokahi is a CLI skeleton that prints a
greeting and exits -- so startLokahiSupernode fails immediately and says why,
naming the two gaps that have to close first: the per-chain RPC routes, and
stack.SupernodeTestControl handing tests an in-process *interop.Interop
pointer that no out-of-process implementation can satisfy. Landing the seam
now means the switch is written and reviewed once rather than re-derived when
lokahi grows a node; its config struct carries the same inputs the Go path
turns into an snconfig.CLIConfig, so the two cannot drift in what they get.

DEVSTACK_L2CL_KIND=lokahi now fails with a pointer at the right variable
instead of falling through to op-node and reporting the run as a lokahi one.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
Covers the default, both explicit values, rejection of unknown values, and
that DEVSTACK_SUPERNODE_KIND is independent of DEVSTACK_L2CL_KIND -- the
acceptance CI variant sets both.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
The kona acceptance variant is where op-supernode gets swapped for lokahi,
rather than a new job: the interop presets have no op-node to swap for
kona-node, so today they run twice unchanged -- 83 of the 92 devstack
instantiations under op-acceptance-tests/tests/interop and tests/supernode are
identical between memory-all-opn-op-reth and the kona variant.

Rename the variant to memory-all-kona-lokahi-op-reth-<fork> so it names the
components it runs, and thread a supernode_kind parameter through to
DEVSTACK_SUPERNODE_KIND. l2_cl_kind keeps selecting kona-node for the
single-chain presets from the same job.

supernode_kind stays op-supernode: lokahi is still a CLI skeleton, and this job
is a ci-gate requirement, so selecting lokahi now would fail every supernode
and interop test. Flipping it is a one-line change once lokahi can host a
chain.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
Brings in the rust-docs fix for kona-engine's private_intra_doc_links.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
NetworkConfig::new leaves gossip_config at libp2p::gossipsub::Config::default(),
whose ValidationMode::Strict is rejected by the MessageAuthenticity::Anonymous the
gossip behaviour is constructed with. Every chain therefore failed to compose with
GossipDriverBuilder(BehaviourError(GossipsubCreationFailed)), so the supernode could
not start a single chain. kona-node never hits this because its CLI always overrides
the field; take the same kona_gossip defaults it starts from, which also attach the
per-chain message-id label.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
Each chain answers on its own socket with the method set a single-chain node has;
this adds one server that answers for the process. It reports the chain set the
configuration resolved to, and it is the only endpoint whose address a caller can
learn without knowing the configuration: it may bind port 0 and logs the address it
got, which is what lets an out-of-process launch wait for one line rather than N.

The surface stays small on purpose. It does not report liveness — a chain's own RPC
answering is the signal a caller needs, and this server would be guessing at it. The
controls that do need process-wide reach, pausing and resuming interop and looking
into backfill, belong here for the same reason and arrive with the phases that need
them.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
Turns the selection seam into a launch: the binary is built or located through
rustbin, its logs are piped into the test logger, and startup waits for lokahi to
say its admin RPC is listening — the same handshake startMixedKonaNode performs on
kona-node. With N chains a flag per chain per setting is not a usable interface, so
the configuration is a generated file instead of an environment, and the chain set
lokahi resolved from it is read back over lokahi_chains so a mistake in generating
it fails here rather than becoming a chain that never started.

Each chain gets its own socket, fronted by a proxy so callers keep one URL, rather
than a /<chainID> route on a shared server as op-supernode has.

The shared-supernode presets still turn lokahi away, and now say why: they drive the
supernode through stack.SupernodeTestControl, whose InteropActivity() returns an
in-process *interop.Interop pointer no out-of-process supernode can provide.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
A two-L2 world where each chain is produced by its own op-node sequencer and batcher
and then verified by one lokahi process over a second execution layer per chain.
Nothing joins the two sides but L1, so a safe head advancing under lokahi is lokahi
deriving that chain rather than being handed it.

The test asserts the three things a supernode has to get right that a single-chain
node cannot get wrong: both chains derive, each endpoint answers for its own chain,
and taking one chain's execution layer away stalls that chain alone while the process
and the other chain carry on.

Also builds and paths the binary, without which none of this could run: lokahi is not
a workspace default member, so the release job's package list and the acceptance job's
RUST_BINARY_PATH_* exports both skipped it, and the persisted kona-* glob would not
have covered it either.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
The unsafe block signer was taken from the batcher address, which is not the key
sequenced blocks are signed with. kona-node never has to be told: it resolves the
signer from the chain's SystemConfig contract when no flag gives it one. lokahi has no
such path and a devnet chain is not in the superchain registry, so the devstack states
the address the sequencer's P2P key signs with.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
claude and others added 2 commits August 21, 2026 10:42
TestLokahiTwoChains timed out waiting for either chain's safe head to leave
genesis, and lokahi made no engine call to either verifier execution layer for
the whole three minutes. A kona validator gates derivation behind EL-sync
completion and only marks EL sync complete once a forkchoiceUpdated comes back
VALID; in validator mode the only thing that starts that is an unsafe payload
arriving over gossip, whose parents the execution layer then backfills from its
peer. The runtime peered neither side, so the derivation actor sat in
AwaitingELSyncCompletion forever. op-node has no such gate, which is why the
op-node runtimes derive from L1 unpeered, and why the isolated-verifier SDM test
is op-node-only.

Peer each chain's lokahi endpoint and verifier execution layer with the node
producing that chain, the way every other devstack runtime does. The mesh
supplies the unsafe head only: the safe head is still set solely by attributes
derived from L1, so the test still proves what it says it proves.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEDeDWcHt2X1mZYhvHmnZ5

# Conflicts:
#	rust/kona/bin/node/src/flags/metrics.rs
#	rust/kona/crates/node/engine/src/lib.rs
#	rust/kona/crates/node/engine/src/state/core.rs
#	rust/kona/crates/node/engine/src/task_queue/core.rs
#	rust/kona/crates/node/engine/src/task_queue/tasks/consolidate/task.rs
#	rust/kona/crates/node/engine/src/task_queue/tasks/insert/task.rs
#	rust/kona/crates/node/engine/src/task_queue/tasks/seal/task.rs
#	rust/kona/crates/node/engine/src/task_queue/tasks/seal/task_test.rs
#	rust/kona/crates/node/engine/src/task_queue/tasks/task.rs
#	rust/kona/crates/node/engine/src/test_utils/engine_state.rs
#	rust/kona/crates/node/service/src/actors/chain_controller/controller.rs
#	rust/kona/crates/node/service/src/lib.rs
#	rust/kona/crates/node/service/src/service/node.rs
#	rust/kona/crates/protocol/protocol/src/sync.rs
#	rust/kona/crates/providers/providers-alloy/src/beacon_client.rs
#	rust/kona/crates/providers/providers-alloy/src/pipeline.rs
#	rust/kona/crates/providers/providers-local/src/buffer.rs
#	rust/kona/tests/node/reorgs/l2_reorg_after_l1_reorgs_test.go
@claude
claude Bot requested review from a team as code owners August 22, 2026 14:37
claude and others added 2 commits August 22, 2026 14:50
Develop's #22466 removed the HashMap collect in beacon_client.rs; the
merge resolution kept this branch's import line, which all-features
clippy rejects as an unused import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEDeDWcHt2X1mZYhvHmnZ5
…m develop merge)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Sebastian Stammler <seb@oplabs.co>
Claude-Session: https://claude.ai/code/session_01WEDeDWcHt2X1mZYhvHmnZ5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant