Skip to content

lokahi: serve the experimental opstack block-building namespace on chain routes - #22644

Open
claude[bot] wants to merge 4 commits into
claude/lokahi-single-socket-cl-routesfrom
claude/lokahi-opstack-namespace
Open

lokahi: serve the experimental opstack block-building namespace on chain routes#22644
claude[bot] wants to merge 4 commits into
claude/lokahi-single-socket-cl-routesfrom
claude/lokahi-opstack-namespace

Conversation

@claude

@claude claude Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Requested by Sebastian Stammler · Slack thread

Before

A lokahi chain route refused every opstack_* call with -32601 Method not found. op-supernode's virtual op-nodes serve the experimental block-building namespace on every chain's route (op-node/node/node.go registerAPIs, gated on ExperimentalOPStackAPI, which the devstack's makeNodeCfg always sets), and the op-test-sequencer's standard builder/committer/publisher drive block building through it: opstack_openBlockV1, opstack_cancelBlockV1, opstack_sealBlockV1, opstack_commitBlockV1, opstack_publishBlockV1. Under lokahi, every acceptance test that sequences through the test sequencer (dsl/sequencer.go New()/Next()) failed at the first opstack_openBlockV1 — the 18-test failure bucket of CircleCI job 5510201 (e.g. unsafe_head_test.go:82).

After

Each chain's route serves the namespace with op-node's semantics, exactly when the chain's configuration asks for it, and the devstack asks for it the way it does on op-supernode.

How

op-node is the spec, and each lokahi chain is a kona node, so the namespace is implemented in kona and registered where op-node registers its own — in the node's RPC module set, which lokahi's launcher routes under /<chain-id>:

  • kona-rpcopstack jsonrpsee namespace with wire types matching the Go types field for field (eth.BlockID, eth.PayloadInfo, opsigner.SignedExecutionPayloadEnvelope; serde tests pin Go-shaped JSON), and op-node's -401xx build error codes (op-service/apis/opstack.go). RpcBuilder gains experimental_opstack, default off.
  • kona-node-serviceOpStackRpc (actors/rpc/opstack.rs) delegates as op-node's does:
    • openBlockV1 = EngineController.OpenBlock: parent-exists check, then a direct engine_forkchoiceUpdated with head = parent and the given attributes (safe/finalized labels from the engine-state watch), with startPayload's error mapping code for code.
    • sealBlockV1/cancelBlockV1 = SealBlock/CancelBlock: a direct engine_getPayload versioned by the job's timestamp; sealing does not canonicalize. An unknown payload id is -40120.
    • commitBlockV1 = CommitBlock: engine_newPayload + unsafe-head move + canonicalizing FCU. Those writes move state the ChainController owns, so the request goes through its queue as a new ChainControllerRequest::CommitBlock.
    • publishBlockV1 = OpNode.PublishBlock: the signed envelope goes out on the chain's gossip topic with the caller's signature as given (the publish channel item can now carry a pre-made signature; the sequencer's own unsigned path is unchanged).
  • kona-engine — new CommitTask: an InsertTask with a result channel, following SealTask's always-succeeds-with-channel pattern, so the caller hears the newPayload verdict once instead of the queue retrying (or silently dropping an invalid payload) behind their back.
  • kona-node--rpc.experimental-opstack-api, so a standalone kona-node can serve the namespace too.
  • lokahiexperimental-opstack-api in the TOML ([defaults] and per chain, default off), passed into each chain's RpcBuilder.
  • op-devstack — the generated lokahi TOML sets experimental-opstack-api = true in [defaults], mirroring makeNodeCfg's ExperimentalOPStackAPI: true for op-supernode's virtual nodes.

Deliberate, documented divergences from op-node:

  • publishBlockV1 schedules the payload onto the network actor's publish queue and answers; op-node awaits the gossipsub publish. A gossip failure is logged by the network actor rather than returned (the devstack dsl ignores publish errors either way).
  • commitBlockV1 refuses a payload that does not descend from the local-safe head (kona's unsafe-head admission rule); op-node has no counterpart. The refusal is returned to the caller, not silently dropped like the gossip path drops it.
  • open/seal/cancel are direct engine calls, as in op-node, but kona serializes them against the queue's own engine traffic only through the execution layer, not through op-node's controller mutex; the payload build job lives in the EL, so a concurrent queue FCU does not invalidate it.

Testing

  • rust/lokahi/tests/opstack_route.rs (two_chains.rs style, real binary against stubbed L1/EL): fail-first verified — with the namespace unregistered, the flag-on test fails with -32601 where op-node behaviour is expected; with it, both tests pass. Gate off → -32601; gate on → openBlockV1 refuses a missing parent with op-node's message and code, sealBlockV1 fails as the -40199 build error, commitBlockV1 reaches the controller and returns the engine's refusal, publishBlockV1 schedules and answers, and the namespace stays off the supernode root.
  • kona-engine CommitTask unit tests (refusal answered + no retry; invalid payload reaches the caller once); kona-rpc serde parity tests against Go JSON; lokahi config-resolution test; devstack TOML test.
  • cargo test -p lokahi -p lokahi-interop -p kona-engine -p kona-rpc -p kona-node-service -p kona-node, scoped clippy --all-features --all-targets -- -D warnings, nightly fmt.
  • A full local acceptance-test run does not fit this machine's disk (contracts + release Rust builds); the 18-test bucket is measured via the combined branch's CI run after merging this into lokahi-interop: combined measurement branch for the interop suites (DO NOT MERGE) #22634.

Measured (CircleCI job 5511460, head b9383a3 on #22634)

The RPC seam works end to end: zero Method not found in the whole run. In TestReorgInitExecMsg's log the test sequencer's forced reorg goes through this namespace and sticks — committer.go:54 "Committed block to op-stack" block=a3727f..:13, op-reth canonicalizes the sibling 13–15, lokahi's own sequencer continues on the new fork at 16+ (so CommitTask's state update mirrors op-node's SetUnsafeHead + FCU correctly), and the DSL confirms reorg on divergence block on that chain. TestReorgUnsafeHead (the seam bucket's pure-reorg test) went fail → pass.

The other former Method-not-found tests now progress past sequencing and stop at pre-existing lokahi gaps that this PR does not claim to implement:

  1. Interop invalidation is not applied. The verifier detects the invalid block and reaches Decision::Invalidate, but the phase deliberately holds instead of applying it — lokahi-interop/src/verifier.rs progress(): Decision::Invalidate | Decision::Rewind => Self::hold(..), logging Interop verification reached a decision this phase does not apply every 500 ms round. The deposits-only replacement never happens, so the dependent chain's head never reorgs (expected head to reorg … after 30–50 attempts). Affects TestReorgInitExecMsg, TestReorgInvalidExecMsgs/*, and the supernode reorg/replacement tests; TestL2ReorgAfterL1Reorg (failing identically before this PR) sits on the same phase's unapplied Rewind decision. op-supernode's counterpart is the supervisor-driven invalidate → deposits-only replacement flow; in lokahi that is the verifier's decision-application phase, a separate feature.
  2. cross-unsafe is not tracked. kona has no cross-unsafe head; lokahi/src/query/wire.rs documents cross_unsafe_l2 decoding as zero Go-side. TestPostInteropUpgradeComprehensive waits for cross-unsafe ≥ 3 and reads 0 forever.
  3. Supernode query gaps. TestInteropFaultProofs / TestPreinteropFaultProofs* reach no optimistic output for chain N at timestamp T: QueryChain::optimistic_at answers None on ChainAt::NotYet, which ChainAt::from_snapshot also returns for a local-safe head whose L1 pairing is Unpaired; lokahi also has no invalidated-output archive (documented in lokahi/src/query/chain.rs). TestFPP fails with no longer records which L1 block made its block at timestamp T safe — safe-db early-history, failing identically before this PR (job 5510201).
  4. TestSequencingWindowExpiry: lokahi exits with Error: chain 901: EngineTask(Consolidate(SealTaskFailed(UnsafeHeadChangedSinceBuild))) — the consolidation deposit-replacement path's seal (kona-engine seal/task.rs, parent-vs-unsafe-head check) has no result channel, so the error escalates to Critical (seal/error.rs) and halts the node; the identical crash appears in job 5510201, before this PR.
  5. interop/proofs/serial 30 m TestMain timeout is downstream of 1 and 3: TestInteropFaultProofs_IntraBlock's subtests burn 300–663 s retry budgets serially and the package alarm fires 21 m 30 s in; several IntraBlock subtests newly pass inside.

The one pass→fail signal, interop/message TestInitExecMsgWithDSL (16 s pass → 30 m silent hang, empty JUnit body), does not touch this PR's code paths: the test never drives the test sequencer, and the test sequencer has no auto-loop (fullseq.Sequencer.forceStart returns ErrNotImplemented), so no opstack_* method is ever called in that test; the namespace registration is inert without calls. The same head passed the op-node matrix job (5511459), and 12 of 13 tests in the same package passed on the lokahi run. A rerun of the lokahi job at the same head (job 5511920) confirms it: TestInitExecMsgWithDSL passes in 17.4 s. Against the pre-opstack baseline (5510201), the rerun's only stable delta is TestReorgUnsafeHead fail → pass; the rerun's one new one-off, TestPreinteropSingleChainFaultProofs, fails with the pre-existing safe-db signature (no longer records which L1 block …, gap 3 above) and passed at the same head in 5511460.

Deliberately unchanged after the measurement: commitBlockV1's local-safe-descendant refusal. None of the measured failures implicate it — the reorg tests stop the batchers before forking, so every commit landed above local-safe and succeeded — and the flow that would commit at or below local-safe (invalid-block replacement) rewinds derivation first in op-node too, so its commits also land above the rewound local-safe.

Review notes: the repo's rust-code-reviewer/go-code-reviewer agents could not be dispatched from this session; the diff was self-reviewed against docs/ai/rust-dev.md and docs/ai/go-dev.md instead.

Part of #22547


Generated by Claude Code

…ain routes

op-supernode's virtual op-nodes register the experimental opstack API on
every chain's route (op-node/node/node.go, registerAPIs, gated on
ExperimentalOPStackAPI, which the devstack's makeNodeCfg always sets), and
the op-test-sequencer's standard builder, committer and publisher drive
block building through it: opstack_openBlockV1, cancelBlockV1, sealBlockV1,
commitBlockV1, publishBlockV1. A lokahi chain route refused all of them
with "Method not found", so every acceptance test that sequences through
the test sequencer (op-devstack/dsl/sequencer.go) failed at its first
opstack_openBlockV1 -- the 18-test failure bucket of CircleCI job 5510201.

Each lokahi chain is a kona node and op-node is the spec, so the namespace
is implemented in kona and registered where op-node registers its own: in
the node's RPC module set, which lokahi's launcher routes under
/<chain-id>.

- kona-rpc: the opstack namespace, wire types matching the Go types field
  for field (serde tests pin Go-shaped JSON), and op-node's -401xx build
  error codes (op-service/apis/opstack.go). RpcBuilder gains
  experimental_opstack, default off.
- kona-node-service: OpStackRpc delegates as op-node's does. openBlockV1
  is EngineController.OpenBlock: a parent-exists check, then a direct
  engine_forkchoiceUpdated with head at the given parent and the given
  attributes, startPayload's error mapping code for code. sealBlockV1 and
  cancelBlockV1 are a direct engine_getPayload versioned by the job's
  timestamp; sealing does not canonicalize. commitBlockV1 moves the unsafe
  head the chain controller owns, so it goes through the controller's
  queue as a new CommitBlock request. publishBlockV1 hands the signed
  envelope to the network actor, whose publish channel now carries either
  an unsigned payload (signed at publish time with the node's own key, the
  sequencer's unchanged path) or a pre-signed one published with the
  signature it was given, as op-node's PublishBlock publishes it.
- kona-engine: CommitTask, an InsertTask with a result channel following
  SealTask's always-succeeds-with-channel pattern: the requester hears the
  newPayload verdict exactly once, instead of the queue retrying -- or
  silently dropping an invalid payload -- behind their back. A requester
  that disconnected before hearing it is logged, not escalated.
- kona-node: --rpc.experimental-opstack-api, op-node's
  --experimental.sequencer-api.
- lokahi: experimental-opstack-api in the TOML, resolved like the admin
  flag ([defaults] as the base, a chain's own entry overriding).
- op-devstack: the generated lokahi TOML enables it in [defaults],
  mirroring makeNodeCfg's ExperimentalOPStackAPI: true.

Documented divergences: publishBlockV1 schedules the gossip publish and
answers rather than awaiting it, and commitBlockV1 refuses a payload that
does not descend from the local-safe head (kona's unsafe-head admission
rule; op-node has no counterpart) instead of corrupting the head ordering.

Fail-first: with the namespace left unregistered, the new integration test
(rust/lokahi/tests/opstack_route.rs, two_chains.rs style) fails with
-32601 where op-node behaviour is expected; with it, both tests pass --
the gate stays -32601 without the flag, and with it the route answers
openBlockV1's parent refusal, sealBlockV1's -40199, commitBlockV1's insert
verdict through the controller, and publishBlockV1's scheduling, while the
supernode root serves none of it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEDeDWcHt2X1mZYhvHmnZ5
@claude
claude Bot requested a review from a team as a code owner August 22, 2026 02:49
claude Bot pushed a commit that referenced this pull request Aug 22, 2026
…bined measurement

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEDeDWcHt2X1mZYhvHmnZ5
`just lint-docs` (nightly rustdoc, -D warnings) refused two intra-doc links
the scoped clippy run has no opinion on: `ChainControllerRequest` is not in
scope in request.rs, and `PayloadId` is not imported where the opstack
module doc names it. Both are now written as the paths rustdoc can resolve.

CircleCI job 5511222 on #22644.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEDeDWcHt2X1mZYhvHmnZ5
claude Bot pushed a commit that referenced this pull request Aug 22, 2026
…asurement

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEDeDWcHt2X1mZYhvHmnZ5
claude and others added 2 commits August 22, 2026 16:04
07bc020)

Resolves the seam with develop's #22466 imported-block sink: the engine
export list and controller import list take both sides' additions, and
CommitTask gains the block_sink it forwards to its InsertTask - a
committed block is an imported block, so the system-config lookup that
reads imported blocks must see the opstack commit path's blocks too.

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
… 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