@@ -152,7 +152,7 @@ func retryWithBackoffOnPayloadStatus(ctx context.Context, fn func() error, maxRe
152152// EngineRPCClient abstracts Engine API RPC calls for tracing and testing.
153153type EngineRPCClient interface {
154154 // ForkchoiceUpdated updates the forkchoice state and optionally starts payload building.
155- ForkchoiceUpdated (ctx context.Context , state engine.ForkchoiceStateV1 , args map [ string ] any ) (* engine.ForkChoiceResponse , error )
155+ ForkchoiceUpdated (ctx context.Context , state engine.ForkchoiceStateV1 , args interface {} ) (* engine.ForkChoiceResponse , error )
156156
157157 // GetPayload retrieves a previously requested execution payload.
158158 GetPayload (ctx context.Context , payloadID engine.PayloadID ) (* engine.ExecutionPayloadEnvelope , error )
@@ -396,7 +396,16 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight
396396 }
397397
398398 // 2. Prepare payload attributes
399- txsPayload := c .filterTransactions (txs )
399+ // Use zero-alloc struct instead of map to avoid reflection overhead
400+ evPayloadAttrs := PayloadAttributesV3 {
401+ Timestamp : timestamp .Unix (),
402+ PrevRandao : c .derivePrevRandao (blockHeight ).Hex (),
403+ SuggestedFeeRecipient : c .feeRecipient .Hex (),
404+ Withdrawals : emptyWithdrawals ,
405+ ParentBeaconBlockRoot : zeroHashHex ,
406+ Transactions : PayloadTransactions (txs ),
407+ GasLimit : prevGasLimit ,
408+ }
400409
401410 // Cache parent block hash for safe-block lookups.
402411 c .cacheBlockHash (blockHeight - 1 , prevBlockHash )
@@ -411,20 +420,6 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight
411420 }
412421 c .mu .Unlock ()
413422
414- // update forkchoice to get the next payload id
415- evPayloadAttrs := map [string ]any {
416- // Standard Ethereum payload attributes (flattened) - using camelCase as expected by JSON
417- "timestamp" : timestamp .Unix (),
418- "prevRandao" : c .derivePrevRandao (blockHeight ),
419- "suggestedFeeRecipient" : c .feeRecipient ,
420- "withdrawals" : emptyWithdrawals ,
421- // V3 requires parentBeaconBlockRoot
422- "parentBeaconBlockRoot" : zeroHashHex , // Use zero hash for evolve
423- // evolve-specific fields
424- "transactions" : txsPayload ,
425- "gasLimit" : prevGasLimit , // Use camelCase to match JSON conventions
426- }
427-
428423 c .logger .Debug ().
429424 Uint64 ("height" , blockHeight ).
430425 Int ("tx_count" , len (txs )).
@@ -874,20 +869,6 @@ func (c *EngineClient) reconcileExecutionAtHeight(ctx context.Context, height ui
874869 return nil , nil , false , nil
875870}
876871
877- // filterTransactions formats transactions for the payload.
878- // DA transactions should already be filtered via FilterTxs before reaching here.
879- // Mempool transactions are already validated when added to mempool.
880- func (c * EngineClient ) filterTransactions (txs [][]byte ) []string {
881- validTxs := make ([]string , 0 , len (txs ))
882- for _ , tx := range txs {
883- if len (tx ) == 0 {
884- continue
885- }
886- validTxs = append (validTxs , "0x" + hex .EncodeToString (tx ))
887- }
888- return validTxs
889- }
890-
891872// GetExecutionInfo returns current execution layer parameters.
892873func (c * EngineClient ) GetExecutionInfo (ctx context.Context ) (execution.ExecutionInfo , error ) {
893874 if cached := c .cachedExecutionInfo .Load (); cached != nil {
@@ -1170,3 +1151,77 @@ func getAuthToken(jwtSecret []byte) (string, error) {
11701151 }
11711152 return authToken , nil
11721153}
1154+
1155+ // PayloadAttributesV3 replaces the untyped map for better performance (no reflection)
1156+ type PayloadAttributesV3 struct {
1157+ Timestamp int64 `json:"timestamp"`
1158+ PrevRandao string `json:"prevRandao"`
1159+ SuggestedFeeRecipient string `json:"suggestedFeeRecipient"`
1160+ Withdrawals []* types.Withdrawal `json:"withdrawals"`
1161+ ParentBeaconBlockRoot string `json:"parentBeaconBlockRoot"`
1162+ Transactions PayloadTransactions `json:"transactions"`
1163+ GasLimit uint64 `json:"gasLimit"`
1164+ }
1165+
1166+ // PayloadTransactions implements custom JSON marshaling to avoid intermediate string allocations.
1167+ type PayloadTransactions [][]byte
1168+
1169+ // MarshalJSON encodes transactions as a JSON array of hex strings directly to bytes.
1170+ func (txs PayloadTransactions ) MarshalJSON () ([]byte , error ) {
1171+ if len (txs ) == 0 {
1172+ return []byte ("[]" ), nil
1173+ }
1174+
1175+ // Pre-calculate full size to perform a single allocation:
1176+ // 2 bytes for [] + (len(txs)-1) commas + per tx: 2 quotes + 2 prefix + hex len
1177+ size := 2
1178+ count := 0
1179+ for _ , tx := range txs {
1180+ if len (tx ) == 0 {
1181+ continue
1182+ }
1183+ // comma
1184+ if count > 0 {
1185+ size ++
1186+ }
1187+ // "0x..."
1188+ size += 4 + hex .EncodedLen (len (tx ))
1189+ count ++
1190+ }
1191+
1192+ if count == 0 {
1193+ return []byte ("[]" ), nil
1194+ }
1195+
1196+ buf := make ([]byte , 0 , size )
1197+ buf = append (buf , '[' )
1198+ written := 0
1199+ for _ , tx := range txs {
1200+ if len (tx ) == 0 {
1201+ continue
1202+ }
1203+ if written > 0 {
1204+ buf = append (buf , ',' )
1205+ }
1206+ buf = append (buf , '"' , '0' , 'x' )
1207+
1208+ // Append encoded hex directly
1209+ n := hex .EncodedLen (len (tx ))
1210+ start := len (buf )
1211+ // Ensure capacity
1212+ if cap (buf ) < start + n {
1213+ // grow logic if needed, but make() with exact size should cover it
1214+ newBuf := make ([]byte , len (buf ), start + n * 2 )
1215+ copy (newBuf , buf )
1216+ buf = newBuf
1217+ }
1218+ // Expand length
1219+ buf = buf [:start + n ]
1220+ hex .Encode (buf [start :], tx )
1221+
1222+ buf = append (buf , '"' )
1223+ written ++
1224+ }
1225+ buf = append (buf , ']' )
1226+ return buf , nil
1227+ }
0 commit comments