@@ -160,6 +160,9 @@ type EthRPCClient interface {
160160
161161 // GetTxs retrieves pending transactions from the transaction pool.
162162 GetTxs (ctx context.Context ) ([]string , error )
163+
164+ // GetNextProposer retrieves the proposer selected by the execution layer.
165+ GetNextProposer (ctx context.Context , number * big.Int ) (common.Hash , error )
163166}
164167
165168// EngineClient represents a client that interacts with an Ethereum execution engine
@@ -353,15 +356,15 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight
353356 // Continue execution on error, as it might be transient
354357 } else if found {
355358 if stateRoot != nil {
356- return execution. ExecuteResult { UpdatedStateRoot : stateRoot }, nil
359+ return c . executionResultAfterBlock ( ctx , stateRoot , blockHeight )
357360 }
358361 if payloadID != nil {
359362 // Found in-progress execution, attempt to resume
360363 stateRoot , err := c .processPayload (ctx , * payloadID , txs )
361364 if err != nil {
362365 return execution.ExecuteResult {}, err
363366 }
364- return execution. ExecuteResult { UpdatedStateRoot : stateRoot }, nil
367+ return c . executionResultAfterBlock ( ctx , stateRoot , blockHeight )
365368 }
366369 }
367370
@@ -461,7 +464,7 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight
461464 if err != nil {
462465 return execution.ExecuteResult {}, err
463466 }
464- return execution. ExecuteResult { UpdatedStateRoot : stateRoot }, nil
467+ return c . executionResultAfterBlock ( ctx , stateRoot , blockHeight )
465468}
466469
467470// setHead updates the head block hash without changing safe or finalized.
@@ -858,21 +861,81 @@ func (c *EngineClient) filterTransactions(txs [][]byte) []string {
858861
859862// GetExecutionInfo returns current execution layer parameters.
860863func (c * EngineClient ) GetExecutionInfo (ctx context.Context ) (execution.ExecutionInfo , error ) {
864+ var info execution.ExecutionInfo
861865 if cached := c .cachedExecutionInfo .Load (); cached != nil {
862- return * cached , nil
866+ info .MaxGas = cached .MaxGas
867+ } else {
868+ header , err := c .ethClient .HeaderByNumber (ctx , nil ) // nil = latest
869+ if err != nil {
870+ return execution.ExecutionInfo {}, fmt .Errorf ("failed to get latest block: %w" , err )
871+ }
872+
873+ info .MaxGas = header .GasLimit
874+ c .cachedExecutionInfo .Store (& execution.ExecutionInfo {MaxGas : info .MaxGas })
863875 }
864876
865- header , err := c .ethClient .HeaderByNumber (ctx , nil ) // nil = latest
877+ nextProposer , err := c .ethClient .GetNextProposer (ctx , nil )
866878 if err != nil {
867- return execution.ExecutionInfo {}, fmt .Errorf ("failed to get latest block: %w" , err )
879+ if ! isRPCMethodNotFound (err ) {
880+ return execution.ExecutionInfo {}, fmt .Errorf ("failed to get next proposer: %w" , err )
881+ }
882+ return info , nil
883+ }
884+ if nextProposer != (common.Hash {}) {
885+ info .NextProposerAddress = nextProposer .Bytes ()
868886 }
869-
870- info := execution.ExecutionInfo {MaxGas : header .GasLimit }
871- c .cachedExecutionInfo .Store (& info )
872887
873888 return info , nil
874889}
875890
891+ func isRPCMethodNotFound (err error ) bool {
892+ var rpcErr rpc.Error
893+ return errors .As (err , & rpcErr ) && rpcErr .ErrorCode () == - 32601
894+ }
895+
896+ func (c * EngineClient ) executionResultAfterBlock (ctx context.Context , stateRoot []byte , executedBlockHeight uint64 ) (execution.ExecuteResult , error ) {
897+ nextProposer , err := c .nextProposerChangeAfterBlock (ctx , executedBlockHeight )
898+ if err != nil {
899+ return execution.ExecuteResult {}, err
900+ }
901+ return execution.ExecuteResult {
902+ UpdatedStateRoot : stateRoot ,
903+ NextProposerAddress : nextProposer ,
904+ }, nil
905+ }
906+
907+ func (c * EngineClient ) nextProposerChangeAfterBlock (ctx context.Context , executedBlockHeight uint64 ) ([]byte , error ) {
908+ if executedBlockHeight == 0 {
909+ return nil , nil
910+ }
911+
912+ before , supported , err := c .nextProposerAtBlock (ctx , executedBlockHeight - 1 )
913+ if err != nil || ! supported {
914+ return nil , err
915+ }
916+
917+ after , supported , err := c .nextProposerAtBlock (ctx , executedBlockHeight )
918+ if err != nil || ! supported {
919+ return nil , err
920+ }
921+
922+ if after == (common.Hash {}) || after == before {
923+ return nil , nil
924+ }
925+ return after .Bytes (), nil
926+ }
927+
928+ func (c * EngineClient ) nextProposerAtBlock (ctx context.Context , blockHeight uint64 ) (common.Hash , bool , error ) {
929+ proposer , err := c .ethClient .GetNextProposer (ctx , new (big.Int ).SetUint64 (blockHeight ))
930+ if err != nil {
931+ if isRPCMethodNotFound (err ) {
932+ return common.Hash {}, false , nil
933+ }
934+ return common.Hash {}, true , fmt .Errorf ("failed to get next proposer at block %d: %w" , blockHeight , err )
935+ }
936+ return proposer , true , nil
937+ }
938+
876939// FilterTxs validates force-included transactions and applies gas and size filtering for all passed txs.
877940// If hasForceIncludedTransaction is false, skip filtering entirely - mempool batch is already filtered.
878941// Returns a slice of FilterStatus for each transaction.
0 commit comments