@@ -110,6 +110,30 @@ func retryWithBackoffOnPayloadStatus(ctx context.Context, fn func() error, maxRe
110110 return fmt .Errorf ("max retries (%d) exceeded for %s" , maxRetries , operation )
111111}
112112
113+ // appendUniqueHash ensures we only keep unique, non-zero hash candidates while
114+ // preserving order so we can try canonical first before falling back to legacy.
115+ func appendUniqueHash (candidates []common.Hash , candidate common.Hash ) []common.Hash {
116+ if candidate == (common.Hash {}) {
117+ return candidates
118+ }
119+ for _ , existing := range candidates {
120+ if existing == candidate {
121+ return candidates
122+ }
123+ }
124+ return append (candidates , candidate )
125+ }
126+
127+ // buildHashCandidates returns a deduplicated list of hash candidates in the
128+ // order they should be tried.
129+ func buildHashCandidates (hashes ... common.Hash ) []common.Hash {
130+ candidates := make ([]common.Hash , 0 , len (hashes ))
131+ for _ , h := range hashes {
132+ candidates = appendUniqueHash (candidates , h )
133+ }
134+ return candidates
135+ }
136+
113137// EngineClient represents a client that interacts with an Ethereum execution engine
114138// through the Engine API. It manages connections to both the engine and standard Ethereum
115139// APIs, and maintains state related to block processing.
@@ -185,42 +209,63 @@ func (c *EngineClient) InitChain(ctx context.Context, genesisTime time.Time, ini
185209 return nil , 0 , fmt .Errorf ("initialHeight must be 1, got %d" , initialHeight )
186210 }
187211
188- // Acknowledge the genesis block with retry logic for SYNCING status
189- err := retryWithBackoffOnPayloadStatus ( ctx , func () error {
190- var forkchoiceResult engine. ForkChoiceResponse
191- err := c . engineClient . CallContext ( ctx , & forkchoiceResult , "engine_forkchoiceUpdatedV3" ,
192- engine. ForkchoiceStateV1 {
193- HeadBlockHash : c .genesisHash ,
194- SafeBlockHash : c . genesisHash ,
195- FinalizedBlockHash : c . genesisHash ,
196- },
197- nil ,
198- )
199- if err != nil {
200- return fmt . Errorf ( "engine_forkchoiceUpdatedV3 failed: %w" , err )
212+ genesisBlockHash , stateRoot , gasLimit , _ , err := c . getBlockInfo ( ctx , 0 )
213+ if err != nil {
214+ return nil , 0 , fmt . Errorf ( "failed to get block info: %w" , err )
215+ }
216+
217+ candidates := buildHashCandidates ( c .genesisHash , genesisBlockHash , stateRoot )
218+ var selectedGenesisHash common. Hash
219+
220+ for idx , candidate := range candidates {
221+ args := engine. ForkchoiceStateV1 {
222+ HeadBlockHash : candidate ,
223+ SafeBlockHash : candidate ,
224+ FinalizedBlockHash : candidate ,
201225 }
202226
203- // Validate payload status
204- if err := validatePayloadStatus (forkchoiceResult .PayloadStatus ); err != nil {
227+ err = retryWithBackoffOnPayloadStatus (ctx , func () error {
228+ var forkchoiceResult engine.ForkChoiceResponse
229+ err := c .engineClient .CallContext (ctx , & forkchoiceResult , "engine_forkchoiceUpdatedV3" , args , nil )
230+ if err != nil {
231+ return fmt .Errorf ("engine_forkchoiceUpdatedV3 failed: %w" , err )
232+ }
233+
234+ if err := validatePayloadStatus (forkchoiceResult .PayloadStatus ); err != nil {
235+ c .logger .Warn ().
236+ Str ("status" , forkchoiceResult .PayloadStatus .Status ).
237+ Str ("latestValidHash" , forkchoiceResult .PayloadStatus .LatestValidHash .Hex ()).
238+ Interface ("validationError" , forkchoiceResult .PayloadStatus .ValidationError ).
239+ Msg ("InitChain: engine_forkchoiceUpdatedV3 returned non-VALID status" )
240+ return err
241+ }
242+
243+ return nil
244+ }, MaxPayloadStatusRetries , InitialRetryBackoff , "InitChain" )
245+
246+ if err == nil {
247+ selectedGenesisHash = candidate
248+ break
249+ }
250+
251+ if errors .Is (err , ErrInvalidPayloadStatus ) && idx + 1 < len (candidates ) {
205252 c .logger .Warn ().
206- Str ("status" , forkchoiceResult .PayloadStatus .Status ).
207- Str ("latestValidHash" , forkchoiceResult .PayloadStatus .LatestValidHash .Hex ()).
208- Interface ("validationError" , forkchoiceResult .PayloadStatus .ValidationError ).
209- Msg ("InitChain: engine_forkchoiceUpdatedV3 returned non-VALID status" )
210- return err
253+ Str ("blockHash" , candidate .Hex ()).
254+ Msg ("InitChain: execution engine rejected hash candidate, trying alternate" )
255+ continue
211256 }
212257
213- return nil
214- }, MaxPayloadStatusRetries , InitialRetryBackoff , "InitChain" )
215- if err != nil {
216258 return nil , 0 , err
217259 }
218260
219- _ , stateRoot , gasLimit , _ , err := c .getBlockInfo (ctx , 0 )
220- if err != nil {
221- return nil , 0 , fmt .Errorf ("failed to get block info: %w" , err )
261+ if selectedGenesisHash == (common.Hash {}) {
262+ return nil , 0 , fmt .Errorf ("execution engine rejected all genesis hash candidates" )
222263 }
223264
265+ c .genesisHash = selectedGenesisHash
266+ c .currentHeadBlockHash = selectedGenesisHash
267+ c .currentSafeBlockHash = selectedGenesisHash
268+ c .currentFinalizedBlockHash = selectedGenesisHash
224269 c .initialHeight = initialHeight
225270
226271 return stateRoot [:], gasLimit , nil
@@ -292,16 +337,12 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight
292337 }
293338 txsPayload := validTxs
294339
295- prevBlockHash , _ , prevGasLimit , _ , err := c .getBlockInfo (ctx , blockHeight - 1 )
340+ prevBlockHash , prevBlockStateRoot , prevGasLimit , _ , err := c .getBlockInfo (ctx , blockHeight - 1 )
296341 if err != nil {
297342 return nil , 0 , fmt .Errorf ("failed to get block info: %w" , err )
298343 }
299344
300- args := engine.ForkchoiceStateV1 {
301- HeadBlockHash : prevBlockHash ,
302- SafeBlockHash : prevBlockHash ,
303- FinalizedBlockHash : prevBlockHash ,
304- }
345+ parentHashCandidates := buildHashCandidates (prevBlockHash , prevBlockStateRoot )
305346
306347 // update forkchoice to get the next payload id
307348 // Create evolve-compatible payloadtimestamp.Unix()
@@ -325,46 +366,69 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight
325366
326367 // Call forkchoice update with retry logic for SYNCING status
327368 var payloadID * engine.PayloadID
328- err = retryWithBackoffOnPayloadStatus ( ctx , func () error {
329- var forkchoiceResult engine.ForkChoiceResponse
330- err := c . engineClient . CallContext ( ctx , & forkchoiceResult , "engine_forkchoiceUpdatedV3" , args , evPayloadAttrs )
331- if err != nil {
332- return fmt . Errorf ( "forkchoice update failed: %w" , err )
369+ for idx , candidate := range parentHashCandidates {
370+ args := engine.ForkchoiceStateV1 {
371+ HeadBlockHash : candidate ,
372+ SafeBlockHash : candidate ,
373+ FinalizedBlockHash : candidate ,
333374 }
334375
335- // Validate payload status
336- if err := validatePayloadStatus (forkchoiceResult .PayloadStatus ); err != nil {
337- c .logger .Warn ().
338- Str ("status" , forkchoiceResult .PayloadStatus .Status ).
339- Str ("latestValidHash" , forkchoiceResult .PayloadStatus .LatestValidHash .Hex ()).
340- Interface ("validationError" , forkchoiceResult .PayloadStatus .ValidationError ).
341- Uint64 ("blockHeight" , blockHeight ).
342- Msg ("ExecuteTxs: engine_forkchoiceUpdatedV3 returned non-VALID status" )
343- return err
344- }
376+ err = retryWithBackoffOnPayloadStatus (ctx , func () error {
377+ var forkchoiceResult engine.ForkChoiceResponse
378+ err := c .engineClient .CallContext (ctx , & forkchoiceResult , "engine_forkchoiceUpdatedV3" , args , evPayloadAttrs )
379+ if err != nil {
380+ return fmt .Errorf ("forkchoice update failed: %w" , err )
381+ }
345382
346- if forkchoiceResult .PayloadID == nil {
347- c .logger .Error ().
348- Str ("status" , forkchoiceResult .PayloadStatus .Status ).
349- Str ("latestValidHash" , forkchoiceResult .PayloadStatus .LatestValidHash .Hex ()).
350- Interface ("validationError" , forkchoiceResult .PayloadStatus .ValidationError ).
351- Interface ("forkchoiceState" , args ).
352- Interface ("payloadAttributes" , evPayloadAttrs ).
353- Uint64 ("blockHeight" , blockHeight ).
354- Msg ("returned nil PayloadID" )
383+ // Validate payload status
384+ if err := validatePayloadStatus (forkchoiceResult .PayloadStatus ); err != nil {
385+ c .logger .Warn ().
386+ Str ("status" , forkchoiceResult .PayloadStatus .Status ).
387+ Str ("latestValidHash" , forkchoiceResult .PayloadStatus .LatestValidHash .Hex ()).
388+ Interface ("validationError" , forkchoiceResult .PayloadStatus .ValidationError ).
389+ Uint64 ("blockHeight" , blockHeight ).
390+ Msg ("ExecuteTxs: engine_forkchoiceUpdatedV3 returned non-VALID status" )
391+ return err
392+ }
393+
394+ if forkchoiceResult .PayloadID == nil {
395+ c .logger .Error ().
396+ Str ("status" , forkchoiceResult .PayloadStatus .Status ).
397+ Str ("latestValidHash" , forkchoiceResult .PayloadStatus .LatestValidHash .Hex ()).
398+ Interface ("validationError" , forkchoiceResult .PayloadStatus .ValidationError ).
399+ Interface ("forkchoiceState" , args ).
400+ Interface ("payloadAttributes" , evPayloadAttrs ).
401+ Uint64 ("blockHeight" , blockHeight ).
402+ Msg ("returned nil PayloadID" )
403+
404+ return fmt .Errorf ("returned nil PayloadID - (status: %s, latestValidHash: %s)" ,
405+ forkchoiceResult .PayloadStatus .Status ,
406+ forkchoiceResult .PayloadStatus .LatestValidHash .Hex ())
407+ }
408+
409+ payloadID = forkchoiceResult .PayloadID
410+ return nil
411+ }, MaxPayloadStatusRetries , InitialRetryBackoff , "ExecuteTxs forkchoice" )
355412
356- return fmt .Errorf ("returned nil PayloadID - (status: %s, latestValidHash: %s)" ,
357- forkchoiceResult .PayloadStatus .Status ,
358- forkchoiceResult .PayloadStatus .LatestValidHash .Hex ())
413+ if err == nil {
414+ break
415+ }
416+
417+ if errors .Is (err , ErrInvalidPayloadStatus ) && idx + 1 < len (parentHashCandidates ) {
418+ c .logger .Warn ().
419+ Str ("blockHash" , candidate .Hex ()).
420+ Uint64 ("blockHeight" , blockHeight - 1 ).
421+ Msg ("ExecuteTxs: execution engine rejected parent hash candidate, trying alternate" )
422+ continue
359423 }
360424
361- payloadID = forkchoiceResult .PayloadID
362- return nil
363- }, MaxPayloadStatusRetries , InitialRetryBackoff , "ExecuteTxs forkchoice" )
364- if err != nil {
365425 return nil , 0 , err
366426 }
367427
428+ if payloadID == nil {
429+ return nil , 0 , fmt .Errorf ("engine returned nil PayloadID after trying %d parent hash candidates" , len (parentHashCandidates ))
430+ }
431+
368432 // get payload
369433 var payloadResult engine.ExecutionPayloadEnvelope
370434 err = c .engineClient .CallContext (ctx , & payloadResult , "engine_getPayloadV4" , * payloadID )
0 commit comments