fix(op-node): carry attributes in deposits-only request event#19465
fix(op-node): carry attributes in deposits-only request event#19465
Conversation
…tEvent The deposits-only attributes request event previously depended on lastAttribs (mutable pipeline state) which can be cleared by a reset that interleaves between BuildStartEvent and PayloadProcessEvent. This race condition causes a crash when the SuperAuthority deny path fires after a pipeline reset has cleared lastAttribs. Instead of just changing CriticalErrorEvent to ResetEvent (which papers over the race), this fix makes the event self-contained by carrying the original attributes. The EngineController stores attributes from BuildStartEvent and passes them through to the deposits-only request, so the handler never needs to read from lastAttribs. A new ApplyDepositsOnly method on the pipeline accepts provided attributes directly, skipping the staleness checks that fail when lastAttribs is nil. The fallback path (no attributes in event) uses ResetEvent instead of CriticalErrorEvent for safety. Related: #19417 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the fallback path that used ResetEvent when attributes were missing from the event. Missing or mismatched attributes is now a CriticalErrorEvent — this catches the staleness case where lastBuildAttribs on the EngineController could theoretically be overwritten by a second BuildStartEvent. Validation is done at two levels: - emitDepositsOnlyPayloadAttributesRequest checks that the provided attributes are non-nil and match the parent/derivedFrom before constructing the event - The handler in PipelineDeriver checks the same as defense-in-depth against any future code path that constructs the event directly The super authority deny test is updated to set lastBuildAttribs on the engine controller, matching what onBuildStart does in production. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove excessive whitespace alignment on mock method one-liners that was flagged by the go-lint CI job. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ajsutton
left a comment
There was a problem hiding this comment.
Claude: Reviewed for correctness issues that could cause derivation to reach incorrect results. No issues found.
Analysis of the five areas of concern:
-
lastBuildAttribsstaleness: Safe.EngineController.OnEventholdse.mufor the duration of each event handler. The event chainBuildStartEvent→BuildStartedEvent→BuildSealEvent→BuildSealedEvent→PayloadProcessEventis synchronous, andonBuildStartalways setslastBuildAttribsbefore any downstream handler reads it. No concurrentBuildStartEventcan interleave due to the mutex. -
ApplyDepositsOnlyvs oldDepositsOnlyAttributes: The skipped sanity checks are either inapplicable or relocated. Thebatch != nilcheck is irrelevant when attributes are carried in the event (the attributes are already produced, so batch state doesn't matter). The parent/derivedFrom consistency checks are now performed by bothemitDepositsOnlyPayloadAttributesRequest(engine side) andPipelineDeriver.OnEvent(pipeline side) before callingApplyDepositsOnly.FlushChannel()is still called. Correct. -
lastAttribsupdate inApplyDepositsOnly: Settingaq.lastAttribs = attrskeeps the pipeline'sAttributesQueuestate consistent for any subsequent pipeline stepping or a futureDepositsOnlyAttributescall. This matches the behavior of the old code path and is necessary. -
Validation of
ev.Ref.ParentID()vslastBuildAttribs.Parent.ID():ev.Refis derived from the execution payload viaPayloadToBlockRef, which was built from the same attributes stored inlastBuildAttribs. The parent hash and number will match. The validation guards against the (now-impossible) case where they diverge. -
lastBuildAttribsnever cleared: While it persists across builds, everyPayloadProcessEventis necessarily preceded by aBuildStartEventthat refreshes it. The parent/derivedFrom validation provides a safety net against any hypothetical staleness. This is sufficient — adding an explicit clear (e.g., afterPayloadSuccessEvent) would be a defense-in-depth improvement but is not necessary for correctness.
InteropInvalidateBlockEvent bypasses AttributesHandler's sentAttributes gate and directly emits BuildStartEvent. This can overwrite lastBuildAttribs while a derivation build is in-flight, causing a parent mismatch CriticalErrorEvent when onPayloadProcess reads the stale attributes for deposits-only fallback. Add buildInProgress tracking to EngineController. When an InteropInvalidateBlockEvent arrives during a build, it is stashed and replayed when the build completes. The flag is set on successful startPayload and cleared at every terminal build state: payload process, seal error, build cancel, and sequencer-block sealed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Makes
DepositsOnlyPayloadAttributesRequestEventself-contained by carrying the originalAttributesWithParent, eliminating the race condition where a pipeline reset clearslastAttribsbetweenBuildStartEventandPayloadProcessEvent.This is an alternative to #19417 which changes
CriticalErrorEventtoResetEvent. That fix papers over the race by retrying; this fix eliminates the root cause by removing the dependence on mutable pipeline state.What changed
DepositsOnlyPayloadAttributesRequestEventnow carries anAttributesfieldEngineControllerstores attributes fromBuildStartEventinlastBuildAttribs(survives pipeline resets)build_invalid.gousesev.Attributesdirectly,payload_process.gousese.lastBuildAttribsApplyDepositsOnlymethod onAttributesQueue/DerivationPipelineaccepts provided attributes, flushes channels, and returns deposits-only version — without readinglastAttribsCriticalErrorEvent— no fallbackEmit site verification
There is exactly one place the event is constructed:
emitDepositsOnlyPayloadAttributesRequest. All three call sites go through it:build_invalid.go:47— passesev.Attributesdirectly fromBuildInvalidEvent. Always correct — same build cycle.payload_process.go:46— SuperAuthority deny path. Passese.lastBuildAttribs.payload_process.go:65— HoloceneExecutionInvalid. Passese.lastBuildAttribs.Staleness analysis for
lastBuildAttribslastBuildAttribsis set inonBuildStartand read inonPayloadProcess. Could a secondBuildStartEventoverwrite it before the first build'sPayloadProcessEventfires?The
AttributesHandlerprevents concurrent builds. ItssentAttributesflag (settruebefore emittingBuildStartEvent, checked at line 167) prevents emitting a secondBuildStartEventwhile one is in flight. The flag is only cleared on reset, temporary error, or when attributes are consumed.After a reset, stale builds fail at the EL. If a
ResetEventinterleaves and eventually produces a newBuildStartEvent(overwritinglastBuildAttribs), the stale build'sBuildSealEventcallsengine.GetPayload— butforceResethas already changed the EL's forkchoice viatryUpdateEngine, invalidating the old payload ID. The EL returnsUnknownPayload, producingPayloadSealExpiredErrorEventinstead ofPayloadProcessEvent. The deny check never fires for the stale build.Defense-in-depth validation catches any remaining edge case. Both
emitDepositsOnlyPayloadAttributesRequestand the handler inPipelineDerivervalidate that the carried attributes match the event'sParentandDerivedFrom. IflastBuildAttribsis stale (wrong parent or derivation origin), the mismatch check fires aCriticalErrorEventrather than silently using wrong attributes. This prevents incorrect derivation.Why the race exists
Events are queued, not inline. Between
BuildStartEvent(which queuesForkchoiceUpdateEvent+BuildStartedEvent) andPayloadProcessEvent(where the deny check fires), aResetEventcan interleave and clearlastAttribson theAttributesQueue. The deny check then emitsDepositsOnlyPayloadAttributesRequestEvent, whose handler previously readlastAttribs— which was nil.The
EngineController'slastBuildAttribsfield survives pipeline resets (only the pipeline's internalAttributesQueue.lastAttribsis cleared), bridging the gap.Test plan
TestApplyDepositsOnly_WithNilLastAttribsandTestApplyDepositsOnly_PreservesParentAndDerivedFromTestSuperAuthority/DeniedPayload_EmitsDepositsOnlyRequestto setlastBuildAttribsderive,engine, andattributestests passop-nodebuild succeeds🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com