@@ -2,6 +2,7 @@ package evm
22
33import (
44 "context"
5+ "crypto/sha256"
56 "encoding/binary"
67 "errors"
78 "fmt"
@@ -16,15 +17,18 @@ import (
1617 "github.com/ethereum/go-ethereum/core"
1718 "github.com/ethereum/go-ethereum/core/rawdb"
1819 "github.com/ethereum/go-ethereum/core/state"
20+ "github.com/ethereum/go-ethereum/core/tracing"
1921 "github.com/ethereum/go-ethereum/core/txpool"
2022 "github.com/ethereum/go-ethereum/core/txpool/legacypool"
2123 "github.com/ethereum/go-ethereum/core/types"
2224 "github.com/ethereum/go-ethereum/core/vm"
25+ "github.com/ethereum/go-ethereum/crypto"
2326 "github.com/ethereum/go-ethereum/ethdb"
2427 "github.com/ethereum/go-ethereum/params"
2528 "github.com/ethereum/go-ethereum/rpc"
2629 "github.com/ethereum/go-ethereum/trie"
2730 "github.com/ethereum/go-ethereum/triedb"
31+ "github.com/holiman/uint256"
2832 ds "github.com/ipfs/go-datastore"
2933 "github.com/rs/zerolog"
3034)
3741// baseFeeChangeDenominator is the EIP-1559 base fee change denominator.
3842const baseFeeChangeDenominator = 8
3943
44+ // payloadTTL is how long a payload can remain in the map before being cleaned up.
45+ const payloadTTL = 60 * time .Second
46+
47+ // maxPayloads is the maximum number of payloads to keep in memory.
48+ const maxPayloads = 10
49+
4050// GethBackend holds the in-process geth components.
4151type GethBackend struct {
4252 db ethdb.Database
@@ -67,8 +77,14 @@ type payloadBuildState struct {
6777 transactions [][]byte
6878 gasLimit uint64
6979
80+ // createdAt tracks when this payload was created for TTL cleanup
81+ createdAt time.Time
82+
7083 // built payload (populated after getPayload)
7184 payload * engine.ExecutableData
85+
86+ // buildErr stores any error that occurred during payload build
87+ buildErr error
7288}
7389
7490// gethEngineClient implements EngineRPCClient using in-process geth.
@@ -302,10 +318,26 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine
302318 return nil , fmt .Errorf ("failed to parse payload attributes: %w" , err )
303319 }
304320
305- // Generate payload ID deterministically from attributes
306- g .backend .nextPayloadID ++
307- var payloadID engine.PayloadID
308- binary .BigEndian .PutUint64 (payloadID [:], g .backend .nextPayloadID )
321+ // Generate deterministic payload ID from attributes
322+ payloadID := g .generatePayloadID (payloadState )
323+
324+ // Check if we already have this payload (idempotency)
325+ if existing , ok := g .backend .payloads [payloadID ]; ok {
326+ // Reuse existing payload if it hasn't errored
327+ if existing .buildErr == nil {
328+ response .PayloadID = & payloadID
329+ g .logger .Debug ().
330+ Str ("payload_id" , payloadID .String ()).
331+ Msg ("reusing existing payload" )
332+ return response , nil
333+ }
334+ // Previous build failed, remove it and try again
335+ delete (g .backend .payloads , payloadID )
336+ }
337+
338+ // Clean up old payloads before adding new one
339+ g .cleanupStalePayloads ()
340+
309341 g .backend .payloads [payloadID ] = payloadState
310342 response .PayloadID = & payloadID
311343
@@ -321,11 +353,73 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine
321353 return response , nil
322354}
323355
356+ // generatePayloadID creates a deterministic payload ID from the payload attributes.
357+ // This ensures that the same attributes always produce the same ID for idempotency.
358+ func (g * gethEngineClient ) generatePayloadID (ps * payloadBuildState ) engine.PayloadID {
359+ h := sha256 .New ()
360+ h .Write (ps .parentHash [:])
361+ binary .Write (h , binary .BigEndian , ps .timestamp )
362+ h .Write (ps .prevRandao [:])
363+ h .Write (ps .feeRecipient [:])
364+ binary .Write (h , binary .BigEndian , ps .gasLimit )
365+ // Include transaction count and first tx hash for uniqueness
366+ binary .Write (h , binary .BigEndian , uint64 (len (ps .transactions )))
367+ for _ , tx := range ps .transactions {
368+ h .Write (tx )
369+ }
370+ sum := h .Sum (nil )
371+ var id engine.PayloadID
372+ copy (id [:], sum [:8 ])
373+ return id
374+ }
375+
376+ // cleanupStalePayloads removes payloads that have exceeded their TTL or when we have too many.
377+ func (g * gethEngineClient ) cleanupStalePayloads () {
378+ now := time .Now ()
379+ var staleIDs []engine.PayloadID
380+
381+ // Find stale payloads
382+ for id , ps := range g .backend .payloads {
383+ if now .Sub (ps .createdAt ) > payloadTTL {
384+ staleIDs = append (staleIDs , id )
385+ }
386+ }
387+
388+ // Remove stale payloads
389+ for _ , id := range staleIDs {
390+ delete (g .backend .payloads , id )
391+ g .logger .Debug ().
392+ Str ("payload_id" , id .String ()).
393+ Msg ("cleaned up stale payload" )
394+ }
395+
396+ // If still too many payloads, remove oldest ones
397+ for len (g .backend .payloads ) >= maxPayloads {
398+ var oldestID engine.PayloadID
399+ var oldestTime time.Time
400+ first := true
401+ for id , ps := range g .backend .payloads {
402+ if first || ps .createdAt .Before (oldestTime ) {
403+ oldestID = id
404+ oldestTime = ps .createdAt
405+ first = false
406+ }
407+ }
408+ if ! first {
409+ delete (g .backend .payloads , oldestID )
410+ g .logger .Debug ().
411+ Str ("payload_id" , oldestID .String ()).
412+ Msg ("evicted oldest payload due to limit" )
413+ }
414+ }
415+ }
416+
324417// parsePayloadAttributes extracts payload attributes from the map format.
325418func (g * gethEngineClient ) parsePayloadAttributes (parentHash common.Hash , attrs map [string ]any ) (* payloadBuildState , error ) {
326419 ps := & payloadBuildState {
327420 parentHash : parentHash ,
328421 withdrawals : []* types.Withdrawal {},
422+ createdAt : time .Now (),
329423 }
330424
331425 // Parse timestamp (required)
@@ -439,11 +533,19 @@ func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.Payl
439533 return nil , fmt .Errorf ("unknown payload ID: %s" , payloadID .String ())
440534 }
441535
442- // Build the block if not already built
536+ // Return cached error if previous build failed
537+ if payloadState .buildErr != nil {
538+ delete (g .backend .payloads , payloadID )
539+ return nil , fmt .Errorf ("payload build previously failed: %w" , payloadState .buildErr )
540+ }
541+
542+ // Build the payload if not already built
443543 if payloadState .payload == nil {
444- startTime := time .Now ()
544+ buildStartTime := time .Now ()
445545 payload , err := g .buildPayload (ctx , payloadState )
446546 if err != nil {
547+ // Cache the error so we don't retry on the same payload
548+ payloadState .buildErr = err
447549 return nil , fmt .Errorf ("failed to build payload: %w" , err )
448550 }
449551 payloadState .payload = payload
@@ -454,11 +556,11 @@ func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.Payl
454556 Str ("block_hash" , payload .BlockHash .Hex ()).
455557 Int ("tx_count" , len (payload .Transactions )).
456558 Uint64 ("gas_used" , payload .GasUsed ).
457- Dur ("build_time" , time .Since (startTime )).
559+ Dur ("build_time" , time .Since (buildStartTime )).
458560 Msg ("built payload" )
459561 }
460562
461- // Remove the payload from pending after retrieval
563+ // Remove the payload from pending after retrieval - caller has it now
462564 delete (g .backend .payloads , payloadID )
463565
464566 return & engine.ExecutionPayloadEnvelope {
@@ -593,6 +695,21 @@ func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildSta
593695 Msg ("transaction execution summary" )
594696 }
595697
698+ // Process withdrawals (EIP-4895) - credit ETH to withdrawal recipients
699+ // Withdrawals are processed after all transactions, crediting the specified
700+ // amount (in Gwei) to each recipient address.
701+ if len (ps .withdrawals ) > 0 {
702+ for _ , withdrawal := range ps .withdrawals {
703+ // Withdrawal amount is in Gwei, convert to Wei (multiply by 1e9)
704+ amount := new (big.Int ).SetUint64 (withdrawal .Amount )
705+ amount .Mul (amount , big .NewInt (params .GWei ))
706+ stateDB .AddBalance (withdrawal .Address , uint256 .MustFromBig (amount ), tracing .BalanceIncreaseWithdrawal )
707+ }
708+ g .logger .Debug ().
709+ Int ("count" , len (ps .withdrawals )).
710+ Msg ("processed withdrawals" )
711+ }
712+
596713 // Finalize state
597714 header .GasUsed = gasUsed
598715 header .Root = stateDB .IntermediateRoot (g .backend .chainConfig .IsEIP158 (header .Number ))
@@ -659,7 +776,7 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu
659776 g .backend .mu .Lock ()
660777 defer g .backend .mu .Unlock ()
661778
662- startTime := time .Now ()
779+ validationStart := time .Now ()
663780
664781 // Validate payload
665782 if payload == nil {
@@ -803,7 +920,7 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu
803920 Str ("parent_hash" , payload .ParentHash .Hex ()).
804921 Int ("tx_count" , len (txs )).
805922 Uint64 ("gas_used" , payload .GasUsed ).
806- Dur ("process_time" , time .Since (startTime )).
923+ Dur ("process_time" , time .Since (validationStart )).
807924 Msg ("new payload validated and inserted" )
808925
809926 return & engine.PayloadStatusV1 {
@@ -975,7 +1092,7 @@ func applyTransaction(
9751092
9761093 // Set contract address if this was a contract creation
9771094 if msg .To == nil {
978- receipt .ContractAddress = evmInstance . Origin
1095+ receipt .ContractAddress = crypto . CreateAddress ( msg . From , tx . Nonce ())
9791096 }
9801097
9811098 return receipt , nil
0 commit comments