From 82e799f48eec9dffa6a2acb9bf052d42f63b0f5a Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 16:02:45 +0100 Subject: [PATCH 01/18] feat: geth. --- apps/evm/cmd/run.go | 54 ++- apps/evm/go.mod | 3 + apps/evm/go.sum | 2 + execution/evm/engine_geth.go | 779 ++++++++++++++++++++++++++++++ execution/evm/engine_geth_test.go | 487 +++++++++++++++++++ execution/evm/execution.go | 8 +- execution/evm/flags.go | 3 + execution/evm/go.mod | 3 + execution/evm/go.sum | 2 + 9 files changed, 1321 insertions(+), 20 deletions(-) create mode 100644 execution/evm/engine_geth.go create mode 100644 execution/evm/engine_geth_test.go diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index eef0fa379c..00f9921c21 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -3,12 +3,14 @@ package cmd import ( "bytes" "context" + "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ipfs/go-datastore" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -56,7 +58,13 @@ var RunCmd = &cobra.Command{ } tracingEnabled := nodeConfig.Instrumentation.IsTracingEnabled() - executor, err := createExecutionClient(cmd, datastore, tracingEnabled) + + executor, err := createExecutionClient( + cmd, + datastore, + tracingEnabled, + logger.With().Str("module", "engine_client").Logger(), + ) if err != nil { return err } @@ -67,12 +75,6 @@ var RunCmd = &cobra.Command{ } daClient := block.NewDAClient(blobClient, nodeConfig, logger) - - // Attach logger to the EVM engine client if available - if ec, ok := executor.(*evm.EngineClient); ok { - ec.SetLogger(logger.With().Str("module", "engine_client").Logger()) - } - headerNamespace := da.NamespaceFromString(nodeConfig.DA.GetNamespace()) dataNamespace := da.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) @@ -200,7 +202,33 @@ func createSequencer( return sequencer, nil } -func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool) (execution.Executor, error) { +func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, logger zerolog.Logger) (execution.Executor, error) { + feeRecipientStr, err := cmd.Flags().GetString(evm.FlagEvmFeeRecipient) + if err != nil { + return nil, fmt.Errorf("failed to get '%s' flag: %w", evm.FlagEvmFeeRecipient, err) + } + feeRecipient := common.HexToAddress(feeRecipientStr) + + useGeth, _ := cmd.Flags().GetBool(evm.FlagEVMInProcessGeth) + if useGeth { + genesisPath, _ := cmd.Flags().GetString(evm.FlagEVMGenesisPath) + if len(genesisPath) == 0 { + return nil, fmt.Errorf("genesis path must be provided when using in-process Geth") + } + + genesisBz, err := os.ReadFile(genesisPath) + if err != nil { + return nil, fmt.Errorf("failed to read genesis: %w", err) + } + + var genesis core.Genesis + if err := json.Unmarshal(genesisBz, &genesis); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis: %w", err) + } + + return evm.NewEngineExecutionClientWithGeth(&genesis, feeRecipient, db, logger) + } + // Read execution client parameters from flags ethURL, err := cmd.Flags().GetString(evm.FlagEvmEthURL) if err != nil { @@ -236,16 +264,11 @@ func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEna if err != nil { return nil, fmt.Errorf("failed to get '%s' flag: %w", evm.FlagEvmGenesisHash, err) } - feeRecipientStr, err := cmd.Flags().GetString(evm.FlagEvmFeeRecipient) - if err != nil { - return nil, fmt.Errorf("failed to get '%s' flag: %w", evm.FlagEvmFeeRecipient, err) - } // Convert string parameters to Ethereum types genesisHash := common.HexToHash(genesisHashStr) - feeRecipient := common.HexToAddress(feeRecipientStr) - return evm.NewEngineExecutionClient(ethURL, engineURL, jwtSecret, genesisHash, feeRecipient, db, tracingEnabled) + return evm.NewEngineExecutionClient(ethURL, engineURL, jwtSecret, genesisHash, feeRecipient, db, tracingEnabled, logger) } // addFlags adds flags related to the EVM execution client @@ -256,4 +279,7 @@ func addFlags(cmd *cobra.Command) { cmd.Flags().String(evm.FlagEvmGenesisHash, "", "Hash of the genesis block") cmd.Flags().String(evm.FlagEvmFeeRecipient, "", "Address that will receive transaction fees") cmd.Flags().String(flagForceInclusionServer, "", "Address for force inclusion API server (e.g. 127.0.0.1:8547). If set, enables the server for direct DA submission") + + cmd.Flags().Bool(evm.FlagEVMInProcessGeth, false, "Use in-process Geth for EVM execution instead of external execution client") + cmd.Flags().String(evm.FlagEVMGenesisPath, "", "EVM genesis path for Geth") } diff --git a/apps/evm/go.mod b/apps/evm/go.mod index 15669655f6..66d55b7e76 100644 --- a/apps/evm/go.mod +++ b/apps/evm/go.mod @@ -28,6 +28,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/StackExchange/wmi v1.2.1 // indirect + github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect @@ -49,6 +50,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/filecoin-project/go-clock v0.1.0 // indirect @@ -74,6 +76,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/apps/evm/go.sum b/apps/evm/go.sum index bf94ea865e..20b5416d0c 100644 --- a/apps/evm/go.sum +++ b/apps/evm/go.sum @@ -257,6 +257,8 @@ github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhk github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y= github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY= diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go new file mode 100644 index 0000000000..4e5123b378 --- /dev/null +++ b/execution/evm/engine_geth.go @@ -0,0 +1,779 @@ +package evm + +import ( + "context" + "errors" + "fmt" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/txpool/legacypool" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/triedb" + ds "github.com/ipfs/go-datastore" + "github.com/rs/zerolog" +) + +var ( + _ EngineRPCClient = (*gethEngineClient)(nil) + _ EthRPCClient = (*gethEthClient)(nil) +) + +// GethBackend holds the in-process geth components. +type GethBackend struct { + db ethdb.Database + chainConfig *params.ChainConfig + blockchain *core.BlockChain + txPool *txpool.TxPool + + mu sync.Mutex + // payloadBuilding tracks in-flight payload builds + payloads map[engine.PayloadID]*payloadBuildState + nextPayloadID uint64 +} + +// payloadBuildState tracks the state of a payload being built. +type payloadBuildState struct { + parentHash common.Hash + timestamp uint64 + prevRandao common.Hash + feeRecipient common.Address + withdrawals []*types.Withdrawal + transactions [][]byte + gasLimit uint64 + // built payload (populated after getPayload) + payload *engine.ExecutableData +} + +// gethEngineClient implements EngineRPCClient using in-process geth. +type gethEngineClient struct { + backend *GethBackend + logger zerolog.Logger +} + +// gethEthClient implements EthRPCClient using in-process geth. +type gethEthClient struct { + backend *GethBackend + logger zerolog.Logger +} + +// NewEngineExecutionClientWithGeth creates an EngineClient that uses an in-process +// go-ethereum instance instead of connecting to an external execution engine via RPC. +// +// This is useful for: +// - Testing without needing to run a separate geth/reth process +// - Embedded rollup nodes that want a single binary +// - Development and debugging with full control over the EVM +// +// Parameters: +// - genesis: The genesis configuration for the chain +// - feeRecipient: Address to receive transaction fees +// - db: Datastore for execution metadata (crash recovery) +// - logger: Logger for the client +// +// Returns an EngineClient that behaves identically to the RPC-based client +// but executes everything in-process. +func NewEngineExecutionClientWithGeth( + genesis *core.Genesis, + feeRecipient common.Address, + db ds.Batching, + logger zerolog.Logger, +) (*EngineClient, error) { + if db == nil { + return nil, errors.New("db is required for EVM execution client") + } + if genesis == nil { + return nil, errors.New("genesis configuration is required") + } + + backend, err := newGethBackend(genesis, logger) + if err != nil { + return nil, fmt.Errorf("failed to create geth backend: %w", err) + } + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger.With().Str("component", "geth-engine").Logger(), + } + + ethClient := &gethEthClient{ + backend: backend, + logger: logger.With().Str("component", "geth-eth").Logger(), + } + + genesisBlock := backend.blockchain.Genesis() + genesisHash := genesisBlock.Hash() + + return &EngineClient{ + engineClient: engineClient, + ethClient: ethClient, + genesisHash: genesisHash, + feeRecipient: feeRecipient, + store: NewEVMStore(db), + currentHeadBlockHash: genesisHash, + currentSafeBlockHash: genesisHash, + currentFinalizedBlockHash: genesisHash, + blockHashCache: make(map[uint64]common.Hash), + logger: logger, + }, nil +} + +// newGethBackend creates a new in-process geth backend. +func newGethBackend(genesis *core.Genesis, logger zerolog.Logger) (*GethBackend, error) { + // Create in-memory database + memdb := rawdb.NewMemoryDatabase() + + // Create trie database + trieDB := triedb.NewDatabase(memdb, nil) + + // Initialize the genesis block + chainConfig, genesisHash, _, genesisErr := core.SetupGenesisBlockWithOverride(memdb, trieDB, genesis, nil) + if genesisErr != nil { + return nil, fmt.Errorf("failed to setup genesis: %w", genesisErr) + } + + logger.Info(). + Str("genesis_hash", genesisHash.Hex()). + Str("chain_id", chainConfig.ChainID.String()). + Msg("initialized in-process geth with genesis") + + // Create the consensus engine (beacon/PoS) + consensusEngine := beacon.New(nil) + + // Create blockchain config + bcConfig := core.DefaultConfig().WithStateScheme(rawdb.HashScheme) + + // Create the blockchain + blockchain, err := core.NewBlockChain(memdb, genesis, consensusEngine, bcConfig) + if err != nil { + return nil, fmt.Errorf("failed to create blockchain: %w", err) + } + + backend := &GethBackend{ + db: memdb, + chainConfig: chainConfig, + blockchain: blockchain, + payloads: make(map[engine.PayloadID]*payloadBuildState), + } + + // Create transaction pool + txPoolConfig := legacypool.DefaultConfig + txPoolConfig.NoLocals = true + + legacyPool := legacypool.New(txPoolConfig, blockchain) + txPool, err := txpool.New(0, blockchain, []txpool.SubPool{legacyPool}) + if err != nil { + return nil, fmt.Errorf("failed to create tx pool: %w", err) + } + backend.txPool = txPool + + return backend, nil +} + +// Close shuts down the geth backend. +func (b *GethBackend) Close() error { + if b.txPool != nil { + b.txPool.Close() + } + if b.blockchain != nil { + b.blockchain.Stop() + } + if b.db != nil { + b.db.Close() + } + return nil +} + +// ForkchoiceUpdated implements EngineRPCClient. +func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine.ForkchoiceStateV1, attrs map[string]any) (*engine.ForkChoiceResponse, error) { + g.backend.mu.Lock() + defer g.backend.mu.Unlock() + + // Validate the forkchoice state + headBlock := g.backend.blockchain.GetBlockByHash(fcState.HeadBlockHash) + if headBlock == nil { + return &engine.ForkChoiceResponse{ + PayloadStatus: engine.PayloadStatusV1{ + Status: engine.SYNCING, + }, + }, nil + } + + // Update the canonical chain head + if _, err := g.backend.blockchain.SetCanonical(headBlock); err != nil { + return nil, fmt.Errorf("failed to set canonical head: %w", err) + } + + response := &engine.ForkChoiceResponse{ + PayloadStatus: engine.PayloadStatusV1{ + Status: engine.VALID, + LatestValidHash: &fcState.HeadBlockHash, + }, + } + + // If payload attributes provided, start building a new payload + if attrs != nil { + payloadState, err := g.parsePayloadAttributes(fcState.HeadBlockHash, attrs) + if err != nil { + return nil, fmt.Errorf("failed to parse payload attributes: %w", err) + } + + // Generate payload ID + g.backend.nextPayloadID++ + var payloadID engine.PayloadID + payloadID[0] = byte(g.backend.nextPayloadID >> 56) + payloadID[1] = byte(g.backend.nextPayloadID >> 48) + payloadID[2] = byte(g.backend.nextPayloadID >> 40) + payloadID[3] = byte(g.backend.nextPayloadID >> 32) + payloadID[4] = byte(g.backend.nextPayloadID >> 24) + payloadID[5] = byte(g.backend.nextPayloadID >> 16) + payloadID[6] = byte(g.backend.nextPayloadID >> 8) + payloadID[7] = byte(g.backend.nextPayloadID) + + g.backend.payloads[payloadID] = payloadState + response.PayloadID = &payloadID + + g.logger.Debug(). + Str("payload_id", payloadID.String()). + Uint64("timestamp", payloadState.timestamp). + Int("tx_count", len(payloadState.transactions)). + Msg("started payload build") + } + + return response, nil +} + +// parsePayloadAttributes extracts payload attributes from the map format. +func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs map[string]any) (*payloadBuildState, error) { + ps := &payloadBuildState{ + parentHash: parentHash, + withdrawals: []*types.Withdrawal{}, + } + + // Parse timestamp + if ts, ok := attrs["timestamp"]; ok { + switch v := ts.(type) { + case int64: + ps.timestamp = uint64(v) + case uint64: + ps.timestamp = v + case float64: + ps.timestamp = uint64(v) + default: + return nil, fmt.Errorf("invalid timestamp type: %T", ts) + } + } + + // Parse prevRandao + if pr, ok := attrs["prevRandao"]; ok { + switch v := pr.(type) { + case common.Hash: + ps.prevRandao = v + case string: + ps.prevRandao = common.HexToHash(v) + default: + return nil, fmt.Errorf("invalid prevRandao type: %T", pr) + } + } + + // Parse suggestedFeeRecipient + if fr, ok := attrs["suggestedFeeRecipient"]; ok { + switch v := fr.(type) { + case common.Address: + ps.feeRecipient = v + case string: + ps.feeRecipient = common.HexToAddress(v) + default: + return nil, fmt.Errorf("invalid suggestedFeeRecipient type: %T", fr) + } + } + + // Parse transactions + if txs, ok := attrs["transactions"]; ok { + switch v := txs.(type) { + case []string: + ps.transactions = make([][]byte, len(v)) + for i, txHex := range v { + ps.transactions[i] = common.FromHex(txHex) + } + case [][]byte: + ps.transactions = v + } + } + + // Parse gasLimit + if gl, ok := attrs["gasLimit"]; ok { + switch v := gl.(type) { + case uint64: + ps.gasLimit = v + case int64: + ps.gasLimit = uint64(v) + case float64: + ps.gasLimit = uint64(v) + default: + return nil, fmt.Errorf("invalid gasLimit type: %T", gl) + } + } + + // Parse withdrawals (optional) + if w, ok := attrs["withdrawals"]; ok { + if withdrawals, ok := w.([]*types.Withdrawal); ok { + ps.withdrawals = withdrawals + } + } + + return ps, nil +} + +// GetPayload implements EngineRPCClient. +func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { + g.backend.mu.Lock() + defer g.backend.mu.Unlock() + + payloadState, ok := g.backend.payloads[payloadID] + if !ok { + return nil, fmt.Errorf("unknown payload ID: %s", payloadID.String()) + } + + // Build the block if not already built + if payloadState.payload == nil { + payload, err := g.buildPayload(payloadState) + if err != nil { + return nil, fmt.Errorf("failed to build payload: %w", err) + } + payloadState.payload = payload + } + + return &engine.ExecutionPayloadEnvelope{ + ExecutionPayload: payloadState.payload, + BlockValue: big.NewInt(0), + BlobsBundle: &engine.BlobsBundle{}, + Override: false, + }, nil +} + +// buildPayload constructs an execution payload from the pending state. +func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.ExecutableData, error) { + parent := g.backend.blockchain.GetBlockByHash(ps.parentHash) + if parent == nil { + return nil, fmt.Errorf("parent block not found: %s", ps.parentHash.Hex()) + } + + // Calculate base fee for the new block + var baseFee *big.Int + if g.backend.chainConfig.IsLondon(new(big.Int).Add(parent.Number(), big.NewInt(1))) { + baseFee = calcBaseFee(g.backend.chainConfig, parent.Header()) + } + + gasLimit := ps.gasLimit + if gasLimit == 0 { + gasLimit = parent.GasLimit() + } + + header := &types.Header{ + ParentHash: ps.parentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: ps.feeRecipient, + Root: common.Hash{}, // Will be set after execution + TxHash: types.EmptyTxsHash, + ReceiptHash: types.EmptyReceiptsHash, + Bloom: types.Bloom{}, + Difficulty: big.NewInt(0), + Number: new(big.Int).Add(parent.Number(), big.NewInt(1)), + GasLimit: gasLimit, + GasUsed: 0, + Time: ps.timestamp, + Extra: []byte{}, + MixDigest: ps.prevRandao, + Nonce: types.BlockNonce{}, + BaseFee: baseFee, + WithdrawalsHash: &types.EmptyWithdrawalsHash, + BlobGasUsed: new(uint64), + ExcessBlobGas: new(uint64), + ParentBeaconRoot: &common.Hash{}, + RequestsHash: &types.EmptyRequestsHash, + } + + // Process transactions + stateDB, err := g.backend.blockchain.StateAt(parent.Root()) + if err != nil { + return nil, fmt.Errorf("failed to get parent state: %w", err) + } + + var ( + txs types.Transactions + receipts []*types.Receipt + gasUsed uint64 + ) + + // Create EVM context + blockContext := core.NewEVMBlockContext(header, g.backend.blockchain, nil) + + // Execute transactions + gp := new(core.GasPool).AddGas(gasLimit) + for i, txBytes := range ps.transactions { + if len(txBytes) == 0 { + continue + } + + var tx types.Transaction + if err := tx.UnmarshalBinary(txBytes); err != nil { + g.logger.Debug(). + Int("index", i). + Err(err). + Msg("skipping invalid transaction") + continue + } + + stateDB.SetTxContext(tx.Hash(), len(txs)) + + // Create EVM instance and apply transaction + receipt, err := applyTransaction( + g.backend.chainConfig, + blockContext, + gp, + stateDB, + header, + &tx, + &gasUsed, + ) + if err != nil { + g.logger.Debug(). + Int("index", i). + Str("tx_hash", tx.Hash().Hex()). + Err(err). + Msg("transaction execution failed, skipping") + continue + } + + txs = append(txs, &tx) + receipts = append(receipts, receipt) + } + + // Finalize state + header.GasUsed = gasUsed + header.Root = stateDB.IntermediateRoot(g.backend.chainConfig.IsEIP158(header.Number)) + + // Calculate transaction and receipt hashes + header.TxHash = types.DeriveSha(txs, trie.NewListHasher()) + header.ReceiptHash = types.DeriveSha(types.Receipts(receipts), trie.NewListHasher()) + + // Calculate bloom filter + header.Bloom = createBloomFromReceipts(receipts) + + // Calculate withdrawals hash if withdrawals exist + if len(ps.withdrawals) > 0 { + wh := types.DeriveSha(types.Withdrawals(ps.withdrawals), trie.NewListHasher()) + header.WithdrawalsHash = &wh + } + + // Create the block + block := types.NewBlock(header, &types.Body{ + Transactions: txs, + Uncles: nil, + Withdrawals: ps.withdrawals, + }, receipts, trie.NewListHasher()) + + // Convert to ExecutableData + txData := make([][]byte, len(txs)) + for i, tx := range txs { + data, err := tx.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal tx: %w", err) + } + txData[i] = data + } + + payload := &engine.ExecutableData{ + ParentHash: header.ParentHash, + FeeRecipient: header.Coinbase, + StateRoot: header.Root, + ReceiptsRoot: header.ReceiptHash, + LogsBloom: header.Bloom[:], + Random: header.MixDigest, + Number: header.Number.Uint64(), + GasLimit: header.GasLimit, + GasUsed: header.GasUsed, + Timestamp: header.Time, + ExtraData: header.Extra, + BaseFeePerGas: header.BaseFee, + BlockHash: block.Hash(), + Transactions: txData, + Withdrawals: ps.withdrawals, + BlobGasUsed: header.BlobGasUsed, + ExcessBlobGas: header.ExcessBlobGas, + } + + return payload, nil +} + +// NewPayload implements EngineRPCClient. +func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.ExecutableData, blobHashes []string, parentBeaconBlockRoot string, executionRequests [][]byte) (*engine.PayloadStatusV1, error) { + g.backend.mu.Lock() + defer g.backend.mu.Unlock() + + // Verify parent exists + parent := g.backend.blockchain.GetBlockByHash(payload.ParentHash) + if parent == nil { + return &engine.PayloadStatusV1{ + Status: engine.SYNCING, + }, nil + } + + // Decode transactions + var txs types.Transactions + for _, txData := range payload.Transactions { + var tx types.Transaction + if err := tx.UnmarshalBinary(txData); err != nil { + return &engine.PayloadStatusV1{ + Status: engine.INVALID, + }, nil + } + txs = append(txs, &tx) + } + + gasLimit := payload.GasLimit + if gasLimit == 0 { + gasLimit = parent.GasLimit() + } + + // Reconstruct the header from the payload + header := &types.Header{ + ParentHash: payload.ParentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: payload.FeeRecipient, + Root: payload.StateRoot, + TxHash: types.DeriveSha(txs, trie.NewListHasher()), + ReceiptHash: payload.ReceiptsRoot, + Bloom: types.BytesToBloom(payload.LogsBloom), + Difficulty: big.NewInt(0), + Number: big.NewInt(int64(payload.Number)), + GasLimit: gasLimit, + GasUsed: payload.GasUsed, + Time: payload.Timestamp, + Extra: payload.ExtraData, + MixDigest: payload.Random, + Nonce: types.BlockNonce{}, + BaseFee: payload.BaseFeePerGas, + WithdrawalsHash: &types.EmptyWithdrawalsHash, + BlobGasUsed: payload.BlobGasUsed, + ExcessBlobGas: payload.ExcessBlobGas, + ParentBeaconRoot: &common.Hash{}, + RequestsHash: &types.EmptyRequestsHash, + } + + if len(payload.Withdrawals) > 0 { + wh := types.DeriveSha(types.Withdrawals(payload.Withdrawals), trie.NewListHasher()) + header.WithdrawalsHash = &wh + } + + // Create the block from the payload + block := types.NewBlock(header, &types.Body{ + Transactions: txs, + Uncles: nil, + Withdrawals: payload.Withdrawals, + }, nil, trie.NewListHasher()) + + // Verify the block hash matches the payload + if block.Hash() != payload.BlockHash { + g.logger.Warn(). + Str("expected", payload.BlockHash.Hex()). + Str("calculated", block.Hash().Hex()). + Msg("block hash mismatch") + parentHash := parent.Hash() + return &engine.PayloadStatusV1{ + Status: engine.INVALID, + LatestValidHash: &parentHash, + }, nil + } + + // Use InsertBlockWithoutSetHead which processes, validates, and commits the block + // This ensures proper state validation using go-ethereum's internal processor + _, err := g.backend.blockchain.InsertBlockWithoutSetHead(block, false) + if err != nil { + g.logger.Warn(). + Err(err). + Str("block_hash", block.Hash().Hex()). + Uint64("block_number", block.NumberU64()). + Msg("block validation/insertion failed") + parentHash := parent.Hash() + return &engine.PayloadStatusV1{ + Status: engine.INVALID, + LatestValidHash: &parentHash, + }, nil + } + + blockHash := block.Hash() + return &engine.PayloadStatusV1{ + Status: engine.VALID, + LatestValidHash: &blockHash, + }, nil +} + +// HeaderByNumber implements EthRPCClient. +func (g *gethEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + if number == nil { + // Return current head + header := g.backend.blockchain.CurrentBlock() + if header == nil { + return nil, errors.New("no current block") + } + return header, nil + } + block := g.backend.blockchain.GetBlockByNumber(number.Uint64()) + if block == nil { + return nil, fmt.Errorf("block not found at height %d", number.Uint64()) + } + return block.Header(), nil +} + +// GetTxs implements EthRPCClient. +func (g *gethEthClient) GetTxs(ctx context.Context) ([]string, error) { + pending := g.backend.txPool.Pending(txpool.PendingFilter{}) + var result []string + for _, txs := range pending { + for _, lazyTx := range txs { + // Resolve the lazy transaction to get the actual transaction + tx := lazyTx.Tx + if tx == nil { + continue + } + data, err := tx.MarshalBinary() + if err != nil { + continue + } + result = append(result, "0x"+common.Bytes2Hex(data)) + } + } + return result, nil +} + +// calcBaseFee calculates the base fee for the next block. +func calcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { + // If we're before London, return nil + if !config.IsLondon(new(big.Int).Add(parent.Number, big.NewInt(1))) { + return nil + } + + // Use genesis base fee if this is the first London block + if !config.IsLondon(parent.Number) { + return big.NewInt(params.InitialBaseFee) + } + + // Calculate next base fee based on EIP-1559 + var ( + parentGasTarget = parent.GasLimit / 2 + parentGasUsed = parent.GasUsed + baseFee = new(big.Int).Set(parent.BaseFee) + ) + + if parentGasUsed == parentGasTarget { + return baseFee + } + + if parentGasUsed > parentGasTarget { + // Block was more full than target, increase base fee + gasUsedDelta := new(big.Int).SetUint64(parentGasUsed - parentGasTarget) + x := new(big.Int).Mul(parent.BaseFee, gasUsedDelta) + y := new(big.Int).SetUint64(parentGasTarget) + z := new(big.Int).Div(x, y) + baseFeeChangeDenominator := new(big.Int).SetUint64(8) + delta := new(big.Int).Div(z, baseFeeChangeDenominator) + if delta.Sign() == 0 { + delta = big.NewInt(1) + } + return new(big.Int).Add(baseFee, delta) + } + + // Block was less full than target, decrease base fee + gasUsedDelta := new(big.Int).SetUint64(parentGasTarget - parentGasUsed) + x := new(big.Int).Mul(parent.BaseFee, gasUsedDelta) + y := new(big.Int).SetUint64(parentGasTarget) + z := new(big.Int).Div(x, y) + baseFeeChangeDenominator := new(big.Int).SetUint64(8) + delta := new(big.Int).Div(z, baseFeeChangeDenominator) + baseFee = new(big.Int).Sub(baseFee, delta) + if baseFee.Cmp(big.NewInt(0)) < 0 { + baseFee = big.NewInt(0) + } + return baseFee +} + +// applyTransaction executes a transaction and returns the receipt. +func applyTransaction( + config *params.ChainConfig, + blockContext vm.BlockContext, + gp *core.GasPool, + stateDB *state.StateDB, + header *types.Header, + tx *types.Transaction, + usedGas *uint64, +) (*types.Receipt, error) { + msg, err := core.TransactionToMessage(tx, types.LatestSigner(config), header.BaseFee) + if err != nil { + return nil, err + } + + // Create EVM instance + txContext := core.NewEVMTxContext(msg) + evmInstance := vm.NewEVM(blockContext, stateDB, config, vm.Config{}) + evmInstance.SetTxContext(txContext) + + // Apply the transaction + result, err := core.ApplyMessage(evmInstance, msg, gp) + if err != nil { + return nil, err + } + + *usedGas += result.UsedGas + + // Create the receipt + receipt := &types.Receipt{ + Type: tx.Type(), + PostState: nil, + CumulativeGasUsed: *usedGas, + TxHash: tx.Hash(), + GasUsed: result.UsedGas, + Logs: stateDB.GetLogs(tx.Hash(), header.Number.Uint64(), common.Hash{}, header.Number.Uint64()), + BlockNumber: header.Number, + } + + if result.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + + // Set the receipt logs bloom + receipt.Bloom = types.CreateBloom(receipt) + + // Set contract address if this was a contract creation + if msg.To == nil { + receipt.ContractAddress = evmInstance.Origin + } + + return receipt, nil +} + +// createBloomFromReceipts creates a bloom filter from multiple receipts. +func createBloomFromReceipts(receipts []*types.Receipt) types.Bloom { + var bin types.Bloom + for _, receipt := range receipts { + bloom := types.CreateBloom(receipt) + for i := range bin { + bin[i] |= bloom[i] + } + } + return bin +} diff --git a/execution/evm/engine_geth_test.go b/execution/evm/engine_geth_test.go new file mode 100644 index 0000000000..0a39c0446e --- /dev/null +++ b/execution/evm/engine_geth_test.go @@ -0,0 +1,487 @@ +package evm + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" + ds "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/sync" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testGenesis creates a genesis configuration for testing. +func testGenesis() *core.Genesis { + // Generate a test account with some balance + testKey, _ := crypto.GenerateKey() + testAddr := crypto.PubkeyToAddress(testKey.PublicKey) + + return &core.Genesis{ + Config: params.AllDevChainProtocolChanges, + Difficulty: big.NewInt(0), + GasLimit: 30_000_000, + Alloc: types.GenesisAlloc{ + testAddr: {Balance: new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18))}, + }, + Timestamp: uint64(time.Now().Unix()), + } +} + +// testDatastore creates an in-memory datastore for testing. +func testDatastore() ds.Batching { + return sync.MutexWrap(ds.NewMapDatastore()) +} + +func TestNewEngineExecutionClientWithGeth(t *testing.T) { + genesis := testGenesis() + db := testDatastore() + logger := zerolog.Nop() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + require.NoError(t, err) + require.NotNil(t, client) + + // Verify genesis hash is set + assert.NotEqual(t, common.Hash{}, client.genesisHash) + + // Verify fee recipient is set + assert.Equal(t, feeRecipient, client.feeRecipient) +} + +func TestNewEngineExecutionClientWithGeth_NilDB(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + _, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, nil, logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "db is required") +} + +func TestNewEngineExecutionClientWithGeth_NilGenesis(t *testing.T) { + db := testDatastore() + logger := zerolog.Nop() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + _, err := NewEngineExecutionClientWithGeth(nil, feeRecipient, db, logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "genesis configuration is required") +} + +func TestGethEngineClient_InitChain(t *testing.T) { + genesis := testGenesis() + db := testDatastore() + logger := zerolog.Nop() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + require.NoError(t, err) + + ctx := context.Background() + genesisTime := time.Now() + + stateRoot, maxBytes, err := client.InitChain(ctx, genesisTime, 1, "1337") + require.NoError(t, err) + assert.NotEmpty(t, stateRoot) + // maxBytes is the gas limit from the genesis block + assert.Greater(t, maxBytes, uint64(0)) +} + +func TestGethEngineClient_GetLatestHeight(t *testing.T) { + genesis := testGenesis() + db := testDatastore() + logger := zerolog.Nop() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + require.NoError(t, err) + + ctx := context.Background() + height, err := client.GetLatestHeight(ctx) + require.NoError(t, err) + // At genesis, height should be 0 + assert.Equal(t, uint64(0), height) +} + +func TestGethEthClient_HeaderByNumber(t *testing.T) { + genesis := testGenesis() + db := testDatastore() + logger := zerolog.Nop() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + require.NoError(t, err) + + ctx := context.Background() + + // Get genesis block header (block 0) + header, err := client.ethClient.HeaderByNumber(ctx, big.NewInt(0)) + require.NoError(t, err) + assert.NotNil(t, header) + assert.Equal(t, uint64(0), header.Number.Uint64()) + + // Get latest header + latestHeader, err := client.ethClient.HeaderByNumber(ctx, nil) + require.NoError(t, err) + assert.NotNil(t, latestHeader) +} + +func TestGethEthClient_GetTxs_EmptyPool(t *testing.T) { + genesis := testGenesis() + db := testDatastore() + logger := zerolog.Nop() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + require.NoError(t, err) + + ctx := context.Background() + + // Empty mempool should return empty list + txs, err := client.GetTxs(ctx) + require.NoError(t, err) + assert.Empty(t, txs) +} + +func TestGethEngineClient_ExecuteTxs_EmptyBlock(t *testing.T) { + genesis := testGenesis() + db := testDatastore() + logger := zerolog.Nop() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + require.NoError(t, err) + + ctx := context.Background() + genesisTime := time.Now() + + // Initialize chain first + stateRoot, _, err := client.InitChain(ctx, genesisTime, 1, "1337") + require.NoError(t, err) + + // Execute empty block + newStateRoot, gasUsed, err := client.ExecuteTxs( + ctx, + [][]byte{}, // empty transactions + 1, + genesisTime.Add(time.Second*12), + stateRoot, + ) + require.NoError(t, err) + assert.NotEmpty(t, newStateRoot) + assert.Equal(t, uint64(0), gasUsed) // No transactions, no gas used +} + +func TestGethEngineClient_ForkchoiceUpdated(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + genesisBlock := backend.blockchain.Genesis() + + // Test forkchoice update without payload attributes + resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, nil) + require.NoError(t, err) + assert.Equal(t, engine.VALID, resp.PayloadStatus.Status) + assert.Nil(t, resp.PayloadID) +} + +func TestGethEngineClient_ForkchoiceUpdated_WithPayloadAttributes(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // Test forkchoice update with payload attributes + attrs := map[string]any{ + "timestamp": time.Now().Unix() + 12, + "prevRandao": common.Hash{1, 2, 3}, + "suggestedFeeRecipient": feeRecipient, + "transactions": []string{}, + "gasLimit": uint64(30_000_000), + "withdrawals": []*types.Withdrawal{}, + } + + resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, attrs) + require.NoError(t, err) + assert.Equal(t, engine.VALID, resp.PayloadStatus.Status) + assert.NotNil(t, resp.PayloadID) +} + +func TestGethEngineClient_GetPayload(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // First, create a payload + attrs := map[string]any{ + "timestamp": time.Now().Unix() + 12, + "prevRandao": common.Hash{1, 2, 3}, + "suggestedFeeRecipient": feeRecipient, + "transactions": []string{}, + "gasLimit": uint64(30_000_000), + "withdrawals": []*types.Withdrawal{}, + } + + resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, attrs) + require.NoError(t, err) + require.NotNil(t, resp.PayloadID) + + // Get the payload + envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) + require.NoError(t, err) + assert.NotNil(t, envelope) + assert.NotNil(t, envelope.ExecutionPayload) + assert.Equal(t, uint64(1), envelope.ExecutionPayload.Number) + assert.Equal(t, feeRecipient, envelope.ExecutionPayload.FeeRecipient) +} + +func TestGethEngineClient_NewPayload(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // Create and get a payload + attrs := map[string]any{ + "timestamp": time.Now().Unix() + 12, + "prevRandao": common.Hash{1, 2, 3}, + "suggestedFeeRecipient": feeRecipient, + "transactions": []string{}, + "gasLimit": uint64(30_000_000), + "withdrawals": []*types.Withdrawal{}, + } + + resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, attrs) + require.NoError(t, err) + require.NotNil(t, resp.PayloadID) + + envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) + require.NoError(t, err) + + // Submit the payload + status, err := engineClient.NewPayload(ctx, envelope.ExecutionPayload, nil, "", nil) + require.NoError(t, err) + assert.Equal(t, engine.VALID, status.Status) +} + +func TestGethEngineClient_ForkchoiceUpdated_UnknownHead(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + + // Try to set head to unknown block + unknownHash := common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234") + resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: unknownHash, + SafeBlockHash: unknownHash, + FinalizedBlockHash: unknownHash, + }, nil) + require.NoError(t, err) + assert.Equal(t, engine.SYNCING, resp.PayloadStatus.Status) +} + +func TestGethBackend_Close(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, logger) + require.NoError(t, err) + + // Close should not error + err = backend.Close() + require.NoError(t, err) +} + +func TestCalcBaseFee(t *testing.T) { + config := params.AllDevChainProtocolChanges + + // Create a mock parent header + parent := &types.Header{ + Number: big.NewInt(100), + GasLimit: 30_000_000, + GasUsed: 15_000_000, // 50% full + BaseFee: big.NewInt(1_000_000_000), + } + + baseFee := calcBaseFee(config, parent) + require.NotNil(t, baseFee) + + // When block is exactly 50% full, base fee should remain the same + assert.Equal(t, parent.BaseFee, baseFee) +} + +func TestCalcBaseFee_OverTarget(t *testing.T) { + config := params.AllDevChainProtocolChanges + + // Create a mock parent header that was more than 50% full + parent := &types.Header{ + Number: big.NewInt(100), + GasLimit: 30_000_000, + GasUsed: 20_000_000, // ~67% full + BaseFee: big.NewInt(1_000_000_000), + } + + baseFee := calcBaseFee(config, parent) + require.NotNil(t, baseFee) + + // Base fee should increase when block is more than 50% full + assert.Greater(t, baseFee.Int64(), parent.BaseFee.Int64()) +} + +func TestCalcBaseFee_UnderTarget(t *testing.T) { + config := params.AllDevChainProtocolChanges + + // Create a mock parent header that was less than 50% full + parent := &types.Header{ + Number: big.NewInt(100), + GasLimit: 30_000_000, + GasUsed: 5_000_000, // ~17% full + BaseFee: big.NewInt(1_000_000_000), + } + + baseFee := calcBaseFee(config, parent) + require.NotNil(t, baseFee) + + // Base fee should decrease when block is less than 50% full + assert.Less(t, baseFee.Int64(), parent.BaseFee.Int64()) +} + +func TestParsePayloadAttributes(t *testing.T) { + logger := zerolog.Nop() + engineClient := &gethEngineClient{logger: logger} + + parentHash := common.HexToHash("0xabcd") + feeRecipient := common.HexToAddress("0x1234") + timestamp := int64(1234567890) + + attrs := map[string]any{ + "timestamp": timestamp, + "prevRandao": common.Hash{1, 2, 3}, + "suggestedFeeRecipient": feeRecipient, + "transactions": []string{"0xaabbcc", "0xddeeff"}, + "gasLimit": uint64(30_000_000), + } + + state, err := engineClient.parsePayloadAttributes(parentHash, attrs) + require.NoError(t, err) + assert.Equal(t, parentHash, state.parentHash) + assert.Equal(t, uint64(timestamp), state.timestamp) + assert.Equal(t, feeRecipient, state.feeRecipient) + assert.Equal(t, uint64(30_000_000), state.gasLimit) + assert.Len(t, state.transactions, 2) +} + +func TestListHasher(t *testing.T) { + hasher := trie.NewListHasher() + + // Test Update and Hash + err := hasher.Update([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + hash1 := hasher.Hash() + assert.NotEqual(t, common.Hash{}, hash1) + + // Test Reset + hasher.Reset() + err = hasher.Update([]byte("key2"), []byte("value2")) + require.NoError(t, err) + + hash2 := hasher.Hash() + assert.NotEqual(t, common.Hash{}, hash2) + assert.NotEqual(t, hash1, hash2) +} + +func TestCreateBloomFromReceipts(t *testing.T) { + // Empty receipts + bloom := createBloomFromReceipts([]*types.Receipt{}) + assert.Equal(t, types.Bloom{}, bloom) + + // Single receipt with no logs + receipt := &types.Receipt{ + Status: types.ReceiptStatusSuccessful, + Logs: []*types.Log{}, + } + bloom = createBloomFromReceipts([]*types.Receipt{receipt}) + assert.NotNil(t, bloom) +} diff --git a/execution/evm/execution.go b/execution/evm/execution.go index c310af06d7..1fe19f3f47 100644 --- a/execution/evm/execution.go +++ b/execution/evm/execution.go @@ -189,6 +189,7 @@ func NewEngineExecutionClient( feeRecipient common.Address, db ds.Batching, tracingEnabled bool, + logger zerolog.Logger, ) (*EngineClient, error) { if db == nil { return nil, errors.New("db is required for EVM execution client") @@ -252,15 +253,10 @@ func NewEngineExecutionClient( currentSafeBlockHash: genesisHash, currentFinalizedBlockHash: genesisHash, blockHashCache: make(map[uint64]common.Hash), - logger: zerolog.Nop(), + logger: logger, }, nil } -// SetLogger allows callers to attach a structured logger. -func (c *EngineClient) SetLogger(l zerolog.Logger) { - c.logger = l -} - // InitChain initializes the blockchain with the given genesis parameters func (c *EngineClient) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) { if initialHeight != 1 { diff --git a/execution/evm/flags.go b/execution/evm/flags.go index 3c1bb71259..33c90aa296 100644 --- a/execution/evm/flags.go +++ b/execution/evm/flags.go @@ -6,4 +6,7 @@ const ( FlagEvmJWTSecretFile = "evm.jwt-secret-file" FlagEvmGenesisHash = "evm.genesis-hash" FlagEvmFeeRecipient = "evm.fee-recipient" + + FlagEVMGenesisPath = "evm.geth.genesis-path" + FlagEVMInProcessGeth = "evm.geth" ) diff --git a/execution/evm/go.mod b/execution/evm/go.mod index 8521f9aa3b..96cb74532a 100644 --- a/execution/evm/go.mod +++ b/execution/evm/go.mod @@ -28,6 +28,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/StackExchange/wmi v1.2.1 // indirect + github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/celestiaorg/go-square/v3 v3.0.2 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -40,6 +41,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -53,6 +55,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/go-cid v0.6.0 // indirect diff --git a/execution/evm/go.sum b/execution/evm/go.sum index a43585fa5a..3f1aaba303 100644 --- a/execution/evm/go.sum +++ b/execution/evm/go.sum @@ -8,6 +8,8 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= From 19102bf212e608e1993e967f9b8b102123b7c469 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 16:15:43 +0100 Subject: [PATCH 02/18] hack --- execution/evm/engine_geth.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index 4e5123b378..cbe4ced3e1 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -137,6 +137,18 @@ func newGethBackend(genesis *core.Genesis, logger zerolog.Logger) (*GethBackend, // Create trie database trieDB := triedb.NewDatabase(memdb, nil) + // Ensure blobSchedule is set if Cancun/Prague are enabled + // This is required by go-ethereum v1.16+ + if genesis.Config != nil && genesis.Config.BlobScheduleConfig == nil { + // Check if Cancun or Prague are enabled (time-based forks) + if genesis.Config.CancunTime != nil || genesis.Config.PragueTime != nil { + genesis.Config.BlobScheduleConfig = ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + } + } + } + // Initialize the genesis block chainConfig, genesisHash, _, genesisErr := core.SetupGenesisBlockWithOverride(memdb, trieDB, genesis, nil) if genesisErr != nil { From 045ed658a11db099e6d6515beab0ffee7bbb2b46 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 16:32:59 +0100 Subject: [PATCH 03/18] persistent db wip --- apps/evm/cmd/rollback.go | 3 ++- apps/evm/cmd/run.go | 3 ++- execution/evm/engine_geth.go | 17 ++++++++--------- execution/evm/engine_geth_test.go | 12 ++++++------ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/evm/cmd/rollback.go b/apps/evm/cmd/rollback.go index 8fefb4f0ec..13009b1bee 100644 --- a/apps/evm/cmd/rollback.go +++ b/apps/evm/cmd/rollback.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" ds "github.com/ipfs/go-datastore" + "github.com/rs/zerolog" "github.com/spf13/cobra" goheaderstore "github.com/celestiaorg/go-header/store" @@ -166,5 +167,5 @@ func createRollbackEngineClient(cmd *cobra.Command, db ds.Batching) (*evm.Engine return nil, fmt.Errorf("JWT secret file '%s' is empty", jwtSecretFile) } - return evm.NewEngineExecutionClient(ethURL, engineURL, jwtSecret, common.Hash{}, common.Address{}, db, false) + return evm.NewEngineExecutionClient(ethURL, engineURL, jwtSecret, common.Hash{}, common.Address{}, db, false, zerolog.Nop()) } diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index 00f9921c21..d7e7ef2f44 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -63,6 +63,7 @@ var RunCmd = &cobra.Command{ cmd, datastore, tracingEnabled, + nodeConfig.RootDir, logger.With().Str("module", "engine_client").Logger(), ) if err != nil { @@ -202,7 +203,7 @@ func createSequencer( return sequencer, nil } -func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, logger zerolog.Logger) (execution.Executor, error) { +func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, rootDir string, logger zerolog.Logger) (execution.Executor, error) { feeRecipientStr, err := cmd.Flags().GetString(evm.FlagEvmFeeRecipient) if err != nil { return nil, fmt.Errorf("failed to get '%s' flag: %w", evm.FlagEvmFeeRecipient, err) diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index cbe4ced3e1..ee72da4779 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -97,7 +97,7 @@ func NewEngineExecutionClientWithGeth( return nil, errors.New("genesis configuration is required") } - backend, err := newGethBackend(genesis, logger) + backend, err := newGethBackend(genesis, db, logger) if err != nil { return nil, fmt.Errorf("failed to create geth backend: %w", err) } @@ -130,12 +130,11 @@ func NewEngineExecutionClientWithGeth( } // newGethBackend creates a new in-process geth backend. -func newGethBackend(genesis *core.Genesis, logger zerolog.Logger) (*GethBackend, error) { - // Create in-memory database - memdb := rawdb.NewMemoryDatabase() +func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger) (*GethBackend, error) { + ethdb := rawdb.NewDatabase(db) // Create trie database - trieDB := triedb.NewDatabase(memdb, nil) + trieDB := triedb.NewDatabase(ethdb, nil) // Ensure blobSchedule is set if Cancun/Prague are enabled // This is required by go-ethereum v1.16+ @@ -150,7 +149,7 @@ func newGethBackend(genesis *core.Genesis, logger zerolog.Logger) (*GethBackend, } // Initialize the genesis block - chainConfig, genesisHash, _, genesisErr := core.SetupGenesisBlockWithOverride(memdb, trieDB, genesis, nil) + chainConfig, genesisHash, _, genesisErr := core.SetupGenesisBlockWithOverride(ethdb, trieDB, genesis, nil) if genesisErr != nil { return nil, fmt.Errorf("failed to setup genesis: %w", genesisErr) } @@ -167,13 +166,13 @@ func newGethBackend(genesis *core.Genesis, logger zerolog.Logger) (*GethBackend, bcConfig := core.DefaultConfig().WithStateScheme(rawdb.HashScheme) // Create the blockchain - blockchain, err := core.NewBlockChain(memdb, genesis, consensusEngine, bcConfig) + blockchain, err := core.NewBlockChain(ethdb, genesis, consensusEngine, bcConfig) if err != nil { return nil, fmt.Errorf("failed to create blockchain: %w", err) } backend := &GethBackend{ - db: memdb, + db: ethdb, chainConfig: chainConfig, blockchain: blockchain, payloads: make(map[engine.PayloadID]*payloadBuildState), @@ -256,7 +255,7 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine g.backend.payloads[payloadID] = payloadState response.PayloadID = &payloadID - g.logger.Debug(). + g.logger.Info(). Str("payload_id", payloadID.String()). Uint64("timestamp", payloadState.timestamp). Int("tx_count", len(payloadState.transactions)). diff --git a/execution/evm/engine_geth_test.go b/execution/evm/engine_geth_test.go index 0a39c0446e..0638569d40 100644 --- a/execution/evm/engine_geth_test.go +++ b/execution/evm/engine_geth_test.go @@ -187,7 +187,7 @@ func TestGethEngineClient_ForkchoiceUpdated(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() - backend, err := newGethBackend(genesis, logger) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) defer backend.Close() @@ -214,7 +214,7 @@ func TestGethEngineClient_ForkchoiceUpdated_WithPayloadAttributes(t *testing.T) genesis := testGenesis() logger := zerolog.Nop() - backend, err := newGethBackend(genesis, logger) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) defer backend.Close() @@ -251,7 +251,7 @@ func TestGethEngineClient_GetPayload(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() - backend, err := newGethBackend(genesis, logger) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) defer backend.Close() @@ -295,7 +295,7 @@ func TestGethEngineClient_NewPayload(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() - backend, err := newGethBackend(genesis, logger) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) defer backend.Close() @@ -339,7 +339,7 @@ func TestGethEngineClient_ForkchoiceUpdated_UnknownHead(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() - backend, err := newGethBackend(genesis, logger) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) defer backend.Close() @@ -365,7 +365,7 @@ func TestGethBackend_Close(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() - backend, err := newGethBackend(genesis, logger) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) // Close should not error From 489741133c724326809287be35320778af52600b Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 16:53:36 +0100 Subject: [PATCH 04/18] updates --- execution/evm/engine_geth.go | 2 +- execution/evm/eth_db.go | 375 +++++++++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 execution/evm/eth_db.go diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index ee72da4779..48dee1b64e 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -131,7 +131,7 @@ func NewEngineExecutionClientWithGeth( // newGethBackend creates a new in-process geth backend. func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger) (*GethBackend, error) { - ethdb := rawdb.NewDatabase(db) + ethdb := rawdb.NewDatabase(&wrapper{db}) // Create trie database trieDB := triedb.NewDatabase(ethdb, nil) diff --git a/execution/evm/eth_db.go b/execution/evm/eth_db.go new file mode 100644 index 0000000000..b6a055e468 --- /dev/null +++ b/execution/evm/eth_db.go @@ -0,0 +1,375 @@ +package evm + +import ( + "bytes" + "context" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/query" +) + +var _ ethdb.KeyValueStore = &wrapper{} + +type wrapper struct { + ds datastore.Batching +} + +func keyToDatastoreKey(key []byte) datastore.Key { + return datastore.NewKey(string(key)) +} + +func datastoreKeyToBytes(key string) []byte { + // datastore keys have a leading slash, remove it + if strings.HasPrefix(key, "/") { + return []byte(key[1:]) + } + return []byte(key) +} + +// Close implements ethdb.KeyValueStore. +func (w *wrapper) Close() error { + return w.ds.Close() +} + +// Compact implements ethdb.KeyValueStore. +func (w *wrapper) Compact(start []byte, limit []byte) error { + // Compaction is not supported by go-datastore, this is a no-op + return nil +} + +// Delete implements ethdb.KeyValueStore. +func (w *wrapper) Delete(key []byte) error { + return w.ds.Delete(context.Background(), keyToDatastoreKey(key)) +} + +// DeleteRange implements ethdb.KeyValueStore. +func (w *wrapper) DeleteRange(start []byte, end []byte) error { + // Query all keys and delete those in range + q := query.Query{KeysOnly: true} + results, err := w.ds.Query(context.Background(), q) + if err != nil { + return err + } + defer results.Close() + + for result := range results.Next() { + if result.Error != nil { + return result.Error + } + keyBytes := datastoreKeyToBytes(result.Entry.Key) + if bytes.Compare(keyBytes, start) >= 0 && bytes.Compare(keyBytes, end) < 0 { + if err := w.ds.Delete(context.Background(), datastore.NewKey(result.Entry.Key)); err != nil { + return err + } + } + } + return nil +} + +// Get implements ethdb.KeyValueStore. +func (w *wrapper) Get(key []byte) ([]byte, error) { + val, err := w.ds.Get(context.Background(), keyToDatastoreKey(key)) + if err == datastore.ErrNotFound { + return nil, nil + } + return val, err +} + +// Has implements ethdb.KeyValueStore. +func (w *wrapper) Has(key []byte) (bool, error) { + return w.ds.Has(context.Background(), keyToDatastoreKey(key)) +} + +// NewBatch implements ethdb.KeyValueStore. +func (w *wrapper) NewBatch() ethdb.Batch { + return &batchWrapper{ + ds: w.ds, + ops: nil, + size: 0, + } +} + +// NewBatchWithSize implements ethdb.KeyValueStore. +func (w *wrapper) NewBatchWithSize(size int) ethdb.Batch { + return &batchWrapper{ + ds: w.ds, + ops: make([]batchOp, 0, size), + size: 0, + } +} + +// NewIterator implements ethdb.KeyValueStore. +func (w *wrapper) NewIterator(prefix []byte, start []byte) ethdb.Iterator { + return newIterator(w.ds, prefix, start) +} + +// Put implements ethdb.KeyValueStore. +func (w *wrapper) Put(key []byte, value []byte) error { + return w.ds.Put(context.Background(), keyToDatastoreKey(key), value) +} + +// Stat implements ethdb.KeyValueStore. +func (w *wrapper) Stat() (string, error) { + return "go-datastore wrapper", nil +} + +// SyncKeyValue implements ethdb.KeyValueStore. +func (w *wrapper) SyncKeyValue() error { + return w.ds.Sync(context.Background(), datastore.NewKey("/")) +} + +func NewEVMDB(ds datastore.Batching) ethdb.KeyValueStore { + return &wrapper{ds} +} + +// batchOp represents a single batch operation +type batchOp struct { + key []byte + value []byte // nil means delete + delete bool +} + +// batchWrapper implements ethdb.Batch +type batchWrapper struct { + ds datastore.Batching + ops []batchOp + size int + mu sync.Mutex +} + +var _ ethdb.Batch = &batchWrapper{} + +// Put implements ethdb.Batch. +func (b *batchWrapper) Put(key []byte, value []byte) error { + b.mu.Lock() + defer b.mu.Unlock() + + b.ops = append(b.ops, batchOp{ + key: append([]byte{}, key...), + value: append([]byte{}, value...), + delete: false, + }) + b.size += len(key) + len(value) + return nil +} + +// Delete implements ethdb.Batch. +func (b *batchWrapper) Delete(key []byte) error { + b.mu.Lock() + defer b.mu.Unlock() + + b.ops = append(b.ops, batchOp{ + key: append([]byte{}, key...), + value: nil, + delete: true, + }) + b.size += len(key) + return nil +} + +// DeleteRange implements ethdb.Batch. +func (b *batchWrapper) DeleteRange(start []byte, end []byte) error { + // Query all keys and mark those in range for deletion + q := query.Query{KeysOnly: true} + results, err := b.ds.Query(context.Background(), q) + if err != nil { + return err + } + defer results.Close() + + b.mu.Lock() + defer b.mu.Unlock() + + for result := range results.Next() { + if result.Error != nil { + return result.Error + } + keyBytes := datastoreKeyToBytes(result.Entry.Key) + if bytes.Compare(keyBytes, start) >= 0 && bytes.Compare(keyBytes, end) < 0 { + b.ops = append(b.ops, batchOp{ + key: append([]byte{}, keyBytes...), + value: nil, + delete: true, + }) + b.size += len(keyBytes) + } + } + return nil +} + +// ValueSize implements ethdb.Batch. +func (b *batchWrapper) ValueSize() int { + b.mu.Lock() + defer b.mu.Unlock() + return b.size +} + +// Write implements ethdb.Batch. +func (b *batchWrapper) Write() error { + b.mu.Lock() + defer b.mu.Unlock() + + batch, err := b.ds.Batch(context.Background()) + if err != nil { + return err + } + + for _, op := range b.ops { + if op.delete { + if err := batch.Delete(context.Background(), keyToDatastoreKey(op.key)); err != nil { + return err + } + } else { + if err := batch.Put(context.Background(), keyToDatastoreKey(op.key), op.value); err != nil { + return err + } + } + } + + return batch.Commit(context.Background()) +} + +// Reset implements ethdb.Batch. +func (b *batchWrapper) Reset() { + b.mu.Lock() + defer b.mu.Unlock() + b.ops = b.ops[:0] + b.size = 0 +} + +// Replay implements ethdb.Batch. +func (b *batchWrapper) Replay(w ethdb.KeyValueWriter) error { + b.mu.Lock() + defer b.mu.Unlock() + + for _, op := range b.ops { + if op.delete { + if err := w.Delete(op.key); err != nil { + return err + } + } else { + if err := w.Put(op.key, op.value); err != nil { + return err + } + } + } + return nil +} + +// iteratorWrapper implements ethdb.Iterator +type iteratorWrapper struct { + results query.Results + current query.Entry + prefix []byte + start []byte + err error + started bool + closed bool + mu sync.Mutex +} + +var _ ethdb.Iterator = &iteratorWrapper{} + +func newIterator(ds datastore.Batching, prefix []byte, start []byte) *iteratorWrapper { + q := query.Query{ + KeysOnly: false, + } + + if len(prefix) > 0 { + q.Prefix = "/" + string(prefix) + } + + results, err := ds.Query(context.Background(), q) + + return &iteratorWrapper{ + results: results, + prefix: prefix, + start: start, + err: err, + started: false, + closed: false, + } +} + +// Next implements ethdb.Iterator. +func (it *iteratorWrapper) Next() bool { + it.mu.Lock() + defer it.mu.Unlock() + + if it.closed || it.err != nil { + return false + } + + for { + result, ok := it.results.NextSync() + if !ok { + return false + } + if result.Error != nil { + it.err = result.Error + return false + } + + keyBytes := datastoreKeyToBytes(result.Entry.Key) + + // Check if key matches prefix (if prefix is set) + if len(it.prefix) > 0 && !bytes.HasPrefix(keyBytes, it.prefix) { + continue + } + + // Check if key is >= start (if start is set) + if len(it.start) > 0 && bytes.Compare(keyBytes, it.start) < 0 { + continue + } + + it.current = result.Entry + it.started = true + return true + } +} + +// Error implements ethdb.Iterator. +func (it *iteratorWrapper) Error() error { + it.mu.Lock() + defer it.mu.Unlock() + return it.err +} + +// Key implements ethdb.Iterator. +func (it *iteratorWrapper) Key() []byte { + it.mu.Lock() + defer it.mu.Unlock() + + if !it.started || it.closed { + return nil + } + return datastoreKeyToBytes(it.current.Key) +} + +// Value implements ethdb.Iterator. +func (it *iteratorWrapper) Value() []byte { + it.mu.Lock() + defer it.mu.Unlock() + + if !it.started || it.closed { + return nil + } + return it.current.Value +} + +// Release implements ethdb.Iterator. +func (it *iteratorWrapper) Release() { + it.mu.Lock() + defer it.mu.Unlock() + + if it.closed { + return + } + it.closed = true + if it.results != nil { + it.results.Close() + } +} From 32bb3b0bf5a39cf872224ae93f99156075d0be10 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 16:57:17 +0100 Subject: [PATCH 05/18] cleanup --- execution/evm/engine_geth.go | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index 48dee1b64e..3ef308b54b 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -68,22 +68,7 @@ type gethEthClient struct { logger zerolog.Logger } -// NewEngineExecutionClientWithGeth creates an EngineClient that uses an in-process -// go-ethereum instance instead of connecting to an external execution engine via RPC. -// -// This is useful for: -// - Testing without needing to run a separate geth/reth process -// - Embedded rollup nodes that want a single binary -// - Development and debugging with full control over the EVM -// -// Parameters: -// - genesis: The genesis configuration for the chain -// - feeRecipient: Address to receive transaction fees -// - db: Datastore for execution metadata (crash recovery) -// - logger: Logger for the client -// -// Returns an EngineClient that behaves identically to the RPC-based client -// but executes everything in-process. +// NewEngineExecutionClientWithGeth creates an EngineClient that uses an in-process Geth. func NewEngineExecutionClientWithGeth( genesis *core.Genesis, feeRecipient common.Address, From 16fc2a01311d7eebe20d1e370a6e92fc6e4b6aa3 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 17:13:30 +0100 Subject: [PATCH 06/18] updates --- apps/evm/cmd/run.go | 3 +- apps/evm/go.mod | 1 + apps/evm/go.sum | 35 ++ execution/evm/engine_geth.go | 306 +++++++++++++++--- .../evm/{eth_db.go => engine_geth_db.go} | 3 +- execution/evm/go.mod | 2 +- execution/evm/go.sum | 4 + execution/evm/test/go.mod | 3 + execution/evm/test/go.sum | 2 + test/e2e/go.mod | 3 + test/e2e/go.sum | 2 + 11 files changed, 307 insertions(+), 57 deletions(-) rename execution/evm/{eth_db.go => engine_geth_db.go} (99%) diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index d7e7ef2f44..00f9921c21 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -63,7 +63,6 @@ var RunCmd = &cobra.Command{ cmd, datastore, tracingEnabled, - nodeConfig.RootDir, logger.With().Str("module", "engine_client").Logger(), ) if err != nil { @@ -203,7 +202,7 @@ func createSequencer( return sequencer, nil } -func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, rootDir string, logger zerolog.Logger) (execution.Executor, error) { +func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, logger zerolog.Logger) (execution.Executor, error) { feeRecipientStr, err := cmd.Flags().GetString(evm.FlagEvmFeeRecipient) if err != nil { return nil, fmt.Errorf("failed to get '%s' flag: %w", evm.FlagEvmFeeRecipient, err) diff --git a/apps/evm/go.mod b/apps/evm/go.mod index 66d55b7e76..989bc54e1c 100644 --- a/apps/evm/go.mod +++ b/apps/evm/go.mod @@ -170,6 +170,7 @@ require ( github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect diff --git a/apps/evm/go.sum b/apps/evm/go.sum index 20b5416d0c..341113a810 100644 --- a/apps/evm/go.sum +++ b/apps/evm/go.sum @@ -407,6 +407,9 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= @@ -441,6 +444,7 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= @@ -548,6 +552,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -613,6 +618,7 @@ github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZ github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= @@ -781,8 +787,22 @@ github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOo github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -926,6 +946,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -1129,6 +1150,7 @@ golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1148,6 +1170,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -1161,6 +1184,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1256,6 +1280,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1264,8 +1289,11 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1288,6 +1316,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1446,6 +1475,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1648,10 +1678,15 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index 3ef308b54b..e941497f76 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -2,10 +2,12 @@ package evm import ( "context" + "encoding/binary" "errors" "fmt" "math/big" "sync" + "time" "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" @@ -30,6 +32,11 @@ var ( _ EthRPCClient = (*gethEthClient)(nil) ) +const ( + // baseFeeChangeDenominator is the EIP-1559 base fee change denominator. + baseFeeChangeDenominator = 8 +) + // GethBackend holds the in-process geth components. type GethBackend struct { db ethdb.Database @@ -38,9 +45,11 @@ type GethBackend struct { txPool *txpool.TxPool mu sync.Mutex - // payloadBuilding tracks in-flight payload builds + // payloads tracks in-flight payload builds payloads map[engine.PayloadID]*payloadBuildState nextPayloadID uint64 + + logger zerolog.Logger } // payloadBuildState tracks the state of a payload being built. @@ -52,6 +61,7 @@ type payloadBuildState struct { withdrawals []*types.Withdrawal transactions [][]byte gasLimit uint64 + // built payload (populated after getPayload) payload *engine.ExecutableData } @@ -68,7 +78,8 @@ type gethEthClient struct { logger zerolog.Logger } -// NewEngineExecutionClientWithGeth creates an EngineClient that uses an in-process Geth. +// NewEngineExecutionClientWithGeth creates an EngineClient that uses an in-process +// go-ethereum instance instead of connecting to an external execution engine via RPC. func NewEngineExecutionClientWithGeth( genesis *core.Genesis, feeRecipient common.Address, @@ -78,7 +89,7 @@ func NewEngineExecutionClientWithGeth( if db == nil { return nil, errors.New("db is required for EVM execution client") } - if genesis == nil { + if genesis == nil || genesis.Config == nil { return nil, errors.New("genesis configuration is required") } @@ -100,6 +111,12 @@ func NewEngineExecutionClientWithGeth( genesisBlock := backend.blockchain.Genesis() genesisHash := genesisBlock.Hash() + logger.Info(). + Str("genesis_hash", genesisHash.Hex()). + Str("chain_id", genesis.Config.ChainID.String()). + Uint64("genesis_gas_limit", genesis.GasLimit). + Msg("created in-process geth execution client") + return &EngineClient{ engineClient: engineClient, ethClient: ethClient, @@ -114,22 +131,23 @@ func NewEngineExecutionClientWithGeth( }, nil } -// newGethBackend creates a new in-process geth backend. +// newGethBackend creates a new in-process geth backend with persistent storage. func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger) (*GethBackend, error) { + // Wrap the datastore as an ethdb.Database ethdb := rawdb.NewDatabase(&wrapper{db}) // Create trie database trieDB := triedb.NewDatabase(ethdb, nil) // Ensure blobSchedule is set if Cancun/Prague are enabled - // This is required by go-ethereum v1.16+ + // TODO: remove and fix genesis. if genesis.Config != nil && genesis.Config.BlobScheduleConfig == nil { - // Check if Cancun or Prague are enabled (time-based forks) if genesis.Config.CancunTime != nil || genesis.Config.PragueTime != nil { genesis.Config.BlobScheduleConfig = ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, } + logger.Debug().Msg("auto-populated blobSchedule config for Cancun/Prague forks") } } @@ -144,11 +162,9 @@ func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger Str("chain_id", chainConfig.ChainID.String()). Msg("initialized in-process geth with genesis") - // Create the consensus engine (beacon/PoS) - consensusEngine := beacon.New(nil) - // Create blockchain config bcConfig := core.DefaultConfig().WithStateScheme(rawdb.HashScheme) + consensusEngine := beacon.New(nil) // Create the blockchain blockchain, err := core.NewBlockChain(ethdb, genesis, consensusEngine, bcConfig) @@ -156,11 +172,21 @@ func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger return nil, fmt.Errorf("failed to create blockchain: %w", err) } + // Log current chain head + currentHead := blockchain.CurrentBlock() + if currentHead != nil { + logger.Info(). + Uint64("height", currentHead.Number.Uint64()). + Str("hash", currentHead.Hash().Hex()). + Msg("resuming from existing chain state") + } + backend := &GethBackend{ db: ethdb, chainConfig: chainConfig, blockchain: blockchain, payloads: make(map[engine.PayloadID]*payloadBuildState), + logger: logger, } // Create transaction pool @@ -177,8 +203,12 @@ func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger return backend, nil } -// Close shuts down the geth backend. +// Close shuts down the geth backend gracefully. func (b *GethBackend) Close() error { + b.logger.Info().Msg("shutting down geth backend") + + var errs []error + if b.txPool != nil { b.txPool.Close() } @@ -186,19 +216,33 @@ func (b *GethBackend) Close() error { b.blockchain.Stop() } if b.db != nil { - b.db.Close() + if err := b.db.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close database: %w", err)) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) } return nil } // ForkchoiceUpdated implements EngineRPCClient. func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine.ForkchoiceStateV1, attrs map[string]any) (*engine.ForkChoiceResponse, error) { + // Check context before acquiring lock + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("context cancelled: %w", err) + } + g.backend.mu.Lock() defer g.backend.mu.Unlock() // Validate the forkchoice state headBlock := g.backend.blockchain.GetBlockByHash(fcState.HeadBlockHash) if headBlock == nil { + g.logger.Debug(). + Str("head_hash", fcState.HeadBlockHash.Hex()). + Msg("head block not found, returning SYNCING") return &engine.ForkChoiceResponse{ PayloadStatus: engine.PayloadStatusV1{ Status: engine.SYNCING, @@ -211,6 +255,11 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine return nil, fmt.Errorf("failed to set canonical head: %w", err) } + g.logger.Debug(). + Uint64("height", headBlock.NumberU64()). + Str("hash", headBlock.Hash().Hex()). + Msg("updated canonical head") + response := &engine.ForkChoiceResponse{ PayloadStatus: engine.PayloadStatusV1{ Status: engine.VALID, @@ -225,31 +274,32 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine return nil, fmt.Errorf("failed to parse payload attributes: %w", err) } - // Generate payload ID - g.backend.nextPayloadID++ - var payloadID engine.PayloadID - payloadID[0] = byte(g.backend.nextPayloadID >> 56) - payloadID[1] = byte(g.backend.nextPayloadID >> 48) - payloadID[2] = byte(g.backend.nextPayloadID >> 40) - payloadID[3] = byte(g.backend.nextPayloadID >> 32) - payloadID[4] = byte(g.backend.nextPayloadID >> 24) - payloadID[5] = byte(g.backend.nextPayloadID >> 16) - payloadID[6] = byte(g.backend.nextPayloadID >> 8) - payloadID[7] = byte(g.backend.nextPayloadID) + // Generate payload ID deterministically from attributes + payloadID := g.generatePayloadID(fcState.HeadBlockHash, payloadState) g.backend.payloads[payloadID] = payloadState response.PayloadID = &payloadID g.logger.Info(). Str("payload_id", payloadID.String()). + Str("parent_hash", fcState.HeadBlockHash.Hex()). Uint64("timestamp", payloadState.timestamp). Int("tx_count", len(payloadState.transactions)). + Str("fee_recipient", payloadState.feeRecipient.Hex()). Msg("started payload build") } return response, nil } +// generatePayloadID creates a deterministic payload ID from the build parameters. +func (g *gethEngineClient) generatePayloadID(parentHash common.Hash, ps *payloadBuildState) engine.PayloadID { + g.backend.nextPayloadID++ + var payloadID engine.PayloadID + binary.BigEndian.PutUint64(payloadID[:], g.backend.nextPayloadID) + return payloadID +} + // parsePayloadAttributes extracts payload attributes from the map format. func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs map[string]any) (*payloadBuildState, error) { ps := &payloadBuildState{ @@ -257,7 +307,7 @@ func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs withdrawals: []*types.Withdrawal{}, } - // Parse timestamp + // Parse timestamp (required) if ts, ok := attrs["timestamp"]; ok { switch v := ts.(type) { case int64: @@ -269,46 +319,59 @@ func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs default: return nil, fmt.Errorf("invalid timestamp type: %T", ts) } + } else { + return nil, errors.New("timestamp is required in payload attributes") } - // Parse prevRandao + // Parse prevRandao (required for PoS) if pr, ok := attrs["prevRandao"]; ok { switch v := pr.(type) { case common.Hash: ps.prevRandao = v case string: ps.prevRandao = common.HexToHash(v) + case []byte: + ps.prevRandao = common.BytesToHash(v) default: return nil, fmt.Errorf("invalid prevRandao type: %T", pr) } } - // Parse suggestedFeeRecipient + // Parse suggestedFeeRecipient (required) if fr, ok := attrs["suggestedFeeRecipient"]; ok { switch v := fr.(type) { case common.Address: ps.feeRecipient = v case string: ps.feeRecipient = common.HexToAddress(v) + case []byte: + ps.feeRecipient = common.BytesToAddress(v) default: return nil, fmt.Errorf("invalid suggestedFeeRecipient type: %T", fr) } + } else { + return nil, errors.New("suggestedFeeRecipient is required in payload attributes") } - // Parse transactions + // Parse transactions (optional) if txs, ok := attrs["transactions"]; ok { switch v := txs.(type) { case []string: - ps.transactions = make([][]byte, len(v)) - for i, txHex := range v { - ps.transactions[i] = common.FromHex(txHex) + ps.transactions = make([][]byte, 0, len(v)) + for _, txHex := range v { + txBytes := common.FromHex(txHex) + if len(txBytes) > 0 { + ps.transactions = append(ps.transactions, txBytes) + } } case [][]byte: ps.transactions = v + default: + return nil, fmt.Errorf("invalid transactions type: %T", txs) } } - // Parse gasLimit + // Parse gasLimit (optional) if gl, ok := attrs["gasLimit"]; ok { switch v := gl.(type) { case uint64: @@ -317,6 +380,10 @@ func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs ps.gasLimit = uint64(v) case float64: ps.gasLimit = uint64(v) + case *uint64: + if v != nil { + ps.gasLimit = *v + } default: return nil, fmt.Errorf("invalid gasLimit type: %T", gl) } @@ -324,8 +391,13 @@ func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs // Parse withdrawals (optional) if w, ok := attrs["withdrawals"]; ok { - if withdrawals, ok := w.([]*types.Withdrawal); ok { - ps.withdrawals = withdrawals + switch v := w.(type) { + case []*types.Withdrawal: + ps.withdrawals = v + case nil: + // Keep empty slice + default: + return nil, fmt.Errorf("invalid withdrawals type: %T", w) } } @@ -334,6 +406,10 @@ func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs // GetPayload implements EngineRPCClient. func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("context cancelled: %w", err) + } + g.backend.mu.Lock() defer g.backend.mu.Unlock() @@ -344,13 +420,26 @@ func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.Payl // Build the block if not already built if payloadState.payload == nil { - payload, err := g.buildPayload(payloadState) + startTime := time.Now() + payload, err := g.buildPayload(ctx, payloadState) if err != nil { return nil, fmt.Errorf("failed to build payload: %w", err) } payloadState.payload = payload + + g.logger.Info(). + Str("payload_id", payloadID.String()). + Uint64("block_number", payload.Number). + Str("block_hash", payload.BlockHash.Hex()). + Int("tx_count", len(payload.Transactions)). + Uint64("gas_used", payload.GasUsed). + Dur("build_time", time.Since(startTime)). + Msg("built payload") } + // Remove the payload from pending after retrieval + delete(g.backend.payloads, payloadID) + return &engine.ExecutionPayloadEnvelope{ ExecutionPayload: payloadState.payload, BlockValue: big.NewInt(0), @@ -360,15 +449,23 @@ func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.Payl } // buildPayload constructs an execution payload from the pending state. -func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.ExecutableData, error) { +func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildState) (*engine.ExecutableData, error) { parent := g.backend.blockchain.GetBlockByHash(ps.parentHash) if parent == nil { return nil, fmt.Errorf("parent block not found: %s", ps.parentHash.Hex()) } + // Validate block number continuity + expectedNumber := new(big.Int).Add(parent.Number(), big.NewInt(1)) + + // Validate timestamp + if ps.timestamp <= parent.Time() { + return nil, fmt.Errorf("invalid timestamp: %d must be greater than parent timestamp %d", ps.timestamp, parent.Time()) + } + // Calculate base fee for the new block var baseFee *big.Int - if g.backend.chainConfig.IsLondon(new(big.Int).Add(parent.Number(), big.NewInt(1))) { + if g.backend.chainConfig.IsLondon(expectedNumber) { baseFee = calcBaseFee(g.backend.chainConfig, parent.Header()) } @@ -386,7 +483,7 @@ func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.Executab ReceiptHash: types.EmptyReceiptsHash, Bloom: types.Bloom{}, Difficulty: big.NewInt(0), - Number: new(big.Int).Add(parent.Number(), big.NewInt(1)), + Number: expectedNumber, GasLimit: gasLimit, GasUsed: 0, Time: ps.timestamp, @@ -408,9 +505,11 @@ func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.Executab } var ( - txs types.Transactions - receipts []*types.Receipt - gasUsed uint64 + txs types.Transactions + receipts []*types.Receipt + gasUsed uint64 + txsExecuted int + txsSkipped int ) // Create EVM context @@ -419,7 +518,15 @@ func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.Executab // Execute transactions gp := new(core.GasPool).AddGas(gasLimit) for i, txBytes := range ps.transactions { + // Check context periodically + if i%100 == 0 { + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("context cancelled during tx execution: %w", err) + } + } + if len(txBytes) == 0 { + txsSkipped++ continue } @@ -428,7 +535,8 @@ func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.Executab g.logger.Debug(). Int("index", i). Err(err). - Msg("skipping invalid transaction") + Msg("skipping invalid transaction encoding") + txsSkipped++ continue } @@ -450,11 +558,20 @@ func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.Executab Str("tx_hash", tx.Hash().Hex()). Err(err). Msg("transaction execution failed, skipping") + txsSkipped++ continue } txs = append(txs, &tx) receipts = append(receipts, receipt) + txsExecuted++ + } + + if txsSkipped > 0 { + g.logger.Debug(). + Int("executed", txsExecuted). + Int("skipped", txsSkipped). + Msg("transaction execution summary") } // Finalize state @@ -486,7 +603,7 @@ func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.Executab for i, tx := range txs { data, err := tx.MarshalBinary() if err != nil { - return nil, fmt.Errorf("failed to marshal tx: %w", err) + return nil, fmt.Errorf("failed to marshal tx %d: %w", i, err) } txData[i] = data } @@ -516,24 +633,72 @@ func (g *gethEngineClient) buildPayload(ps *payloadBuildState) (*engine.Executab // NewPayload implements EngineRPCClient. func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.ExecutableData, blobHashes []string, parentBeaconBlockRoot string, executionRequests [][]byte) (*engine.PayloadStatusV1, error) { + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("context cancelled: %w", err) + } + g.backend.mu.Lock() defer g.backend.mu.Unlock() + startTime := time.Now() + + // Validate payload + if payload == nil { + return nil, errors.New("payload is required") + } + // Verify parent exists parent := g.backend.blockchain.GetBlockByHash(payload.ParentHash) if parent == nil { + g.logger.Debug(). + Str("parent_hash", payload.ParentHash.Hex()). + Uint64("block_number", payload.Number). + Msg("parent block not found, returning SYNCING") return &engine.PayloadStatusV1{ Status: engine.SYNCING, }, nil } + // Validate block number + expectedNumber := parent.NumberU64() + 1 + if payload.Number != expectedNumber { + g.logger.Warn(). + Uint64("expected", expectedNumber). + Uint64("got", payload.Number). + Msg("invalid block number") + parentHash := parent.Hash() + return &engine.PayloadStatusV1{ + Status: engine.INVALID, + LatestValidHash: &parentHash, + }, nil + } + + // Validate timestamp + if payload.Timestamp <= parent.Time() { + g.logger.Warn(). + Uint64("payload_timestamp", payload.Timestamp). + Uint64("parent_timestamp", parent.Time()). + Msg("invalid timestamp") + parentHash := parent.Hash() + return &engine.PayloadStatusV1{ + Status: engine.INVALID, + LatestValidHash: &parentHash, + }, nil + } + // Decode transactions var txs types.Transactions - for _, txData := range payload.Transactions { + for i, txData := range payload.Transactions { var tx types.Transaction if err := tx.UnmarshalBinary(txData); err != nil { + g.logger.Warn(). + Int("tx_index", i). + Err(err). + Msg("failed to decode transaction") + parentHash := parent.Hash() return &engine.PayloadStatusV1{ - Status: engine.INVALID, + Status: engine.INVALID, + LatestValidHash: &parentHash, }, nil } txs = append(txs, &tx) @@ -586,6 +751,7 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu g.logger.Warn(). Str("expected", payload.BlockHash.Hex()). Str("calculated", block.Hash().Hex()). + Uint64("block_number", payload.Number). Msg("block hash mismatch") parentHash := parent.Hash() return &engine.PayloadStatusV1{ @@ -611,6 +777,16 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu } blockHash := block.Hash() + + g.logger.Info(). + Uint64("block_number", block.NumberU64()). + Str("block_hash", blockHash.Hex()). + Str("parent_hash", payload.ParentHash.Hex()). + Int("tx_count", len(txs)). + Uint64("gas_used", payload.GasUsed). + Dur("process_time", time.Since(startTime)). + Msg("new payload validated and inserted") + return &engine.PayloadStatusV1{ Status: engine.VALID, LatestValidHash: &blockHash, @@ -619,6 +795,10 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu // HeaderByNumber implements EthRPCClient. func (g *gethEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("context cancelled: %w", err) + } + if number == nil { // Return current head header := g.backend.blockchain.CurrentBlock() @@ -627,6 +807,7 @@ func (g *gethEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*t } return header, nil } + block := g.backend.blockchain.GetBlockByNumber(number.Uint64()) if block == nil { return nil, fmt.Errorf("block not found at height %d", number.Uint64()) @@ -636,17 +817,24 @@ func (g *gethEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*t // GetTxs implements EthRPCClient. func (g *gethEthClient) GetTxs(ctx context.Context) ([]string, error) { + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("context cancelled: %w", err) + } + pending := g.backend.txPool.Pending(txpool.PendingFilter{}) var result []string for _, txs := range pending { for _, lazyTx := range txs { - // Resolve the lazy transaction to get the actual transaction tx := lazyTx.Tx if tx == nil { continue } data, err := tx.MarshalBinary() if err != nil { + g.logger.Debug(). + Str("tx_hash", tx.Hash().Hex()). + Err(err). + Msg("failed to marshal pending tx") continue } result = append(result, "0x"+common.Bytes2Hex(data)) @@ -655,10 +843,12 @@ func (g *gethEthClient) GetTxs(ctx context.Context) ([]string, error) { return result, nil } -// calcBaseFee calculates the base fee for the next block. +// calcBaseFee calculates the base fee for the next block according to EIP-1559. func calcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { + nextBlockNumber := new(big.Int).Add(parent.Number, big.NewInt(1)) + // If we're before London, return nil - if !config.IsLondon(new(big.Int).Add(parent.Number, big.NewInt(1))) { + if !config.IsLondon(nextBlockNumber) { return nil } @@ -667,6 +857,11 @@ func calcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { return big.NewInt(params.InitialBaseFee) } + // Parent must have base fee + if parent.BaseFee == nil { + return big.NewInt(params.InitialBaseFee) + } + // Calculate next base fee based on EIP-1559 var ( parentGasTarget = parent.GasLimit / 2 @@ -674,6 +869,11 @@ func calcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { baseFee = new(big.Int).Set(parent.BaseFee) ) + // Prevent division by zero + if parentGasTarget == 0 { + return baseFee + } + if parentGasUsed == parentGasTarget { return baseFee } @@ -684,8 +884,8 @@ func calcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { x := new(big.Int).Mul(parent.BaseFee, gasUsedDelta) y := new(big.Int).SetUint64(parentGasTarget) z := new(big.Int).Div(x, y) - baseFeeChangeDenominator := new(big.Int).SetUint64(8) - delta := new(big.Int).Div(z, baseFeeChangeDenominator) + baseFeeChangeDenominatorInt := new(big.Int).SetUint64(baseFeeChangeDenominator) + delta := new(big.Int).Div(z, baseFeeChangeDenominatorInt) if delta.Sign() == 0 { delta = big.NewInt(1) } @@ -697,8 +897,8 @@ func calcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { x := new(big.Int).Mul(parent.BaseFee, gasUsedDelta) y := new(big.Int).SetUint64(parentGasTarget) z := new(big.Int).Div(x, y) - baseFeeChangeDenominator := new(big.Int).SetUint64(8) - delta := new(big.Int).Div(z, baseFeeChangeDenominator) + baseFeeChangeDenominatorInt := new(big.Int).SetUint64(baseFeeChangeDenominator) + delta := new(big.Int).Div(z, baseFeeChangeDenominatorInt) baseFee = new(big.Int).Sub(baseFee, delta) if baseFee.Cmp(big.NewInt(0)) < 0 { baseFee = big.NewInt(0) @@ -718,7 +918,7 @@ func applyTransaction( ) (*types.Receipt, error) { msg, err := core.TransactionToMessage(tx, types.LatestSigner(config), header.BaseFee) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert tx to message: %w", err) } // Create EVM instance @@ -729,7 +929,7 @@ func applyTransaction( // Apply the transaction result, err := core.ApplyMessage(evmInstance, msg, gp) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to apply message: %w", err) } *usedGas += result.UsedGas @@ -756,7 +956,7 @@ func applyTransaction( // Set contract address if this was a contract creation if msg.To == nil { - receipt.ContractAddress = evmInstance.Origin + receipt.ContractAddress = evmInstance.TxContext.Origin } return receipt, nil diff --git a/execution/evm/eth_db.go b/execution/evm/engine_geth_db.go similarity index 99% rename from execution/evm/eth_db.go rename to execution/evm/engine_geth_db.go index b6a055e468..f5bd6bc51b 100644 --- a/execution/evm/eth_db.go +++ b/execution/evm/engine_geth_db.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/query" + "github.com/syndtr/goleveldb/leveldb" ) var _ ethdb.KeyValueStore = &wrapper{} @@ -73,7 +74,7 @@ func (w *wrapper) DeleteRange(start []byte, end []byte) error { func (w *wrapper) Get(key []byte) ([]byte, error) { val, err := w.ds.Get(context.Background(), keyToDatastoreKey(key)) if err == datastore.ErrNotFound { - return nil, nil + return nil, leveldb.ErrNotFound } return val, err } diff --git a/execution/evm/go.mod b/execution/evm/go.mod index 96cb74532a..3967c912a6 100644 --- a/execution/evm/go.mod +++ b/execution/evm/go.mod @@ -10,6 +10,7 @@ require ( github.com/ipfs/go-datastore v0.9.0 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d go.opentelemetry.io/otel v1.39.0 go.opentelemetry.io/otel/sdk v1.39.0 go.opentelemetry.io/otel/trace v1.39.0 @@ -88,7 +89,6 @@ require ( github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect - github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect diff --git a/execution/evm/go.sum b/execution/evm/go.sum index 3f1aaba303..26be6bfc57 100644 --- a/execution/evm/go.sum +++ b/execution/evm/go.sum @@ -277,17 +277,20 @@ github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJh github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= @@ -544,6 +547,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/execution/evm/test/go.mod b/execution/evm/test/go.mod index ac056de8fd..a448dbbd1a 100644 --- a/execution/evm/test/go.mod +++ b/execution/evm/test/go.mod @@ -29,6 +29,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/StackExchange/wmi v1.2.1 // indirect + github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/avast/retry-go/v4 v4.6.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect @@ -71,6 +72,7 @@ require ( github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/evstack/ev-node v1.0.0-beta.10 // indirect github.com/evstack/ev-node/core v1.0.0-beta.5 // indirect @@ -104,6 +106,7 @@ require ( github.com/hashicorp/go-metrics v0.5.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/execution/evm/test/go.sum b/execution/evm/test/go.sum index 9663ab8a47..c652e01d93 100644 --- a/execution/evm/test/go.sum +++ b/execution/evm/test/go.sum @@ -49,6 +49,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 269a3afa3f..7080eb86a5 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -47,6 +47,7 @@ require ( github.com/DataDog/zstd v1.5.7 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/avast/retry-go/v4 v4.6.1 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -101,6 +102,7 @@ require ( github.com/dvsekhvalnov/jose2go v1.8.0 // indirect github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/evstack/ev-node/core v1.0.0-beta.5 // indirect github.com/fatih/color v1.16.0 // indirect @@ -148,6 +150,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/huandu/skiplist v1.2.1 // indirect github.com/huin/goupnp v1.3.0 // indirect diff --git a/test/e2e/go.sum b/test/e2e/go.sum index 916a4ed2a0..a70169cb38 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -85,6 +85,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= From 896c520b15e1b6a7e316ead98983a45e3ec19666 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 17:25:32 +0100 Subject: [PATCH 07/18] rpc support --- apps/evm/cmd/run.go | 4 +- execution/evm/engine_geth.go | 706 +++++++++++++++++++++++++++++++++++ execution/evm/flags.go | 1 + 3 files changed, 710 insertions(+), 1 deletion(-) diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index 00f9921c21..fdaf8381a8 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -226,7 +226,8 @@ func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEna return nil, fmt.Errorf("failed to unmarshal genesis: %w", err) } - return evm.NewEngineExecutionClientWithGeth(&genesis, feeRecipient, db, logger) + rpcAddress, _ := cmd.Flags().GetString(evm.FlagEVMRPCAddress) + return evm.NewEngineExecutionClientWithGeth(&genesis, feeRecipient, db, rpcAddress, logger) } // Read execution client parameters from flags @@ -282,4 +283,5 @@ func addFlags(cmd *cobra.Command) { cmd.Flags().Bool(evm.FlagEVMInProcessGeth, false, "Use in-process Geth for EVM execution instead of external execution client") cmd.Flags().String(evm.FlagEVMGenesisPath, "", "EVM genesis path for Geth") + cmd.Flags().String(evm.FlagEVMRPCAddress, "", "Address for in-process Geth JSON-RPC server (e.g., 127.0.0.1:8545)") } diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index e941497f76..ad2e355411 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -3,14 +3,18 @@ package evm import ( "context" "encoding/binary" + "encoding/hex" "errors" "fmt" "math/big" + "net" + "net/http" "sync" "time" "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" @@ -21,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" ds "github.com/ipfs/go-datastore" @@ -49,9 +54,26 @@ type GethBackend struct { payloads map[engine.PayloadID]*payloadBuildState nextPayloadID uint64 + // RPC server + rpcServer *rpc.Server + httpServer *http.Server + rpcListener net.Listener + logger zerolog.Logger } +// EthRPCService implements the eth_ JSON-RPC namespace. +type EthRPCService struct { + backend *GethBackend + logger zerolog.Logger +} + +// TxPoolExtService implements the txpoolExt_ JSON-RPC namespace. +type TxPoolExtService struct { + backend *GethBackend + logger zerolog.Logger +} + // payloadBuildState tracks the state of a payload being built. type payloadBuildState struct { parentHash common.Hash @@ -80,10 +102,13 @@ type gethEthClient struct { // NewEngineExecutionClientWithGeth creates an EngineClient that uses an in-process // go-ethereum instance instead of connecting to an external execution engine via RPC. +// If rpcAddress is non-empty, an HTTP JSON-RPC server will be started on that address +// (e.g., "127.0.0.1:8545") exposing standard eth_ methods. func NewEngineExecutionClientWithGeth( genesis *core.Genesis, feeRecipient common.Address, db ds.Batching, + rpcAddress string, logger zerolog.Logger, ) (*EngineClient, error) { if db == nil { @@ -98,6 +123,14 @@ func NewEngineExecutionClientWithGeth( return nil, fmt.Errorf("failed to create geth backend: %w", err) } + // Start RPC server if address is provided + if rpcAddress != "" { + if err := backend.StartRPCServer(rpcAddress); err != nil { + backend.Close() + return nil, fmt.Errorf("failed to start RPC server: %w", err) + } + } + engineClient := &gethEngineClient{ backend: backend, logger: logger.With().Str("component", "geth-engine").Logger(), @@ -209,6 +242,18 @@ func (b *GethBackend) Close() error { var errs []error + // Stop RPC server first + if b.httpServer != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := b.httpServer.Shutdown(ctx); err != nil { + errs = append(errs, fmt.Errorf("failed to shutdown http server: %w", err)) + } + } + if b.rpcServer != nil { + b.rpcServer.Stop() + } + if b.txPool != nil { b.txPool.Close() } @@ -227,6 +272,667 @@ func (b *GethBackend) Close() error { return nil } +// StartRPCServer starts the JSON-RPC server on the specified address. +// If address is empty, the server is not started. +func (b *GethBackend) StartRPCServer(address string) error { + if address == "" { + return nil + } + + b.rpcServer = rpc.NewServer() + + // Register eth_ namespace + ethService := &EthRPCService{backend: b, logger: b.logger.With().Str("rpc", "eth").Logger()} + if err := b.rpcServer.RegisterName("eth", ethService); err != nil { + return fmt.Errorf("failed to register eth service: %w", err) + } + + // Register txpoolExt_ namespace for compatibility + txpoolService := &TxPoolExtService{backend: b, logger: b.logger.With().Str("rpc", "txpoolExt").Logger()} + if err := b.rpcServer.RegisterName("txpoolExt", txpoolService); err != nil { + return fmt.Errorf("failed to register txpoolExt service: %w", err) + } + + // Register net_ namespace + netService := &NetRPCService{backend: b} + if err := b.rpcServer.RegisterName("net", netService); err != nil { + return fmt.Errorf("failed to register net service: %w", err) + } + + // Register web3_ namespace + web3Service := &Web3RPCService{} + if err := b.rpcServer.RegisterName("web3", web3Service); err != nil { + return fmt.Errorf("failed to register web3 service: %w", err) + } + + // Create HTTP handler with CORS support + handler := &rpcHandler{ + rpcServer: b.rpcServer, + logger: b.logger, + } + + listener, err := net.Listen("tcp", address) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", address, err) + } + b.rpcListener = listener + + b.httpServer = &http.Server{ + Handler: handler, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } + + go func() { + b.logger.Info().Str("address", address).Msg("starting JSON-RPC server") + if err := b.httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + b.logger.Error().Err(err).Msg("JSON-RPC server error") + } + }() + + return nil +} + +// rpcHandler wraps the RPC server with CORS and proper HTTP handling. +type rpcHandler struct { + rpcServer *rpc.Server + logger zerolog.Logger +} + +func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Content-Type", "application/json") + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + // Handle GET requests with a simple response + if r.Method == "GET" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","result":"ev-node in-process geth RPC","id":null}`)) + return + } + + // Handle POST requests via the RPC server + if r.Method == "POST" { + h.rpcServer.ServeHTTP(w, r) + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +// NetRPCService implements the net_ JSON-RPC namespace. +type NetRPCService struct { + backend *GethBackend +} + +// Version returns the network ID. +func (s *NetRPCService) Version() string { + return s.backend.chainConfig.ChainID.String() +} + +// Listening returns true if the node is listening for connections. +func (s *NetRPCService) Listening() bool { + return true +} + +// PeerCount returns the number of connected peers. +func (s *NetRPCService) PeerCount() hexutil.Uint { + return 0 +} + +// Web3RPCService implements the web3_ JSON-RPC namespace. +type Web3RPCService struct{} + +// ClientVersion returns the client version. +func (s *Web3RPCService) ClientVersion() string { + return "ev-node/geth/1.0.0" +} + +// Sha3 returns the Keccak-256 hash of the input. +func (s *Web3RPCService) Sha3(input hexutil.Bytes) hexutil.Bytes { + hash := common.BytesToHash(input) + return hash[:] +} + +// ChainId returns the chain ID. +func (s *EthRPCService) ChainId() *hexutil.Big { + return (*hexutil.Big)(s.backend.chainConfig.ChainID) +} + +// BlockNumber returns the current block number. +func (s *EthRPCService) BlockNumber() hexutil.Uint64 { + header := s.backend.blockchain.CurrentBlock() + if header == nil { + return 0 + } + return hexutil.Uint64(header.Number.Uint64()) +} + +// GetBlockByNumber returns block information by number. +func (s *EthRPCService) GetBlockByNumber(blockNr rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { + var block *types.Block + if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { + header := s.backend.blockchain.CurrentBlock() + if header == nil { + return nil, nil + } + block = s.backend.blockchain.GetBlock(header.Hash(), header.Number.Uint64()) + } else { + block = s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) + } + if block == nil { + return nil, nil + } + return s.formatBlock(block, fullTx), nil +} + +// GetBlockByHash returns block information by hash. +func (s *EthRPCService) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + block := s.backend.blockchain.GetBlockByHash(hash) + if block == nil { + return nil, nil + } + return s.formatBlock(block, fullTx), nil +} + +// formatBlock formats a block for JSON-RPC response. +func (s *EthRPCService) formatBlock(block *types.Block, fullTx bool) map[string]interface{} { + header := block.Header() + result := map[string]interface{}{ + "number": (*hexutil.Big)(header.Number), + "hash": block.Hash(), + "parentHash": header.ParentHash, + "nonce": header.Nonce, + "sha3Uncles": header.UncleHash, + "logsBloom": header.Bloom, + "transactionsRoot": header.TxHash, + "stateRoot": header.Root, + "receiptsRoot": header.ReceiptHash, + "miner": header.Coinbase, + "difficulty": (*hexutil.Big)(header.Difficulty), + "extraData": hexutil.Bytes(header.Extra), + "size": hexutil.Uint64(block.Size()), + "gasLimit": hexutil.Uint64(header.GasLimit), + "gasUsed": hexutil.Uint64(header.GasUsed), + "timestamp": hexutil.Uint64(header.Time), + } + + if header.BaseFee != nil { + result["baseFeePerGas"] = (*hexutil.Big)(header.BaseFee) + } + + txs := block.Transactions() + if fullTx { + txList := make([]map[string]interface{}, len(txs)) + for i, tx := range txs { + txList[i] = s.formatTransaction(tx, block.Hash(), header.Number.Uint64(), uint64(i)) + } + result["transactions"] = txList + } else { + txHashes := make([]common.Hash, len(txs)) + for i, tx := range txs { + txHashes[i] = tx.Hash() + } + result["transactions"] = txHashes + } + + result["uncles"] = []common.Hash{} + return result +} + +// formatTransaction formats a transaction for JSON-RPC response. +func (s *EthRPCService) formatTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64) map[string]interface{} { + signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) + from, _ := types.Sender(signer, tx) + + result := map[string]interface{}{ + "hash": tx.Hash(), + "nonce": hexutil.Uint64(tx.Nonce()), + "blockHash": blockHash, + "blockNumber": (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)), + "transactionIndex": hexutil.Uint64(index), + "from": from, + "to": tx.To(), + "value": (*hexutil.Big)(tx.Value()), + "gas": hexutil.Uint64(tx.Gas()), + "input": hexutil.Bytes(tx.Data()), + } + + if tx.Type() == types.LegacyTxType { + result["gasPrice"] = (*hexutil.Big)(tx.GasPrice()) + } else { + result["gasPrice"] = (*hexutil.Big)(tx.GasPrice()) + result["maxFeePerGas"] = (*hexutil.Big)(tx.GasFeeCap()) + result["maxPriorityFeePerGas"] = (*hexutil.Big)(tx.GasTipCap()) + result["type"] = hexutil.Uint64(tx.Type()) + } + + v, r, ss := tx.RawSignatureValues() + result["v"] = (*hexutil.Big)(v) + result["r"] = (*hexutil.Big)(r) + result["s"] = (*hexutil.Big)(ss) + + return result +} + +// GetBalance returns the balance of an account. +func (s *EthRPCService) GetBalance(address common.Address, blockNr rpc.BlockNumber) (*hexutil.Big, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + return (*hexutil.Big)(stateDB.GetBalance(address).ToBig()), nil +} + +// GetTransactionCount returns the nonce of an account. +func (s *EthRPCService) GetTransactionCount(address common.Address, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return 0, err + } + return hexutil.Uint64(stateDB.GetNonce(address)), nil +} + +// GetCode returns the code at an address. +func (s *EthRPCService) GetCode(address common.Address, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + return stateDB.GetCode(address), nil +} + +// GetStorageAt returns the storage value at a position. +func (s *EthRPCService) GetStorageAt(address common.Address, position hexutil.Big, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + value := stateDB.GetState(address, common.BigToHash((*big.Int)(&position))) + return value[:], nil +} + +// stateAtBlock returns the state at the given block number. +func (s *EthRPCService) stateAtBlock(blockNr rpc.BlockNumber) (*state.StateDB, error) { + var header *types.Header + if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { + header = s.backend.blockchain.CurrentBlock() + } else { + block := s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) + if block == nil { + return nil, fmt.Errorf("block %d not found", blockNr) + } + header = block.Header() + } + if header == nil { + return nil, errors.New("no current block") + } + return s.backend.blockchain.StateAt(header.Root) +} + +// Call executes a call without creating a transaction. +func (s *EthRPCService) Call(args TransactionArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + + header := s.backend.blockchain.CurrentBlock() + if header == nil { + return nil, errors.New("no current block") + } + + msg := args.ToMessage(header.BaseFee) + blockContext := core.NewEVMBlockContext(header, s.backend.blockchain, nil) + txContext := core.NewEVMTxContext(msg) + evm := vm.NewEVM(blockContext, stateDB, s.backend.chainConfig, vm.Config{}) + evm.SetTxContext(txContext) + + gp := new(core.GasPool).AddGas(header.GasLimit) + result, err := core.ApplyMessage(evm, msg, gp) + if err != nil { + return nil, err + } + if result.Err != nil { + return nil, result.Err + } + return result.Return(), nil +} + +// EstimateGas estimates the gas needed for a transaction. +func (s *EthRPCService) EstimateGas(args TransactionArgs, blockNr *rpc.BlockNumber) (hexutil.Uint64, error) { + blockNumber := rpc.LatestBlockNumber + if blockNr != nil { + blockNumber = *blockNr + } + + stateDB, err := s.stateAtBlock(blockNumber) + if err != nil { + return 0, err + } + + header := s.backend.blockchain.CurrentBlock() + if header == nil { + return 0, errors.New("no current block") + } + + // Use a high gas limit for estimation + hi := header.GasLimit + if args.Gas != nil && uint64(*args.Gas) < hi { + hi = uint64(*args.Gas) + } + + lo := uint64(21000) + + // Binary search for the optimal gas + for lo+1 < hi { + mid := (lo + hi) / 2 + args.Gas = (*hexutil.Uint64)(&mid) + + msg := args.ToMessage(header.BaseFee) + stateCopy := stateDB.Copy() + blockContext := core.NewEVMBlockContext(header, s.backend.blockchain, nil) + txContext := core.NewEVMTxContext(msg) + evm := vm.NewEVM(blockContext, stateCopy, s.backend.chainConfig, vm.Config{}) + evm.SetTxContext(txContext) + + gp := new(core.GasPool).AddGas(mid) + result, err := core.ApplyMessage(evm, msg, gp) + if err != nil || result.Failed() { + lo = mid + } else { + hi = mid + } + } + + return hexutil.Uint64(hi), nil +} + +// GasPrice returns the current gas price. +func (s *EthRPCService) GasPrice() *hexutil.Big { + header := s.backend.blockchain.CurrentBlock() + if header == nil || header.BaseFee == nil { + return (*hexutil.Big)(big.NewInt(params.InitialBaseFee)) + } + // Return base fee + 1 gwei tip + tip := big.NewInt(1e9) + return (*hexutil.Big)(new(big.Int).Add(header.BaseFee, tip)) +} + +// MaxPriorityFeePerGas returns the suggested priority fee. +func (s *EthRPCService) MaxPriorityFeePerGas() *hexutil.Big { + return (*hexutil.Big)(big.NewInt(1e9)) // 1 gwei +} + +// SendRawTransaction sends a signed transaction. +func (s *EthRPCService) SendRawTransaction(encodedTx hexutil.Bytes) (common.Hash, error) { + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(encodedTx); err != nil { + return common.Hash{}, err + } + + errs := s.backend.txPool.Add([]*types.Transaction{tx}, true) + if len(errs) > 0 && errs[0] != nil { + return common.Hash{}, errs[0] + } + + s.logger.Info(). + Str("tx_hash", tx.Hash().Hex()). + Msg("received raw transaction") + + return tx.Hash(), nil +} + +// GetTransactionByHash returns transaction info by hash. +func (s *EthRPCService) GetTransactionByHash(hash common.Hash) (map[string]interface{}, error) { + // Search in recent blocks + currentBlock := s.backend.blockchain.CurrentBlock() + if currentBlock == nil { + return nil, nil + } + + // Search backwards through blocks + for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { + block := s.backend.blockchain.GetBlockByNumber(i) + if block == nil { + continue + } + for idx, tx := range block.Transactions() { + if tx.Hash() == hash { + return s.formatTransaction(tx, block.Hash(), block.NumberU64(), uint64(idx)), nil + } + } + } + return nil, nil +} + +// GetTransactionReceipt returns the receipt of a transaction. +func (s *EthRPCService) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + // Search in recent blocks + currentBlock := s.backend.blockchain.CurrentBlock() + if currentBlock == nil { + return nil, nil + } + + // Search backwards through blocks + for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { + block := s.backend.blockchain.GetBlockByNumber(i) + if block == nil { + continue + } + for idx, tx := range block.Transactions() { + if tx.Hash() == hash { + receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) + if len(receipts) <= idx { + return nil, nil + } + receipt := receipts[idx] + + signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) + from, _ := types.Sender(signer, tx) + + result := map[string]interface{}{ + "transactionHash": hash, + "transactionIndex": hexutil.Uint64(idx), + "blockHash": block.Hash(), + "blockNumber": (*hexutil.Big)(block.Number()), + "from": from, + "to": tx.To(), + "cumulativeGasUsed": hexutil.Uint64(receipt.CumulativeGasUsed), + "gasUsed": hexutil.Uint64(receipt.GasUsed), + "contractAddress": nil, + "logs": receipt.Logs, + "logsBloom": receipt.Bloom, + "status": hexutil.Uint(receipt.Status), + "effectiveGasPrice": (*hexutil.Big)(tx.GasPrice()), + "type": hexutil.Uint(tx.Type()), + } + + if receipt.ContractAddress != (common.Address{}) { + result["contractAddress"] = receipt.ContractAddress + } + + return result, nil + } + } + } + return nil, nil +} + +// GetLogs returns logs matching the filter criteria. +func (s *EthRPCService) GetLogs(filter FilterQuery) ([]*types.Log, error) { + var fromBlock, toBlock uint64 + + if filter.FromBlock == nil { + fromBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() + } else { + fromBlock = filter.FromBlock.Uint64() + } + + if filter.ToBlock == nil { + toBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() + } else { + toBlock = filter.ToBlock.Uint64() + } + + var logs []*types.Log + for i := fromBlock; i <= toBlock; i++ { + block := s.backend.blockchain.GetBlockByNumber(i) + if block == nil { + continue + } + receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) + for _, receipt := range receipts { + for _, log := range receipt.Logs { + if s.matchLog(log, filter) { + logs = append(logs, log) + } + } + } + } + return logs, nil +} + +// matchLog checks if a log matches the filter criteria. +func (s *EthRPCService) matchLog(log *types.Log, filter FilterQuery) bool { + if len(filter.Addresses) > 0 { + found := false + for _, addr := range filter.Addresses { + if log.Address == addr { + found = true + break + } + } + if !found { + return false + } + } + + for i, topics := range filter.Topics { + if len(topics) == 0 { + continue + } + if i >= len(log.Topics) { + return false + } + found := false + for _, topic := range topics { + if log.Topics[i] == topic { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +// FilterQuery represents a filter for logs. +type FilterQuery struct { + FromBlock *big.Int `json:"fromBlock"` + ToBlock *big.Int `json:"toBlock"` + Addresses []common.Address `json:"address"` + Topics [][]common.Hash `json:"topics"` +} + +// TransactionArgs represents the arguments to construct a transaction. +type TransactionArgs struct { + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` + MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` + Value *hexutil.Big `json:"value"` + Data *hexutil.Bytes `json:"data"` + Input *hexutil.Bytes `json:"input"` + Nonce *hexutil.Uint64 `json:"nonce"` +} + +// ToMessage converts TransactionArgs to a core.Message. +func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message { + var from common.Address + if args.From != nil { + from = *args.From + } + + var to *common.Address + if args.To != nil { + to = args.To + } + + var gas uint64 = 50000000 + if args.Gas != nil { + gas = uint64(*args.Gas) + } + + var value *big.Int + if args.Value != nil { + value = (*big.Int)(args.Value) + } else { + value = big.NewInt(0) + } + + var data []byte + if args.Data != nil { + data = *args.Data + } else if args.Input != nil { + data = *args.Input + } + + var gasPrice *big.Int + if args.GasPrice != nil { + gasPrice = (*big.Int)(args.GasPrice) + } else if baseFee != nil { + gasPrice = new(big.Int).Add(baseFee, big.NewInt(1e9)) + } else { + gasPrice = big.NewInt(params.InitialBaseFee) + } + + msg := &core.Message{ + From: from, + To: to, + Value: value, + GasLimit: gas, + GasPrice: gasPrice, + GasFeeCap: gasPrice, + GasTipCap: big.NewInt(0), + Data: data, + SkipNonceChecks: true, + } + return msg +} + +// GetTxs returns pending transactions (for txpoolExt compatibility). +func (s *TxPoolExtService) GetTxs() ([]string, error) { + pending := s.backend.txPool.Pending(txpool.PendingFilter{}) + var result []string + for _, txs := range pending { + for _, lazyTx := range txs { + tx := lazyTx.Tx + if tx == nil { + continue + } + data, err := tx.MarshalBinary() + if err != nil { + continue + } + result = append(result, "0x"+hex.EncodeToString(data)) + } + } + return result, nil +} + // ForkchoiceUpdated implements EngineRPCClient. func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine.ForkchoiceStateV1, attrs map[string]any) (*engine.ForkChoiceResponse, error) { // Check context before acquiring lock diff --git a/execution/evm/flags.go b/execution/evm/flags.go index 33c90aa296..c2ccf63252 100644 --- a/execution/evm/flags.go +++ b/execution/evm/flags.go @@ -9,4 +9,5 @@ const ( FlagEVMGenesisPath = "evm.geth.genesis-path" FlagEVMInProcessGeth = "evm.geth" + FlagEVMRPCAddress = "evm.geth.rpc-address" ) From b20dbb59b6fdc0147398670dad246f771020a714 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 17:29:23 +0100 Subject: [PATCH 08/18] fix tests --- execution/evm/engine_geth_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/execution/evm/engine_geth_test.go b/execution/evm/engine_geth_test.go index 0638569d40..e6cc96786d 100644 --- a/execution/evm/engine_geth_test.go +++ b/execution/evm/engine_geth_test.go @@ -48,7 +48,7 @@ func TestNewEngineExecutionClientWithGeth(t *testing.T) { logger := zerolog.Nop() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, "", logger) require.NoError(t, err) require.NotNil(t, client) @@ -64,7 +64,7 @@ func TestNewEngineExecutionClientWithGeth_NilDB(t *testing.T) { logger := zerolog.Nop() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - _, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, nil, logger) + _, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, nil, "", logger) require.Error(t, err) assert.Contains(t, err.Error(), "db is required") } @@ -74,7 +74,7 @@ func TestNewEngineExecutionClientWithGeth_NilGenesis(t *testing.T) { logger := zerolog.Nop() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - _, err := NewEngineExecutionClientWithGeth(nil, feeRecipient, db, logger) + _, err := NewEngineExecutionClientWithGeth(nil, feeRecipient, db, "", logger) require.Error(t, err) assert.Contains(t, err.Error(), "genesis configuration is required") } @@ -85,7 +85,7 @@ func TestGethEngineClient_InitChain(t *testing.T) { logger := zerolog.Nop() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, "", logger) require.NoError(t, err) ctx := context.Background() @@ -104,7 +104,7 @@ func TestGethEngineClient_GetLatestHeight(t *testing.T) { logger := zerolog.Nop() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, "", logger) require.NoError(t, err) ctx := context.Background() @@ -120,7 +120,7 @@ func TestGethEthClient_HeaderByNumber(t *testing.T) { logger := zerolog.Nop() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, "", logger) require.NoError(t, err) ctx := context.Background() @@ -143,7 +143,7 @@ func TestGethEthClient_GetTxs_EmptyPool(t *testing.T) { logger := zerolog.Nop() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, "", logger) require.NoError(t, err) ctx := context.Background() @@ -160,7 +160,7 @@ func TestGethEngineClient_ExecuteTxs_EmptyBlock(t *testing.T) { logger := zerolog.Nop() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, logger) + client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, "", logger) require.NoError(t, err) ctx := context.Background() From 1ca0a16ee216ea924352cc45a9cbc8216a2e618f Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 17:36:19 +0100 Subject: [PATCH 09/18] better init split --- apps/evm/cmd/run.go | 81 ++++++++++++++++++++++++++---------------- execution/evm/flags.go | 2 +- 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index fdaf8381a8..aead783e69 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -59,14 +59,28 @@ var RunCmd = &cobra.Command{ tracingEnabled := nodeConfig.Instrumentation.IsTracingEnabled() - executor, err := createExecutionClient( - cmd, - datastore, - tracingEnabled, - logger.With().Str("module", "engine_client").Logger(), - ) - if err != nil { - return err + var executor execution.Executor + useGeth, _ := cmd.Flags().GetBool(evm.FlagEVMInProcessGeth) + if useGeth { + executor, err = createGethExecutionClient( + cmd, + datastore, + tracingEnabled, + logger.With().Str("module", "geth_client").Logger(), + ) + if err != nil { + return err + } + } else { + executor, err = createRethExecutionClient( + cmd, + datastore, + tracingEnabled, + logger.With().Str("module", "engine_client").Logger(), + ) + if err != nil { + return err + } } blobClient, err := blobrpc.NewClient(context.Background(), nodeConfig.DA.Address, nodeConfig.DA.AuthToken, "") @@ -202,34 +216,13 @@ func createSequencer( return sequencer, nil } -func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, logger zerolog.Logger) (execution.Executor, error) { +func createRethExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, logger zerolog.Logger) (execution.Executor, error) { feeRecipientStr, err := cmd.Flags().GetString(evm.FlagEvmFeeRecipient) if err != nil { return nil, fmt.Errorf("failed to get '%s' flag: %w", evm.FlagEvmFeeRecipient, err) } feeRecipient := common.HexToAddress(feeRecipientStr) - useGeth, _ := cmd.Flags().GetBool(evm.FlagEVMInProcessGeth) - if useGeth { - genesisPath, _ := cmd.Flags().GetString(evm.FlagEVMGenesisPath) - if len(genesisPath) == 0 { - return nil, fmt.Errorf("genesis path must be provided when using in-process Geth") - } - - genesisBz, err := os.ReadFile(genesisPath) - if err != nil { - return nil, fmt.Errorf("failed to read genesis: %w", err) - } - - var genesis core.Genesis - if err := json.Unmarshal(genesisBz, &genesis); err != nil { - return nil, fmt.Errorf("failed to unmarshal genesis: %w", err) - } - - rpcAddress, _ := cmd.Flags().GetString(evm.FlagEVMRPCAddress) - return evm.NewEngineExecutionClientWithGeth(&genesis, feeRecipient, db, rpcAddress, logger) - } - // Read execution client parameters from flags ethURL, err := cmd.Flags().GetString(evm.FlagEvmEthURL) if err != nil { @@ -272,6 +265,32 @@ func createExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEna return evm.NewEngineExecutionClient(ethURL, engineURL, jwtSecret, genesisHash, feeRecipient, db, tracingEnabled, logger) } +func createGethExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, logger zerolog.Logger) (execution.Executor, error) { + feeRecipientStr, err := cmd.Flags().GetString(evm.FlagEvmFeeRecipient) + if err != nil { + return nil, fmt.Errorf("failed to get '%s' flag: %w", evm.FlagEvmFeeRecipient, err) + } + feeRecipient := common.HexToAddress(feeRecipientStr) + + genesisPath, _ := cmd.Flags().GetString(evm.FlagEVMGenesisPath) + if len(genesisPath) == 0 { + return nil, fmt.Errorf("genesis path must be provided when using in-process Geth") + } + + genesisBz, err := os.ReadFile(genesisPath) + if err != nil { + return nil, fmt.Errorf("failed to read genesis: %w", err) + } + + var genesis core.Genesis + if err := json.Unmarshal(genesisBz, &genesis); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis: %w", err) + } + + rpcAddress, _ := cmd.Flags().GetString(evm.FlagEVMRPCAddress) + return evm.NewEngineExecutionClientWithGeth(&genesis, feeRecipient, db, rpcAddress, logger) +} + // addFlags adds flags related to the EVM execution client func addFlags(cmd *cobra.Command) { cmd.Flags().String(evm.FlagEvmEthURL, "http://localhost:8545", "URL of the Ethereum JSON-RPC endpoint") @@ -284,4 +303,6 @@ func addFlags(cmd *cobra.Command) { cmd.Flags().Bool(evm.FlagEVMInProcessGeth, false, "Use in-process Geth for EVM execution instead of external execution client") cmd.Flags().String(evm.FlagEVMGenesisPath, "", "EVM genesis path for Geth") cmd.Flags().String(evm.FlagEVMRPCAddress, "", "Address for in-process Geth JSON-RPC server (e.g., 127.0.0.1:8545)") + + cmd.MarkFlagsMutuallyExclusive(evm.FlagEVMInProcessGeth, evm.FlagEvmEthURL, evm.FlagEvmEngineURL, evm.FlagEvmJWTSecretFile, evm.FlagEvmGenesisHash) } diff --git a/execution/evm/flags.go b/execution/evm/flags.go index c2ccf63252..9aa7c95729 100644 --- a/execution/evm/flags.go +++ b/execution/evm/flags.go @@ -7,7 +7,7 @@ const ( FlagEvmGenesisHash = "evm.genesis-hash" FlagEvmFeeRecipient = "evm.fee-recipient" - FlagEVMGenesisPath = "evm.geth.genesis-path" FlagEVMInProcessGeth = "evm.geth" + FlagEVMGenesisPath = "evm.geth.genesis-path" FlagEVMRPCAddress = "evm.geth.rpc-address" ) From a016c854ad71e8000a35769dbe34f3dc723a8ea3 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 17:44:40 +0100 Subject: [PATCH 10/18] split rpc --- execution/evm/engine_geth.go | 678 +----------------------------- execution/evm/engine_geth_rpc.go | 695 +++++++++++++++++++++++++++++++ 2 files changed, 696 insertions(+), 677 deletions(-) create mode 100644 execution/evm/engine_geth_rpc.go diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index ad2e355411..6c2d859ee8 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -3,7 +3,6 @@ package evm import ( "context" "encoding/binary" - "encoding/hex" "errors" "fmt" "math/big" @@ -14,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" @@ -62,18 +60,6 @@ type GethBackend struct { logger zerolog.Logger } -// EthRPCService implements the eth_ JSON-RPC namespace. -type EthRPCService struct { - backend *GethBackend - logger zerolog.Logger -} - -// TxPoolExtService implements the txpoolExt_ JSON-RPC namespace. -type TxPoolExtService struct { - backend *GethBackend - logger zerolog.Logger -} - // payloadBuildState tracks the state of a payload being built. type payloadBuildState struct { parentHash common.Hash @@ -166,7 +152,6 @@ func NewEngineExecutionClientWithGeth( // newGethBackend creates a new in-process geth backend with persistent storage. func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger) (*GethBackend, error) { - // Wrap the datastore as an ethdb.Database ethdb := rawdb.NewDatabase(&wrapper{db}) // Create trie database @@ -272,667 +257,6 @@ func (b *GethBackend) Close() error { return nil } -// StartRPCServer starts the JSON-RPC server on the specified address. -// If address is empty, the server is not started. -func (b *GethBackend) StartRPCServer(address string) error { - if address == "" { - return nil - } - - b.rpcServer = rpc.NewServer() - - // Register eth_ namespace - ethService := &EthRPCService{backend: b, logger: b.logger.With().Str("rpc", "eth").Logger()} - if err := b.rpcServer.RegisterName("eth", ethService); err != nil { - return fmt.Errorf("failed to register eth service: %w", err) - } - - // Register txpoolExt_ namespace for compatibility - txpoolService := &TxPoolExtService{backend: b, logger: b.logger.With().Str("rpc", "txpoolExt").Logger()} - if err := b.rpcServer.RegisterName("txpoolExt", txpoolService); err != nil { - return fmt.Errorf("failed to register txpoolExt service: %w", err) - } - - // Register net_ namespace - netService := &NetRPCService{backend: b} - if err := b.rpcServer.RegisterName("net", netService); err != nil { - return fmt.Errorf("failed to register net service: %w", err) - } - - // Register web3_ namespace - web3Service := &Web3RPCService{} - if err := b.rpcServer.RegisterName("web3", web3Service); err != nil { - return fmt.Errorf("failed to register web3 service: %w", err) - } - - // Create HTTP handler with CORS support - handler := &rpcHandler{ - rpcServer: b.rpcServer, - logger: b.logger, - } - - listener, err := net.Listen("tcp", address) - if err != nil { - return fmt.Errorf("failed to listen on %s: %w", address, err) - } - b.rpcListener = listener - - b.httpServer = &http.Server{ - Handler: handler, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - } - - go func() { - b.logger.Info().Str("address", address).Msg("starting JSON-RPC server") - if err := b.httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { - b.logger.Error().Err(err).Msg("JSON-RPC server error") - } - }() - - return nil -} - -// rpcHandler wraps the RPC server with CORS and proper HTTP handling. -type rpcHandler struct { - rpcServer *rpc.Server - logger zerolog.Logger -} - -func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Set CORS headers - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - w.Header().Set("Content-Type", "application/json") - - // Handle preflight requests - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - // Handle GET requests with a simple response - if r.Method == "GET" { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"jsonrpc":"2.0","result":"ev-node in-process geth RPC","id":null}`)) - return - } - - // Handle POST requests via the RPC server - if r.Method == "POST" { - h.rpcServer.ServeHTTP(w, r) - return - } - - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) -} - -// NetRPCService implements the net_ JSON-RPC namespace. -type NetRPCService struct { - backend *GethBackend -} - -// Version returns the network ID. -func (s *NetRPCService) Version() string { - return s.backend.chainConfig.ChainID.String() -} - -// Listening returns true if the node is listening for connections. -func (s *NetRPCService) Listening() bool { - return true -} - -// PeerCount returns the number of connected peers. -func (s *NetRPCService) PeerCount() hexutil.Uint { - return 0 -} - -// Web3RPCService implements the web3_ JSON-RPC namespace. -type Web3RPCService struct{} - -// ClientVersion returns the client version. -func (s *Web3RPCService) ClientVersion() string { - return "ev-node/geth/1.0.0" -} - -// Sha3 returns the Keccak-256 hash of the input. -func (s *Web3RPCService) Sha3(input hexutil.Bytes) hexutil.Bytes { - hash := common.BytesToHash(input) - return hash[:] -} - -// ChainId returns the chain ID. -func (s *EthRPCService) ChainId() *hexutil.Big { - return (*hexutil.Big)(s.backend.chainConfig.ChainID) -} - -// BlockNumber returns the current block number. -func (s *EthRPCService) BlockNumber() hexutil.Uint64 { - header := s.backend.blockchain.CurrentBlock() - if header == nil { - return 0 - } - return hexutil.Uint64(header.Number.Uint64()) -} - -// GetBlockByNumber returns block information by number. -func (s *EthRPCService) GetBlockByNumber(blockNr rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { - var block *types.Block - if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { - header := s.backend.blockchain.CurrentBlock() - if header == nil { - return nil, nil - } - block = s.backend.blockchain.GetBlock(header.Hash(), header.Number.Uint64()) - } else { - block = s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) - } - if block == nil { - return nil, nil - } - return s.formatBlock(block, fullTx), nil -} - -// GetBlockByHash returns block information by hash. -func (s *EthRPCService) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { - block := s.backend.blockchain.GetBlockByHash(hash) - if block == nil { - return nil, nil - } - return s.formatBlock(block, fullTx), nil -} - -// formatBlock formats a block for JSON-RPC response. -func (s *EthRPCService) formatBlock(block *types.Block, fullTx bool) map[string]interface{} { - header := block.Header() - result := map[string]interface{}{ - "number": (*hexutil.Big)(header.Number), - "hash": block.Hash(), - "parentHash": header.ParentHash, - "nonce": header.Nonce, - "sha3Uncles": header.UncleHash, - "logsBloom": header.Bloom, - "transactionsRoot": header.TxHash, - "stateRoot": header.Root, - "receiptsRoot": header.ReceiptHash, - "miner": header.Coinbase, - "difficulty": (*hexutil.Big)(header.Difficulty), - "extraData": hexutil.Bytes(header.Extra), - "size": hexutil.Uint64(block.Size()), - "gasLimit": hexutil.Uint64(header.GasLimit), - "gasUsed": hexutil.Uint64(header.GasUsed), - "timestamp": hexutil.Uint64(header.Time), - } - - if header.BaseFee != nil { - result["baseFeePerGas"] = (*hexutil.Big)(header.BaseFee) - } - - txs := block.Transactions() - if fullTx { - txList := make([]map[string]interface{}, len(txs)) - for i, tx := range txs { - txList[i] = s.formatTransaction(tx, block.Hash(), header.Number.Uint64(), uint64(i)) - } - result["transactions"] = txList - } else { - txHashes := make([]common.Hash, len(txs)) - for i, tx := range txs { - txHashes[i] = tx.Hash() - } - result["transactions"] = txHashes - } - - result["uncles"] = []common.Hash{} - return result -} - -// formatTransaction formats a transaction for JSON-RPC response. -func (s *EthRPCService) formatTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64) map[string]interface{} { - signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) - from, _ := types.Sender(signer, tx) - - result := map[string]interface{}{ - "hash": tx.Hash(), - "nonce": hexutil.Uint64(tx.Nonce()), - "blockHash": blockHash, - "blockNumber": (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)), - "transactionIndex": hexutil.Uint64(index), - "from": from, - "to": tx.To(), - "value": (*hexutil.Big)(tx.Value()), - "gas": hexutil.Uint64(tx.Gas()), - "input": hexutil.Bytes(tx.Data()), - } - - if tx.Type() == types.LegacyTxType { - result["gasPrice"] = (*hexutil.Big)(tx.GasPrice()) - } else { - result["gasPrice"] = (*hexutil.Big)(tx.GasPrice()) - result["maxFeePerGas"] = (*hexutil.Big)(tx.GasFeeCap()) - result["maxPriorityFeePerGas"] = (*hexutil.Big)(tx.GasTipCap()) - result["type"] = hexutil.Uint64(tx.Type()) - } - - v, r, ss := tx.RawSignatureValues() - result["v"] = (*hexutil.Big)(v) - result["r"] = (*hexutil.Big)(r) - result["s"] = (*hexutil.Big)(ss) - - return result -} - -// GetBalance returns the balance of an account. -func (s *EthRPCService) GetBalance(address common.Address, blockNr rpc.BlockNumber) (*hexutil.Big, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return nil, err - } - return (*hexutil.Big)(stateDB.GetBalance(address).ToBig()), nil -} - -// GetTransactionCount returns the nonce of an account. -func (s *EthRPCService) GetTransactionCount(address common.Address, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return 0, err - } - return hexutil.Uint64(stateDB.GetNonce(address)), nil -} - -// GetCode returns the code at an address. -func (s *EthRPCService) GetCode(address common.Address, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return nil, err - } - return stateDB.GetCode(address), nil -} - -// GetStorageAt returns the storage value at a position. -func (s *EthRPCService) GetStorageAt(address common.Address, position hexutil.Big, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return nil, err - } - value := stateDB.GetState(address, common.BigToHash((*big.Int)(&position))) - return value[:], nil -} - -// stateAtBlock returns the state at the given block number. -func (s *EthRPCService) stateAtBlock(blockNr rpc.BlockNumber) (*state.StateDB, error) { - var header *types.Header - if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { - header = s.backend.blockchain.CurrentBlock() - } else { - block := s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) - if block == nil { - return nil, fmt.Errorf("block %d not found", blockNr) - } - header = block.Header() - } - if header == nil { - return nil, errors.New("no current block") - } - return s.backend.blockchain.StateAt(header.Root) -} - -// Call executes a call without creating a transaction. -func (s *EthRPCService) Call(args TransactionArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return nil, err - } - - header := s.backend.blockchain.CurrentBlock() - if header == nil { - return nil, errors.New("no current block") - } - - msg := args.ToMessage(header.BaseFee) - blockContext := core.NewEVMBlockContext(header, s.backend.blockchain, nil) - txContext := core.NewEVMTxContext(msg) - evm := vm.NewEVM(blockContext, stateDB, s.backend.chainConfig, vm.Config{}) - evm.SetTxContext(txContext) - - gp := new(core.GasPool).AddGas(header.GasLimit) - result, err := core.ApplyMessage(evm, msg, gp) - if err != nil { - return nil, err - } - if result.Err != nil { - return nil, result.Err - } - return result.Return(), nil -} - -// EstimateGas estimates the gas needed for a transaction. -func (s *EthRPCService) EstimateGas(args TransactionArgs, blockNr *rpc.BlockNumber) (hexutil.Uint64, error) { - blockNumber := rpc.LatestBlockNumber - if blockNr != nil { - blockNumber = *blockNr - } - - stateDB, err := s.stateAtBlock(blockNumber) - if err != nil { - return 0, err - } - - header := s.backend.blockchain.CurrentBlock() - if header == nil { - return 0, errors.New("no current block") - } - - // Use a high gas limit for estimation - hi := header.GasLimit - if args.Gas != nil && uint64(*args.Gas) < hi { - hi = uint64(*args.Gas) - } - - lo := uint64(21000) - - // Binary search for the optimal gas - for lo+1 < hi { - mid := (lo + hi) / 2 - args.Gas = (*hexutil.Uint64)(&mid) - - msg := args.ToMessage(header.BaseFee) - stateCopy := stateDB.Copy() - blockContext := core.NewEVMBlockContext(header, s.backend.blockchain, nil) - txContext := core.NewEVMTxContext(msg) - evm := vm.NewEVM(blockContext, stateCopy, s.backend.chainConfig, vm.Config{}) - evm.SetTxContext(txContext) - - gp := new(core.GasPool).AddGas(mid) - result, err := core.ApplyMessage(evm, msg, gp) - if err != nil || result.Failed() { - lo = mid - } else { - hi = mid - } - } - - return hexutil.Uint64(hi), nil -} - -// GasPrice returns the current gas price. -func (s *EthRPCService) GasPrice() *hexutil.Big { - header := s.backend.blockchain.CurrentBlock() - if header == nil || header.BaseFee == nil { - return (*hexutil.Big)(big.NewInt(params.InitialBaseFee)) - } - // Return base fee + 1 gwei tip - tip := big.NewInt(1e9) - return (*hexutil.Big)(new(big.Int).Add(header.BaseFee, tip)) -} - -// MaxPriorityFeePerGas returns the suggested priority fee. -func (s *EthRPCService) MaxPriorityFeePerGas() *hexutil.Big { - return (*hexutil.Big)(big.NewInt(1e9)) // 1 gwei -} - -// SendRawTransaction sends a signed transaction. -func (s *EthRPCService) SendRawTransaction(encodedTx hexutil.Bytes) (common.Hash, error) { - tx := new(types.Transaction) - if err := tx.UnmarshalBinary(encodedTx); err != nil { - return common.Hash{}, err - } - - errs := s.backend.txPool.Add([]*types.Transaction{tx}, true) - if len(errs) > 0 && errs[0] != nil { - return common.Hash{}, errs[0] - } - - s.logger.Info(). - Str("tx_hash", tx.Hash().Hex()). - Msg("received raw transaction") - - return tx.Hash(), nil -} - -// GetTransactionByHash returns transaction info by hash. -func (s *EthRPCService) GetTransactionByHash(hash common.Hash) (map[string]interface{}, error) { - // Search in recent blocks - currentBlock := s.backend.blockchain.CurrentBlock() - if currentBlock == nil { - return nil, nil - } - - // Search backwards through blocks - for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { - block := s.backend.blockchain.GetBlockByNumber(i) - if block == nil { - continue - } - for idx, tx := range block.Transactions() { - if tx.Hash() == hash { - return s.formatTransaction(tx, block.Hash(), block.NumberU64(), uint64(idx)), nil - } - } - } - return nil, nil -} - -// GetTransactionReceipt returns the receipt of a transaction. -func (s *EthRPCService) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { - // Search in recent blocks - currentBlock := s.backend.blockchain.CurrentBlock() - if currentBlock == nil { - return nil, nil - } - - // Search backwards through blocks - for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { - block := s.backend.blockchain.GetBlockByNumber(i) - if block == nil { - continue - } - for idx, tx := range block.Transactions() { - if tx.Hash() == hash { - receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) - if len(receipts) <= idx { - return nil, nil - } - receipt := receipts[idx] - - signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) - from, _ := types.Sender(signer, tx) - - result := map[string]interface{}{ - "transactionHash": hash, - "transactionIndex": hexutil.Uint64(idx), - "blockHash": block.Hash(), - "blockNumber": (*hexutil.Big)(block.Number()), - "from": from, - "to": tx.To(), - "cumulativeGasUsed": hexutil.Uint64(receipt.CumulativeGasUsed), - "gasUsed": hexutil.Uint64(receipt.GasUsed), - "contractAddress": nil, - "logs": receipt.Logs, - "logsBloom": receipt.Bloom, - "status": hexutil.Uint(receipt.Status), - "effectiveGasPrice": (*hexutil.Big)(tx.GasPrice()), - "type": hexutil.Uint(tx.Type()), - } - - if receipt.ContractAddress != (common.Address{}) { - result["contractAddress"] = receipt.ContractAddress - } - - return result, nil - } - } - } - return nil, nil -} - -// GetLogs returns logs matching the filter criteria. -func (s *EthRPCService) GetLogs(filter FilterQuery) ([]*types.Log, error) { - var fromBlock, toBlock uint64 - - if filter.FromBlock == nil { - fromBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() - } else { - fromBlock = filter.FromBlock.Uint64() - } - - if filter.ToBlock == nil { - toBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() - } else { - toBlock = filter.ToBlock.Uint64() - } - - var logs []*types.Log - for i := fromBlock; i <= toBlock; i++ { - block := s.backend.blockchain.GetBlockByNumber(i) - if block == nil { - continue - } - receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) - for _, receipt := range receipts { - for _, log := range receipt.Logs { - if s.matchLog(log, filter) { - logs = append(logs, log) - } - } - } - } - return logs, nil -} - -// matchLog checks if a log matches the filter criteria. -func (s *EthRPCService) matchLog(log *types.Log, filter FilterQuery) bool { - if len(filter.Addresses) > 0 { - found := false - for _, addr := range filter.Addresses { - if log.Address == addr { - found = true - break - } - } - if !found { - return false - } - } - - for i, topics := range filter.Topics { - if len(topics) == 0 { - continue - } - if i >= len(log.Topics) { - return false - } - found := false - for _, topic := range topics { - if log.Topics[i] == topic { - found = true - break - } - } - if !found { - return false - } - } - return true -} - -// FilterQuery represents a filter for logs. -type FilterQuery struct { - FromBlock *big.Int `json:"fromBlock"` - ToBlock *big.Int `json:"toBlock"` - Addresses []common.Address `json:"address"` - Topics [][]common.Hash `json:"topics"` -} - -// TransactionArgs represents the arguments to construct a transaction. -type TransactionArgs struct { - From *common.Address `json:"from"` - To *common.Address `json:"to"` - Gas *hexutil.Uint64 `json:"gas"` - GasPrice *hexutil.Big `json:"gasPrice"` - MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` - MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` - Value *hexutil.Big `json:"value"` - Data *hexutil.Bytes `json:"data"` - Input *hexutil.Bytes `json:"input"` - Nonce *hexutil.Uint64 `json:"nonce"` -} - -// ToMessage converts TransactionArgs to a core.Message. -func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message { - var from common.Address - if args.From != nil { - from = *args.From - } - - var to *common.Address - if args.To != nil { - to = args.To - } - - var gas uint64 = 50000000 - if args.Gas != nil { - gas = uint64(*args.Gas) - } - - var value *big.Int - if args.Value != nil { - value = (*big.Int)(args.Value) - } else { - value = big.NewInt(0) - } - - var data []byte - if args.Data != nil { - data = *args.Data - } else if args.Input != nil { - data = *args.Input - } - - var gasPrice *big.Int - if args.GasPrice != nil { - gasPrice = (*big.Int)(args.GasPrice) - } else if baseFee != nil { - gasPrice = new(big.Int).Add(baseFee, big.NewInt(1e9)) - } else { - gasPrice = big.NewInt(params.InitialBaseFee) - } - - msg := &core.Message{ - From: from, - To: to, - Value: value, - GasLimit: gas, - GasPrice: gasPrice, - GasFeeCap: gasPrice, - GasTipCap: big.NewInt(0), - Data: data, - SkipNonceChecks: true, - } - return msg -} - -// GetTxs returns pending transactions (for txpoolExt compatibility). -func (s *TxPoolExtService) GetTxs() ([]string, error) { - pending := s.backend.txPool.Pending(txpool.PendingFilter{}) - var result []string - for _, txs := range pending { - for _, lazyTx := range txs { - tx := lazyTx.Tx - if tx == nil { - continue - } - data, err := tx.MarshalBinary() - if err != nil { - continue - } - result = append(result, "0x"+hex.EncodeToString(data)) - } - } - return result, nil -} - // ForkchoiceUpdated implements EngineRPCClient. func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine.ForkchoiceStateV1, attrs map[string]any) (*engine.ForkChoiceResponse, error) { // Check context before acquiring lock @@ -1662,7 +986,7 @@ func applyTransaction( // Set contract address if this was a contract creation if msg.To == nil { - receipt.ContractAddress = evmInstance.TxContext.Origin + receipt.ContractAddress = evmInstance.Origin } return receipt, nil diff --git a/execution/evm/engine_geth_rpc.go b/execution/evm/engine_geth_rpc.go new file mode 100644 index 0000000000..26a24d3bbf --- /dev/null +++ b/execution/evm/engine_geth_rpc.go @@ -0,0 +1,695 @@ +package evm + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "net" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/rs/zerolog" +) + +// EthRPCService implements the eth_ JSON-RPC namespace. +type EthRPCService struct { + backend *GethBackend + logger zerolog.Logger +} + +// TxPoolExtService implements the txpoolExt_ JSON-RPC namespace. +type TxPoolExtService struct { + backend *GethBackend + logger zerolog.Logger +} + +// NetRPCService implements the net_ JSON-RPC namespace. +type NetRPCService struct { + backend *GethBackend +} + +// Web3RPCService implements the web3_ JSON-RPC namespace. +type Web3RPCService struct{} + +// rpcHandler wraps the RPC server with CORS and proper HTTP handling. +type rpcHandler struct { + rpcServer *rpc.Server + logger zerolog.Logger +} + +// FilterQuery represents a filter for logs. +type FilterQuery struct { + FromBlock *big.Int `json:"fromBlock"` + ToBlock *big.Int `json:"toBlock"` + Addresses []common.Address `json:"address"` + Topics [][]common.Hash `json:"topics"` +} + +// TransactionArgs represents the arguments to construct a transaction. +type TransactionArgs struct { + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` + MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` + Value *hexutil.Big `json:"value"` + Data *hexutil.Bytes `json:"data"` + Input *hexutil.Bytes `json:"input"` + Nonce *hexutil.Uint64 `json:"nonce"` +} + +// StartRPCServer starts the JSON-RPC server on the specified address. +// If address is empty, the server is not started. +func (b *GethBackend) StartRPCServer(address string) error { + if address == "" { + return nil + } + + b.rpcServer = rpc.NewServer() + + // Register eth_ namespace + ethService := &EthRPCService{backend: b, logger: b.logger.With().Str("rpc", "eth").Logger()} + if err := b.rpcServer.RegisterName("eth", ethService); err != nil { + return fmt.Errorf("failed to register eth service: %w", err) + } + + // Register txpoolExt_ namespace for compatibility + txpoolService := &TxPoolExtService{backend: b, logger: b.logger.With().Str("rpc", "txpoolExt").Logger()} + if err := b.rpcServer.RegisterName("txpoolExt", txpoolService); err != nil { + return fmt.Errorf("failed to register txpoolExt service: %w", err) + } + + // Register net_ namespace + netService := &NetRPCService{backend: b} + if err := b.rpcServer.RegisterName("net", netService); err != nil { + return fmt.Errorf("failed to register net service: %w", err) + } + + // Register web3_ namespace + web3Service := &Web3RPCService{} + if err := b.rpcServer.RegisterName("web3", web3Service); err != nil { + return fmt.Errorf("failed to register web3 service: %w", err) + } + + // Create HTTP handler with CORS support + handler := &rpcHandler{ + rpcServer: b.rpcServer, + logger: b.logger, + } + + listener, err := net.Listen("tcp", address) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", address, err) + } + b.rpcListener = listener + + b.httpServer = &http.Server{ + Handler: handler, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } + + go func() { + b.logger.Info().Str("address", address).Msg("starting JSON-RPC server") + if err := b.httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + b.logger.Error().Err(err).Msg("JSON-RPC server error") + } + }() + + return nil +} + +func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Content-Type", "application/json") + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + // Handle GET requests with a simple response + if r.Method == "GET" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","result":"ev-node in-process geth RPC","id":null}`)) + return + } + + // Handle POST requests via the RPC server + if r.Method == "POST" { + h.rpcServer.ServeHTTP(w, r) + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +// Version returns the network ID. +func (s *NetRPCService) Version() string { + return s.backend.chainConfig.ChainID.String() +} + +// Listening returns true if the node is listening for connections. +func (s *NetRPCService) Listening() bool { + return true +} + +// PeerCount returns the number of connected peers. +func (s *NetRPCService) PeerCount() hexutil.Uint { + return 0 +} + +// ClientVersion returns the client version. +func (s *Web3RPCService) ClientVersion() string { + return "ev-node/geth/1.0.0" +} + +// Sha3 returns the Keccak-256 hash of the input. +func (s *Web3RPCService) Sha3(input hexutil.Bytes) hexutil.Bytes { + hash := common.BytesToHash(input) + return hash[:] +} + +// ChainId returns the chain ID. +func (s *EthRPCService) ChainId() *hexutil.Big { + return (*hexutil.Big)(s.backend.chainConfig.ChainID) +} + +// BlockNumber returns the current block number. +func (s *EthRPCService) BlockNumber() hexutil.Uint64 { + header := s.backend.blockchain.CurrentBlock() + if header == nil { + return 0 + } + return hexutil.Uint64(header.Number.Uint64()) +} + +// GetBlockByNumber returns block information by number. +func (s *EthRPCService) GetBlockByNumber(blockNr rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { + var block *types.Block + if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { + header := s.backend.blockchain.CurrentBlock() + if header == nil { + return nil, nil + } + block = s.backend.blockchain.GetBlock(header.Hash(), header.Number.Uint64()) + } else { + block = s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) + } + if block == nil { + return nil, nil + } + return s.formatBlock(block, fullTx), nil +} + +// GetBlockByHash returns block information by hash. +func (s *EthRPCService) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + block := s.backend.blockchain.GetBlockByHash(hash) + if block == nil { + return nil, nil + } + return s.formatBlock(block, fullTx), nil +} + +// formatBlock formats a block for JSON-RPC response. +func (s *EthRPCService) formatBlock(block *types.Block, fullTx bool) map[string]interface{} { + header := block.Header() + result := map[string]interface{}{ + "number": (*hexutil.Big)(header.Number), + "hash": block.Hash(), + "parentHash": header.ParentHash, + "nonce": header.Nonce, + "sha3Uncles": header.UncleHash, + "logsBloom": header.Bloom, + "transactionsRoot": header.TxHash, + "stateRoot": header.Root, + "receiptsRoot": header.ReceiptHash, + "miner": header.Coinbase, + "difficulty": (*hexutil.Big)(header.Difficulty), + "extraData": hexutil.Bytes(header.Extra), + "size": hexutil.Uint64(block.Size()), + "gasLimit": hexutil.Uint64(header.GasLimit), + "gasUsed": hexutil.Uint64(header.GasUsed), + "timestamp": hexutil.Uint64(header.Time), + } + + if header.BaseFee != nil { + result["baseFeePerGas"] = (*hexutil.Big)(header.BaseFee) + } + + txs := block.Transactions() + if fullTx { + txList := make([]map[string]interface{}, len(txs)) + for i, tx := range txs { + txList[i] = s.formatTransaction(tx, block.Hash(), header.Number.Uint64(), uint64(i)) + } + result["transactions"] = txList + } else { + txHashes := make([]common.Hash, len(txs)) + for i, tx := range txs { + txHashes[i] = tx.Hash() + } + result["transactions"] = txHashes + } + + result["uncles"] = []common.Hash{} + return result +} + +// formatTransaction formats a transaction for JSON-RPC response. +func (s *EthRPCService) formatTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64) map[string]interface{} { + signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) + from, _ := types.Sender(signer, tx) + + result := map[string]interface{}{ + "hash": tx.Hash(), + "nonce": hexutil.Uint64(tx.Nonce()), + "blockHash": blockHash, + "blockNumber": (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)), + "transactionIndex": hexutil.Uint64(index), + "from": from, + "to": tx.To(), + "value": (*hexutil.Big)(tx.Value()), + "gas": hexutil.Uint64(tx.Gas()), + "input": hexutil.Bytes(tx.Data()), + } + + if tx.Type() == types.LegacyTxType { + result["gasPrice"] = (*hexutil.Big)(tx.GasPrice()) + } else { + result["gasPrice"] = (*hexutil.Big)(tx.GasPrice()) + result["maxFeePerGas"] = (*hexutil.Big)(tx.GasFeeCap()) + result["maxPriorityFeePerGas"] = (*hexutil.Big)(tx.GasTipCap()) + result["type"] = hexutil.Uint64(tx.Type()) + } + + v, r, ss := tx.RawSignatureValues() + result["v"] = (*hexutil.Big)(v) + result["r"] = (*hexutil.Big)(r) + result["s"] = (*hexutil.Big)(ss) + + return result +} + +// GetBalance returns the balance of an account. +func (s *EthRPCService) GetBalance(address common.Address, blockNr rpc.BlockNumber) (*hexutil.Big, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + return (*hexutil.Big)(stateDB.GetBalance(address).ToBig()), nil +} + +// GetTransactionCount returns the nonce of an account. +func (s *EthRPCService) GetTransactionCount(address common.Address, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return 0, err + } + return hexutil.Uint64(stateDB.GetNonce(address)), nil +} + +// GetCode returns the code at an address. +func (s *EthRPCService) GetCode(address common.Address, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + return stateDB.GetCode(address), nil +} + +// GetStorageAt returns the storage value at a position. +func (s *EthRPCService) GetStorageAt(address common.Address, position hexutil.Big, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + value := stateDB.GetState(address, common.BigToHash((*big.Int)(&position))) + return value[:], nil +} + +// stateAtBlock returns the state at the given block number. +func (s *EthRPCService) stateAtBlock(blockNr rpc.BlockNumber) (*state.StateDB, error) { + var header *types.Header + if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { + header = s.backend.blockchain.CurrentBlock() + } else { + block := s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) + if block == nil { + return nil, fmt.Errorf("block %d not found", blockNr) + } + header = block.Header() + } + if header == nil { + return nil, errors.New("no current block") + } + return s.backend.blockchain.StateAt(header.Root) +} + +// Call executes a call without creating a transaction. +func (s *EthRPCService) Call(args TransactionArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + + header := s.backend.blockchain.CurrentBlock() + if header == nil { + return nil, errors.New("no current block") + } + + msg := args.ToMessage(header.BaseFee) + blockContext := core.NewEVMBlockContext(header, s.backend.blockchain, nil) + txContext := core.NewEVMTxContext(msg) + evm := vm.NewEVM(blockContext, stateDB, s.backend.chainConfig, vm.Config{}) + evm.SetTxContext(txContext) + + gp := new(core.GasPool).AddGas(header.GasLimit) + result, err := core.ApplyMessage(evm, msg, gp) + if err != nil { + return nil, err + } + if result.Err != nil { + return nil, result.Err + } + return result.Return(), nil +} + +// EstimateGas estimates the gas needed for a transaction. +func (s *EthRPCService) EstimateGas(args TransactionArgs, blockNr *rpc.BlockNumber) (hexutil.Uint64, error) { + blockNumber := rpc.LatestBlockNumber + if blockNr != nil { + blockNumber = *blockNr + } + + stateDB, err := s.stateAtBlock(blockNumber) + if err != nil { + return 0, err + } + + header := s.backend.blockchain.CurrentBlock() + if header == nil { + return 0, errors.New("no current block") + } + + // Use a high gas limit for estimation + hi := header.GasLimit + if args.Gas != nil && uint64(*args.Gas) < hi { + hi = uint64(*args.Gas) + } + + lo := uint64(21000) + + // Binary search for the optimal gas + for lo+1 < hi { + mid := (lo + hi) / 2 + args.Gas = (*hexutil.Uint64)(&mid) + + msg := args.ToMessage(header.BaseFee) + stateCopy := stateDB.Copy() + blockContext := core.NewEVMBlockContext(header, s.backend.blockchain, nil) + txContext := core.NewEVMTxContext(msg) + evm := vm.NewEVM(blockContext, stateCopy, s.backend.chainConfig, vm.Config{}) + evm.SetTxContext(txContext) + + gp := new(core.GasPool).AddGas(mid) + result, err := core.ApplyMessage(evm, msg, gp) + if err != nil || result.Failed() { + lo = mid + } else { + hi = mid + } + } + + return hexutil.Uint64(hi), nil +} + +// GasPrice returns the current gas price. +func (s *EthRPCService) GasPrice() *hexutil.Big { + header := s.backend.blockchain.CurrentBlock() + if header == nil || header.BaseFee == nil { + return (*hexutil.Big)(big.NewInt(params.InitialBaseFee)) + } + // Return base fee + 1 gwei tip + tip := big.NewInt(1e9) + return (*hexutil.Big)(new(big.Int).Add(header.BaseFee, tip)) +} + +// MaxPriorityFeePerGas returns the suggested priority fee. +func (s *EthRPCService) MaxPriorityFeePerGas() *hexutil.Big { + return (*hexutil.Big)(big.NewInt(1e9)) // 1 gwei +} + +// SendRawTransaction sends a signed transaction. +func (s *EthRPCService) SendRawTransaction(encodedTx hexutil.Bytes) (common.Hash, error) { + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(encodedTx); err != nil { + return common.Hash{}, err + } + + errs := s.backend.txPool.Add([]*types.Transaction{tx}, true) + if len(errs) > 0 && errs[0] != nil { + return common.Hash{}, errs[0] + } + + s.logger.Info(). + Str("tx_hash", tx.Hash().Hex()). + Msg("received raw transaction") + + return tx.Hash(), nil +} + +// GetTransactionByHash returns transaction info by hash. +func (s *EthRPCService) GetTransactionByHash(hash common.Hash) (map[string]interface{}, error) { + // Search in recent blocks + currentBlock := s.backend.blockchain.CurrentBlock() + if currentBlock == nil { + return nil, nil + } + + // Search backwards through blocks + for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { + block := s.backend.blockchain.GetBlockByNumber(i) + if block == nil { + continue + } + for idx, tx := range block.Transactions() { + if tx.Hash() == hash { + return s.formatTransaction(tx, block.Hash(), block.NumberU64(), uint64(idx)), nil + } + } + } + return nil, nil +} + +// GetTransactionReceipt returns the receipt of a transaction. +func (s *EthRPCService) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + // Search in recent blocks + currentBlock := s.backend.blockchain.CurrentBlock() + if currentBlock == nil { + return nil, nil + } + + // Search backwards through blocks + for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { + block := s.backend.blockchain.GetBlockByNumber(i) + if block == nil { + continue + } + for idx, tx := range block.Transactions() { + if tx.Hash() == hash { + receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) + if len(receipts) <= idx { + return nil, nil + } + receipt := receipts[idx] + + signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) + from, _ := types.Sender(signer, tx) + + result := map[string]interface{}{ + "transactionHash": hash, + "transactionIndex": hexutil.Uint64(idx), + "blockHash": block.Hash(), + "blockNumber": (*hexutil.Big)(block.Number()), + "from": from, + "to": tx.To(), + "cumulativeGasUsed": hexutil.Uint64(receipt.CumulativeGasUsed), + "gasUsed": hexutil.Uint64(receipt.GasUsed), + "contractAddress": nil, + "logs": receipt.Logs, + "logsBloom": receipt.Bloom, + "status": hexutil.Uint(receipt.Status), + "effectiveGasPrice": (*hexutil.Big)(tx.GasPrice()), + "type": hexutil.Uint(tx.Type()), + } + + if receipt.ContractAddress != (common.Address{}) { + result["contractAddress"] = receipt.ContractAddress + } + + return result, nil + } + } + } + return nil, nil +} + +// GetLogs returns logs matching the filter criteria. +func (s *EthRPCService) GetLogs(filter FilterQuery) ([]*types.Log, error) { + var fromBlock, toBlock uint64 + + if filter.FromBlock == nil { + fromBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() + } else { + fromBlock = filter.FromBlock.Uint64() + } + + if filter.ToBlock == nil { + toBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() + } else { + toBlock = filter.ToBlock.Uint64() + } + + var logs []*types.Log + for i := fromBlock; i <= toBlock; i++ { + block := s.backend.blockchain.GetBlockByNumber(i) + if block == nil { + continue + } + receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) + for _, receipt := range receipts { + for _, log := range receipt.Logs { + if s.matchLog(log, filter) { + logs = append(logs, log) + } + } + } + } + return logs, nil +} + +// matchLog checks if a log matches the filter criteria. +func (s *EthRPCService) matchLog(log *types.Log, filter FilterQuery) bool { + if len(filter.Addresses) > 0 { + found := false + for _, addr := range filter.Addresses { + if log.Address == addr { + found = true + break + } + } + if !found { + return false + } + } + + for i, topics := range filter.Topics { + if len(topics) == 0 { + continue + } + if i >= len(log.Topics) { + return false + } + found := false + for _, topic := range topics { + if log.Topics[i] == topic { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +// ToMessage converts TransactionArgs to a core.Message. +func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message { + var from common.Address + if args.From != nil { + from = *args.From + } + + var to *common.Address + if args.To != nil { + to = args.To + } + + var gas uint64 = 50000000 + if args.Gas != nil { + gas = uint64(*args.Gas) + } + + var value *big.Int + if args.Value != nil { + value = (*big.Int)(args.Value) + } else { + value = big.NewInt(0) + } + + var data []byte + if args.Data != nil { + data = *args.Data + } else if args.Input != nil { + data = *args.Input + } + + var gasPrice *big.Int + if args.GasPrice != nil { + gasPrice = (*big.Int)(args.GasPrice) + } else if baseFee != nil { + gasPrice = new(big.Int).Add(baseFee, big.NewInt(1e9)) + } else { + gasPrice = big.NewInt(params.InitialBaseFee) + } + + msg := &core.Message{ + From: from, + To: to, + Value: value, + GasLimit: gas, + GasPrice: gasPrice, + GasFeeCap: gasPrice, + GasTipCap: big.NewInt(0), + Data: data, + SkipNonceChecks: true, + } + return msg +} + +// GetTxs returns pending transactions (for txpoolExt compatibility). +func (s *TxPoolExtService) GetTxs() ([]string, error) { + pending := s.backend.txPool.Pending(txpool.PendingFilter{}) + var result []string + for _, txs := range pending { + for _, lazyTx := range txs { + tx := lazyTx.Tx + if tx == nil { + continue + } + data, err := tx.MarshalBinary() + if err != nil { + continue + } + result = append(result, "0x"+hex.EncodeToString(data)) + } + } + return result, nil +} From f3244aac6fe015cda6816bf792bb053444e773f1 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 17:53:01 +0100 Subject: [PATCH 11/18] Update execution_test.go --- execution/evm/test/execution_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/execution/evm/test/execution_test.go b/execution/evm/test/execution_test.go index be347aeea1..184c5f50ef 100644 --- a/execution/evm/test/execution_test.go +++ b/execution/evm/test/execution_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ds "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" + "github.com/rs/zerolog" "github.com/stretchr/testify/require" "github.com/evstack/ev-node/execution/evm" @@ -75,6 +76,7 @@ func TestEngineExecution(t *testing.T) { common.Address{}, store, false, + zerolog.Nop(), ) require.NoError(tt, err) @@ -176,6 +178,7 @@ func TestEngineExecution(t *testing.T) { common.Address{}, store, false, + zerolog.Nop(), ) require.NoError(tt, err) From 8df657c603d952dd68a57acb87c8cd07c9fb34b6 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Sat, 17 Jan 2026 00:51:18 +0100 Subject: [PATCH 12/18] allow subseconds blocks (does not work yet) --- execution/evm/engine_geth.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index 6c2d859ee8..756c993a08 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -488,8 +488,8 @@ func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildSta // Validate block number continuity expectedNumber := new(big.Int).Add(parent.Number(), big.NewInt(1)) - // Validate timestamp - if ps.timestamp <= parent.Time() { + // Validate timestamp allow equal time to allow sub seconds blocks + if ps.timestamp < parent.Time() { return nil, fmt.Errorf("invalid timestamp: %d must be greater than parent timestamp %d", ps.timestamp, parent.Time()) } @@ -704,7 +704,7 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu } // Validate timestamp - if payload.Timestamp <= parent.Time() { + if payload.Timestamp < parent.Time() { g.logger.Warn(). Uint64("payload_timestamp", payload.Timestamp). Uint64("parent_timestamp", parent.Time()). From ebf415b277810d9ea862efd78c603c53f06c7df7 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 23 Jan 2026 13:08:46 +0100 Subject: [PATCH 13/18] go mod tidy --- execution/evm/test/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/execution/evm/test/go.mod b/execution/evm/test/go.mod index 9a6bd74f6f..a7bb560a88 100644 --- a/execution/evm/test/go.mod +++ b/execution/evm/test/go.mod @@ -8,6 +8,7 @@ require ( github.com/evstack/ev-node/execution/evm v0.0.0-00010101000000-000000000000 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/ipfs/go-datastore v0.9.0 + github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 ) @@ -150,7 +151,6 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/rs/zerolog v1.34.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect From 9fba0c08fc75178ec3ad679217fb5859aae4fd85 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 23 Jan 2026 15:36:48 +0100 Subject: [PATCH 14/18] loosen consensus requirements to allow sub second blocks --- execution/evm/engine_geth.go | 16 ++--- execution/evm/engine_geth_consensus.go | 93 ++++++++++++++++++++++++++ execution/evm/engine_geth_test.go | 9 +-- 3 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 execution/evm/engine_geth_consensus.go diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index 756c993a08..85af3cb72e 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -13,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" @@ -35,10 +34,8 @@ var ( _ EthRPCClient = (*gethEthClient)(nil) ) -const ( - // baseFeeChangeDenominator is the EIP-1559 base fee change denominator. - baseFeeChangeDenominator = 8 -) +// baseFeeChangeDenominator is the EIP-1559 base fee change denominator. +const baseFeeChangeDenominator = 8 // GethBackend holds the in-process geth components. type GethBackend struct { @@ -182,7 +179,8 @@ func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger // Create blockchain config bcConfig := core.DefaultConfig().WithStateScheme(rawdb.HashScheme) - consensusEngine := beacon.New(nil) + // Use sovereign beacon consensus that allows equal timestamps for subsecond block times + consensusEngine := newSovereignBeacon() // Create the blockchain blockchain, err := core.NewBlockChain(ethdb, genesis, consensusEngine, bcConfig) @@ -488,9 +486,8 @@ func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildSta // Validate block number continuity expectedNumber := new(big.Int).Add(parent.Number(), big.NewInt(1)) - // Validate timestamp allow equal time to allow sub seconds blocks if ps.timestamp < parent.Time() { - return nil, fmt.Errorf("invalid timestamp: %d must be greater than parent timestamp %d", ps.timestamp, parent.Time()) + return nil, fmt.Errorf("invalid timestamp: %d must be >= parent timestamp %d", ps.timestamp, parent.Time()) } // Calculate base fee for the new block @@ -517,7 +514,6 @@ func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildSta GasLimit: gasLimit, GasUsed: 0, Time: ps.timestamp, - Extra: []byte{}, MixDigest: ps.prevRandao, Nonce: types.BlockNonce{}, BaseFee: baseFee, @@ -708,7 +704,7 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu g.logger.Warn(). Uint64("payload_timestamp", payload.Timestamp). Uint64("parent_timestamp", parent.Time()). - Msg("invalid timestamp") + Msg("invalid timestamp: must be >= parent timestamp") parentHash := parent.Hash() return &engine.PayloadStatusV1{ Status: engine.INVALID, diff --git a/execution/evm/engine_geth_consensus.go b/execution/evm/engine_geth_consensus.go new file mode 100644 index 0000000000..54cc955736 --- /dev/null +++ b/execution/evm/engine_geth_consensus.go @@ -0,0 +1,93 @@ +package evm + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +// sovereignBeacon wraps the standard beacon consensus engine but allows +// equal timestamps (timestamp >= parent.timestamp) instead of requiring +// strictly increasing timestamps (timestamp > parent.timestamp). +// This enables subsecond block times for sovereign rollups while keeping +// all other beacon consensus rules intact. +type sovereignBeacon struct { + consensus.Engine +} + +// newSovereignBeacon creates a beacon consensus engine that allows equal timestamps. +func newSovereignBeacon() *sovereignBeacon { + return &sovereignBeacon{ + Engine: beacon.New(nil), + } +} + +// VerifyHeader checks whether a header conforms to the consensus rules. +// This override allows equal timestamps for subsecond block times. +func (sb *sovereignBeacon) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header) error { + // Get parent header + parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) + if parent == nil { + return consensus.ErrUnknownAncestor + } + + // Check timestamp - allow equal (>=) instead of strictly greater (>) + if header.Time < parent.Time { + return errors.New("invalid timestamp: must be >= parent timestamp") + } + + // Verify difficulty is zero (PoS requirement) + if header.Difficulty.Cmp(common.Big0) != 0 { + return errors.New("invalid difficulty: must be zero for PoS") + } + + // Verify nonce is zero (PoS requirement) + if header.Nonce != (types.BlockNonce{}) { + return errors.New("invalid nonce: must be zero for PoS") + } + + // Verify uncle hash is empty (PoS requirement) + if header.UncleHash != types.EmptyUncleHash { + return errors.New("invalid uncle hash: must be empty for PoS") + } + + if header.GasLimit > params.MaxGasLimit { + return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, params.MaxGasLimit) + } + + // Verify that the gasUsed is <= gasLimit + if header.GasUsed > header.GasLimit { + return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit) + } + + // Verify the header's EIP-1559 attributes. + if err := eip1559.VerifyEIP1559Header(chain.Config(), parent, header); err != nil { + return err + } + + return nil +} + +// VerifyHeaders verifies a batch of headers concurrently. +func (sb *sovereignBeacon) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) { + abort := make(chan struct{}) + results := make(chan error, len(headers)) + + go func() { + for _, header := range headers { + select { + case <-abort: + return + case results <- sb.VerifyHeader(chain, header): + } + } + }() + + return abort, results +} diff --git a/execution/evm/engine_geth_test.go b/execution/evm/engine_geth_test.go index e6cc96786d..87aa20770b 100644 --- a/execution/evm/engine_geth_test.go +++ b/execution/evm/engine_geth_test.go @@ -91,11 +91,9 @@ func TestGethEngineClient_InitChain(t *testing.T) { ctx := context.Background() genesisTime := time.Now() - stateRoot, maxBytes, err := client.InitChain(ctx, genesisTime, 1, "1337") + stateRoot, err := client.InitChain(ctx, genesisTime, 1, "1337") require.NoError(t, err) assert.NotEmpty(t, stateRoot) - // maxBytes is the gas limit from the genesis block - assert.Greater(t, maxBytes, uint64(0)) } func TestGethEngineClient_GetLatestHeight(t *testing.T) { @@ -167,11 +165,11 @@ func TestGethEngineClient_ExecuteTxs_EmptyBlock(t *testing.T) { genesisTime := time.Now() // Initialize chain first - stateRoot, _, err := client.InitChain(ctx, genesisTime, 1, "1337") + stateRoot, err := client.InitChain(ctx, genesisTime, 1, "1337") require.NoError(t, err) // Execute empty block - newStateRoot, gasUsed, err := client.ExecuteTxs( + newStateRoot, err := client.ExecuteTxs( ctx, [][]byte{}, // empty transactions 1, @@ -180,7 +178,6 @@ func TestGethEngineClient_ExecuteTxs_EmptyBlock(t *testing.T) { ) require.NoError(t, err) assert.NotEmpty(t, newStateRoot) - assert.Equal(t, uint64(0), gasUsed) // No transactions, no gas used } func TestGethEngineClient_ForkchoiceUpdated(t *testing.T) { From b9890aa26737538e7b0a6d12f53839b5c28228d3 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 23 Jan 2026 16:32:04 +0100 Subject: [PATCH 15/18] updates --- apps/evm/cmd/run.go | 9 +++++---- execution/evm/engine_geth.go | 13 +++---------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index 8981e61c73..b52398f6f5 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -65,7 +65,6 @@ var RunCmd = &cobra.Command{ executor, err = createGethExecutionClient( cmd, datastore, - tracingEnabled, logger.With().Str("module", "geth_client").Logger(), ) if err != nil { @@ -263,7 +262,7 @@ func createRethExecutionClient(cmd *cobra.Command, db datastore.Batching, tracin return evm.NewEngineExecutionClient(ethURL, engineURL, jwtSecret, genesisHash, feeRecipient, db, tracingEnabled, logger) } -func createGethExecutionClient(cmd *cobra.Command, db datastore.Batching, tracingEnabled bool, logger zerolog.Logger) (execution.Executor, error) { +func createGethExecutionClient(cmd *cobra.Command, db datastore.Batching, logger zerolog.Logger) (execution.Executor, error) { feeRecipientStr, err := cmd.Flags().GetString(evm.FlagEvmFeeRecipient) if err != nil { return nil, fmt.Errorf("failed to get '%s' flag: %w", evm.FlagEvmFeeRecipient, err) @@ -297,10 +296,12 @@ func addFlags(cmd *cobra.Command) { cmd.Flags().String(evm.FlagEvmGenesisHash, "", "Hash of the genesis block") cmd.Flags().String(evm.FlagEvmFeeRecipient, "", "Address that will receive transaction fees") cmd.Flags().String(flagForceInclusionServer, "", "Address for force inclusion API server (e.g. 127.0.0.1:8547). If set, enables the server for direct DA submission") - cmd.Flags().Bool(evm.FlagEVMInProcessGeth, false, "Use in-process Geth for EVM execution instead of external execution client") cmd.Flags().String(evm.FlagEVMGenesisPath, "", "EVM genesis path for Geth") cmd.Flags().String(evm.FlagEVMRPCAddress, "", "Address for in-process Geth JSON-RPC server (e.g., 127.0.0.1:8545)") - cmd.MarkFlagsMutuallyExclusive(evm.FlagEVMInProcessGeth, evm.FlagEvmEthURL, evm.FlagEvmEngineURL, evm.FlagEvmJWTSecretFile, evm.FlagEvmGenesisHash) + cmd.MarkFlagsMutuallyExclusive(evm.FlagEVMInProcessGeth, evm.FlagEvmEthURL) + cmd.MarkFlagsMutuallyExclusive(evm.FlagEVMInProcessGeth, evm.FlagEvmEngineURL) + cmd.MarkFlagsMutuallyExclusive(evm.FlagEVMInProcessGeth, evm.FlagEvmJWTSecretFile) + cmd.MarkFlagsMutuallyExclusive(evm.FlagEVMInProcessGeth, evm.FlagEvmGenesisHash) } diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index 85af3cb72e..c7b8d4ad89 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -303,8 +303,9 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine } // Generate payload ID deterministically from attributes - payloadID := g.generatePayloadID(fcState.HeadBlockHash, payloadState) - + g.backend.nextPayloadID++ + var payloadID engine.PayloadID + binary.BigEndian.PutUint64(payloadID[:], g.backend.nextPayloadID) g.backend.payloads[payloadID] = payloadState response.PayloadID = &payloadID @@ -320,14 +321,6 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine return response, nil } -// generatePayloadID creates a deterministic payload ID from the build parameters. -func (g *gethEngineClient) generatePayloadID(parentHash common.Hash, ps *payloadBuildState) engine.PayloadID { - g.backend.nextPayloadID++ - var payloadID engine.PayloadID - binary.BigEndian.PutUint64(payloadID[:], g.backend.nextPayloadID) - return payloadID -} - // parsePayloadAttributes extracts payload attributes from the map format. func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs map[string]any) (*payloadBuildState, error) { ps := &payloadBuildState{ From caa5ec3112a309b702ab624e2f72f4ec35a75b89 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 23 Jan 2026 17:35:12 +0100 Subject: [PATCH 16/18] updates --- execution/evm/engine_geth.go | 139 ++++++++- execution/evm/engine_geth_consensus.go | 40 +++ execution/evm/engine_geth_db.go | 195 ++++++++++--- execution/evm/engine_geth_test.go | 390 +++++++++++++++++++++++++ execution/evm/go.mod | 2 +- 5 files changed, 706 insertions(+), 60 deletions(-) diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index c7b8d4ad89..8f8c364066 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -2,6 +2,7 @@ package evm import ( "context" + "crypto/sha256" "encoding/binary" "errors" "fmt" @@ -16,15 +17,18 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool/legacypool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" + "github.com/holiman/uint256" ds "github.com/ipfs/go-datastore" "github.com/rs/zerolog" ) @@ -37,6 +41,12 @@ var ( // baseFeeChangeDenominator is the EIP-1559 base fee change denominator. const baseFeeChangeDenominator = 8 +// payloadTTL is how long a payload can remain in the map before being cleaned up. +const payloadTTL = 60 * time.Second + +// maxPayloads is the maximum number of payloads to keep in memory. +const maxPayloads = 10 + // GethBackend holds the in-process geth components. type GethBackend struct { db ethdb.Database @@ -67,8 +77,14 @@ type payloadBuildState struct { transactions [][]byte gasLimit uint64 + // createdAt tracks when this payload was created for TTL cleanup + createdAt time.Time + // built payload (populated after getPayload) payload *engine.ExecutableData + + // buildErr stores any error that occurred during payload build + buildErr error } // gethEngineClient implements EngineRPCClient using in-process geth. @@ -302,10 +318,26 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine return nil, fmt.Errorf("failed to parse payload attributes: %w", err) } - // Generate payload ID deterministically from attributes - g.backend.nextPayloadID++ - var payloadID engine.PayloadID - binary.BigEndian.PutUint64(payloadID[:], g.backend.nextPayloadID) + // Generate deterministic payload ID from attributes + payloadID := g.generatePayloadID(payloadState) + + // Check if we already have this payload (idempotency) + if existing, ok := g.backend.payloads[payloadID]; ok { + // Reuse existing payload if it hasn't errored + if existing.buildErr == nil { + response.PayloadID = &payloadID + g.logger.Debug(). + Str("payload_id", payloadID.String()). + Msg("reusing existing payload") + return response, nil + } + // Previous build failed, remove it and try again + delete(g.backend.payloads, payloadID) + } + + // Clean up old payloads before adding new one + g.cleanupStalePayloads() + g.backend.payloads[payloadID] = payloadState response.PayloadID = &payloadID @@ -321,11 +353,73 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine return response, nil } +// generatePayloadID creates a deterministic payload ID from the payload attributes. +// This ensures that the same attributes always produce the same ID for idempotency. +func (g *gethEngineClient) generatePayloadID(ps *payloadBuildState) engine.PayloadID { + h := sha256.New() + h.Write(ps.parentHash[:]) + binary.Write(h, binary.BigEndian, ps.timestamp) + h.Write(ps.prevRandao[:]) + h.Write(ps.feeRecipient[:]) + binary.Write(h, binary.BigEndian, ps.gasLimit) + // Include transaction count and first tx hash for uniqueness + binary.Write(h, binary.BigEndian, uint64(len(ps.transactions))) + for _, tx := range ps.transactions { + h.Write(tx) + } + sum := h.Sum(nil) + var id engine.PayloadID + copy(id[:], sum[:8]) + return id +} + +// cleanupStalePayloads removes payloads that have exceeded their TTL or when we have too many. +func (g *gethEngineClient) cleanupStalePayloads() { + now := time.Now() + var staleIDs []engine.PayloadID + + // Find stale payloads + for id, ps := range g.backend.payloads { + if now.Sub(ps.createdAt) > payloadTTL { + staleIDs = append(staleIDs, id) + } + } + + // Remove stale payloads + for _, id := range staleIDs { + delete(g.backend.payloads, id) + g.logger.Debug(). + Str("payload_id", id.String()). + Msg("cleaned up stale payload") + } + + // If still too many payloads, remove oldest ones + for len(g.backend.payloads) >= maxPayloads { + var oldestID engine.PayloadID + var oldestTime time.Time + first := true + for id, ps := range g.backend.payloads { + if first || ps.createdAt.Before(oldestTime) { + oldestID = id + oldestTime = ps.createdAt + first = false + } + } + if !first { + delete(g.backend.payloads, oldestID) + g.logger.Debug(). + Str("payload_id", oldestID.String()). + Msg("evicted oldest payload due to limit") + } + } +} + // parsePayloadAttributes extracts payload attributes from the map format. func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs map[string]any) (*payloadBuildState, error) { ps := &payloadBuildState{ parentHash: parentHash, withdrawals: []*types.Withdrawal{}, + createdAt: time.Now(), } // Parse timestamp (required) @@ -439,11 +533,19 @@ func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.Payl return nil, fmt.Errorf("unknown payload ID: %s", payloadID.String()) } - // Build the block if not already built + // Return cached error if previous build failed + if payloadState.buildErr != nil { + delete(g.backend.payloads, payloadID) + return nil, fmt.Errorf("payload build previously failed: %w", payloadState.buildErr) + } + + // Build the payload if not already built if payloadState.payload == nil { - startTime := time.Now() + buildStartTime := time.Now() payload, err := g.buildPayload(ctx, payloadState) if err != nil { + // Cache the error so we don't retry on the same payload + payloadState.buildErr = err return nil, fmt.Errorf("failed to build payload: %w", err) } payloadState.payload = payload @@ -454,11 +556,11 @@ func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.Payl Str("block_hash", payload.BlockHash.Hex()). Int("tx_count", len(payload.Transactions)). Uint64("gas_used", payload.GasUsed). - Dur("build_time", time.Since(startTime)). + Dur("build_time", time.Since(buildStartTime)). Msg("built payload") } - // Remove the payload from pending after retrieval + // Remove the payload from pending after retrieval - caller has it now delete(g.backend.payloads, payloadID) return &engine.ExecutionPayloadEnvelope{ @@ -593,6 +695,21 @@ func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildSta Msg("transaction execution summary") } + // Process withdrawals (EIP-4895) - credit ETH to withdrawal recipients + // Withdrawals are processed after all transactions, crediting the specified + // amount (in Gwei) to each recipient address. + if len(ps.withdrawals) > 0 { + for _, withdrawal := range ps.withdrawals { + // Withdrawal amount is in Gwei, convert to Wei (multiply by 1e9) + amount := new(big.Int).SetUint64(withdrawal.Amount) + amount.Mul(amount, big.NewInt(params.GWei)) + stateDB.AddBalance(withdrawal.Address, uint256.MustFromBig(amount), tracing.BalanceIncreaseWithdrawal) + } + g.logger.Debug(). + Int("count", len(ps.withdrawals)). + Msg("processed withdrawals") + } + // Finalize state header.GasUsed = gasUsed header.Root = stateDB.IntermediateRoot(g.backend.chainConfig.IsEIP158(header.Number)) @@ -659,7 +776,7 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu g.backend.mu.Lock() defer g.backend.mu.Unlock() - startTime := time.Now() + validationStart := time.Now() // Validate payload if payload == nil { @@ -803,7 +920,7 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu Str("parent_hash", payload.ParentHash.Hex()). Int("tx_count", len(txs)). Uint64("gas_used", payload.GasUsed). - Dur("process_time", time.Since(startTime)). + Dur("process_time", time.Since(validationStart)). Msg("new payload validated and inserted") return &engine.PayloadStatusV1{ @@ -975,7 +1092,7 @@ func applyTransaction( // Set contract address if this was a contract creation if msg.To == nil { - receipt.ContractAddress = evmInstance.Origin + receipt.ContractAddress = crypto.CreateAddress(msg.From, tx.Nonce()) } return receipt, nil diff --git a/execution/evm/engine_geth_consensus.go b/execution/evm/engine_geth_consensus.go index 54cc955736..844583bcb7 100644 --- a/execution/evm/engine_geth_consensus.go +++ b/execution/evm/engine_geth_consensus.go @@ -8,10 +8,23 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" ) +const ( + // maxExtraDataSize is the maximum allowed size for block extra data (32 bytes). + maxExtraDataSize = 32 + + // gasLimitBoundDivisor is the bound divisor for gas limit changes between blocks. + // Gas limit can only change by 1/1024 per block. + gasLimitBoundDivisor = 1024 + + // minGasLimit is the minimum gas limit allowed for blocks. + minGasLimit = 5000 +) + // sovereignBeacon wraps the standard beacon consensus engine but allows // equal timestamps (timestamp >= parent.timestamp) instead of requiring // strictly increasing timestamps (timestamp > parent.timestamp). @@ -57,9 +70,28 @@ func (sb *sovereignBeacon) VerifyHeader(chain consensus.ChainHeaderReader, heade return errors.New("invalid uncle hash: must be empty for PoS") } + // Verify extra data size limit + if len(header.Extra) > maxExtraDataSize { + return fmt.Errorf("invalid extra data size: have %d, max %d", len(header.Extra), maxExtraDataSize) + } + + // Verify gas limit bounds if header.GasLimit > params.MaxGasLimit { return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, params.MaxGasLimit) } + if header.GasLimit < minGasLimit { + return fmt.Errorf("invalid gasLimit: have %v, min %v", header.GasLimit, minGasLimit) + } + + // Verify gas limit change is within bounds (can only change by 1/1024 per block) + diff := int64(header.GasLimit) - int64(parent.GasLimit) + if diff < 0 { + diff = -diff + } + limit := parent.GasLimit / gasLimitBoundDivisor + if uint64(diff) >= limit { + return fmt.Errorf("invalid gas limit: have %d, want %d ± %d", header.GasLimit, parent.GasLimit, limit-1) + } // Verify that the gasUsed is <= gasLimit if header.GasUsed > header.GasLimit { @@ -71,6 +103,14 @@ func (sb *sovereignBeacon) VerifyHeader(chain consensus.ChainHeaderReader, heade return err } + // Verify EIP-4844 blob gas fields if Cancun is active + config := chain.Config() + if config.IsCancun(header.Number, header.Time) { + if err := eip4844.VerifyEIP4844Header(config, parent, header); err != nil { + return err + } + } + return nil } diff --git a/execution/evm/engine_geth_db.go b/execution/evm/engine_geth_db.go index f5bd6bc51b..5fdb70360c 100644 --- a/execution/evm/engine_geth_db.go +++ b/execution/evm/engine_geth_db.go @@ -3,6 +3,7 @@ package evm import ( "bytes" "context" + "sort" "strings" "sync" @@ -12,6 +13,10 @@ import ( "github.com/syndtr/goleveldb/leveldb" ) +// deleteRangeBatchSize is the number of keys to delete in a single batch +// to avoid holding locks for too long. +const deleteRangeBatchSize = 1000 + var _ ethdb.KeyValueStore = &wrapper{} type wrapper struct { @@ -47,29 +52,89 @@ func (w *wrapper) Delete(key []byte) error { } // DeleteRange implements ethdb.KeyValueStore. +// Optimized to use prefix-based querying when possible and batch deletions. func (w *wrapper) DeleteRange(start []byte, end []byte) error { - // Query all keys and delete those in range - q := query.Query{KeysOnly: true} - results, err := w.ds.Query(context.Background(), q) + ctx := context.Background() + + // Find common prefix between start and end to narrow the query + prefix := commonPrefix(start, end) + + q := query.Query{ + KeysOnly: true, + } + if len(prefix) > 0 { + q.Prefix = "/" + string(prefix) + } + + results, err := w.ds.Query(ctx, q) if err != nil { return err } defer results.Close() + // Collect keys to delete in batches + var keysToDelete []datastore.Key for result := range results.Next() { if result.Error != nil { return result.Error } keyBytes := datastoreKeyToBytes(result.Entry.Key) if bytes.Compare(keyBytes, start) >= 0 && bytes.Compare(keyBytes, end) < 0 { - if err := w.ds.Delete(context.Background(), datastore.NewKey(result.Entry.Key)); err != nil { - return err + keysToDelete = append(keysToDelete, datastore.NewKey(result.Entry.Key)) + + // Process in batches to avoid holding too many keys in memory + if len(keysToDelete) >= deleteRangeBatchSize { + if err := w.deleteBatch(ctx, keysToDelete); err != nil { + return err + } + keysToDelete = keysToDelete[:0] } } } + + // Delete remaining keys + if len(keysToDelete) > 0 { + return w.deleteBatch(ctx, keysToDelete) + } return nil } +// deleteBatch deletes a batch of keys using a batched operation. +func (w *wrapper) deleteBatch(ctx context.Context, keys []datastore.Key) error { + batch, err := w.ds.Batch(ctx) + if err != nil { + // Fallback to individual deletes if batching not supported + for _, key := range keys { + if err := w.ds.Delete(ctx, key); err != nil { + return err + } + } + return nil + } + + for _, key := range keys { + if err := batch.Delete(ctx, key); err != nil { + return err + } + } + return batch.Commit(ctx) +} + +// commonPrefix returns the common prefix between two byte slices. +func commonPrefix(a, b []byte) []byte { + minLen := len(a) + if len(b) < minLen { + minLen = len(b) + } + var i int + for i = 0; i < minLen; i++ { + if a[i] != b[i] { + break + } + } + return a[:i] +} + // Get implements ethdb.KeyValueStore. func (w *wrapper) Get(key []byte) ([]byte, error) { val, err := w.ds.Get(context.Background(), keyToDatastoreKey(key)) @@ -104,7 +169,12 @@ func (w *wrapper) NewBatchWithSize(size int) ethdb.Batch { // NewIterator implements ethdb.KeyValueStore. func (w *wrapper) NewIterator(prefix []byte, start []byte) ethdb.Iterator { - return newIterator(w.ds, prefix, start) + return newIterator(w.ds, prefix, start, 0) +} + +// NewIteratorWithSize creates an iterator with an estimated size hint for buffer allocation. +func (w *wrapper) NewIteratorWithSize(prefix []byte, start []byte, sizeHint int) ethdb.Iterator { + return newIterator(w.ds, prefix, start, sizeHint) } // Put implements ethdb.KeyValueStore. @@ -173,9 +243,17 @@ func (b *batchWrapper) Delete(key []byte) error { // DeleteRange implements ethdb.Batch. func (b *batchWrapper) DeleteRange(start []byte, end []byte) error { - // Query all keys and mark those in range for deletion + ctx := context.Background() + + // Use common prefix to narrow query + prefix := commonPrefix(start, end) + q := query.Query{KeysOnly: true} - results, err := b.ds.Query(context.Background(), q) + if len(prefix) > 0 { + q.Prefix = "/" + string(prefix) + } + + results, err := b.ds.Query(ctx, q) if err != nil { return err } @@ -261,20 +339,20 @@ func (b *batchWrapper) Replay(w ethdb.KeyValueWriter) error { } // iteratorWrapper implements ethdb.Iterator +// It provides sorted iteration over keys with optional prefix and start position. type iteratorWrapper struct { - results query.Results - current query.Entry + entries []query.Entry // Pre-sorted entries for deterministic iteration + index int prefix []byte start []byte err error - started bool closed bool mu sync.Mutex } var _ ethdb.Iterator = &iteratorWrapper{} -func newIterator(ds datastore.Batching, prefix []byte, start []byte) *iteratorWrapper { +func newIterator(ds datastore.Batching, prefix []byte, start []byte, sizeHint int) *iteratorWrapper { q := query.Query{ KeysOnly: false, } @@ -284,13 +362,59 @@ func newIterator(ds datastore.Batching, prefix []byte, start []byte) *iteratorWr } results, err := ds.Query(context.Background(), q) + if err != nil { + return &iteratorWrapper{ + err: err, + closed: false, + index: -1, + } + } + + // Collect and sort all entries for deterministic ordering + // go-datastore doesn't guarantee order, but ethdb.Iterator expects sorted keys + var entries []query.Entry + if sizeHint > 0 { + entries = make([]query.Entry, 0, sizeHint) + } + + for result := range results.Next() { + if result.Error != nil { + results.Close() + return &iteratorWrapper{ + err: result.Error, + closed: false, + index: -1, + } + } + + keyBytes := datastoreKeyToBytes(result.Entry.Key) + + // Filter by prefix + if len(prefix) > 0 && !bytes.HasPrefix(keyBytes, prefix) { + continue + } + + // Filter by start position + if len(start) > 0 && bytes.Compare(keyBytes, start) < 0 { + continue + } + + entries = append(entries, result.Entry) + } + results.Close() + + // Sort entries by key for deterministic iteration order + sort.Slice(entries, func(i, j int) bool { + keyI := datastoreKeyToBytes(entries[i].Key) + keyJ := datastoreKeyToBytes(entries[j].Key) + return bytes.Compare(keyI, keyJ) < 0 + }) return &iteratorWrapper{ - results: results, + entries: entries, prefix: prefix, start: start, - err: err, - started: false, + index: -1, closed: false, } } @@ -304,32 +428,8 @@ func (it *iteratorWrapper) Next() bool { return false } - for { - result, ok := it.results.NextSync() - if !ok { - return false - } - if result.Error != nil { - it.err = result.Error - return false - } - - keyBytes := datastoreKeyToBytes(result.Entry.Key) - - // Check if key matches prefix (if prefix is set) - if len(it.prefix) > 0 && !bytes.HasPrefix(keyBytes, it.prefix) { - continue - } - - // Check if key is >= start (if start is set) - if len(it.start) > 0 && bytes.Compare(keyBytes, it.start) < 0 { - continue - } - - it.current = result.Entry - it.started = true - return true - } + it.index++ + return it.index < len(it.entries) } // Error implements ethdb.Iterator. @@ -344,10 +444,10 @@ func (it *iteratorWrapper) Key() []byte { it.mu.Lock() defer it.mu.Unlock() - if !it.started || it.closed { + if it.closed || it.index < 0 || it.index >= len(it.entries) { return nil } - return datastoreKeyToBytes(it.current.Key) + return datastoreKeyToBytes(it.entries[it.index].Key) } // Value implements ethdb.Iterator. @@ -355,10 +455,10 @@ func (it *iteratorWrapper) Value() []byte { it.mu.Lock() defer it.mu.Unlock() - if !it.started || it.closed { + if it.closed || it.index < 0 || it.index >= len(it.entries) { return nil } - return it.current.Value + return it.entries[it.index].Value } // Release implements ethdb.Iterator. @@ -370,7 +470,6 @@ func (it *iteratorWrapper) Release() { return } it.closed = true - if it.results != nil { - it.results.Close() - } + // Clear entries to allow GC + it.entries = nil } diff --git a/execution/evm/engine_geth_test.go b/execution/evm/engine_geth_test.go index 87aa20770b..e1cf0c345d 100644 --- a/execution/evm/engine_geth_test.go +++ b/execution/evm/engine_geth_test.go @@ -482,3 +482,393 @@ func TestCreateBloomFromReceipts(t *testing.T) { bloom = createBloomFromReceipts([]*types.Receipt{receipt}) assert.NotNil(t, bloom) } + +func TestGethEngineClient_PayloadIdempotency(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // Create payload attributes + timestamp := time.Now().Unix() + 12 + attrs := map[string]any{ + "timestamp": timestamp, + "prevRandao": common.Hash{1, 2, 3}, + "suggestedFeeRecipient": feeRecipient, + "transactions": []string{}, + "gasLimit": uint64(30_000_000), + "withdrawals": []*types.Withdrawal{}, + } + + // First call should create a new payload + resp1, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, attrs) + require.NoError(t, err) + require.NotNil(t, resp1.PayloadID) + + // Second call with same attributes should return the same payload ID (idempotency) + resp2, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, attrs) + require.NoError(t, err) + require.NotNil(t, resp2.PayloadID) + + // Payload IDs should be identical + assert.Equal(t, *resp1.PayloadID, *resp2.PayloadID) + + // Should still have only one payload in the map + assert.Len(t, backend.payloads, 1) +} + +func TestGethEngineClient_DeterministicPayloadID(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + timestamp := uint64(time.Now().Unix() + 12) + + // Create two payload states with identical attributes + ps1 := &payloadBuildState{ + parentHash: genesisBlock.Hash(), + timestamp: timestamp, + prevRandao: common.Hash{1, 2, 3}, + feeRecipient: feeRecipient, + gasLimit: 30_000_000, + transactions: [][]byte{}, + withdrawals: []*types.Withdrawal{}, + createdAt: time.Now(), + } + + ps2 := &payloadBuildState{ + parentHash: genesisBlock.Hash(), + timestamp: timestamp, + prevRandao: common.Hash{1, 2, 3}, + feeRecipient: feeRecipient, + gasLimit: 30_000_000, + transactions: [][]byte{}, + withdrawals: []*types.Withdrawal{}, + createdAt: time.Now().Add(time.Hour), // Different creation time + } + + // Both should generate the same payload ID + id1 := engineClient.generatePayloadID(ps1) + id2 := engineClient.generatePayloadID(ps2) + assert.Equal(t, id1, id2) + + // Different attributes should generate different IDs + ps3 := &payloadBuildState{ + parentHash: genesisBlock.Hash(), + timestamp: timestamp + 1, // Different timestamp + prevRandao: common.Hash{1, 2, 3}, + feeRecipient: feeRecipient, + gasLimit: 30_000_000, + transactions: [][]byte{}, + withdrawals: []*types.Withdrawal{}, + createdAt: time.Now(), + } + id3 := engineClient.generatePayloadID(ps3) + assert.NotEqual(t, id1, id3) +} + +func TestGethEngineClient_GetPayloadRemovesFromMap(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // Create a payload + attrs := map[string]any{ + "timestamp": time.Now().Unix() + 12, + "prevRandao": common.Hash{1, 2, 3}, + "suggestedFeeRecipient": feeRecipient, + "transactions": []string{}, + "gasLimit": uint64(30_000_000), + "withdrawals": []*types.Withdrawal{}, + } + + resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, attrs) + require.NoError(t, err) + require.NotNil(t, resp.PayloadID) + + // Payload should be in the map + assert.Len(t, backend.payloads, 1) + + // Get the payload + envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) + require.NoError(t, err) + assert.NotNil(t, envelope) + + // Payload should be removed from the map after retrieval + assert.Len(t, backend.payloads, 0) + + // Second call should fail with unknown payload ID + _, err = engineClient.GetPayload(ctx, *resp.PayloadID) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown payload ID") +} + +func TestGethEngineClient_PayloadCleanup(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // Create more payloads than maxPayloads (10) + baseTimestamp := time.Now().Unix() + 100 + for i := 0; i < 15; i++ { + attrs := map[string]any{ + "timestamp": baseTimestamp + int64(i), + "prevRandao": common.Hash{byte(i)}, + "suggestedFeeRecipient": feeRecipient, + "transactions": []string{}, + "gasLimit": uint64(30_000_000), + "withdrawals": []*types.Withdrawal{}, + } + + _, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, attrs) + require.NoError(t, err) + } + + // Should have at most maxPayloads entries + assert.LessOrEqual(t, len(backend.payloads), 10) +} + +func TestGethEngineClient_DifferentTransactionsGenerateDifferentIDs(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + timestamp := uint64(time.Now().Unix() + 12) + + // Payload with no transactions + ps1 := &payloadBuildState{ + parentHash: genesisBlock.Hash(), + timestamp: timestamp, + prevRandao: common.Hash{1, 2, 3}, + feeRecipient: feeRecipient, + gasLimit: 30_000_000, + transactions: [][]byte{}, + withdrawals: []*types.Withdrawal{}, + createdAt: time.Now(), + } + + // Payload with some transaction + ps2 := &payloadBuildState{ + parentHash: genesisBlock.Hash(), + timestamp: timestamp, + prevRandao: common.Hash{1, 2, 3}, + feeRecipient: feeRecipient, + gasLimit: 30_000_000, + transactions: [][]byte{{0xaa, 0xbb, 0xcc}}, + withdrawals: []*types.Withdrawal{}, + createdAt: time.Now(), + } + + id1 := engineClient.generatePayloadID(ps1) + id2 := engineClient.generatePayloadID(ps2) + + // Different transactions should produce different IDs + assert.NotEqual(t, id1, id2) +} + +func TestGethEngineClient_WithdrawalProcessing(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() + + engineClient := &gethEngineClient{ + backend: backend, + logger: logger, + } + + ctx := context.Background() + genesisBlock := backend.blockchain.Genesis() + feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // Create withdrawal recipients + withdrawalAddr1 := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + withdrawalAddr2 := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + + // Check initial balances are zero + stateDB, err := backend.blockchain.StateAt(genesisBlock.Root()) + require.NoError(t, err) + initialBalance1 := stateDB.GetBalance(withdrawalAddr1) + initialBalance2 := stateDB.GetBalance(withdrawalAddr2) + assert.True(t, initialBalance1.IsZero(), "initial balance should be zero") + assert.True(t, initialBalance2.IsZero(), "initial balance should be zero") + + // Create withdrawals (amounts are in Gwei) + withdrawals := []*types.Withdrawal{ + { + Index: 0, + Validator: 1, + Address: withdrawalAddr1, + Amount: 1000000000, // 1 ETH in Gwei + }, + { + Index: 1, + Validator: 2, + Address: withdrawalAddr2, + Amount: 500000000, // 0.5 ETH in Gwei + }, + } + + // Create payload with withdrawals + attrs := map[string]any{ + "timestamp": time.Now().Unix() + 12, + "prevRandao": common.Hash{1, 2, 3}, + "suggestedFeeRecipient": feeRecipient, + "transactions": []string{}, + "gasLimit": uint64(30_000_000), + "withdrawals": withdrawals, + } + + resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: genesisBlock.Hash(), + SafeBlockHash: genesisBlock.Hash(), + FinalizedBlockHash: genesisBlock.Hash(), + }, attrs) + require.NoError(t, err) + require.NotNil(t, resp.PayloadID) + + // Get the payload + envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) + require.NoError(t, err) + assert.NotNil(t, envelope) + assert.Len(t, envelope.ExecutionPayload.Withdrawals, 2) + + // Submit the payload to actually apply state changes + status, err := engineClient.NewPayload(ctx, envelope.ExecutionPayload, nil, "", nil) + require.NoError(t, err) + assert.Equal(t, engine.VALID, status.Status) + + // Update head to the new block + _, err = engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: envelope.ExecutionPayload.BlockHash, + SafeBlockHash: envelope.ExecutionPayload.BlockHash, + FinalizedBlockHash: envelope.ExecutionPayload.BlockHash, + }, nil) + require.NoError(t, err) + + // Check balances after withdrawals are processed + newBlock := backend.blockchain.GetBlockByHash(envelope.ExecutionPayload.BlockHash) + require.NotNil(t, newBlock) + + newStateDB, err := backend.blockchain.StateAt(newBlock.Root()) + require.NoError(t, err) + + // Withdrawal amounts are in Gwei, balances are in Wei + // 1 ETH = 1e9 Gwei = 1e18 Wei + expectedBalance1 := new(big.Int).Mul(big.NewInt(1000000000), big.NewInt(1e9)) // 1 ETH in Wei + expectedBalance2 := new(big.Int).Mul(big.NewInt(500000000), big.NewInt(1e9)) // 0.5 ETH in Wei + + balance1 := newStateDB.GetBalance(withdrawalAddr1) + balance2 := newStateDB.GetBalance(withdrawalAddr2) + + assert.Equal(t, expectedBalance1.String(), balance1.ToBig().String(), "withdrawal 1 balance mismatch") + assert.Equal(t, expectedBalance2.String(), balance2.ToBig().String(), "withdrawal 2 balance mismatch") +} + +func TestGethEngineClient_ContractCreationAddress(t *testing.T) { + // This test verifies that contract creation addresses are calculated correctly + // using crypto.CreateAddress(sender, nonce) rather than the incorrect evmInstance.Origin + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() + + // The contract address should be derived from sender address and nonce + // This is tested implicitly through the applyTransaction function + // For a full test, we would need to create a signed contract creation tx + + // Verify the crypto.CreateAddress function works as expected + sender := common.HexToAddress("0x1234567890123456789012345678901234567890") + nonce := uint64(0) + expectedAddr := crypto.CreateAddress(sender, nonce) + + // The address should be deterministic + expectedAddr2 := crypto.CreateAddress(sender, nonce) + assert.Equal(t, expectedAddr, expectedAddr2) + + // Different nonce should produce different address + differentAddr := crypto.CreateAddress(sender, nonce+1) + assert.NotEqual(t, expectedAddr, differentAddr) + + // Different sender should produce different address + differentSender := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + differentAddr2 := crypto.CreateAddress(differentSender, nonce) + assert.NotEqual(t, expectedAddr, differentAddr2) + + _ = backend // use backend to avoid unused variable warning +} diff --git a/execution/evm/go.mod b/execution/evm/go.mod index e3ec3388c3..931ecc0f94 100644 --- a/execution/evm/go.mod +++ b/execution/evm/go.mod @@ -7,6 +7,7 @@ require ( github.com/evstack/ev-node v1.0.0-beta.10 github.com/evstack/ev-node/core v1.0.0-beta.5 github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/holiman/uint256 v1.3.2 github.com/ipfs/go-datastore v0.9.0 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 @@ -57,7 +58,6 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/go-cid v0.6.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect From 3752b8e25caa3815e2a746c1d22a2dff98ff4fe1 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 23 Jan 2026 17:52:40 +0100 Subject: [PATCH 17/18] cleanups --- execution/evm/engine_geth.go | 743 +++++++---------------------- execution/evm/engine_geth_db.go | 318 +++---------- execution/evm/engine_geth_rpc.go | 749 +++++++++++++++--------------- execution/evm/engine_geth_test.go | 568 ++++++++-------------- 4 files changed, 837 insertions(+), 1541 deletions(-) diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index 8f8c364066..776a2c4dee 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -2,8 +2,6 @@ package evm import ( "context" - "crypto/sha256" - "encoding/binary" "errors" "fmt" "math/big" @@ -38,37 +36,25 @@ var ( _ EthRPCClient = (*gethEthClient)(nil) ) -// baseFeeChangeDenominator is the EIP-1559 base fee change denominator. -const baseFeeChangeDenominator = 8 - -// payloadTTL is how long a payload can remain in the map before being cleaned up. -const payloadTTL = 60 * time.Second - -// maxPayloads is the maximum number of payloads to keep in memory. -const maxPayloads = 10 - -// GethBackend holds the in-process geth components. +// GethBackend is the in-process geth execution engine. type GethBackend struct { db ethdb.Database chainConfig *params.ChainConfig blockchain *core.BlockChain txPool *txpool.TxPool - mu sync.Mutex - // payloads tracks in-flight payload builds - payloads map[engine.PayloadID]*payloadBuildState - nextPayloadID uint64 + mu sync.Mutex + pendingPayload *pendingPayload - // RPC server rpcServer *rpc.Server httpServer *http.Server rpcListener net.Listener - - logger zerolog.Logger + logger zerolog.Logger } -// payloadBuildState tracks the state of a payload being built. -type payloadBuildState struct { +// pendingPayload represents a single in-flight payload build. +type pendingPayload struct { + id engine.PayloadID parentHash common.Hash timestamp uint64 prevRandao common.Hash @@ -76,33 +62,20 @@ type payloadBuildState struct { withdrawals []*types.Withdrawal transactions [][]byte gasLimit uint64 - - // createdAt tracks when this payload was created for TTL cleanup - createdAt time.Time - - // built payload (populated after getPayload) - payload *engine.ExecutableData - - // buildErr stores any error that occurred during payload build - buildErr error + built *engine.ExecutableData } -// gethEngineClient implements EngineRPCClient using in-process geth. type gethEngineClient struct { backend *GethBackend logger zerolog.Logger } -// gethEthClient implements EthRPCClient using in-process geth. type gethEthClient struct { backend *GethBackend logger zerolog.Logger } -// NewEngineExecutionClientWithGeth creates an EngineClient that uses an in-process -// go-ethereum instance instead of connecting to an external execution engine via RPC. -// If rpcAddress is non-empty, an HTTP JSON-RPC server will be started on that address -// (e.g., "127.0.0.1:8545") exposing standard eth_ methods. +// NewEngineExecutionClientWithGeth creates an EngineClient using in-process geth. func NewEngineExecutionClientWithGeth( genesis *core.Genesis, feeRecipient common.Address, @@ -111,7 +84,7 @@ func NewEngineExecutionClientWithGeth( logger zerolog.Logger, ) (*EngineClient, error) { if db == nil { - return nil, errors.New("db is required for EVM execution client") + return nil, errors.New("db is required") } if genesis == nil || genesis.Config == nil { return nil, errors.New("genesis configuration is required") @@ -122,7 +95,6 @@ func NewEngineExecutionClientWithGeth( return nil, fmt.Errorf("failed to create geth backend: %w", err) } - // Start RPC server if address is provided if rpcAddress != "" { if err := backend.StartRPCServer(rpcAddress); err != nil { backend.Close() @@ -130,28 +102,17 @@ func NewEngineExecutionClientWithGeth( } } - engineClient := &gethEngineClient{ - backend: backend, - logger: logger.With().Str("component", "geth-engine").Logger(), - } - - ethClient := &gethEthClient{ - backend: backend, - logger: logger.With().Str("component", "geth-eth").Logger(), - } - genesisBlock := backend.blockchain.Genesis() genesisHash := genesisBlock.Hash() logger.Info(). Str("genesis_hash", genesisHash.Hex()). Str("chain_id", genesis.Config.ChainID.String()). - Uint64("genesis_gas_limit", genesis.GasLimit). Msg("created in-process geth execution client") return &EngineClient{ - engineClient: engineClient, - ethClient: ethClient, + engineClient: &gethEngineClient{backend: backend, logger: logger.With().Str("component", "geth-engine").Logger()}, + ethClient: &gethEthClient{backend: backend, logger: logger.With().Str("component", "geth-eth").Logger()}, genesisHash: genesisHash, feeRecipient: feeRecipient, store: NewEVMStore(db), @@ -163,68 +124,46 @@ func NewEngineExecutionClientWithGeth( }, nil } -// newGethBackend creates a new in-process geth backend with persistent storage. func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger) (*GethBackend, error) { ethdb := rawdb.NewDatabase(&wrapper{db}) - - // Create trie database trieDB := triedb.NewDatabase(ethdb, nil) - // Ensure blobSchedule is set if Cancun/Prague are enabled - // TODO: remove and fix genesis. + // Auto-populate blob config for Cancun/Prague if needed if genesis.Config != nil && genesis.Config.BlobScheduleConfig == nil { if genesis.Config.CancunTime != nil || genesis.Config.PragueTime != nil { genesis.Config.BlobScheduleConfig = ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, } - logger.Debug().Msg("auto-populated blobSchedule config for Cancun/Prague forks") } } - // Initialize the genesis block - chainConfig, genesisHash, _, genesisErr := core.SetupGenesisBlockWithOverride(ethdb, trieDB, genesis, nil) - if genesisErr != nil { - return nil, fmt.Errorf("failed to setup genesis: %w", genesisErr) + chainConfig, genesisHash, _, err := core.SetupGenesisBlockWithOverride(ethdb, trieDB, genesis, nil) + if err != nil { + return nil, fmt.Errorf("failed to setup genesis: %w", err) } - logger.Info(). - Str("genesis_hash", genesisHash.Hex()). - Str("chain_id", chainConfig.ChainID.String()). - Msg("initialized in-process geth with genesis") + logger.Info().Str("genesis_hash", genesisHash.Hex()).Msg("initialized genesis") - // Create blockchain config bcConfig := core.DefaultConfig().WithStateScheme(rawdb.HashScheme) - // Use sovereign beacon consensus that allows equal timestamps for subsecond block times - consensusEngine := newSovereignBeacon() - - // Create the blockchain - blockchain, err := core.NewBlockChain(ethdb, genesis, consensusEngine, bcConfig) + blockchain, err := core.NewBlockChain(ethdb, genesis, newSovereignBeacon(), bcConfig) if err != nil { return nil, fmt.Errorf("failed to create blockchain: %w", err) } - // Log current chain head - currentHead := blockchain.CurrentBlock() - if currentHead != nil { - logger.Info(). - Uint64("height", currentHead.Number.Uint64()). - Str("hash", currentHead.Hash().Hex()). - Msg("resuming from existing chain state") + if head := blockchain.CurrentBlock(); head != nil { + logger.Info().Uint64("height", head.Number.Uint64()).Msg("resuming from chain state") } backend := &GethBackend{ db: ethdb, chainConfig: chainConfig, blockchain: blockchain, - payloads: make(map[engine.PayloadID]*payloadBuildState), logger: logger, } - // Create transaction pool txPoolConfig := legacypool.DefaultConfig txPoolConfig.NoLocals = true - legacyPool := legacypool.New(txPoolConfig, blockchain) txPool, err := txpool.New(0, blockchain, []txpool.SubPool{legacyPool}) if err != nil { @@ -235,24 +174,18 @@ func newGethBackend(genesis *core.Genesis, db ds.Batching, logger zerolog.Logger return backend, nil } -// Close shuts down the geth backend gracefully. +// Close shuts down the backend. func (b *GethBackend) Close() error { b.logger.Info().Msg("shutting down geth backend") - var errs []error - - // Stop RPC server first if b.httpServer != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - if err := b.httpServer.Shutdown(ctx); err != nil { - errs = append(errs, fmt.Errorf("failed to shutdown http server: %w", err)) - } + _ = b.httpServer.Shutdown(ctx) } if b.rpcServer != nil { b.rpcServer.Stop() } - if b.txPool != nil { b.txPool.Close() } @@ -260,50 +193,27 @@ func (b *GethBackend) Close() error { b.blockchain.Stop() } if b.db != nil { - if err := b.db.Close(); err != nil { - errs = append(errs, fmt.Errorf("failed to close database: %w", err)) - } - } - - if len(errs) > 0 { - return errors.Join(errs...) + return b.db.Close() } return nil } -// ForkchoiceUpdated implements EngineRPCClient. +// ForkchoiceUpdated handles forkchoice updates and optionally starts payload building. func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine.ForkchoiceStateV1, attrs map[string]any) (*engine.ForkChoiceResponse, error) { - // Check context before acquiring lock - if err := ctx.Err(); err != nil { - return nil, fmt.Errorf("context cancelled: %w", err) - } - g.backend.mu.Lock() defer g.backend.mu.Unlock() - // Validate the forkchoice state headBlock := g.backend.blockchain.GetBlockByHash(fcState.HeadBlockHash) if headBlock == nil { - g.logger.Debug(). - Str("head_hash", fcState.HeadBlockHash.Hex()). - Msg("head block not found, returning SYNCING") return &engine.ForkChoiceResponse{ - PayloadStatus: engine.PayloadStatusV1{ - Status: engine.SYNCING, - }, + PayloadStatus: engine.PayloadStatusV1{Status: engine.SYNCING}, }, nil } - // Update the canonical chain head if _, err := g.backend.blockchain.SetCanonical(headBlock); err != nil { return nil, fmt.Errorf("failed to set canonical head: %w", err) } - g.logger.Debug(). - Uint64("height", headBlock.NumberU64()). - Str("hash", headBlock.Hash().Hex()). - Msg("updated canonical head") - response := &engine.ForkChoiceResponse{ PayloadStatus: engine.PayloadStatusV1{ Status: engine.VALID, @@ -311,306 +221,181 @@ func (g *gethEngineClient) ForkchoiceUpdated(ctx context.Context, fcState engine }, } - // If payload attributes provided, start building a new payload if attrs != nil { - payloadState, err := g.parsePayloadAttributes(fcState.HeadBlockHash, attrs) + payload, err := g.parsePayloadAttributes(fcState.HeadBlockHash, attrs) if err != nil { - return nil, fmt.Errorf("failed to parse payload attributes: %w", err) + return nil, fmt.Errorf("invalid payload attributes: %w", err) } - - // Generate deterministic payload ID from attributes - payloadID := g.generatePayloadID(payloadState) - - // Check if we already have this payload (idempotency) - if existing, ok := g.backend.payloads[payloadID]; ok { - // Reuse existing payload if it hasn't errored - if existing.buildErr == nil { - response.PayloadID = &payloadID - g.logger.Debug(). - Str("payload_id", payloadID.String()). - Msg("reusing existing payload") - return response, nil - } - // Previous build failed, remove it and try again - delete(g.backend.payloads, payloadID) - } - - // Clean up old payloads before adding new one - g.cleanupStalePayloads() - - g.backend.payloads[payloadID] = payloadState - response.PayloadID = &payloadID + g.backend.pendingPayload = payload + response.PayloadID = &payload.id g.logger.Info(). - Str("payload_id", payloadID.String()). - Str("parent_hash", fcState.HeadBlockHash.Hex()). - Uint64("timestamp", payloadState.timestamp). - Int("tx_count", len(payloadState.transactions)). - Str("fee_recipient", payloadState.feeRecipient.Hex()). + Str("payload_id", payload.id.String()). + Str("parent", fcState.HeadBlockHash.Hex()). + Uint64("timestamp", payload.timestamp). + Int("txs", len(payload.transactions)). Msg("started payload build") } return response, nil } -// generatePayloadID creates a deterministic payload ID from the payload attributes. -// This ensures that the same attributes always produce the same ID for idempotency. -func (g *gethEngineClient) generatePayloadID(ps *payloadBuildState) engine.PayloadID { - h := sha256.New() - h.Write(ps.parentHash[:]) - binary.Write(h, binary.BigEndian, ps.timestamp) - h.Write(ps.prevRandao[:]) - h.Write(ps.feeRecipient[:]) - binary.Write(h, binary.BigEndian, ps.gasLimit) - // Include transaction count and first tx hash for uniqueness - binary.Write(h, binary.BigEndian, uint64(len(ps.transactions))) - for _, tx := range ps.transactions { - h.Write(tx) - } - sum := h.Sum(nil) - var id engine.PayloadID - copy(id[:], sum[:8]) - return id -} - -// cleanupStalePayloads removes payloads that have exceeded their TTL or when we have too many. -func (g *gethEngineClient) cleanupStalePayloads() { - now := time.Now() - var staleIDs []engine.PayloadID - - // Find stale payloads - for id, ps := range g.backend.payloads { - if now.Sub(ps.createdAt) > payloadTTL { - staleIDs = append(staleIDs, id) - } - } - - // Remove stale payloads - for _, id := range staleIDs { - delete(g.backend.payloads, id) - g.logger.Debug(). - Str("payload_id", id.String()). - Msg("cleaned up stale payload") - } - - // If still too many payloads, remove oldest ones - for len(g.backend.payloads) >= maxPayloads { - var oldestID engine.PayloadID - var oldestTime time.Time - first := true - for id, ps := range g.backend.payloads { - if first || ps.createdAt.Before(oldestTime) { - oldestID = id - oldestTime = ps.createdAt - first = false - } - } - if !first { - delete(g.backend.payloads, oldestID) - g.logger.Debug(). - Str("payload_id", oldestID.String()). - Msg("evicted oldest payload due to limit") - } - } -} - -// parsePayloadAttributes extracts payload attributes from the map format. -func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs map[string]any) (*payloadBuildState, error) { - ps := &payloadBuildState{ +func (g *gethEngineClient) parsePayloadAttributes(parentHash common.Hash, attrs map[string]any) (*pendingPayload, error) { + p := &pendingPayload{ parentHash: parentHash, withdrawals: []*types.Withdrawal{}, - createdAt: time.Now(), } - // Parse timestamp (required) + // Generate simple sequential payload ID + p.id = engine.PayloadID{byte(time.Now().UnixNano() & 0xFF)} + + // Timestamp (required) if ts, ok := attrs["timestamp"]; ok { switch v := ts.(type) { case int64: - ps.timestamp = uint64(v) + p.timestamp = uint64(v) case uint64: - ps.timestamp = v + p.timestamp = v case float64: - ps.timestamp = uint64(v) + p.timestamp = uint64(v) default: return nil, fmt.Errorf("invalid timestamp type: %T", ts) } } else { - return nil, errors.New("timestamp is required in payload attributes") + return nil, errors.New("timestamp required") } - // Parse prevRandao (required for PoS) + // PrevRandao if pr, ok := attrs["prevRandao"]; ok { switch v := pr.(type) { case common.Hash: - ps.prevRandao = v + p.prevRandao = v case string: - ps.prevRandao = common.HexToHash(v) + p.prevRandao = common.HexToHash(v) case []byte: - ps.prevRandao = common.BytesToHash(v) - default: - return nil, fmt.Errorf("invalid prevRandao type: %T", pr) + p.prevRandao = common.BytesToHash(v) } } - // Parse suggestedFeeRecipient (required) + // Fee recipient (required) if fr, ok := attrs["suggestedFeeRecipient"]; ok { switch v := fr.(type) { case common.Address: - ps.feeRecipient = v + p.feeRecipient = v case string: - ps.feeRecipient = common.HexToAddress(v) + p.feeRecipient = common.HexToAddress(v) case []byte: - ps.feeRecipient = common.BytesToAddress(v) - default: - return nil, fmt.Errorf("invalid suggestedFeeRecipient type: %T", fr) + p.feeRecipient = common.BytesToAddress(v) } } else { - return nil, errors.New("suggestedFeeRecipient is required in payload attributes") + return nil, errors.New("suggestedFeeRecipient required") } - // Parse transactions (optional) + // Transactions if txs, ok := attrs["transactions"]; ok { switch v := txs.(type) { case []string: - ps.transactions = make([][]byte, 0, len(v)) for _, txHex := range v { - txBytes := common.FromHex(txHex) - if len(txBytes) > 0 { - ps.transactions = append(ps.transactions, txBytes) + if txBytes := common.FromHex(txHex); len(txBytes) > 0 { + p.transactions = append(p.transactions, txBytes) } } case [][]byte: - ps.transactions = v - default: - return nil, fmt.Errorf("invalid transactions type: %T", txs) + p.transactions = v } } - // Parse gasLimit (optional) + // Gas limit if gl, ok := attrs["gasLimit"]; ok { switch v := gl.(type) { case uint64: - ps.gasLimit = v + p.gasLimit = v case int64: - ps.gasLimit = uint64(v) + p.gasLimit = uint64(v) case float64: - ps.gasLimit = uint64(v) + p.gasLimit = uint64(v) case *uint64: if v != nil { - ps.gasLimit = *v + p.gasLimit = *v } - default: - return nil, fmt.Errorf("invalid gasLimit type: %T", gl) } } - // Parse withdrawals (optional) + // Withdrawals if w, ok := attrs["withdrawals"]; ok { - switch v := w.(type) { - case []*types.Withdrawal: - ps.withdrawals = v - case nil: - // Keep empty slice - default: - return nil, fmt.Errorf("invalid withdrawals type: %T", w) + if withdrawals, ok := w.([]*types.Withdrawal); ok { + p.withdrawals = withdrawals } } - return ps, nil + return p, nil } -// GetPayload implements EngineRPCClient. +// GetPayload builds and returns the pending payload. func (g *gethEngineClient) GetPayload(ctx context.Context, payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { - if err := ctx.Err(); err != nil { - return nil, fmt.Errorf("context cancelled: %w", err) - } - g.backend.mu.Lock() defer g.backend.mu.Unlock() - payloadState, ok := g.backend.payloads[payloadID] - if !ok { + p := g.backend.pendingPayload + if p == nil || p.id != payloadID { return nil, fmt.Errorf("unknown payload ID: %s", payloadID.String()) } - // Return cached error if previous build failed - if payloadState.buildErr != nil { - delete(g.backend.payloads, payloadID) - return nil, fmt.Errorf("payload build previously failed: %w", payloadState.buildErr) - } - - // Build the payload if not already built - if payloadState.payload == nil { - buildStartTime := time.Now() - payload, err := g.buildPayload(ctx, payloadState) + if p.built == nil { + built, err := g.buildPayload(ctx, p) if err != nil { - // Cache the error so we don't retry on the same payload - payloadState.buildErr = err return nil, fmt.Errorf("failed to build payload: %w", err) } - payloadState.payload = payload + p.built = built g.logger.Info(). - Str("payload_id", payloadID.String()). - Uint64("block_number", payload.Number). - Str("block_hash", payload.BlockHash.Hex()). - Int("tx_count", len(payload.Transactions)). - Uint64("gas_used", payload.GasUsed). - Dur("build_time", time.Since(buildStartTime)). + Uint64("number", built.Number). + Str("hash", built.BlockHash.Hex()). + Int("txs", len(built.Transactions)). + Uint64("gas_used", built.GasUsed). Msg("built payload") } - // Remove the payload from pending after retrieval - caller has it now - delete(g.backend.payloads, payloadID) + // Clear pending after retrieval + g.backend.pendingPayload = nil return &engine.ExecutionPayloadEnvelope{ - ExecutionPayload: payloadState.payload, + ExecutionPayload: p.built, BlockValue: big.NewInt(0), BlobsBundle: &engine.BlobsBundle{}, - Override: false, }, nil } -// buildPayload constructs an execution payload from the pending state. -func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildState) (*engine.ExecutableData, error) { - parent := g.backend.blockchain.GetBlockByHash(ps.parentHash) +func (g *gethEngineClient) buildPayload(ctx context.Context, p *pendingPayload) (*engine.ExecutableData, error) { + parent := g.backend.blockchain.GetBlockByHash(p.parentHash) if parent == nil { - return nil, fmt.Errorf("parent block not found: %s", ps.parentHash.Hex()) + return nil, fmt.Errorf("parent not found: %s", p.parentHash.Hex()) } - // Validate block number continuity - expectedNumber := new(big.Int).Add(parent.Number(), big.NewInt(1)) + if p.timestamp < parent.Time() { + return nil, fmt.Errorf("timestamp %d < parent %d", p.timestamp, parent.Time()) + } - if ps.timestamp < parent.Time() { - return nil, fmt.Errorf("invalid timestamp: %d must be >= parent timestamp %d", ps.timestamp, parent.Time()) + number := new(big.Int).Add(parent.Number(), big.NewInt(1)) + gasLimit := p.gasLimit + if gasLimit == 0 { + gasLimit = parent.GasLimit() } - // Calculate base fee for the new block var baseFee *big.Int - if g.backend.chainConfig.IsLondon(expectedNumber) { + if g.backend.chainConfig.IsLondon(number) { baseFee = calcBaseFee(g.backend.chainConfig, parent.Header()) } - gasLimit := ps.gasLimit - if gasLimit == 0 { - gasLimit = parent.GasLimit() - } - header := &types.Header{ - ParentHash: ps.parentHash, + ParentHash: p.parentHash, UncleHash: types.EmptyUncleHash, - Coinbase: ps.feeRecipient, - Root: common.Hash{}, // Will be set after execution - TxHash: types.EmptyTxsHash, - ReceiptHash: types.EmptyReceiptsHash, - Bloom: types.Bloom{}, - Difficulty: big.NewInt(0), - Number: expectedNumber, + Coinbase: p.feeRecipient, + Number: number, GasLimit: gasLimit, - GasUsed: 0, - Time: ps.timestamp, - MixDigest: ps.prevRandao, - Nonce: types.BlockNonce{}, + Time: p.timestamp, + MixDigest: p.prevRandao, + Difficulty: big.NewInt(0), BaseFee: baseFee, WithdrawalsHash: &types.EmptyWithdrawalsHash, BlobGasUsed: new(uint64), @@ -619,132 +404,70 @@ func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildSta RequestsHash: &types.EmptyRequestsHash, } - // Process transactions stateDB, err := g.backend.blockchain.StateAt(parent.Root()) if err != nil { - return nil, fmt.Errorf("failed to get parent state: %w", err) + return nil, fmt.Errorf("failed to get state: %w", err) } var ( - txs types.Transactions - receipts []*types.Receipt - gasUsed uint64 - txsExecuted int - txsSkipped int + txs types.Transactions + receipts []*types.Receipt + gasUsed uint64 ) - // Create EVM context blockContext := core.NewEVMBlockContext(header, g.backend.blockchain, nil) - - // Execute transactions gp := new(core.GasPool).AddGas(gasLimit) - for i, txBytes := range ps.transactions { - // Check context periodically - if i%100 == 0 { - if err := ctx.Err(); err != nil { - return nil, fmt.Errorf("context cancelled during tx execution: %w", err) - } - } + for _, txBytes := range p.transactions { if len(txBytes) == 0 { - txsSkipped++ continue } var tx types.Transaction if err := tx.UnmarshalBinary(txBytes); err != nil { - g.logger.Debug(). - Int("index", i). - Err(err). - Msg("skipping invalid transaction encoding") - txsSkipped++ continue } stateDB.SetTxContext(tx.Hash(), len(txs)) - - // Create EVM instance and apply transaction - receipt, err := applyTransaction( - g.backend.chainConfig, - blockContext, - gp, - stateDB, - header, - &tx, - &gasUsed, - ) + receipt, err := applyTransaction(g.backend.chainConfig, blockContext, gp, stateDB, header, &tx, &gasUsed) if err != nil { - g.logger.Debug(). - Int("index", i). - Str("tx_hash", tx.Hash().Hex()). - Err(err). - Msg("transaction execution failed, skipping") - txsSkipped++ + g.logger.Debug().Str("tx", tx.Hash().Hex()).Err(err).Msg("tx failed") continue } txs = append(txs, &tx) receipts = append(receipts, receipt) - txsExecuted++ } - if txsSkipped > 0 { - g.logger.Debug(). - Int("executed", txsExecuted). - Int("skipped", txsSkipped). - Msg("transaction execution summary") + // Process withdrawals + for _, w := range p.withdrawals { + amount := new(big.Int).SetUint64(w.Amount) + amount.Mul(amount, big.NewInt(params.GWei)) + stateDB.AddBalance(w.Address, uint256.MustFromBig(amount), tracing.BalanceIncreaseWithdrawal) } - // Process withdrawals (EIP-4895) - credit ETH to withdrawal recipients - // Withdrawals are processed after all transactions, crediting the specified - // amount (in Gwei) to each recipient address. - if len(ps.withdrawals) > 0 { - for _, withdrawal := range ps.withdrawals { - // Withdrawal amount is in Gwei, convert to Wei (multiply by 1e9) - amount := new(big.Int).SetUint64(withdrawal.Amount) - amount.Mul(amount, big.NewInt(params.GWei)) - stateDB.AddBalance(withdrawal.Address, uint256.MustFromBig(amount), tracing.BalanceIncreaseWithdrawal) - } - g.logger.Debug(). - Int("count", len(ps.withdrawals)). - Msg("processed withdrawals") - } - - // Finalize state header.GasUsed = gasUsed - header.Root = stateDB.IntermediateRoot(g.backend.chainConfig.IsEIP158(header.Number)) - - // Calculate transaction and receipt hashes + header.Root = stateDB.IntermediateRoot(g.backend.chainConfig.IsEIP158(number)) header.TxHash = types.DeriveSha(txs, trie.NewListHasher()) header.ReceiptHash = types.DeriveSha(types.Receipts(receipts), trie.NewListHasher()) + header.Bloom = createBloom(receipts) - // Calculate bloom filter - header.Bloom = createBloomFromReceipts(receipts) - - // Calculate withdrawals hash if withdrawals exist - if len(ps.withdrawals) > 0 { - wh := types.DeriveSha(types.Withdrawals(ps.withdrawals), trie.NewListHasher()) + if len(p.withdrawals) > 0 { + wh := types.DeriveSha(types.Withdrawals(p.withdrawals), trie.NewListHasher()) header.WithdrawalsHash = &wh } - // Create the block block := types.NewBlock(header, &types.Body{ Transactions: txs, - Uncles: nil, - Withdrawals: ps.withdrawals, + Withdrawals: p.withdrawals, }, receipts, trie.NewListHasher()) - // Convert to ExecutableData txData := make([][]byte, len(txs)) for i, tx := range txs { - data, err := tx.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal tx %d: %w", i, err) - } - txData[i] = data + txData[i], _ = tx.MarshalBinary() } - payload := &engine.ExecutableData{ + return &engine.ExecutableData{ ParentHash: header.ParentHash, FeeRecipient: header.Coinbase, StateRoot: header.Root, @@ -759,83 +482,42 @@ func (g *gethEngineClient) buildPayload(ctx context.Context, ps *payloadBuildSta BaseFeePerGas: header.BaseFee, BlockHash: block.Hash(), Transactions: txData, - Withdrawals: ps.withdrawals, + Withdrawals: p.withdrawals, BlobGasUsed: header.BlobGasUsed, ExcessBlobGas: header.ExcessBlobGas, - } - - return payload, nil + }, nil } -// NewPayload implements EngineRPCClient. +// NewPayload validates and inserts a new block. func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.ExecutableData, blobHashes []string, parentBeaconBlockRoot string, executionRequests [][]byte) (*engine.PayloadStatusV1, error) { - if err := ctx.Err(); err != nil { - return nil, fmt.Errorf("context cancelled: %w", err) - } - g.backend.mu.Lock() defer g.backend.mu.Unlock() - validationStart := time.Now() - - // Validate payload if payload == nil { - return nil, errors.New("payload is required") + return nil, errors.New("payload required") } - // Verify parent exists parent := g.backend.blockchain.GetBlockByHash(payload.ParentHash) if parent == nil { - g.logger.Debug(). - Str("parent_hash", payload.ParentHash.Hex()). - Uint64("block_number", payload.Number). - Msg("parent block not found, returning SYNCING") - return &engine.PayloadStatusV1{ - Status: engine.SYNCING, - }, nil + return &engine.PayloadStatusV1{Status: engine.SYNCING}, nil } - // Validate block number - expectedNumber := parent.NumberU64() + 1 - if payload.Number != expectedNumber { - g.logger.Warn(). - Uint64("expected", expectedNumber). - Uint64("got", payload.Number). - Msg("invalid block number") + if payload.Number != parent.NumberU64()+1 { parentHash := parent.Hash() - return &engine.PayloadStatusV1{ - Status: engine.INVALID, - LatestValidHash: &parentHash, - }, nil + return &engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &parentHash}, nil } - // Validate timestamp if payload.Timestamp < parent.Time() { - g.logger.Warn(). - Uint64("payload_timestamp", payload.Timestamp). - Uint64("parent_timestamp", parent.Time()). - Msg("invalid timestamp: must be >= parent timestamp") parentHash := parent.Hash() - return &engine.PayloadStatusV1{ - Status: engine.INVALID, - LatestValidHash: &parentHash, - }, nil + return &engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &parentHash}, nil } - // Decode transactions var txs types.Transactions - for i, txData := range payload.Transactions { + for _, txData := range payload.Transactions { var tx types.Transaction if err := tx.UnmarshalBinary(txData); err != nil { - g.logger.Warn(). - Int("tx_index", i). - Err(err). - Msg("failed to decode transaction") parentHash := parent.Hash() - return &engine.PayloadStatusV1{ - Status: engine.INVALID, - LatestValidHash: &parentHash, - }, nil + return &engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &parentHash}, nil } txs = append(txs, &tx) } @@ -845,7 +527,6 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu gasLimit = parent.GasLimit() } - // Reconstruct the header from the payload header := &types.Header{ ParentHash: payload.ParentHash, UncleHash: types.EmptyUncleHash, @@ -861,7 +542,6 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu Time: payload.Timestamp, Extra: payload.ExtraData, MixDigest: payload.Random, - Nonce: types.BlockNonce{}, BaseFee: payload.BaseFeePerGas, WithdrawalsHash: &types.EmptyWithdrawalsHash, BlobGasUsed: payload.BlobGasUsed, @@ -875,169 +555,106 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu header.WithdrawalsHash = &wh } - // Create the block from the payload block := types.NewBlock(header, &types.Body{ Transactions: txs, - Uncles: nil, Withdrawals: payload.Withdrawals, }, nil, trie.NewListHasher()) - // Verify the block hash matches the payload if block.Hash() != payload.BlockHash { g.logger.Warn(). Str("expected", payload.BlockHash.Hex()). - Str("calculated", block.Hash().Hex()). - Uint64("block_number", payload.Number). + Str("got", block.Hash().Hex()). Msg("block hash mismatch") parentHash := parent.Hash() - return &engine.PayloadStatusV1{ - Status: engine.INVALID, - LatestValidHash: &parentHash, - }, nil + return &engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &parentHash}, nil } - // Use InsertBlockWithoutSetHead which processes, validates, and commits the block - // This ensures proper state validation using go-ethereum's internal processor - _, err := g.backend.blockchain.InsertBlockWithoutSetHead(block, false) - if err != nil { - g.logger.Warn(). - Err(err). - Str("block_hash", block.Hash().Hex()). - Uint64("block_number", block.NumberU64()). - Msg("block validation/insertion failed") + if _, err := g.backend.blockchain.InsertBlockWithoutSetHead(block, false); err != nil { + g.logger.Warn().Err(err).Str("hash", block.Hash().Hex()).Msg("block insertion failed") parentHash := parent.Hash() - return &engine.PayloadStatusV1{ - Status: engine.INVALID, - LatestValidHash: &parentHash, - }, nil + return &engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &parentHash}, nil } blockHash := block.Hash() - g.logger.Info(). - Uint64("block_number", block.NumberU64()). - Str("block_hash", blockHash.Hex()). - Str("parent_hash", payload.ParentHash.Hex()). - Int("tx_count", len(txs)). - Uint64("gas_used", payload.GasUsed). - Dur("process_time", time.Since(validationStart)). - Msg("new payload validated and inserted") - - return &engine.PayloadStatusV1{ - Status: engine.VALID, - LatestValidHash: &blockHash, - }, nil + Uint64("number", block.NumberU64()). + Str("hash", blockHash.Hex()). + Int("txs", len(txs)). + Msg("payload validated") + + return &engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &blockHash}, nil } -// HeaderByNumber implements EthRPCClient. +// HeaderByNumber returns a header by number. func (g *gethEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { - if err := ctx.Err(); err != nil { - return nil, fmt.Errorf("context cancelled: %w", err) - } - if number == nil { - // Return current head header := g.backend.blockchain.CurrentBlock() if header == nil { return nil, errors.New("no current block") } return header, nil } - block := g.backend.blockchain.GetBlockByNumber(number.Uint64()) if block == nil { - return nil, fmt.Errorf("block not found at height %d", number.Uint64()) + return nil, fmt.Errorf("block %d not found", number.Uint64()) } return block.Header(), nil } -// GetTxs implements EthRPCClient. +// GetTxs returns pending transactions. func (g *gethEthClient) GetTxs(ctx context.Context) ([]string, error) { - if err := ctx.Err(); err != nil { - return nil, fmt.Errorf("context cancelled: %w", err) - } - pending := g.backend.txPool.Pending(txpool.PendingFilter{}) var result []string for _, txs := range pending { for _, lazyTx := range txs { - tx := lazyTx.Tx - if tx == nil { - continue - } - data, err := tx.MarshalBinary() - if err != nil { - g.logger.Debug(). - Str("tx_hash", tx.Hash().Hex()). - Err(err). - Msg("failed to marshal pending tx") - continue + if tx := lazyTx.Tx; tx != nil { + if data, err := tx.MarshalBinary(); err == nil { + result = append(result, "0x"+common.Bytes2Hex(data)) + } } - result = append(result, "0x"+common.Bytes2Hex(data)) } } return result, nil } -// calcBaseFee calculates the base fee for the next block according to EIP-1559. +// calcBaseFee calculates EIP-1559 base fee. func calcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { - nextBlockNumber := new(big.Int).Add(parent.Number, big.NewInt(1)) - - // If we're before London, return nil - if !config.IsLondon(nextBlockNumber) { + next := new(big.Int).Add(parent.Number, big.NewInt(1)) + if !config.IsLondon(next) { return nil } - - // Use genesis base fee if this is the first London block - if !config.IsLondon(parent.Number) { + if !config.IsLondon(parent.Number) || parent.BaseFee == nil { return big.NewInt(params.InitialBaseFee) } - // Parent must have base fee - if parent.BaseFee == nil { - return big.NewInt(params.InitialBaseFee) - } - - // Calculate next base fee based on EIP-1559 - var ( - parentGasTarget = parent.GasLimit / 2 - parentGasUsed = parent.GasUsed - baseFee = new(big.Int).Set(parent.BaseFee) - ) - - // Prevent division by zero - if parentGasTarget == 0 { - return baseFee + target := parent.GasLimit / 2 + if target == 0 { + return new(big.Int).Set(parent.BaseFee) } - if parentGasUsed == parentGasTarget { + baseFee := new(big.Int).Set(parent.BaseFee) + if parent.GasUsed == target { return baseFee } - if parentGasUsed > parentGasTarget { - // Block was more full than target, increase base fee - gasUsedDelta := new(big.Int).SetUint64(parentGasUsed - parentGasTarget) - x := new(big.Int).Mul(parent.BaseFee, gasUsedDelta) - y := new(big.Int).SetUint64(parentGasTarget) - z := new(big.Int).Div(x, y) - baseFeeChangeDenominatorInt := new(big.Int).SetUint64(baseFeeChangeDenominator) - delta := new(big.Int).Div(z, baseFeeChangeDenominatorInt) + if parent.GasUsed > target { + delta := new(big.Int).SetUint64(parent.GasUsed - target) + delta.Mul(delta, parent.BaseFee) + delta.Div(delta, new(big.Int).SetUint64(target)) + delta.Div(delta, big.NewInt(8)) if delta.Sign() == 0 { delta = big.NewInt(1) } - return new(big.Int).Add(baseFee, delta) + return baseFee.Add(baseFee, delta) } - // Block was less full than target, decrease base fee - gasUsedDelta := new(big.Int).SetUint64(parentGasTarget - parentGasUsed) - x := new(big.Int).Mul(parent.BaseFee, gasUsedDelta) - y := new(big.Int).SetUint64(parentGasTarget) - z := new(big.Int).Div(x, y) - baseFeeChangeDenominatorInt := new(big.Int).SetUint64(baseFeeChangeDenominator) - delta := new(big.Int).Div(z, baseFeeChangeDenominatorInt) - baseFee = new(big.Int).Sub(baseFee, delta) - if baseFee.Cmp(big.NewInt(0)) < 0 { - baseFee = big.NewInt(0) + delta := new(big.Int).SetUint64(target - parent.GasUsed) + delta.Mul(delta, parent.BaseFee) + delta.Div(delta, new(big.Int).SetUint64(target)) + delta.Div(delta, big.NewInt(8)) + baseFee.Sub(baseFee, delta) + if baseFee.Sign() < 0 { + return big.NewInt(0) } return baseFee } @@ -1054,26 +671,21 @@ func applyTransaction( ) (*types.Receipt, error) { msg, err := core.TransactionToMessage(tx, types.LatestSigner(config), header.BaseFee) if err != nil { - return nil, fmt.Errorf("failed to convert tx to message: %w", err) + return nil, err } - // Create EVM instance - txContext := core.NewEVMTxContext(msg) - evmInstance := vm.NewEVM(blockContext, stateDB, config, vm.Config{}) - evmInstance.SetTxContext(txContext) + evm := vm.NewEVM(blockContext, stateDB, config, vm.Config{}) + evm.SetTxContext(core.NewEVMTxContext(msg)) - // Apply the transaction - result, err := core.ApplyMessage(evmInstance, msg, gp) + result, err := core.ApplyMessage(evm, msg, gp) if err != nil { - return nil, fmt.Errorf("failed to apply message: %w", err) + return nil, err } *usedGas += result.UsedGas - // Create the receipt receipt := &types.Receipt{ Type: tx.Type(), - PostState: nil, CumulativeGasUsed: *usedGas, TxHash: tx.Hash(), GasUsed: result.UsedGas, @@ -1087,10 +699,8 @@ func applyTransaction( receipt.Status = types.ReceiptStatusSuccessful } - // Set the receipt logs bloom receipt.Bloom = types.CreateBloom(receipt) - // Set contract address if this was a contract creation if msg.To == nil { receipt.ContractAddress = crypto.CreateAddress(msg.From, tx.Nonce()) } @@ -1098,14 +708,13 @@ func applyTransaction( return receipt, nil } -// createBloomFromReceipts creates a bloom filter from multiple receipts. -func createBloomFromReceipts(receipts []*types.Receipt) types.Bloom { - var bin types.Bloom - for _, receipt := range receipts { - bloom := types.CreateBloom(receipt) - for i := range bin { - bin[i] |= bloom[i] +func createBloom(receipts []*types.Receipt) types.Bloom { + var bloom types.Bloom + for _, r := range receipts { + b := types.CreateBloom(r) + for i := range bloom { + bloom[i] |= b[i] } } - return bin + return bloom } diff --git a/execution/evm/engine_geth_db.go b/execution/evm/engine_geth_db.go index 5fdb70360c..bc54ac6a4a 100644 --- a/execution/evm/engine_geth_db.go +++ b/execution/evm/engine_geth_db.go @@ -3,6 +3,7 @@ package evm import ( "bytes" "context" + "errors" "sort" "strings" "sync" @@ -13,247 +14,132 @@ import ( "github.com/syndtr/goleveldb/leveldb" ) -// deleteRangeBatchSize is the number of keys to delete in a single batch -// to avoid holding locks for too long. -const deleteRangeBatchSize = 1000 - -var _ ethdb.KeyValueStore = &wrapper{} +var _ ethdb.KeyValueStore = (*wrapper)(nil) type wrapper struct { ds datastore.Batching } -func keyToDatastoreKey(key []byte) datastore.Key { +// NewEVMDB creates an ethdb.KeyValueStore backed by a go-datastore. +func NewEVMDB(ds datastore.Batching) ethdb.KeyValueStore { + return &wrapper{ds} +} + +func toKey(key []byte) datastore.Key { return datastore.NewKey(string(key)) } -func datastoreKeyToBytes(key string) []byte { - // datastore keys have a leading slash, remove it +func fromKey(key string) []byte { if strings.HasPrefix(key, "/") { return []byte(key[1:]) } return []byte(key) } -// Close implements ethdb.KeyValueStore. -func (w *wrapper) Close() error { - return w.ds.Close() +func (w *wrapper) Has(key []byte) (bool, error) { + return w.ds.Has(context.Background(), toKey(key)) } -// Compact implements ethdb.KeyValueStore. -func (w *wrapper) Compact(start []byte, limit []byte) error { - // Compaction is not supported by go-datastore, this is a no-op - return nil +func (w *wrapper) Get(key []byte) ([]byte, error) { + val, err := w.ds.Get(context.Background(), toKey(key)) + if errors.Is(err, datastore.ErrNotFound) { + return nil, leveldb.ErrNotFound + } + return val, err +} + +func (w *wrapper) Put(key []byte, value []byte) error { + return w.ds.Put(context.Background(), toKey(key), value) } -// Delete implements ethdb.KeyValueStore. func (w *wrapper) Delete(key []byte) error { - return w.ds.Delete(context.Background(), keyToDatastoreKey(key)) + return w.ds.Delete(context.Background(), toKey(key)) } -// DeleteRange implements ethdb.KeyValueStore. -// Optimized to use prefix-based querying when possible and batch deletions. -func (w *wrapper) DeleteRange(start []byte, end []byte) error { +func (w *wrapper) DeleteRange(start, end []byte) error { ctx := context.Background() - - // Find common prefix between start and end to narrow the query - prefix := commonPrefix(start, end) - - q := query.Query{ - KeysOnly: true, - } - if len(prefix) > 0 { - q.Prefix = "/" + string(prefix) - } - - results, err := w.ds.Query(ctx, q) + results, err := w.ds.Query(ctx, query.Query{KeysOnly: true}) if err != nil { return err } defer results.Close() - // Collect keys to delete in batches - var keysToDelete []datastore.Key for result := range results.Next() { if result.Error != nil { return result.Error } - keyBytes := datastoreKeyToBytes(result.Entry.Key) + keyBytes := fromKey(result.Key) if bytes.Compare(keyBytes, start) >= 0 && bytes.Compare(keyBytes, end) < 0 { - keysToDelete = append(keysToDelete, datastore.NewKey(result.Entry.Key)) - - // Process in batches to avoid holding too many keys in memory - if len(keysToDelete) >= deleteRangeBatchSize { - if err := w.deleteBatch(ctx, keysToDelete); err != nil { - return err - } - keysToDelete = keysToDelete[:0] - } - } - } - - // Delete remaining keys - if len(keysToDelete) > 0 { - return w.deleteBatch(ctx, keysToDelete) - } - return nil -} - -// deleteBatch deletes a batch of keys using a batched operation. -func (w *wrapper) deleteBatch(ctx context.Context, keys []datastore.Key) error { - batch, err := w.ds.Batch(ctx) - if err != nil { - // Fallback to individual deletes if batching not supported - for _, key := range keys { - if err := w.ds.Delete(ctx, key); err != nil { + if err := w.ds.Delete(ctx, datastore.NewKey(result.Key)); err != nil { return err } } - return nil - } - - for _, key := range keys { - if err := batch.Delete(ctx, key); err != nil { - return err - } - } - return batch.Commit(ctx) -} - -// commonPrefix returns the common prefix between two byte slices. -func commonPrefix(a, b []byte) []byte { - minLen := len(a) - if len(b) < minLen { - minLen = len(b) - } - var i int - for i = 0; i < minLen; i++ { - if a[i] != b[i] { - break - } - } - return a[:i] -} - -// Get implements ethdb.KeyValueStore. -func (w *wrapper) Get(key []byte) ([]byte, error) { - val, err := w.ds.Get(context.Background(), keyToDatastoreKey(key)) - if err == datastore.ErrNotFound { - return nil, leveldb.ErrNotFound } - return val, err -} - -// Has implements ethdb.KeyValueStore. -func (w *wrapper) Has(key []byte) (bool, error) { - return w.ds.Has(context.Background(), keyToDatastoreKey(key)) + return nil } -// NewBatch implements ethdb.KeyValueStore. func (w *wrapper) NewBatch() ethdb.Batch { - return &batchWrapper{ - ds: w.ds, - ops: nil, - size: 0, - } + return &batch{ds: w.ds} } -// NewBatchWithSize implements ethdb.KeyValueStore. func (w *wrapper) NewBatchWithSize(size int) ethdb.Batch { - return &batchWrapper{ - ds: w.ds, - ops: make([]batchOp, 0, size), - size: 0, - } + return &batch{ds: w.ds, ops: make([]batchOp, 0, size)} } -// NewIterator implements ethdb.KeyValueStore. func (w *wrapper) NewIterator(prefix []byte, start []byte) ethdb.Iterator { - return newIterator(w.ds, prefix, start, 0) -} - -// NewIteratorWithSize creates an iterator with an estimated size hint for buffer allocation. -func (w *wrapper) NewIteratorWithSize(prefix []byte, start []byte, sizeHint int) ethdb.Iterator { - return newIterator(w.ds, prefix, start, sizeHint) -} - -// Put implements ethdb.KeyValueStore. -func (w *wrapper) Put(key []byte, value []byte) error { - return w.ds.Put(context.Background(), keyToDatastoreKey(key), value) + return newIterator(w.ds, prefix, start) } -// Stat implements ethdb.KeyValueStore. func (w *wrapper) Stat() (string, error) { return "go-datastore wrapper", nil } -// SyncKeyValue implements ethdb.KeyValueStore. func (w *wrapper) SyncKeyValue() error { return w.ds.Sync(context.Background(), datastore.NewKey("/")) } -func NewEVMDB(ds datastore.Batching) ethdb.KeyValueStore { - return &wrapper{ds} +func (w *wrapper) Compact(start []byte, limit []byte) error { + return nil // no-op +} + +func (w *wrapper) Close() error { + return w.ds.Close() } -// batchOp represents a single batch operation +// --- Batch --- + type batchOp struct { key []byte - value []byte // nil means delete + value []byte delete bool } -// batchWrapper implements ethdb.Batch -type batchWrapper struct { +type batch struct { ds datastore.Batching ops []batchOp size int mu sync.Mutex } -var _ ethdb.Batch = &batchWrapper{} - -// Put implements ethdb.Batch. -func (b *batchWrapper) Put(key []byte, value []byte) error { +func (b *batch) Put(key, value []byte) error { b.mu.Lock() defer b.mu.Unlock() - - b.ops = append(b.ops, batchOp{ - key: append([]byte{}, key...), - value: append([]byte{}, value...), - delete: false, - }) + b.ops = append(b.ops, batchOp{key: append([]byte{}, key...), value: append([]byte{}, value...)}) b.size += len(key) + len(value) return nil } -// Delete implements ethdb.Batch. -func (b *batchWrapper) Delete(key []byte) error { +func (b *batch) Delete(key []byte) error { b.mu.Lock() defer b.mu.Unlock() - - b.ops = append(b.ops, batchOp{ - key: append([]byte{}, key...), - value: nil, - delete: true, - }) + b.ops = append(b.ops, batchOp{key: append([]byte{}, key...), delete: true}) b.size += len(key) return nil } -// DeleteRange implements ethdb.Batch. -func (b *batchWrapper) DeleteRange(start []byte, end []byte) error { +func (b *batch) DeleteRange(start, end []byte) error { ctx := context.Background() - - // Use common prefix to narrow query - prefix := commonPrefix(start, end) - - q := query.Query{KeysOnly: true} - if len(prefix) > 0 { - q.Prefix = "/" + string(prefix) - } - - results, err := b.ds.Query(ctx, q) + results, err := b.ds.Query(ctx, query.Query{KeysOnly: true}) if err != nil { return err } @@ -266,61 +152,53 @@ func (b *batchWrapper) DeleteRange(start []byte, end []byte) error { if result.Error != nil { return result.Error } - keyBytes := datastoreKeyToBytes(result.Entry.Key) + keyBytes := fromKey(result.Key) if bytes.Compare(keyBytes, start) >= 0 && bytes.Compare(keyBytes, end) < 0 { - b.ops = append(b.ops, batchOp{ - key: append([]byte{}, keyBytes...), - value: nil, - delete: true, - }) + b.ops = append(b.ops, batchOp{key: append([]byte{}, keyBytes...), delete: true}) b.size += len(keyBytes) } } return nil } -// ValueSize implements ethdb.Batch. -func (b *batchWrapper) ValueSize() int { +func (b *batch) ValueSize() int { b.mu.Lock() defer b.mu.Unlock() return b.size } -// Write implements ethdb.Batch. -func (b *batchWrapper) Write() error { +func (b *batch) Write() error { b.mu.Lock() defer b.mu.Unlock() - batch, err := b.ds.Batch(context.Background()) + dsBatch, err := b.ds.Batch(context.Background()) if err != nil { return err } + ctx := context.Background() for _, op := range b.ops { if op.delete { - if err := batch.Delete(context.Background(), keyToDatastoreKey(op.key)); err != nil { + if err := dsBatch.Delete(ctx, toKey(op.key)); err != nil { return err } } else { - if err := batch.Put(context.Background(), keyToDatastoreKey(op.key), op.value); err != nil { + if err := dsBatch.Put(ctx, toKey(op.key), op.value); err != nil { return err } } } - - return batch.Commit(context.Background()) + return dsBatch.Commit(ctx) } -// Reset implements ethdb.Batch. -func (b *batchWrapper) Reset() { +func (b *batch) Reset() { b.mu.Lock() defer b.mu.Unlock() b.ops = b.ops[:0] b.size = 0 } -// Replay implements ethdb.Batch. -func (b *batchWrapper) Replay(w ethdb.KeyValueWriter) error { +func (b *batch) Replay(w ethdb.KeyValueWriter) error { b.mu.Lock() defer b.mu.Unlock() @@ -338,120 +216,81 @@ func (b *batchWrapper) Replay(w ethdb.KeyValueWriter) error { return nil } -// iteratorWrapper implements ethdb.Iterator -// It provides sorted iteration over keys with optional prefix and start position. -type iteratorWrapper struct { - entries []query.Entry // Pre-sorted entries for deterministic iteration +// --- Iterator --- + +type iterator struct { + entries []query.Entry index int - prefix []byte - start []byte err error closed bool mu sync.Mutex } -var _ ethdb.Iterator = &iteratorWrapper{} - -func newIterator(ds datastore.Batching, prefix []byte, start []byte, sizeHint int) *iteratorWrapper { - q := query.Query{ - KeysOnly: false, - } - +func newIterator(ds datastore.Batching, prefix, start []byte) *iterator { + q := query.Query{} if len(prefix) > 0 { q.Prefix = "/" + string(prefix) } results, err := ds.Query(context.Background(), q) if err != nil { - return &iteratorWrapper{ - err: err, - closed: false, - index: -1, - } + return &iterator{err: err, index: -1} } - // Collect and sort all entries for deterministic ordering - // go-datastore doesn't guarantee order, but ethdb.Iterator expects sorted keys var entries []query.Entry - if sizeHint > 0 { - entries = make([]query.Entry, 0, sizeHint) - } - for result := range results.Next() { if result.Error != nil { results.Close() - return &iteratorWrapper{ - err: result.Error, - closed: false, - index: -1, - } + return &iterator{err: result.Error, index: -1} } - keyBytes := datastoreKeyToBytes(result.Entry.Key) - - // Filter by prefix + keyBytes := fromKey(result.Key) if len(prefix) > 0 && !bytes.HasPrefix(keyBytes, prefix) { continue } - - // Filter by start position if len(start) > 0 && bytes.Compare(keyBytes, start) < 0 { continue } - - entries = append(entries, result.Entry) + entries = append(entries, query.Entry{Key: result.Key, Value: result.Value}) } results.Close() - // Sort entries by key for deterministic iteration order + // Sort for deterministic ordering sort.Slice(entries, func(i, j int) bool { - keyI := datastoreKeyToBytes(entries[i].Key) - keyJ := datastoreKeyToBytes(entries[j].Key) - return bytes.Compare(keyI, keyJ) < 0 + return bytes.Compare(fromKey(entries[i].Key), fromKey(entries[j].Key)) < 0 }) - return &iteratorWrapper{ - entries: entries, - prefix: prefix, - start: start, - index: -1, - closed: false, - } + return &iterator{entries: entries, index: -1} } -// Next implements ethdb.Iterator. -func (it *iteratorWrapper) Next() bool { +func (it *iterator) Next() bool { it.mu.Lock() defer it.mu.Unlock() if it.closed || it.err != nil { return false } - it.index++ return it.index < len(it.entries) } -// Error implements ethdb.Iterator. -func (it *iteratorWrapper) Error() error { +func (it *iterator) Error() error { it.mu.Lock() defer it.mu.Unlock() return it.err } -// Key implements ethdb.Iterator. -func (it *iteratorWrapper) Key() []byte { +func (it *iterator) Key() []byte { it.mu.Lock() defer it.mu.Unlock() if it.closed || it.index < 0 || it.index >= len(it.entries) { return nil } - return datastoreKeyToBytes(it.entries[it.index].Key) + return fromKey(it.entries[it.index].Key) } -// Value implements ethdb.Iterator. -func (it *iteratorWrapper) Value() []byte { +func (it *iterator) Value() []byte { it.mu.Lock() defer it.mu.Unlock() @@ -461,15 +300,10 @@ func (it *iteratorWrapper) Value() []byte { return it.entries[it.index].Value } -// Release implements ethdb.Iterator. -func (it *iteratorWrapper) Release() { +func (it *iterator) Release() { it.mu.Lock() defer it.mu.Unlock() - if it.closed { - return - } it.closed = true - // Clear entries to allow GC it.entries = nil } diff --git a/execution/evm/engine_geth_rpc.go b/execution/evm/engine_geth_rpc.go index 26a24d3bbf..dfa6d18465 100644 --- a/execution/evm/engine_geth_rpc.go +++ b/execution/evm/engine_geth_rpc.go @@ -1,7 +1,6 @@ package evm import ( - "encoding/hex" "errors" "fmt" "math/big" @@ -13,26 +12,20 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/rs/zerolog" ) -// EthRPCService implements the eth_ JSON-RPC namespace. +// EthRPCService implements essential eth_ JSON-RPC methods for block explorers. type EthRPCService struct { backend *GethBackend logger zerolog.Logger } -// TxPoolExtService implements the txpoolExt_ JSON-RPC namespace. -type TxPoolExtService struct { - backend *GethBackend - logger zerolog.Logger -} - // NetRPCService implements the net_ JSON-RPC namespace. type NetRPCService struct { backend *GethBackend @@ -41,21 +34,7 @@ type NetRPCService struct { // Web3RPCService implements the web3_ JSON-RPC namespace. type Web3RPCService struct{} -// rpcHandler wraps the RPC server with CORS and proper HTTP handling. -type rpcHandler struct { - rpcServer *rpc.Server - logger zerolog.Logger -} - -// FilterQuery represents a filter for logs. -type FilterQuery struct { - FromBlock *big.Int `json:"fromBlock"` - ToBlock *big.Int `json:"toBlock"` - Addresses []common.Address `json:"address"` - Topics [][]common.Hash `json:"topics"` -} - -// TransactionArgs represents the arguments to construct a transaction. +// TransactionArgs represents transaction call arguments. type TransactionArgs struct { From *common.Address `json:"from"` To *common.Address `json:"to"` @@ -69,8 +48,7 @@ type TransactionArgs struct { Nonce *hexutil.Uint64 `json:"nonce"` } -// StartRPCServer starts the JSON-RPC server on the specified address. -// If address is empty, the server is not started. +// StartRPCServer starts a minimal JSON-RPC server for block explorer compatibility. func (b *GethBackend) StartRPCServer(address string) error { if address == "" { return nil @@ -78,34 +56,14 @@ func (b *GethBackend) StartRPCServer(address string) error { b.rpcServer = rpc.NewServer() - // Register eth_ namespace - ethService := &EthRPCService{backend: b, logger: b.logger.With().Str("rpc", "eth").Logger()} - if err := b.rpcServer.RegisterName("eth", ethService); err != nil { - return fmt.Errorf("failed to register eth service: %w", err) + if err := b.rpcServer.RegisterName("eth", &EthRPCService{backend: b, logger: b.logger.With().Str("rpc", "eth").Logger()}); err != nil { + return fmt.Errorf("failed to register eth: %w", err) } - - // Register txpoolExt_ namespace for compatibility - txpoolService := &TxPoolExtService{backend: b, logger: b.logger.With().Str("rpc", "txpoolExt").Logger()} - if err := b.rpcServer.RegisterName("txpoolExt", txpoolService); err != nil { - return fmt.Errorf("failed to register txpoolExt service: %w", err) + if err := b.rpcServer.RegisterName("net", &NetRPCService{backend: b}); err != nil { + return fmt.Errorf("failed to register net: %w", err) } - - // Register net_ namespace - netService := &NetRPCService{backend: b} - if err := b.rpcServer.RegisterName("net", netService); err != nil { - return fmt.Errorf("failed to register net service: %w", err) - } - - // Register web3_ namespace - web3Service := &Web3RPCService{} - if err := b.rpcServer.RegisterName("web3", web3Service); err != nil { - return fmt.Errorf("failed to register web3 service: %w", err) - } - - // Create HTTP handler with CORS support - handler := &rpcHandler{ - rpcServer: b.rpcServer, - logger: b.logger, + if err := b.rpcServer.RegisterName("web3", &Web3RPCService{}); err != nil { + return fmt.Errorf("failed to register web3: %w", err) } listener, err := net.Listen("tcp", address) @@ -115,7 +73,7 @@ func (b *GethBackend) StartRPCServer(address string) error { b.rpcListener = listener b.httpServer = &http.Server{ - Handler: handler, + Handler: &rpcHandler{rpcServer: b.rpcServer}, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } @@ -130,84 +88,126 @@ func (b *GethBackend) StartRPCServer(address string) error { return nil } +type rpcHandler struct { + rpcServer *rpc.Server +} + func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Set CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Content-Type", "application/json") - // Handle preflight requests if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) return } - - // Handle GET requests with a simple response if r.Method == "GET" { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"jsonrpc":"2.0","result":"ev-node in-process geth RPC","id":null}`)) + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","result":"ev-node RPC","id":null}`)) return } - - // Handle POST requests via the RPC server if r.Method == "POST" { h.rpcServer.ServeHTTP(w, r) return } - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } -// Version returns the network ID. +// --- Web3 namespace --- + +func (s *Web3RPCService) ClientVersion() string { + return "ev-node/1.0.0" +} + +func (s *Web3RPCService) Sha3(input hexutil.Bytes) hexutil.Bytes { + return crypto.Keccak256(input) +} + +// --- Net namespace --- + func (s *NetRPCService) Version() string { return s.backend.chainConfig.ChainID.String() } -// Listening returns true if the node is listening for connections. func (s *NetRPCService) Listening() bool { return true } -// PeerCount returns the number of connected peers. func (s *NetRPCService) PeerCount() hexutil.Uint { return 0 } -// ClientVersion returns the client version. -func (s *Web3RPCService) ClientVersion() string { - return "ev-node/geth/1.0.0" -} - -// Sha3 returns the Keccak-256 hash of the input. -func (s *Web3RPCService) Sha3(input hexutil.Bytes) hexutil.Bytes { - hash := common.BytesToHash(input) - return hash[:] -} +// --- Eth namespace --- -// ChainId returns the chain ID. func (s *EthRPCService) ChainId() *hexutil.Big { return (*hexutil.Big)(s.backend.chainConfig.ChainID) } -// BlockNumber returns the current block number. func (s *EthRPCService) BlockNumber() hexutil.Uint64 { + if header := s.backend.blockchain.CurrentBlock(); header != nil { + return hexutil.Uint64(header.Number.Uint64()) + } + return 0 +} + +func (s *EthRPCService) GasPrice() *hexutil.Big { header := s.backend.blockchain.CurrentBlock() - if header == nil { - return 0 + if header == nil || header.BaseFee == nil { + return (*hexutil.Big)(big.NewInt(params.InitialBaseFee)) + } + return (*hexutil.Big)(new(big.Int).Add(header.BaseFee, big.NewInt(1e9))) +} + +func (s *EthRPCService) MaxPriorityFeePerGas() *hexutil.Big { + return (*hexutil.Big)(big.NewInt(1e9)) +} + +func (s *EthRPCService) Syncing() (interface{}, error) { + return false, nil +} + +func (s *EthRPCService) Accounts() []common.Address { + return []common.Address{} +} + +func (s *EthRPCService) GetBalance(address common.Address, blockNr rpc.BlockNumber) (*hexutil.Big, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err } - return hexutil.Uint64(header.Number.Uint64()) + return (*hexutil.Big)(stateDB.GetBalance(address).ToBig()), nil +} + +func (s *EthRPCService) GetTransactionCount(address common.Address, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return 0, err + } + return hexutil.Uint64(stateDB.GetNonce(address)), nil +} + +func (s *EthRPCService) GetCode(address common.Address, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + return stateDB.GetCode(address), nil +} + +func (s *EthRPCService) GetStorageAt(address common.Address, position hexutil.Big, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + stateDB, err := s.stateAtBlock(blockNr) + if err != nil { + return nil, err + } + value := stateDB.GetState(address, common.BigToHash((*big.Int)(&position))) + return value[:], nil } -// GetBlockByNumber returns block information by number. func (s *EthRPCService) GetBlockByNumber(blockNr rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { var block *types.Block if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { - header := s.backend.blockchain.CurrentBlock() - if header == nil { - return nil, nil + if header := s.backend.blockchain.CurrentBlock(); header != nil { + block = s.backend.blockchain.GetBlock(header.Hash(), header.Number.Uint64()) } - block = s.backend.blockchain.GetBlock(header.Hash(), header.Number.Uint64()) } else { block = s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) } @@ -217,7 +217,6 @@ func (s *EthRPCService) GetBlockByNumber(blockNr rpc.BlockNumber, fullTx bool) ( return s.formatBlock(block, fullTx), nil } -// GetBlockByHash returns block information by hash. func (s *EthRPCService) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { block := s.backend.blockchain.GetBlockByHash(hash) if block == nil { @@ -226,142 +225,188 @@ func (s *EthRPCService) GetBlockByHash(hash common.Hash, fullTx bool) (map[strin return s.formatBlock(block, fullTx), nil } -// formatBlock formats a block for JSON-RPC response. -func (s *EthRPCService) formatBlock(block *types.Block, fullTx bool) map[string]interface{} { - header := block.Header() - result := map[string]interface{}{ - "number": (*hexutil.Big)(header.Number), - "hash": block.Hash(), - "parentHash": header.ParentHash, - "nonce": header.Nonce, - "sha3Uncles": header.UncleHash, - "logsBloom": header.Bloom, - "transactionsRoot": header.TxHash, - "stateRoot": header.Root, - "receiptsRoot": header.ReceiptHash, - "miner": header.Coinbase, - "difficulty": (*hexutil.Big)(header.Difficulty), - "extraData": hexutil.Bytes(header.Extra), - "size": hexutil.Uint64(block.Size()), - "gasLimit": hexutil.Uint64(header.GasLimit), - "gasUsed": hexutil.Uint64(header.GasUsed), - "timestamp": hexutil.Uint64(header.Time), +func (s *EthRPCService) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { + block := s.backend.blockchain.GetBlockByHash(hash) + if block == nil { + return nil } + n := hexutil.Uint(len(block.Transactions())) + return &n +} - if header.BaseFee != nil { - result["baseFeePerGas"] = (*hexutil.Big)(header.BaseFee) +func (s *EthRPCService) GetBlockTransactionCountByNumber(blockNr rpc.BlockNumber) *hexutil.Uint { + var block *types.Block + if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { + if header := s.backend.blockchain.CurrentBlock(); header != nil { + block = s.backend.blockchain.GetBlock(header.Hash(), header.Number.Uint64()) + } + } else { + block = s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) + } + if block == nil { + return nil } + n := hexutil.Uint(len(block.Transactions())) + return &n +} - txs := block.Transactions() - if fullTx { - txList := make([]map[string]interface{}, len(txs)) - for i, tx := range txs { - txList[i] = s.formatTransaction(tx, block.Hash(), header.Number.Uint64(), uint64(i)) +func (s *EthRPCService) GetTransactionByHash(hash common.Hash) (map[string]interface{}, error) { + currentBlock := s.backend.blockchain.CurrentBlock() + if currentBlock == nil { + return nil, nil + } + + for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { + block := s.backend.blockchain.GetBlockByNumber(i) + if block == nil { + continue } - result["transactions"] = txList - } else { - txHashes := make([]common.Hash, len(txs)) - for i, tx := range txs { - txHashes[i] = tx.Hash() + for idx, tx := range block.Transactions() { + if tx.Hash() == hash { + return s.formatTransaction(tx, block.Hash(), block.NumberU64(), uint64(idx)), nil + } } - result["transactions"] = txHashes } - - result["uncles"] = []common.Hash{} - return result + return nil, nil } -// formatTransaction formats a transaction for JSON-RPC response. -func (s *EthRPCService) formatTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64) map[string]interface{} { - signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) - from, _ := types.Sender(signer, tx) - - result := map[string]interface{}{ - "hash": tx.Hash(), - "nonce": hexutil.Uint64(tx.Nonce()), - "blockHash": blockHash, - "blockNumber": (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)), - "transactionIndex": hexutil.Uint64(index), - "from": from, - "to": tx.To(), - "value": (*hexutil.Big)(tx.Value()), - "gas": hexutil.Uint64(tx.Gas()), - "input": hexutil.Bytes(tx.Data()), +func (s *EthRPCService) GetTransactionByBlockHashAndIndex(hash common.Hash, index hexutil.Uint) map[string]interface{} { + block := s.backend.blockchain.GetBlockByHash(hash) + if block == nil { + return nil + } + txs := block.Transactions() + if int(index) >= len(txs) { + return nil } + return s.formatTransaction(txs[index], block.Hash(), block.NumberU64(), uint64(index)) +} - if tx.Type() == types.LegacyTxType { - result["gasPrice"] = (*hexutil.Big)(tx.GasPrice()) +func (s *EthRPCService) GetTransactionByBlockNumberAndIndex(blockNr rpc.BlockNumber, index hexutil.Uint) map[string]interface{} { + var block *types.Block + if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { + if header := s.backend.blockchain.CurrentBlock(); header != nil { + block = s.backend.blockchain.GetBlock(header.Hash(), header.Number.Uint64()) + } } else { - result["gasPrice"] = (*hexutil.Big)(tx.GasPrice()) - result["maxFeePerGas"] = (*hexutil.Big)(tx.GasFeeCap()) - result["maxPriorityFeePerGas"] = (*hexutil.Big)(tx.GasTipCap()) - result["type"] = hexutil.Uint64(tx.Type()) + block = s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) + } + if block == nil { + return nil } + txs := block.Transactions() + if int(index) >= len(txs) { + return nil + } + return s.formatTransaction(txs[index], block.Hash(), block.NumberU64(), uint64(index)) +} - v, r, ss := tx.RawSignatureValues() - result["v"] = (*hexutil.Big)(v) - result["r"] = (*hexutil.Big)(r) - result["s"] = (*hexutil.Big)(ss) +func (s *EthRPCService) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + currentBlock := s.backend.blockchain.CurrentBlock() + if currentBlock == nil { + return nil, nil + } - return result -} + for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { + block := s.backend.blockchain.GetBlockByNumber(i) + if block == nil { + continue + } + for idx, tx := range block.Transactions() { + if tx.Hash() == hash { + receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) + if idx >= len(receipts) { + return nil, nil + } + receipt := receipts[idx] + signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) + from, _ := types.Sender(signer, tx) -// GetBalance returns the balance of an account. -func (s *EthRPCService) GetBalance(address common.Address, blockNr rpc.BlockNumber) (*hexutil.Big, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return nil, err + result := map[string]interface{}{ + "transactionHash": hash, + "transactionIndex": hexutil.Uint64(idx), + "blockHash": block.Hash(), + "blockNumber": (*hexutil.Big)(block.Number()), + "from": from, + "to": tx.To(), + "cumulativeGasUsed": hexutil.Uint64(receipt.CumulativeGasUsed), + "gasUsed": hexutil.Uint64(receipt.GasUsed), + "contractAddress": nil, + "logs": receipt.Logs, + "logsBloom": receipt.Bloom, + "status": hexutil.Uint(receipt.Status), + "effectiveGasPrice": (*hexutil.Big)(tx.GasPrice()), + "type": hexutil.Uint(tx.Type()), + } + if receipt.ContractAddress != (common.Address{}) { + result["contractAddress"] = receipt.ContractAddress + } + if result["logs"] == nil { + result["logs"] = []*types.Log{} + } + return result, nil + } + } } - return (*hexutil.Big)(stateDB.GetBalance(address).ToBig()), nil + return nil, nil } -// GetTransactionCount returns the nonce of an account. -func (s *EthRPCService) GetTransactionCount(address common.Address, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return 0, err +func (s *EthRPCService) GetLogs(args map[string]interface{}) ([]*types.Log, error) { + currentBlock := s.backend.blockchain.CurrentBlock() + if currentBlock == nil { + return []*types.Log{}, nil } - return hexutil.Uint64(stateDB.GetNonce(address)), nil -} -// GetCode returns the code at an address. -func (s *EthRPCService) GetCode(address common.Address, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return nil, err + fromBlock := currentBlock.Number.Uint64() + toBlock := currentBlock.Number.Uint64() + + if fb, ok := args["fromBlock"]; ok { + if fbStr, ok := fb.(string); ok && fbStr != "latest" && fbStr != "pending" { + if n, err := hexutil.DecodeUint64(fbStr); err == nil { + fromBlock = n + } + } + } + if tb, ok := args["toBlock"]; ok { + if tbStr, ok := tb.(string); ok && tbStr != "latest" && tbStr != "pending" { + if n, err := hexutil.DecodeUint64(tbStr); err == nil { + toBlock = n + } + } } - return stateDB.GetCode(address), nil -} -// GetStorageAt returns the storage value at a position. -func (s *EthRPCService) GetStorageAt(address common.Address, position hexutil.Big, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { - stateDB, err := s.stateAtBlock(blockNr) - if err != nil { - return nil, err + var addresses []common.Address + if addr, ok := args["address"]; ok { + switch v := addr.(type) { + case string: + addresses = append(addresses, common.HexToAddress(v)) + case []interface{}: + for _, a := range v { + if s, ok := a.(string); ok { + addresses = append(addresses, common.HexToAddress(s)) + } + } + } } - value := stateDB.GetState(address, common.BigToHash((*big.Int)(&position))) - return value[:], nil -} -// stateAtBlock returns the state at the given block number. -func (s *EthRPCService) stateAtBlock(blockNr rpc.BlockNumber) (*state.StateDB, error) { - var header *types.Header - if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { - header = s.backend.blockchain.CurrentBlock() - } else { - block := s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) + var logs []*types.Log + for i := fromBlock; i <= toBlock && i <= fromBlock+1000; i++ { + block := s.backend.blockchain.GetBlockByNumber(i) if block == nil { - return nil, fmt.Errorf("block %d not found", blockNr) + continue + } + receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) + for _, receipt := range receipts { + for _, log := range receipt.Logs { + if len(addresses) == 0 || containsAddress(addresses, log.Address) { + logs = append(logs, log) + } + } } - header = block.Header() - } - if header == nil { - return nil, errors.New("no current block") } - return s.backend.blockchain.StateAt(header.Root) + return logs, nil } -// Call executes a call without creating a transaction. func (s *EthRPCService) Call(args TransactionArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { stateDB, err := s.stateAtBlock(blockNr) if err != nil { @@ -373,11 +418,10 @@ func (s *EthRPCService) Call(args TransactionArgs, blockNr rpc.BlockNumber) (hex return nil, errors.New("no current block") } - msg := args.ToMessage(header.BaseFee) + msg := args.toMessage(header.BaseFee) blockContext := core.NewEVMBlockContext(header, s.backend.blockchain, nil) - txContext := core.NewEVMTxContext(msg) evm := vm.NewEVM(blockContext, stateDB, s.backend.chainConfig, vm.Config{}) - evm.SetTxContext(txContext) + evm.SetTxContext(core.NewEVMTxContext(msg)) gp := new(core.GasPool).AddGas(header.GasLimit) result, err := core.ApplyMessage(evm, msg, gp) @@ -390,14 +434,13 @@ func (s *EthRPCService) Call(args TransactionArgs, blockNr rpc.BlockNumber) (hex return result.Return(), nil } -// EstimateGas estimates the gas needed for a transaction. func (s *EthRPCService) EstimateGas(args TransactionArgs, blockNr *rpc.BlockNumber) (hexutil.Uint64, error) { - blockNumber := rpc.LatestBlockNumber + bn := rpc.LatestBlockNumber if blockNr != nil { - blockNumber = *blockNr + bn = *blockNr } - stateDB, err := s.stateAtBlock(blockNumber) + stateDB, err := s.stateAtBlock(bn) if err != nil { return 0, err } @@ -407,25 +450,21 @@ func (s *EthRPCService) EstimateGas(args TransactionArgs, blockNr *rpc.BlockNumb return 0, errors.New("no current block") } - // Use a high gas limit for estimation hi := header.GasLimit if args.Gas != nil && uint64(*args.Gas) < hi { hi = uint64(*args.Gas) } - lo := uint64(21000) - // Binary search for the optimal gas for lo+1 < hi { mid := (lo + hi) / 2 args.Gas = (*hexutil.Uint64)(&mid) - msg := args.ToMessage(header.BaseFee) + msg := args.toMessage(header.BaseFee) stateCopy := stateDB.Copy() blockContext := core.NewEVMBlockContext(header, s.backend.blockchain, nil) - txContext := core.NewEVMTxContext(msg) evm := vm.NewEVM(blockContext, stateCopy, s.backend.chainConfig, vm.Config{}) - evm.SetTxContext(txContext) + evm.SetTxContext(core.NewEVMTxContext(msg)) gp := new(core.GasPool).AddGas(mid) result, err := core.ApplyMessage(evm, msg, gp) @@ -439,23 +478,6 @@ func (s *EthRPCService) EstimateGas(args TransactionArgs, blockNr *rpc.BlockNumb return hexutil.Uint64(hi), nil } -// GasPrice returns the current gas price. -func (s *EthRPCService) GasPrice() *hexutil.Big { - header := s.backend.blockchain.CurrentBlock() - if header == nil || header.BaseFee == nil { - return (*hexutil.Big)(big.NewInt(params.InitialBaseFee)) - } - // Return base fee + 1 gwei tip - tip := big.NewInt(1e9) - return (*hexutil.Big)(new(big.Int).Add(header.BaseFee, tip)) -} - -// MaxPriorityFeePerGas returns the suggested priority fee. -func (s *EthRPCService) MaxPriorityFeePerGas() *hexutil.Big { - return (*hexutil.Big)(big.NewInt(1e9)) // 1 gwei -} - -// SendRawTransaction sends a signed transaction. func (s *EthRPCService) SendRawTransaction(encodedTx hexutil.Bytes) (common.Hash, error) { tx := new(types.Transaction) if err := tx.UnmarshalBinary(encodedTx); err != nil { @@ -467,171 +489,176 @@ func (s *EthRPCService) SendRawTransaction(encodedTx hexutil.Bytes) (common.Hash return common.Hash{}, errs[0] } - s.logger.Info(). - Str("tx_hash", tx.Hash().Hex()). - Msg("received raw transaction") - + s.logger.Info().Str("tx_hash", tx.Hash().Hex()).Msg("received transaction") return tx.Hash(), nil } -// GetTransactionByHash returns transaction info by hash. -func (s *EthRPCService) GetTransactionByHash(hash common.Hash) (map[string]interface{}, error) { - // Search in recent blocks - currentBlock := s.backend.blockchain.CurrentBlock() - if currentBlock == nil { - return nil, nil +func (s *EthRPCService) FeeHistory(blockCount hexutil.Uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (map[string]interface{}, error) { + if blockCount == 0 || blockCount > 1024 { + blockCount = 1024 } - // Search backwards through blocks - for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { + var endBlock uint64 + if lastBlock == rpc.LatestBlockNumber || lastBlock == rpc.PendingBlockNumber { + endBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() + } else { + endBlock = uint64(lastBlock) + } + + startBlock := endBlock - uint64(blockCount) + 1 + if startBlock > endBlock { + startBlock = 0 + } + + baseFees := make([]*hexutil.Big, 0) + gasUsedRatios := make([]float64, 0) + + for i := startBlock; i <= endBlock; i++ { block := s.backend.blockchain.GetBlockByNumber(i) if block == nil { continue } - for idx, tx := range block.Transactions() { - if tx.Hash() == hash { - return s.formatTransaction(tx, block.Hash(), block.NumberU64(), uint64(idx)), nil - } + header := block.Header() + + if header.BaseFee != nil { + baseFees = append(baseFees, (*hexutil.Big)(header.BaseFee)) + } else { + baseFees = append(baseFees, (*hexutil.Big)(big.NewInt(0))) + } + + if header.GasLimit > 0 { + gasUsedRatios = append(gasUsedRatios, float64(header.GasUsed)/float64(header.GasLimit)) + } else { + gasUsedRatios = append(gasUsedRatios, 0) } } - return nil, nil -} -// GetTransactionReceipt returns the receipt of a transaction. -func (s *EthRPCService) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { - // Search in recent blocks - currentBlock := s.backend.blockchain.CurrentBlock() - if currentBlock == nil { - return nil, nil + if len(baseFees) > 0 { + baseFees = append(baseFees, baseFees[len(baseFees)-1]) } - // Search backwards through blocks - for i := currentBlock.Number.Uint64(); i > 0 && i > currentBlock.Number.Uint64()-1000; i-- { - block := s.backend.blockchain.GetBlockByNumber(i) - if block == nil { - continue - } - for idx, tx := range block.Transactions() { - if tx.Hash() == hash { - receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) - if len(receipts) <= idx { - return nil, nil - } - receipt := receipts[idx] + return map[string]interface{}{ + "oldestBlock": (*hexutil.Big)(new(big.Int).SetUint64(startBlock)), + "baseFeePerGas": baseFees, + "gasUsedRatio": gasUsedRatios, + }, nil +} - signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) - from, _ := types.Sender(signer, tx) +// GetUncleCountByBlockHash returns 0 (no uncles in PoS). +func (s *EthRPCService) GetUncleCountByBlockHash(hash common.Hash) *hexutil.Uint { + n := hexutil.Uint(0) + return &n +} - result := map[string]interface{}{ - "transactionHash": hash, - "transactionIndex": hexutil.Uint64(idx), - "blockHash": block.Hash(), - "blockNumber": (*hexutil.Big)(block.Number()), - "from": from, - "to": tx.To(), - "cumulativeGasUsed": hexutil.Uint64(receipt.CumulativeGasUsed), - "gasUsed": hexutil.Uint64(receipt.GasUsed), - "contractAddress": nil, - "logs": receipt.Logs, - "logsBloom": receipt.Bloom, - "status": hexutil.Uint(receipt.Status), - "effectiveGasPrice": (*hexutil.Big)(tx.GasPrice()), - "type": hexutil.Uint(tx.Type()), - } +// GetUncleCountByBlockNumber returns 0 (no uncles in PoS). +func (s *EthRPCService) GetUncleCountByBlockNumber(blockNr rpc.BlockNumber) *hexutil.Uint { + n := hexutil.Uint(0) + return &n +} - if receipt.ContractAddress != (common.Address{}) { - result["contractAddress"] = receipt.ContractAddress - } +// --- Helpers --- - return result, nil - } +func (s *EthRPCService) stateAtBlock(blockNr rpc.BlockNumber) (*state.StateDB, error) { + var header *types.Header + if blockNr == rpc.LatestBlockNumber || blockNr == rpc.PendingBlockNumber { + header = s.backend.blockchain.CurrentBlock() + } else { + block := s.backend.blockchain.GetBlockByNumber(uint64(blockNr)) + if block == nil { + return nil, fmt.Errorf("block %d not found", blockNr) } + header = block.Header() } - return nil, nil + if header == nil { + return nil, errors.New("no current block") + } + return s.backend.blockchain.StateAt(header.Root) } -// GetLogs returns logs matching the filter criteria. -func (s *EthRPCService) GetLogs(filter FilterQuery) ([]*types.Log, error) { - var fromBlock, toBlock uint64 - - if filter.FromBlock == nil { - fromBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() - } else { - fromBlock = filter.FromBlock.Uint64() +func (s *EthRPCService) formatBlock(block *types.Block, fullTx bool) map[string]interface{} { + header := block.Header() + result := map[string]interface{}{ + "number": (*hexutil.Big)(header.Number), + "hash": block.Hash(), + "parentHash": header.ParentHash, + "nonce": header.Nonce, + "sha3Uncles": header.UncleHash, + "logsBloom": header.Bloom, + "transactionsRoot": header.TxHash, + "stateRoot": header.Root, + "receiptsRoot": header.ReceiptHash, + "miner": header.Coinbase, + "difficulty": (*hexutil.Big)(header.Difficulty), + "extraData": hexutil.Bytes(header.Extra), + "size": hexutil.Uint64(block.Size()), + "gasLimit": hexutil.Uint64(header.GasLimit), + "gasUsed": hexutil.Uint64(header.GasUsed), + "timestamp": hexutil.Uint64(header.Time), + "mixHash": header.MixDigest, + "totalDifficulty": (*hexutil.Big)(big.NewInt(0)), + "uncles": []common.Hash{}, } - if filter.ToBlock == nil { - toBlock = s.backend.blockchain.CurrentBlock().Number.Uint64() - } else { - toBlock = filter.ToBlock.Uint64() + if header.BaseFee != nil { + result["baseFeePerGas"] = (*hexutil.Big)(header.BaseFee) } - var logs []*types.Log - for i := fromBlock; i <= toBlock; i++ { - block := s.backend.blockchain.GetBlockByNumber(i) - if block == nil { - continue + txs := block.Transactions() + if fullTx { + txList := make([]map[string]interface{}, len(txs)) + for i, tx := range txs { + txList[i] = s.formatTransaction(tx, block.Hash(), header.Number.Uint64(), uint64(i)) } - receipts := s.backend.blockchain.GetReceiptsByHash(block.Hash()) - for _, receipt := range receipts { - for _, log := range receipt.Logs { - if s.matchLog(log, filter) { - logs = append(logs, log) - } - } + result["transactions"] = txList + } else { + txHashes := make([]common.Hash, len(txs)) + for i, tx := range txs { + txHashes[i] = tx.Hash() } + result["transactions"] = txHashes } - return logs, nil + + return result } -// matchLog checks if a log matches the filter criteria. -func (s *EthRPCService) matchLog(log *types.Log, filter FilterQuery) bool { - if len(filter.Addresses) > 0 { - found := false - for _, addr := range filter.Addresses { - if log.Address == addr { - found = true - break - } - } - if !found { - return false - } +func (s *EthRPCService) formatTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64) map[string]interface{} { + signer := types.LatestSignerForChainID(s.backend.chainConfig.ChainID) + from, _ := types.Sender(signer, tx) + + result := map[string]interface{}{ + "hash": tx.Hash(), + "nonce": hexutil.Uint64(tx.Nonce()), + "blockHash": blockHash, + "blockNumber": (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)), + "transactionIndex": hexutil.Uint64(index), + "from": from, + "to": tx.To(), + "value": (*hexutil.Big)(tx.Value()), + "gas": hexutil.Uint64(tx.Gas()), + "gasPrice": (*hexutil.Big)(tx.GasPrice()), + "input": hexutil.Bytes(tx.Data()), } - for i, topics := range filter.Topics { - if len(topics) == 0 { - continue - } - if i >= len(log.Topics) { - return false - } - found := false - for _, topic := range topics { - if log.Topics[i] == topic { - found = true - break - } - } - if !found { - return false - } + if tx.Type() != types.LegacyTxType { + result["maxFeePerGas"] = (*hexutil.Big)(tx.GasFeeCap()) + result["maxPriorityFeePerGas"] = (*hexutil.Big)(tx.GasTipCap()) + result["type"] = hexutil.Uint64(tx.Type()) } - return true + + v, r, ss := tx.RawSignatureValues() + result["v"] = (*hexutil.Big)(v) + result["r"] = (*hexutil.Big)(r) + result["s"] = (*hexutil.Big)(ss) + + return result } -// ToMessage converts TransactionArgs to a core.Message. -func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message { +func (args *TransactionArgs) toMessage(baseFee *big.Int) *core.Message { var from common.Address if args.From != nil { from = *args.From } - var to *common.Address - if args.To != nil { - to = args.To - } - var gas uint64 = 50000000 if args.Gas != nil { gas = uint64(*args.Gas) @@ -660,9 +687,9 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message { gasPrice = big.NewInt(params.InitialBaseFee) } - msg := &core.Message{ + return &core.Message{ From: from, - To: to, + To: args.To, Value: value, GasLimit: gas, GasPrice: gasPrice, @@ -671,25 +698,13 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message { Data: data, SkipNonceChecks: true, } - return msg } -// GetTxs returns pending transactions (for txpoolExt compatibility). -func (s *TxPoolExtService) GetTxs() ([]string, error) { - pending := s.backend.txPool.Pending(txpool.PendingFilter{}) - var result []string - for _, txs := range pending { - for _, lazyTx := range txs { - tx := lazyTx.Tx - if tx == nil { - continue - } - data, err := tx.MarshalBinary() - if err != nil { - continue - } - result = append(result, "0x"+hex.EncodeToString(data)) +func containsAddress(addrs []common.Address, addr common.Address) bool { + for _, a := range addrs { + if a == addr { + return true } } - return result, nil + return false } diff --git a/execution/evm/engine_geth_test.go b/execution/evm/engine_geth_test.go index e1cf0c345d..a6ceb453dd 100644 --- a/execution/evm/engine_geth_test.go +++ b/execution/evm/engine_geth_test.go @@ -8,11 +8,12 @@ import ( "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/rpc" ds "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/sync" "github.com/rs/zerolog" @@ -20,9 +21,7 @@ import ( "github.com/stretchr/testify/require" ) -// testGenesis creates a genesis configuration for testing. func testGenesis() *core.Genesis { - // Generate a test account with some balance testKey, _ := crypto.GenerateKey() testAddr := crypto.PubkeyToAddress(testKey.PublicKey) @@ -37,7 +36,6 @@ func testGenesis() *core.Genesis { } } -// testDatastore creates an in-memory datastore for testing. func testDatastore() ds.Batching { return sync.MutexWrap(ds.NewMapDatastore()) } @@ -51,11 +49,7 @@ func TestNewEngineExecutionClientWithGeth(t *testing.T) { client, err := NewEngineExecutionClientWithGeth(genesis, feeRecipient, db, "", logger) require.NoError(t, err) require.NotNil(t, client) - - // Verify genesis hash is set assert.NotEqual(t, common.Hash{}, client.genesisHash) - - // Verify fee recipient is set assert.Equal(t, feeRecipient, client.feeRecipient) } @@ -108,7 +102,6 @@ func TestGethEngineClient_GetLatestHeight(t *testing.T) { ctx := context.Background() height, err := client.GetLatestHeight(ctx) require.NoError(t, err) - // At genesis, height should be 0 assert.Equal(t, uint64(0), height) } @@ -123,13 +116,11 @@ func TestGethEthClient_HeaderByNumber(t *testing.T) { ctx := context.Background() - // Get genesis block header (block 0) header, err := client.ethClient.HeaderByNumber(ctx, big.NewInt(0)) require.NoError(t, err) assert.NotNil(t, header) assert.Equal(t, uint64(0), header.Number.Uint64()) - // Get latest header latestHeader, err := client.ethClient.HeaderByNumber(ctx, nil) require.NoError(t, err) assert.NotNil(t, latestHeader) @@ -145,8 +136,6 @@ func TestGethEthClient_GetTxs_EmptyPool(t *testing.T) { require.NoError(t, err) ctx := context.Background() - - // Empty mempool should return empty list txs, err := client.GetTxs(ctx) require.NoError(t, err) assert.Empty(t, txs) @@ -164,18 +153,10 @@ func TestGethEngineClient_ExecuteTxs_EmptyBlock(t *testing.T) { ctx := context.Background() genesisTime := time.Now() - // Initialize chain first stateRoot, err := client.InitChain(ctx, genesisTime, 1, "1337") require.NoError(t, err) - // Execute empty block - newStateRoot, err := client.ExecuteTxs( - ctx, - [][]byte{}, // empty transactions - 1, - genesisTime.Add(time.Second*12), - stateRoot, - ) + newStateRoot, err := client.ExecuteTxs(ctx, [][]byte{}, 1, genesisTime.Add(time.Second*12), stateRoot) require.NoError(t, err) assert.NotEmpty(t, newStateRoot) } @@ -188,15 +169,10 @@ func TestGethEngineClient_ForkchoiceUpdated(t *testing.T) { require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - + engineClient := &gethEngineClient{backend: backend, logger: logger} ctx := context.Background() genesisBlock := backend.blockchain.Genesis() - // Test forkchoice update without payload attributes resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ HeadBlockHash: genesisBlock.Hash(), SafeBlockHash: genesisBlock.Hash(), @@ -215,16 +191,11 @@ func TestGethEngineClient_ForkchoiceUpdated_WithPayloadAttributes(t *testing.T) require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - + engineClient := &gethEngineClient{backend: backend, logger: logger} ctx := context.Background() genesisBlock := backend.blockchain.Genesis() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - // Test forkchoice update with payload attributes attrs := map[string]any{ "timestamp": time.Now().Unix() + 12, "prevRandao": common.Hash{1, 2, 3}, @@ -252,16 +223,11 @@ func TestGethEngineClient_GetPayload(t *testing.T) { require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - + engineClient := &gethEngineClient{backend: backend, logger: logger} ctx := context.Background() genesisBlock := backend.blockchain.Genesis() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - // First, create a payload attrs := map[string]any{ "timestamp": time.Now().Unix() + 12, "prevRandao": common.Hash{1, 2, 3}, @@ -279,7 +245,6 @@ func TestGethEngineClient_GetPayload(t *testing.T) { require.NoError(t, err) require.NotNil(t, resp.PayloadID) - // Get the payload envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) require.NoError(t, err) assert.NotNil(t, envelope) @@ -296,16 +261,11 @@ func TestGethEngineClient_NewPayload(t *testing.T) { require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - + engineClient := &gethEngineClient{backend: backend, logger: logger} ctx := context.Background() genesisBlock := backend.blockchain.Genesis() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - // Create and get a payload attrs := map[string]any{ "timestamp": time.Now().Unix() + 12, "prevRandao": common.Hash{1, 2, 3}, @@ -326,7 +286,6 @@ func TestGethEngineClient_NewPayload(t *testing.T) { envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) require.NoError(t, err) - // Submit the payload status, err := engineClient.NewPayload(ctx, envelope.ExecutionPayload, nil, "", nil) require.NoError(t, err) assert.Equal(t, engine.VALID, status.Status) @@ -340,14 +299,9 @@ func TestGethEngineClient_ForkchoiceUpdated_UnknownHead(t *testing.T) { require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - + engineClient := &gethEngineClient{backend: backend, logger: logger} ctx := context.Background() - // Try to set head to unknown block unknownHash := common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234") resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ HeadBlockHash: unknownHash, @@ -365,7 +319,6 @@ func TestGethBackend_Close(t *testing.T) { backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) - // Close should not error err = backend.Close() require.NoError(t, err) } @@ -373,54 +326,43 @@ func TestGethBackend_Close(t *testing.T) { func TestCalcBaseFee(t *testing.T) { config := params.AllDevChainProtocolChanges - // Create a mock parent header + // At exactly 50% full, base fee stays the same parent := &types.Header{ Number: big.NewInt(100), GasLimit: 30_000_000, - GasUsed: 15_000_000, // 50% full + GasUsed: 15_000_000, BaseFee: big.NewInt(1_000_000_000), } - baseFee := calcBaseFee(config, parent) require.NotNil(t, baseFee) - - // When block is exactly 50% full, base fee should remain the same assert.Equal(t, parent.BaseFee, baseFee) } func TestCalcBaseFee_OverTarget(t *testing.T) { config := params.AllDevChainProtocolChanges - // Create a mock parent header that was more than 50% full parent := &types.Header{ Number: big.NewInt(100), GasLimit: 30_000_000, - GasUsed: 20_000_000, // ~67% full + GasUsed: 20_000_000, // >50% full BaseFee: big.NewInt(1_000_000_000), } - baseFee := calcBaseFee(config, parent) require.NotNil(t, baseFee) - - // Base fee should increase when block is more than 50% full assert.Greater(t, baseFee.Int64(), parent.BaseFee.Int64()) } func TestCalcBaseFee_UnderTarget(t *testing.T) { config := params.AllDevChainProtocolChanges - // Create a mock parent header that was less than 50% full parent := &types.Header{ Number: big.NewInt(100), GasLimit: 30_000_000, - GasUsed: 5_000_000, // ~17% full + GasUsed: 5_000_000, // <50% full BaseFee: big.NewInt(1_000_000_000), } - baseFee := calcBaseFee(config, parent) require.NotNil(t, baseFee) - - // Base fee should decrease when block is less than 50% full assert.Less(t, baseFee.Int64(), parent.BaseFee.Int64()) } @@ -440,50 +382,25 @@ func TestParsePayloadAttributes(t *testing.T) { "gasLimit": uint64(30_000_000), } - state, err := engineClient.parsePayloadAttributes(parentHash, attrs) - require.NoError(t, err) - assert.Equal(t, parentHash, state.parentHash) - assert.Equal(t, uint64(timestamp), state.timestamp) - assert.Equal(t, feeRecipient, state.feeRecipient) - assert.Equal(t, uint64(30_000_000), state.gasLimit) - assert.Len(t, state.transactions, 2) -} - -func TestListHasher(t *testing.T) { - hasher := trie.NewListHasher() - - // Test Update and Hash - err := hasher.Update([]byte("key1"), []byte("value1")) + payload, err := engineClient.parsePayloadAttributes(parentHash, attrs) require.NoError(t, err) - - hash1 := hasher.Hash() - assert.NotEqual(t, common.Hash{}, hash1) - - // Test Reset - hasher.Reset() - err = hasher.Update([]byte("key2"), []byte("value2")) - require.NoError(t, err) - - hash2 := hasher.Hash() - assert.NotEqual(t, common.Hash{}, hash2) - assert.NotEqual(t, hash1, hash2) + assert.Equal(t, parentHash, payload.parentHash) + assert.Equal(t, uint64(timestamp), payload.timestamp) + assert.Equal(t, feeRecipient, payload.feeRecipient) + assert.Equal(t, uint64(30_000_000), payload.gasLimit) + assert.Len(t, payload.transactions, 2) } -func TestCreateBloomFromReceipts(t *testing.T) { - // Empty receipts - bloom := createBloomFromReceipts([]*types.Receipt{}) +func TestCreateBloom(t *testing.T) { + bloom := createBloom([]*types.Receipt{}) assert.Equal(t, types.Bloom{}, bloom) - // Single receipt with no logs - receipt := &types.Receipt{ - Status: types.ReceiptStatusSuccessful, - Logs: []*types.Log{}, - } - bloom = createBloomFromReceipts([]*types.Receipt{receipt}) + receipt := &types.Receipt{Status: types.ReceiptStatusSuccessful, Logs: []*types.Log{}} + bloom = createBloom([]*types.Receipt{receipt}) assert.NotNil(t, bloom) } -func TestGethEngineClient_PayloadIdempotency(t *testing.T) { +func TestGethEngineClient_GetPayloadRemovesFromPending(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() @@ -491,19 +408,13 @@ func TestGethEngineClient_PayloadIdempotency(t *testing.T) { require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - + engineClient := &gethEngineClient{backend: backend, logger: logger} ctx := context.Background() genesisBlock := backend.blockchain.Genesis() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - // Create payload attributes - timestamp := time.Now().Unix() + 12 attrs := map[string]any{ - "timestamp": timestamp, + "timestamp": time.Now().Unix() + 12, "prevRandao": common.Hash{1, 2, 3}, "suggestedFeeRecipient": feeRecipient, "transactions": []string{}, @@ -511,32 +422,28 @@ func TestGethEngineClient_PayloadIdempotency(t *testing.T) { "withdrawals": []*types.Withdrawal{}, } - // First call should create a new payload - resp1, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ HeadBlockHash: genesisBlock.Hash(), SafeBlockHash: genesisBlock.Hash(), FinalizedBlockHash: genesisBlock.Hash(), }, attrs) require.NoError(t, err) - require.NotNil(t, resp1.PayloadID) + require.NotNil(t, resp.PayloadID) - // Second call with same attributes should return the same payload ID (idempotency) - resp2, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ - HeadBlockHash: genesisBlock.Hash(), - SafeBlockHash: genesisBlock.Hash(), - FinalizedBlockHash: genesisBlock.Hash(), - }, attrs) + assert.NotNil(t, backend.pendingPayload) + + envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) require.NoError(t, err) - require.NotNil(t, resp2.PayloadID) + assert.NotNil(t, envelope) - // Payload IDs should be identical - assert.Equal(t, *resp1.PayloadID, *resp2.PayloadID) + assert.Nil(t, backend.pendingPayload) - // Should still have only one payload in the map - assert.Len(t, backend.payloads, 1) + _, err = engineClient.GetPayload(ctx, *resp.PayloadID) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown payload ID") } -func TestGethEngineClient_DeterministicPayloadID(t *testing.T) { +func TestGethEngineClient_WithdrawalProcessing(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() @@ -544,83 +451,31 @@ func TestGethEngineClient_DeterministicPayloadID(t *testing.T) { require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - + engineClient := &gethEngineClient{backend: backend, logger: logger} + ctx := context.Background() genesisBlock := backend.blockchain.Genesis() feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - timestamp := uint64(time.Now().Unix() + 12) - - // Create two payload states with identical attributes - ps1 := &payloadBuildState{ - parentHash: genesisBlock.Hash(), - timestamp: timestamp, - prevRandao: common.Hash{1, 2, 3}, - feeRecipient: feeRecipient, - gasLimit: 30_000_000, - transactions: [][]byte{}, - withdrawals: []*types.Withdrawal{}, - createdAt: time.Now(), - } - ps2 := &payloadBuildState{ - parentHash: genesisBlock.Hash(), - timestamp: timestamp, - prevRandao: common.Hash{1, 2, 3}, - feeRecipient: feeRecipient, - gasLimit: 30_000_000, - transactions: [][]byte{}, - withdrawals: []*types.Withdrawal{}, - createdAt: time.Now().Add(time.Hour), // Different creation time - } - - // Both should generate the same payload ID - id1 := engineClient.generatePayloadID(ps1) - id2 := engineClient.generatePayloadID(ps2) - assert.Equal(t, id1, id2) - - // Different attributes should generate different IDs - ps3 := &payloadBuildState{ - parentHash: genesisBlock.Hash(), - timestamp: timestamp + 1, // Different timestamp - prevRandao: common.Hash{1, 2, 3}, - feeRecipient: feeRecipient, - gasLimit: 30_000_000, - transactions: [][]byte{}, - withdrawals: []*types.Withdrawal{}, - createdAt: time.Now(), - } - id3 := engineClient.generatePayloadID(ps3) - assert.NotEqual(t, id1, id3) -} - -func TestGethEngineClient_GetPayloadRemovesFromMap(t *testing.T) { - genesis := testGenesis() - logger := zerolog.Nop() + withdrawalAddr1 := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + withdrawalAddr2 := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") - backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + stateDB, err := backend.blockchain.StateAt(genesisBlock.Root()) require.NoError(t, err) - defer backend.Close() + assert.True(t, stateDB.GetBalance(withdrawalAddr1).IsZero()) + assert.True(t, stateDB.GetBalance(withdrawalAddr2).IsZero()) - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, + withdrawals := []*types.Withdrawal{ + {Index: 0, Validator: 1, Address: withdrawalAddr1, Amount: 1000000000}, // 1 ETH in Gwei + {Index: 1, Validator: 2, Address: withdrawalAddr2, Amount: 500000000}, // 0.5 ETH in Gwei } - ctx := context.Background() - genesisBlock := backend.blockchain.Genesis() - feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - - // Create a payload attrs := map[string]any{ "timestamp": time.Now().Unix() + 12, "prevRandao": common.Hash{1, 2, 3}, "suggestedFeeRecipient": feeRecipient, "transactions": []string{}, "gasLimit": uint64(30_000_000), - "withdrawals": []*types.Withdrawal{}, + "withdrawals": withdrawals, } resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ @@ -631,24 +486,73 @@ func TestGethEngineClient_GetPayloadRemovesFromMap(t *testing.T) { require.NoError(t, err) require.NotNil(t, resp.PayloadID) - // Payload should be in the map - assert.Len(t, backend.payloads, 1) - - // Get the payload envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) require.NoError(t, err) - assert.NotNil(t, envelope) + assert.Len(t, envelope.ExecutionPayload.Withdrawals, 2) - // Payload should be removed from the map after retrieval - assert.Len(t, backend.payloads, 0) + status, err := engineClient.NewPayload(ctx, envelope.ExecutionPayload, nil, "", nil) + require.NoError(t, err) + assert.Equal(t, engine.VALID, status.Status) - // Second call should fail with unknown payload ID - _, err = engineClient.GetPayload(ctx, *resp.PayloadID) - require.Error(t, err) - assert.Contains(t, err.Error(), "unknown payload ID") + _, err = engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ + HeadBlockHash: envelope.ExecutionPayload.BlockHash, + SafeBlockHash: envelope.ExecutionPayload.BlockHash, + FinalizedBlockHash: envelope.ExecutionPayload.BlockHash, + }, nil) + require.NoError(t, err) + + newBlock := backend.blockchain.GetBlockByHash(envelope.ExecutionPayload.BlockHash) + require.NotNil(t, newBlock) + + newStateDB, err := backend.blockchain.StateAt(newBlock.Root()) + require.NoError(t, err) + + expectedBalance1 := new(big.Int).Mul(big.NewInt(1000000000), big.NewInt(1e9)) + expectedBalance2 := new(big.Int).Mul(big.NewInt(500000000), big.NewInt(1e9)) + + assert.Equal(t, expectedBalance1.String(), newStateDB.GetBalance(withdrawalAddr1).ToBig().String()) + assert.Equal(t, expectedBalance2.String(), newStateDB.GetBalance(withdrawalAddr2).ToBig().String()) } -func TestGethEngineClient_PayloadCleanup(t *testing.T) { +func TestGethEngineClient_ContractCreationAddress(t *testing.T) { + sender := common.HexToAddress("0x1234567890123456789012345678901234567890") + nonce := uint64(0) + expectedAddr := crypto.CreateAddress(sender, nonce) + + expectedAddr2 := crypto.CreateAddress(sender, nonce) + assert.Equal(t, expectedAddr, expectedAddr2) + + differentAddr := crypto.CreateAddress(sender, nonce+1) + assert.NotEqual(t, expectedAddr, differentAddr) + + differentSender := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + differentAddr2 := crypto.CreateAddress(differentSender, nonce) + assert.NotEqual(t, expectedAddr, differentAddr2) +} + +func TestWeb3Sha3(t *testing.T) { + service := &Web3RPCService{} + + input := hexutil.Bytes("hello") + result := service.Sha3(input) + + expected := common.FromHex("0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8") + assert.Equal(t, expected, []byte(result)) +} + +func TestEthRPCService_Accounts(t *testing.T) { + service := &EthRPCService{} + assert.Empty(t, service.Accounts()) +} + +func TestEthRPCService_Syncing(t *testing.T) { + service := &EthRPCService{} + result, err := service.Syncing() + require.NoError(t, err) + assert.Equal(t, false, result) +} + +func TestEthRPCService_FeeHistory(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() @@ -656,40 +560,15 @@ func TestGethEngineClient_PayloadCleanup(t *testing.T) { require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - - ctx := context.Background() - genesisBlock := backend.blockchain.Genesis() - feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + service := &EthRPCService{backend: backend, logger: logger} - // Create more payloads than maxPayloads (10) - baseTimestamp := time.Now().Unix() + 100 - for i := 0; i < 15; i++ { - attrs := map[string]any{ - "timestamp": baseTimestamp + int64(i), - "prevRandao": common.Hash{byte(i)}, - "suggestedFeeRecipient": feeRecipient, - "transactions": []string{}, - "gasLimit": uint64(30_000_000), - "withdrawals": []*types.Withdrawal{}, - } - - _, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ - HeadBlockHash: genesisBlock.Hash(), - SafeBlockHash: genesisBlock.Hash(), - FinalizedBlockHash: genesisBlock.Hash(), - }, attrs) - require.NoError(t, err) - } - - // Should have at most maxPayloads entries - assert.LessOrEqual(t, len(backend.payloads), 10) + result, err := service.FeeHistory(10, rpc.LatestBlockNumber, []float64{25, 50, 75}) + require.NoError(t, err) + assert.NotNil(t, result) + assert.NotNil(t, result["oldestBlock"]) } -func TestGethEngineClient_DifferentTransactionsGenerateDifferentIDs(t *testing.T) { +func TestEthRPCService_UnclesMethods(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() @@ -697,47 +576,18 @@ func TestGethEngineClient_DifferentTransactionsGenerateDifferentIDs(t *testing.T require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } - - genesisBlock := backend.blockchain.Genesis() - feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") - timestamp := uint64(time.Now().Unix() + 12) - - // Payload with no transactions - ps1 := &payloadBuildState{ - parentHash: genesisBlock.Hash(), - timestamp: timestamp, - prevRandao: common.Hash{1, 2, 3}, - feeRecipient: feeRecipient, - gasLimit: 30_000_000, - transactions: [][]byte{}, - withdrawals: []*types.Withdrawal{}, - createdAt: time.Now(), - } - - // Payload with some transaction - ps2 := &payloadBuildState{ - parentHash: genesisBlock.Hash(), - timestamp: timestamp, - prevRandao: common.Hash{1, 2, 3}, - feeRecipient: feeRecipient, - gasLimit: 30_000_000, - transactions: [][]byte{{0xaa, 0xbb, 0xcc}}, - withdrawals: []*types.Withdrawal{}, - createdAt: time.Now(), - } + service := &EthRPCService{backend: backend, logger: logger} - id1 := engineClient.generatePayloadID(ps1) - id2 := engineClient.generatePayloadID(ps2) + count := service.GetUncleCountByBlockHash(common.Hash{}) + assert.NotNil(t, count) + assert.Equal(t, hexutil.Uint(0), *count) - // Different transactions should produce different IDs - assert.NotEqual(t, id1, id2) + count = service.GetUncleCountByBlockNumber(rpc.LatestBlockNumber) + assert.NotNil(t, count) + assert.Equal(t, hexutil.Uint(0), *count) } -func TestGethEngineClient_WithdrawalProcessing(t *testing.T) { +func TestEthRPCService_BlockTransactionCount(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() @@ -745,102 +595,85 @@ func TestGethEngineClient_WithdrawalProcessing(t *testing.T) { require.NoError(t, err) defer backend.Close() - engineClient := &gethEngineClient{ - backend: backend, - logger: logger, - } + service := &EthRPCService{backend: backend, logger: logger} - ctx := context.Background() - genesisBlock := backend.blockchain.Genesis() - feeRecipient := common.HexToAddress("0x1234567890123456789012345678901234567890") + genesisHash := backend.blockchain.Genesis().Hash() + count := service.GetBlockTransactionCountByHash(genesisHash) + assert.NotNil(t, count) + assert.Equal(t, hexutil.Uint(0), *count) - // Create withdrawal recipients - withdrawalAddr1 := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - withdrawalAddr2 := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + count = service.GetBlockTransactionCountByNumber(0) + assert.NotNil(t, count) + assert.Equal(t, hexutil.Uint(0), *count) +} - // Check initial balances are zero - stateDB, err := backend.blockchain.StateAt(genesisBlock.Root()) +func TestEthRPCService_GetBlockByNumber(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) - initialBalance1 := stateDB.GetBalance(withdrawalAddr1) - initialBalance2 := stateDB.GetBalance(withdrawalAddr2) - assert.True(t, initialBalance1.IsZero(), "initial balance should be zero") - assert.True(t, initialBalance2.IsZero(), "initial balance should be zero") + defer backend.Close() - // Create withdrawals (amounts are in Gwei) - withdrawals := []*types.Withdrawal{ - { - Index: 0, - Validator: 1, - Address: withdrawalAddr1, - Amount: 1000000000, // 1 ETH in Gwei - }, - { - Index: 1, - Validator: 2, - Address: withdrawalAddr2, - Amount: 500000000, // 0.5 ETH in Gwei - }, - } + service := &EthRPCService{backend: backend, logger: logger} - // Create payload with withdrawals - attrs := map[string]any{ - "timestamp": time.Now().Unix() + 12, - "prevRandao": common.Hash{1, 2, 3}, - "suggestedFeeRecipient": feeRecipient, - "transactions": []string{}, - "gasLimit": uint64(30_000_000), - "withdrawals": withdrawals, - } - - resp, err := engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ - HeadBlockHash: genesisBlock.Hash(), - SafeBlockHash: genesisBlock.Hash(), - FinalizedBlockHash: genesisBlock.Hash(), - }, attrs) + block, err := service.GetBlockByNumber(0, false) require.NoError(t, err) - require.NotNil(t, resp.PayloadID) + assert.NotNil(t, block) + assert.Equal(t, (*hexutil.Big)(big.NewInt(0)), block["number"]) - // Get the payload - envelope, err := engineClient.GetPayload(ctx, *resp.PayloadID) + block, err = service.GetBlockByNumber(rpc.LatestBlockNumber, true) require.NoError(t, err) - assert.NotNil(t, envelope) - assert.Len(t, envelope.ExecutionPayload.Withdrawals, 2) + assert.NotNil(t, block) +} - // Submit the payload to actually apply state changes - status, err := engineClient.NewPayload(ctx, envelope.ExecutionPayload, nil, "", nil) +func TestEthRPCService_GetBlockByHash(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() + + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) - assert.Equal(t, engine.VALID, status.Status) + defer backend.Close() - // Update head to the new block - _, err = engineClient.ForkchoiceUpdated(ctx, engine.ForkchoiceStateV1{ - HeadBlockHash: envelope.ExecutionPayload.BlockHash, - SafeBlockHash: envelope.ExecutionPayload.BlockHash, - FinalizedBlockHash: envelope.ExecutionPayload.BlockHash, - }, nil) + service := &EthRPCService{backend: backend, logger: logger} + + genesisHash := backend.blockchain.Genesis().Hash() + block, err := service.GetBlockByHash(genesisHash, false) require.NoError(t, err) + assert.NotNil(t, block) + assert.Equal(t, genesisHash, block["hash"]) +} - // Check balances after withdrawals are processed - newBlock := backend.blockchain.GetBlockByHash(envelope.ExecutionPayload.BlockHash) - require.NotNil(t, newBlock) +func TestEthRPCService_GasPrice(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() - newStateDB, err := backend.blockchain.StateAt(newBlock.Root()) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) require.NoError(t, err) + defer backend.Close() + + service := &EthRPCService{backend: backend, logger: logger} + + gasPrice := service.GasPrice() + assert.NotNil(t, gasPrice) + assert.Greater(t, (*big.Int)(gasPrice).Int64(), int64(0)) +} - // Withdrawal amounts are in Gwei, balances are in Wei - // 1 ETH = 1e9 Gwei = 1e18 Wei - expectedBalance1 := new(big.Int).Mul(big.NewInt(1000000000), big.NewInt(1e9)) // 1 ETH in Wei - expectedBalance2 := new(big.Int).Mul(big.NewInt(500000000), big.NewInt(1e9)) // 0.5 ETH in Wei +func TestEthRPCService_ChainId(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() - balance1 := newStateDB.GetBalance(withdrawalAddr1) - balance2 := newStateDB.GetBalance(withdrawalAddr2) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() + + service := &EthRPCService{backend: backend, logger: logger} - assert.Equal(t, expectedBalance1.String(), balance1.ToBig().String(), "withdrawal 1 balance mismatch") - assert.Equal(t, expectedBalance2.String(), balance2.ToBig().String(), "withdrawal 2 balance mismatch") + chainId := service.ChainId() + assert.NotNil(t, chainId) } -func TestGethEngineClient_ContractCreationAddress(t *testing.T) { - // This test verifies that contract creation addresses are calculated correctly - // using crypto.CreateAddress(sender, nonce) rather than the incorrect evmInstance.Origin +func TestEthRPCService_BlockNumber(t *testing.T) { genesis := testGenesis() logger := zerolog.Nop() @@ -848,27 +681,32 @@ func TestGethEngineClient_ContractCreationAddress(t *testing.T) { require.NoError(t, err) defer backend.Close() - // The contract address should be derived from sender address and nonce - // This is tested implicitly through the applyTransaction function - // For a full test, we would need to create a signed contract creation tx + service := &EthRPCService{backend: backend, logger: logger} - // Verify the crypto.CreateAddress function works as expected - sender := common.HexToAddress("0x1234567890123456789012345678901234567890") - nonce := uint64(0) - expectedAddr := crypto.CreateAddress(sender, nonce) + blockNumber := service.BlockNumber() + assert.Equal(t, hexutil.Uint64(0), blockNumber) +} - // The address should be deterministic - expectedAddr2 := crypto.CreateAddress(sender, nonce) - assert.Equal(t, expectedAddr, expectedAddr2) +func TestNetRPCService(t *testing.T) { + genesis := testGenesis() + logger := zerolog.Nop() - // Different nonce should produce different address - differentAddr := crypto.CreateAddress(sender, nonce+1) - assert.NotEqual(t, expectedAddr, differentAddr) + backend, err := newGethBackend(genesis, ds.NewMapDatastore(), logger) + require.NoError(t, err) + defer backend.Close() - // Different sender should produce different address - differentSender := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - differentAddr2 := crypto.CreateAddress(differentSender, nonce) - assert.NotEqual(t, expectedAddr, differentAddr2) + service := &NetRPCService{backend: backend} + + assert.NotEmpty(t, service.Version()) + assert.True(t, service.Listening()) + assert.Equal(t, hexutil.Uint(0), service.PeerCount()) +} + +func TestWeb3RPCService(t *testing.T) { + service := &Web3RPCService{} + + assert.NotEmpty(t, service.ClientVersion()) - _ = backend // use backend to avoid unused variable warning + hash := service.Sha3([]byte("test")) + assert.Len(t, hash, 32) } From 1bf3cd5d430ad08d34f2b7cceaa10c43549245ef Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 23 Jan 2026 18:04:58 +0100 Subject: [PATCH 18/18] fix --- execution/evm/engine_geth.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/execution/evm/engine_geth.go b/execution/evm/engine_geth.go index 776a2c4dee..d70a4d012e 100644 --- a/execution/evm/engine_geth.go +++ b/execution/evm/engine_geth.go @@ -527,6 +527,14 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu gasLimit = parent.GasLimit() } + // Build the header from payload data + // NOTE: We must set TxHash and ReceiptHash from the payload, not recompute them, + // because types.NewBlock would overwrite them based on the passed transactions/receipts. + withdrawalsHash := types.EmptyWithdrawalsHash + if len(payload.Withdrawals) > 0 { + withdrawalsHash = types.DeriveSha(types.Withdrawals(payload.Withdrawals), trie.NewListHasher()) + } + header := &types.Header{ ParentHash: payload.ParentHash, UncleHash: types.EmptyUncleHash, @@ -543,22 +551,18 @@ func (g *gethEngineClient) NewPayload(ctx context.Context, payload *engine.Execu Extra: payload.ExtraData, MixDigest: payload.Random, BaseFee: payload.BaseFeePerGas, - WithdrawalsHash: &types.EmptyWithdrawalsHash, + WithdrawalsHash: &withdrawalsHash, BlobGasUsed: payload.BlobGasUsed, ExcessBlobGas: payload.ExcessBlobGas, ParentBeaconRoot: &common.Hash{}, RequestsHash: &types.EmptyRequestsHash, } - if len(payload.Withdrawals) > 0 { - wh := types.DeriveSha(types.Withdrawals(payload.Withdrawals), trie.NewListHasher()) - header.WithdrawalsHash = &wh - } - - block := types.NewBlock(header, &types.Body{ + // Compute block hash directly from header - don't use types.NewBlock which overwrites hashes + block := types.NewBlockWithHeader(header).WithBody(types.Body{ Transactions: txs, Withdrawals: payload.Withdrawals, - }, nil, trie.NewListHasher()) + }) if block.Hash() != payload.BlockHash { g.logger.Warn().