lokahi: serve the experimental opstack block-building namespace on chain routes - #22644
Open
claude[bot] wants to merge 4 commits into
Open
lokahi: serve the experimental opstack block-building namespace on chain routes#22644claude[bot] wants to merge 4 commits into
claude[bot] wants to merge 4 commits into
Conversation
…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 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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.goregisterAPIs, gated onExperimentalOPStackAPI, which the devstack'smakeNodeCfgalways 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.goNew()/Next()) failed at the firstopstack_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>:opstackjsonrpsee 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-401xxbuild error codes (op-service/apis/opstack.go).RpcBuildergainsexperimental_opstack, default off.OpStackRpc(actors/rpc/opstack.rs) delegates as op-node's does:openBlockV1=EngineController.OpenBlock: parent-exists check, then a directengine_forkchoiceUpdatedwith head = parent and the given attributes (safe/finalized labels from the engine-state watch), withstartPayload's error mapping code for code.sealBlockV1/cancelBlockV1=SealBlock/CancelBlock: a directengine_getPayloadversioned 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 theChainControllerowns, so the request goes through its queue as a newChainControllerRequest::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).CommitTask: anInsertTaskwith a result channel, followingSealTask's always-succeeds-with-channel pattern, so the caller hears thenewPayloadverdict once instead of the queue retrying (or silently dropping an invalid payload) behind their back.--rpc.experimental-opstack-api, so a standalone kona-node can serve the namespace too.experimental-opstack-apiin the TOML ([defaults]and per chain, default off), passed into each chain'sRpcBuilder.experimental-opstack-api = truein[defaults], mirroringmakeNodeCfg'sExperimentalOPStackAPI: truefor op-supernode's virtual nodes.Deliberate, documented divergences from op-node:
publishBlockV1schedules 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).commitBlockV1refuses 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.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-32601where op-node behaviour is expected; with it, both tests pass. Gate off →-32601; gate on →openBlockV1refuses a missing parent with op-node's message and code,sealBlockV1fails as the-40199build error,commitBlockV1reaches the controller and returns the engine's refusal,publishBlockV1schedules and answers, and the namespace stays off the supernode root.CommitTaskunit 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.Measured (CircleCI job 5511460, head b9383a3 on #22634)
The RPC seam works end to end: zero
Method not foundin the whole run. InTestReorgInitExecMsg'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+ (soCommitTask's state update mirrors op-node'sSetUnsafeHead+ FCU correctly), and the DSL confirmsreorg on divergence blockon 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:
Decision::Invalidate, but the phase deliberately holds instead of applying it —lokahi-interop/src/verifier.rsprogress():Decision::Invalidate | Decision::Rewind => Self::hold(..), loggingInterop verification reached a decision this phase does not applyevery 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). AffectsTestReorgInitExecMsg,TestReorgInvalidExecMsgs/*, and the supernode reorg/replacement tests;TestL2ReorgAfterL1Reorg(failing identically before this PR) sits on the same phase's unappliedRewinddecision. 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.lokahi/src/query/wire.rsdocumentscross_unsafe_l2decoding as zero Go-side.TestPostInteropUpgradeComprehensivewaits for cross-unsafe ≥ 3 and reads 0 forever.TestInteropFaultProofs/TestPreinteropFaultProofs*reachno optimistic output for chain N at timestamp T:QueryChain::optimistic_atanswersNoneonChainAt::NotYet, whichChainAt::from_snapshotalso returns for a local-safe head whose L1 pairing isUnpaired; lokahi also has no invalidated-output archive (documented inlokahi/src/query/chain.rs).TestFPPfails withno longer records which L1 block made its block at timestamp T safe— safe-db early-history, failing identically before this PR (job 5510201).TestSequencingWindowExpiry: lokahi exits withError: chain 901: EngineTask(Consolidate(SealTaskFailed(UnsafeHeadChangedSinceBuild)))— the consolidation deposit-replacement path's seal (kona-engineseal/task.rs, parent-vs-unsafe-head check) has no result channel, so the error escalates toCritical(seal/error.rs) and halts the node; the identical crash appears in job 5510201, before this PR.interop/proofs/serial30 mTestMaintimeout 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.forceStartreturnsErrNotImplemented), so noopstack_*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:TestInitExecMsgWithDSLpasses in 17.4 s. Against the pre-opstack baseline (5510201), the rerun's only stable delta isTestReorgUnsafeHeadfail → 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-revieweragents could not be dispatched from this session; the diff was self-reviewed againstdocs/ai/rust-dev.mdanddocs/ai/go-dev.mdinstead.Part of #22547
Generated by Claude Code