diff --git a/.github/workflows/scripts/rpc_version.env b/.github/workflows/scripts/rpc_version.env
index ac45a36e07c..11116da10a6 100644
--- a/.github/workflows/scripts/rpc_version.env
+++ b/.github/workflows/scripts/rpc_version.env
@@ -1,2 +1,3 @@
-RPC_VERSION=v2.17.0
+RPC_VERSION=v2.18.0
+
diff --git a/cmd/rpcdaemon/README.md b/cmd/rpcdaemon/README.md
index 38d426b6054..a0c24e1cb70 100644
--- a/cmd/rpcdaemon/README.md
+++ b/cmd/rpcdaemon/README.md
@@ -310,6 +310,7 @@ The following table shows the current implementation status of Erigon's RPC daem
| eth_sendTransaction | - | not yet implemented |
| eth_sign | No | deprecated |
| eth_signTransaction | - | not yet implemented |
+| eth_fillTransaction | Yes | Blob sidecar generation (KZG commitments/proofs from raw blobs) not yet supported |
| | | |
| eth_getProof | Yes | Limited to last 100000 blocks |
| | | |
diff --git a/rpc/ethapi/api.go b/rpc/ethapi/api.go
index c3eba4e8f92..f293bad8824 100644
--- a/rpc/ethapi/api.go
+++ b/rpc/ethapi/api.go
@@ -20,6 +20,7 @@
package ethapi
import (
+ "encoding/json"
"errors"
"fmt"
"math/big"
@@ -286,6 +287,7 @@ func (args *CallArgs) ToTransaction(globalGasCap uint64, baseFee *uint256.Int) (
TipCap: *msg.TipCap(),
AccessList: al,
}
+ // Unlike Geth, an explicit accessList with gasPrice produces type 1 rather than dropping the list.
case args.AccessList != nil:
al := types.AccessList{}
if args.AccessList != nil {
@@ -495,6 +497,50 @@ func RPCMarshalBlockExDeprecated(block *types.Block, inclTx bool, fullTx bool, b
return fields, nil
}
+// SignTransactionResult represents a RLP-encoded transaction paired with its JSON form.
+type SignTransactionResult struct {
+ Raw hexutil.Bytes `json:"raw"`
+ Tx *RPCTransaction `json:"tx"`
+}
+
+func (r SignTransactionResult) MarshalJSON() ([]byte, error) {
+ if r.Tx == nil {
+ return nil, errors.New("nil transaction")
+ }
+ type plain struct {
+ Raw hexutil.Bytes `json:"raw"`
+ Tx json.RawMessage `json:"tx"`
+ }
+ txBytes, err := json.Marshal(r.Tx)
+ if err != nil {
+ return nil, err
+ }
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(txBytes, &m); err != nil {
+ return nil, err
+ }
+ for _, k := range []string{"blockHash", "blockNumber", "blockTimestamp", "transactionIndex", "from"} {
+ delete(m, k)
+ }
+ nullVal := json.RawMessage("null")
+ for _, k := range []string{"gasPrice", "maxFeePerGas", "maxPriorityFeePerGas"} {
+ if _, ok := m[k]; !ok {
+ m[k] = nullVal
+ }
+ }
+ zeroHex := json.RawMessage(`"0x0"`)
+ for _, k := range []string{"v", "r", "s"} {
+ if v, ok := m[k]; !ok || string(v) == "null" {
+ m[k] = zeroHex
+ }
+ }
+ stripped, err := json.Marshal(m)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(plain{Raw: r.Raw, Tx: stripped})
+}
+
// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
type RPCTransaction struct {
BlockHash *common.Hash `json:"blockHash"`
diff --git a/rpc/ethapi/api_test.go b/rpc/ethapi/api_test.go
index d8397746e8e..833f0ef44d5 100644
--- a/rpc/ethapi/api_test.go
+++ b/rpc/ethapi/api_test.go
@@ -1,6 +1,8 @@
package ethapi
import (
+ "bytes"
+ "encoding/json"
"testing"
"github.com/holiman/uint256"
@@ -88,6 +90,92 @@ func TestNewRPCTransaction_EIP1559_AllZeroSig(t *testing.T) {
require.EqualValues(t, 0, result.S.ToInt().Int64())
}
+func txFields(t *testing.T, r SignTransactionResult) map[string]json.RawMessage {
+ t.Helper()
+ data, err := json.Marshal(r)
+ require.NoError(t, err)
+ var outer struct {
+ Tx json.RawMessage `json:"tx"`
+ }
+ require.NoError(t, json.Unmarshal(data, &outer))
+ var fields map[string]json.RawMessage
+ require.NoError(t, json.Unmarshal(outer.Tx, &fields))
+ return fields
+}
+
+func TestSignTransactionResultMarshalJSON_EIP1559(t *testing.T) {
+ to := common.HexToAddress("0x1234567890123456789012345678901234567890")
+ chainID := uint256.NewInt(1)
+ tx := &types.DynamicFeeTransaction{
+ CommonTx: types.CommonTx{
+ Nonce: 1,
+ GasLimit: 21000,
+ To: &to,
+ },
+ ChainID: *chainID,
+ TipCap: *uint256.NewInt(1e9),
+ FeeCap: *uint256.NewInt(2e9),
+ }
+ var buf bytes.Buffer
+ require.NoError(t, tx.MarshalBinary(&buf))
+
+ fields := txFields(t, SignTransactionResult{Raw: buf.Bytes(), Tx: NewRPCTransaction(tx, common.Hash{}, 0, 0, 0, nil)})
+
+ // EIP-1559 without known baseFee: gasPrice is null (matching Geth).
+ require.Equal(t, "null", string(fields["gasPrice"]), "gasPrice must be null when baseFee is unknown")
+
+ // maxFeePerGas and maxPriorityFeePerGas must be set.
+ require.NotEqual(t, "null", string(fields["maxFeePerGas"]))
+ require.NotEqual(t, "null", string(fields["maxPriorityFeePerGas"]))
+
+ // unsigned tx: v/r/s must be "0x0", not null.
+ require.Equal(t, `"0x0"`, string(fields["v"]))
+ require.Equal(t, `"0x0"`, string(fields["r"]))
+ require.Equal(t, `"0x0"`, string(fields["s"]))
+
+ // block-placement and sender fields must be absent.
+ for _, k := range []string{"from", "blockHash", "blockNumber", "blockTimestamp", "transactionIndex"} {
+ _, present := fields[k]
+ require.False(t, present, "%s must be absent", k)
+ }
+}
+
+func TestSignTransactionResultMarshalJSON_Legacy(t *testing.T) {
+ to := common.HexToAddress("0x1234567890123456789012345678901234567890")
+ tx := &types.LegacyTx{
+ CommonTx: types.CommonTx{
+ Nonce: 1,
+ GasLimit: 21000,
+ To: &to,
+ },
+ GasPrice: *uint256.NewInt(1e9),
+ }
+ var buf bytes.Buffer
+ require.NoError(t, tx.MarshalBinary(&buf))
+
+ fields := txFields(t, SignTransactionResult{Raw: buf.Bytes(), Tx: NewRPCTransaction(tx, common.Hash{}, 0, 0, 0, nil)})
+
+ // Legacy: gasPrice must be set.
+ gasPrice, ok := fields["gasPrice"]
+ require.True(t, ok, "gasPrice must be present")
+ require.NotEqual(t, "null", string(gasPrice))
+
+ // Legacy: maxFeePerGas and maxPriorityFeePerGas are inapplicable and must be null (matching Geth).
+ require.Equal(t, "null", string(fields["maxFeePerGas"]), "maxFeePerGas must be null for legacy tx")
+ require.Equal(t, "null", string(fields["maxPriorityFeePerGas"]), "maxPriorityFeePerGas must be null for legacy tx")
+
+ // unsigned tx: v/r/s must be "0x0", not null.
+ require.Equal(t, `"0x0"`, string(fields["v"]))
+ require.Equal(t, `"0x0"`, string(fields["r"]))
+ require.Equal(t, `"0x0"`, string(fields["s"]))
+}
+
+func TestSignTransactionResultMarshalJSON_NilTx(t *testing.T) {
+ _, err := json.Marshal(SignTransactionResult{Tx: nil})
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "nil transaction")
+}
+
func TestNewRPCTransaction_AccessList_AllZeroSig(t *testing.T) {
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
chainID := uint256.NewInt(1)
diff --git a/rpc/jsonrpc/eth_api.go b/rpc/jsonrpc/eth_api.go
index 9a657b6c8a6..98163d39a39 100644
--- a/rpc/jsonrpc/eth_api.go
+++ b/rpc/jsonrpc/eth_api.go
@@ -125,6 +125,7 @@ type EthAPI interface {
SendTransaction(_ context.Context, txObject any) (common.Hash, error)
Sign(ctx context.Context, _ common.Address, _ hexutil.Bytes) (hexutil.Bytes, error)
SignTransaction(_ context.Context, txObject any) (common.Hash, error)
+ FillTransaction(ctx context.Context, args ethapi.CallArgs) (*ethapi.SignTransactionResult, error)
GetProof(ctx context.Context, address common.Address, storageKeys []hexutil.Bytes, blockNr *rpc.BlockNumberOrHash) (*accounts.AccProofResult, error)
CreateAccessList(ctx context.Context, args ethapi.CallArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *ethapi2.StateOverrides, optimizeGas *bool) (*accessListResult, error)
diff --git a/rpc/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
new file mode 100644
index 00000000000..86f92520c9e
--- /dev/null
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -0,0 +1,212 @@
+// Copyright 2026 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package jsonrpc
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "math/big"
+
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/hexutil"
+ "github.com/erigontech/erigon/db/kv"
+ "github.com/erigontech/erigon/db/rawdb"
+ "github.com/erigontech/erigon/execution/protocol/misc"
+ "github.com/erigontech/erigon/execution/protocol/params"
+ "github.com/erigontech/erigon/execution/types"
+ "github.com/erigontech/erigon/node/ethconfig"
+ "github.com/erigontech/erigon/rpc"
+ "github.com/erigontech/erigon/rpc/ethapi"
+ "github.com/erigontech/erigon/rpc/gasprice"
+)
+
+// FillTransaction implements eth_fillTransaction.
+func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (*ethapi.SignTransactionResult, error) {
+ if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) {
+ return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
+ }
+ if args.MaxFeePerBlobGas != nil && args.MaxFeePerBlobGas.ToInt().Sign() == 0 {
+ return nil, errors.New("maxFeePerBlobGas, if specified, must be non-zero")
+ }
+
+ dbTx, err := api.db.BeginTemporalRo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer dbTx.Rollback()
+
+ cc, err := api.chainConfig(ctx, dbTx)
+ if err != nil {
+ return nil, err
+ }
+ head := rawdb.ReadCurrentHeader(dbTx)
+ if head == nil {
+ return nil, errors.New("missing current header")
+ }
+
+ if args.Value == nil {
+ args.Value = new(hexutil.Big)
+ }
+
+ if args.Nonce == nil {
+ var nonce uint64
+ if args.From != nil {
+ pendingBlock := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
+ count, err := api.GetTransactionCount(ctx, *args.From, &pendingBlock)
+ if err != nil {
+ return nil, err
+ }
+ nonce = uint64(*count)
+ }
+ args.Nonce = (*hexutil.Uint64)(&nonce)
+ }
+
+ if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
+ return nil, errors.New(`both "data" and "input" are set and not equal. Please use "input" to pass transaction call data`)
+ }
+
+ if args.BlobVersionedHashes != nil {
+ if n := len(args.BlobVersionedHashes); n == 0 {
+ return nil, errors.New("need at least 1 blob for a blob transaction")
+ } else if n > params.MaxBlobsPerTxn {
+ return nil, fmt.Errorf("too many blobs in transaction (have=%d, max=%d)", n, params.MaxBlobsPerTxn)
+ }
+ }
+
+ if args.To == nil {
+ if args.BlobVersionedHashes != nil {
+ return nil, errors.New(`missing "to" in blob transaction`)
+ }
+ hasData := (args.Input != nil && len(*args.Input) > 0) || (args.Data != nil && len(*args.Data) > 0)
+ if !hasData {
+ return nil, errors.New(`contract creation without any data provided`)
+ }
+ if args.AuthorizationList != nil {
+ return nil, errors.New(`authorizationList provided for contract creation, but "to" field is missing`)
+ }
+ }
+
+ if err := api.fillFeeDefaults(ctx, &args, head, dbTx); err != nil {
+ return nil, err
+ }
+
+ if args.BlobVersionedHashes != nil {
+ if head.ExcessBlobGas == nil {
+ return nil, errors.New("blob transactions not supported before Cancun")
+ }
+ if args.MaxFeePerBlobGas == nil {
+ blobFee, err := misc.GetBlobGasPrice(cc, *head.ExcessBlobGas, head.Time)
+ if err != nil {
+ return nil, err
+ }
+ b := blobFee.ToBig()
+ args.MaxFeePerBlobGas = (*hexutil.Big)(b.Lsh(b, 1))
+ }
+ }
+
+ chainIDBig := cc.ChainID.ToBig()
+ if args.ChainID == nil {
+ args.ChainID = (*hexutil.Big)(chainIDBig)
+ } else if have := args.ChainID.ToInt(); have.Cmp(chainIDBig) != 0 {
+ return nil, fmt.Errorf("chainId does not match node's (have=%v, want=%v)", have, cc.ChainID)
+ }
+
+ if args.Gas == nil {
+ estimated, err := api.EstimateGas(ctx, &args, &latestNumOrHash, nil, nil)
+ if err != nil {
+ return nil, err
+ }
+ args.Gas = &estimated
+ }
+
+ txn, err := args.ToTransaction(0, head.BaseFee)
+ if err != nil {
+ return nil, err
+ }
+
+ var buf bytes.Buffer
+ if err = txn.MarshalBinary(&buf); err != nil {
+ return nil, err
+ }
+
+ return ðapi.SignTransactionResult{
+ Raw: buf.Bytes(),
+ Tx: ethapi.NewRPCTransaction(txn, common.Hash{}, 0, 0, 0, nil),
+ }, nil
+}
+
+func (api *APIImpl) newGasOracle(dbTx kv.TemporalTx) *gasprice.Oracle {
+ return gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, api.feeHistoryCache, api.logger.New("app", "gasPriceOracle"))
+}
+
+func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs, head *types.Header, dbTx kv.TemporalTx) error {
+ if head.BaseFee == nil {
+ if args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil {
+ return errors.New("maxFeePerGas and maxPriorityFeePerGas are not valid before London is active")
+ }
+ if args.GasPrice == nil {
+ price, err := api.newGasOracle(dbTx).SuggestTipCap(ctx)
+ if err != nil {
+ return err
+ }
+ args.GasPrice = (*hexutil.Big)(price.ToBig())
+ }
+ return nil
+ }
+
+ if args.GasPrice == nil && args.MaxFeePerGas != nil && args.MaxPriorityFeePerGas != nil {
+ if args.MaxFeePerGas.ToInt().Sign() == 0 {
+ return errors.New("maxFeePerGas must be non-zero")
+ }
+ if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 {
+ return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas)
+ }
+ return nil
+ }
+
+ if args.GasPrice != nil {
+ if args.GasPrice.ToInt().Sign() == 0 {
+ return errors.New("gasPrice must be non-zero after london fork")
+ }
+ return nil
+ }
+
+ autoFilledPriorityFee := args.MaxPriorityFeePerGas == nil
+ if autoFilledPriorityFee {
+ tip, err := api.newGasOracle(dbTx).SuggestTipCap(ctx)
+ if err != nil {
+ return err
+ }
+ args.MaxPriorityFeePerGas = (*hexutil.Big)(tip.ToBig())
+ }
+ if args.MaxFeePerGas == nil {
+ val := new(big.Int).Add(
+ args.MaxPriorityFeePerGas.ToInt(),
+ new(big.Int).Lsh(head.BaseFee.ToBig(), 1),
+ )
+ args.MaxFeePerGas = (*hexutil.Big)(val)
+ }
+ if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 {
+ if autoFilledPriorityFee {
+ return fmt.Errorf("suggested maxPriorityFeePerGas (%v) exceeds provided maxFeePerGas (%v)", args.MaxPriorityFeePerGas, args.MaxFeePerGas)
+ }
+ return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas)
+ }
+ return nil
+}
diff --git a/rpc/jsonrpc/eth_fill_transaction_test.go b/rpc/jsonrpc/eth_fill_transaction_test.go
new file mode 100644
index 00000000000..17777ba9c76
--- /dev/null
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -0,0 +1,412 @@
+// Copyright 2026 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package jsonrpc
+
+import (
+ "context"
+ "errors"
+ "math/big"
+ "testing"
+
+ "github.com/holiman/uint256"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc"
+
+ "github.com/erigontech/erigon/cmd/rpcdaemon/rpcdaemontest"
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/hexutil"
+ "github.com/erigontech/erigon/execution/chain"
+ "github.com/erigontech/erigon/execution/execmodule/execmoduletester"
+ "github.com/erigontech/erigon/execution/protocol/misc"
+ "github.com/erigontech/erigon/execution/types"
+ "github.com/erigontech/erigon/node/gointerfaces/txpoolproto"
+ "github.com/erigontech/erigon/rpc/ethapi"
+)
+
+// errPoolClient simulates a txpool gRPC call failure.
+type errPoolClient struct{ stubTxPoolClient }
+
+func (errPoolClient) Nonce(_ context.Context, _ *txpoolproto.NonceRequest, _ ...grpc.CallOption) (*txpoolproto.NonceReply, error) {
+ return nil, errors.New("pool unavailable")
+}
+
+// fixedNoncePoolClient always reports a specific pending nonce for the sender.
+type fixedNoncePoolClient struct {
+ stubTxPoolClient
+ nonce uint64
+}
+
+func (c fixedNoncePoolClient) Nonce(_ context.Context, _ *txpoolproto.NonceRequest, _ ...grpc.CallOption) (*txpoolproto.NonceReply, error) {
+ return &txpoolproto.NonceReply{Found: true, Nonce: c.nonce}, nil
+}
+
+// newLondonApiForTest returns an API backed by a fresh London-activated chain (genesis only).
+// The genesis baseFee is params.InitialBaseFee (1_000_000_000 wei).
+func newLondonApiForTest(t *testing.T) *APIImpl {
+ londonCfg := &chain.Config{
+ ChainID: uint256.NewInt(1337),
+ Rules: chain.EtHashRules,
+ HomesteadBlock: common.NewUint64(0),
+ TangerineWhistleBlock: common.NewUint64(0),
+ SpuriousDragonBlock: common.NewUint64(0),
+ ByzantiumBlock: common.NewUint64(0),
+ ConstantinopleBlock: common.NewUint64(0),
+ PetersburgBlock: common.NewUint64(0),
+ IstanbulBlock: common.NewUint64(0),
+ MuirGlacierBlock: common.NewUint64(0),
+ BerlinBlock: common.NewUint64(0),
+ LondonBlock: common.NewUint64(0),
+ Ethash: new(chain.EthashConfig),
+ }
+ m := execmoduletester.New(t, execmoduletester.WithChainConfig(londonCfg))
+ return newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+}
+
+func TestFillTransactionFillsDefaults(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ To: &to,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.NotEmpty(t, result.Raw)
+ require.NotNil(t, result.Tx)
+ require.Greater(t, uint64(result.Tx.Gas), uint64(0))
+ require.True(t, result.Tx.GasPrice != nil || result.Tx.MaxFeePerGas != nil)
+}
+
+func TestFillTransactionConflictingFees(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ gasPrice := (*hexutil.Big)(big.NewInt(1e9))
+ maxFeePerGas := (*hexutil.Big)(big.NewInt(2e9))
+
+ _, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ To: &to,
+ GasPrice: gasPrice,
+ MaxFeePerGas: maxFeePerGas,
+ })
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "gasPrice")
+}
+
+func TestFillTransactionChainIDMismatch(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ wrongChainID := (*hexutil.Big)(big.NewInt(999999))
+
+ _, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ To: &to,
+ ChainID: wrongChainID,
+ })
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "chainId")
+}
+
+func TestFillTransactionContractCreationNoData(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+
+ _, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ })
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "contract creation")
+}
+
+func TestFillTransactionNoFrom(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ To: &to,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.Equal(t, hexutil.Uint64(0), result.Tx.Nonce, "nonce must default to 0 when From is absent")
+}
+
+func TestFillTransactionExplicitNoncePreserved(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ nonce := hexutil.Uint64(7)
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ To: &to,
+ Nonce: &nonce,
+ })
+ require.NoError(t, err)
+ require.Equal(t, nonce, result.Tx.Nonce, "explicit nonce must not be overwritten")
+}
+
+func TestFillTransactionExplicitGasPreserved(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ gas := hexutil.Uint64(50000)
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ To: &to,
+ Gas: &gas,
+ })
+ require.NoError(t, err)
+ require.Equal(t, gas, result.Tx.Gas, "explicit gas must not be overwritten by estimation")
+}
+
+func TestFillTransactionBlobPreCancun(t *testing.T) {
+ // TestChainBerlinConfig has no Cancun (ExcessBlobGas == nil on head).
+ // A blob tx request must return a clear error, not panic.
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ blobHash := common.HexToHash("0x0100000000000000000000000000000000000000000000000000000000000001")
+
+ _, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ To: &to,
+ BlobVersionedHashes: []common.Hash{blobHash},
+ })
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "Cancun")
+}
+
+func TestFillTransactionBlobPreCancunExplicitBlobFee(t *testing.T) {
+ // Even with an explicit maxFeePerBlobGas, blob txs on a pre-Cancun chain must error.
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ blobHash := common.HexToHash("0x0100000000000000000000000000000000000000000000000000000000000001")
+ gas := hexutil.Uint64(21000)
+
+ _, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ To: &to,
+ Gas: &gas,
+ BlobVersionedHashes: []common.Hash{blobHash},
+ MaxFeePerBlobGas: (*hexutil.Big)(big.NewInt(1e9)),
+ })
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "Cancun")
+}
+
+func TestFillTransactionPoolErrorPropagates(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, errPoolClient{}, nil)
+
+ from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+
+ _, err := api.FillTransaction(context.Background(), ethapi.CallArgs{From: &from, To: &to})
+ require.Error(t, err, "pool gRPC error must propagate to the caller")
+ require.Contains(t, err.Error(), "pool unavailable")
+}
+
+func TestFillTransactionGasPriceWithAccessListIsTypeOne(t *testing.T) {
+ api := newLondonApiForTest(t)
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ gas := hexutil.Uint64(21000)
+ al := types.AccessList{{Address: to, StorageKeys: nil}}
+
+ // Erigon preserves an explicit accessList even when gasPrice is set (type 1),
+ // unlike Geth which silently drops it to a LegacyTx (type 0).
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ To: &to,
+ Gas: &gas,
+ GasPrice: (*hexutil.Big)(big.NewInt(10_000_000_000)),
+ AccessList: &al,
+ })
+ require.NoError(t, err)
+ require.Equal(t, hexutil.Uint64(types.AccessListTxType), result.Tx.Type)
+}
+
+func TestFillTransactionUserGasAboveCapPreserved(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ // FillTransaction passes globalGasCap=0 to ToTransaction, so no capping occurs.
+ userGas := hexutil.Uint64(10_000_000)
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ From: &from,
+ To: &to,
+ Gas: &userGas,
+ })
+ require.NoError(t, err)
+ require.Equal(t, userGas, result.Tx.Gas, "user-provided gas must not be silently capped")
+}
+
+func TestFillTransactionPoolNonce(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ const pendingNonce = uint64(5)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, fixedNoncePoolClient{nonce: pendingNonce}, nil)
+
+ from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{From: &from, To: &to})
+ require.NoError(t, err)
+ require.Equal(t, hexutil.Uint64(pendingNonce+1), result.Tx.Nonce, "nonce must be pool nonce + 1")
+}
+
+func TestFillTransactionOnlyMaxFeePerGas(t *testing.T) {
+ api := newLondonApiForTest(t)
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ gas := hexutil.Uint64(21000)
+ maxFee := (*hexutil.Big)(new(big.Int).SetUint64(1_000_000_000_000_000_000)) // 1e18 wei
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ To: &to,
+ Gas: &gas,
+ MaxFeePerGas: maxFee,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, result.Tx.MaxPriorityFeePerGas, "oracle must fill maxPriorityFeePerGas")
+ require.GreaterOrEqual(t, result.Tx.MaxFeePerGas.ToInt().Cmp(result.Tx.MaxPriorityFeePerGas.ToInt()), 0)
+ require.Equal(t, hexutil.Uint64(types.DynamicFeeTxType), result.Tx.Type)
+}
+
+func TestFillTransactionOnlyMaxPriorityFeePerGas(t *testing.T) {
+ api := newLondonApiForTest(t)
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ gas := hexutil.Uint64(21000)
+ const userTip = int64(1_000_000_000) // 1 gwei
+ // London genesis baseFee = params.InitialBaseFee = 1_000_000_000
+ const initialBaseFee = int64(1_000_000_000)
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ To: &to,
+ Gas: &gas,
+ MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(userTip)),
+ })
+ require.NoError(t, err)
+ require.NotNil(t, result.Tx.MaxFeePerGas, "oracle must fill maxFeePerGas")
+ require.Equal(t, userTip+2*initialBaseFee, result.Tx.MaxFeePerGas.ToInt().Int64())
+ require.Equal(t, hexutil.Uint64(types.DynamicFeeTxType), result.Tx.Type)
+}
+
+func TestFillTransactionMaxFeePerGasTooLow(t *testing.T) {
+ api := newLondonApiForTest(t)
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ gas := hexutil.Uint64(21000)
+
+ _, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ To: &to,
+ Gas: &gas,
+ MaxFeePerGas: (*hexutil.Big)(big.NewInt(1)),
+ MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(1_000_000_000)),
+ })
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "maxFeePerGas")
+}
+
+func TestFillTransactionGasPricePostLondon(t *testing.T) {
+ api := newLondonApiForTest(t)
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ gas := hexutil.Uint64(21000)
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ To: &to,
+ Gas: &gas,
+ GasPrice: (*hexutil.Big)(big.NewInt(10_000_000_000)), // 10 gwei
+ })
+ require.NoError(t, err)
+ require.Equal(t, hexutil.Uint64(types.LegacyTxType), result.Tx.Type, "explicit gasPrice must produce a legacy tx")
+}
+
+func TestFillTransactionBlobFeeUsesHeadExcess(t *testing.T) {
+ const headExcessBlobGas = uint64(10_000_000)
+
+ cancunCfg := &chain.Config{
+ ChainID: uint256.NewInt(1337),
+ Rules: chain.EtHashRules,
+ HomesteadBlock: common.NewUint64(0),
+ TangerineWhistleBlock: common.NewUint64(0),
+ SpuriousDragonBlock: common.NewUint64(0),
+ ByzantiumBlock: common.NewUint64(0),
+ ConstantinopleBlock: common.NewUint64(0),
+ PetersburgBlock: common.NewUint64(0),
+ IstanbulBlock: common.NewUint64(0),
+ MuirGlacierBlock: common.NewUint64(0),
+ BerlinBlock: common.NewUint64(0),
+ LondonBlock: common.NewUint64(0),
+ ShanghaiTime: common.NewUint64(0),
+ CancunTime: common.NewUint64(0),
+ Ethash: new(chain.EthashConfig),
+ }
+
+ excess := headExcessBlobGas
+ blobUsed := uint64(0)
+ gspec := &types.Genesis{
+ Config: cancunCfg,
+ ExcessBlobGas: &excess,
+ BlobGasUsed: &blobUsed,
+ }
+
+ m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(gspec))
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, stubTxPoolClient{}, nil)
+
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+ gas := hexutil.Uint64(21000)
+ blobHash := common.HexToHash("0x0100000000000000000000000000000000000000000000000000000000000001")
+
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
+ To: &to,
+ Gas: &gas,
+ BlobVersionedHashes: []common.Hash{blobHash},
+ MaxFeePerGas: (*hexutil.Big)(big.NewInt(10_000_000_000)),
+ MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(1_000_000_000)),
+ })
+ require.NoError(t, err)
+ require.NotNil(t, result.Tx.MaxFeePerBlobGas)
+
+ head := m.Genesis.HeaderNoCopy()
+ expectedFee, err := misc.GetBlobGasPrice(cancunCfg, *head.ExcessBlobGas, head.Time)
+ require.NoError(t, err)
+ b := expectedFee.ToBig()
+ require.Equal(t, b.Lsh(b, 1), result.Tx.MaxFeePerBlobGas.ToInt())
+}
diff --git a/rpc/jsonrpc/eth_system.go b/rpc/jsonrpc/eth_system.go
index 247b909b365..bde5df501b8 100644
--- a/rpc/jsonrpc/eth_system.go
+++ b/rpc/jsonrpc/eth_system.go
@@ -36,7 +36,6 @@ import (
"github.com/erigontech/erigon/execution/types"
"github.com/erigontech/erigon/execution/vm"
"github.com/erigontech/erigon/execution/vm/evmtypes"
- "github.com/erigontech/erigon/node/ethconfig"
"github.com/erigontech/erigon/p2p/forkid"
"github.com/erigontech/erigon/rpc"
"github.com/erigontech/erigon/rpc/gasprice"
@@ -272,7 +271,7 @@ func (api *APIImpl) GasPrice(ctx context.Context) (*hexutil.Big, error) {
return nil, err
}
defer tx.Rollback()
- oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, tx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
+ oracle := api.newGasOracle(tx)
tipcap, err := oracle.SuggestTipCap(ctx)
gasResult := uint256.NewInt(0)
@@ -294,7 +293,7 @@ func (api *APIImpl) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, err
return nil, err
}
defer tx.Rollback()
- oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, tx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
+ oracle := api.newGasOracle(tx)
tipcap, err := oracle.SuggestTipCap(ctx)
if err != nil {
return nil, err
@@ -317,7 +316,7 @@ func (api *APIImpl) FeeHistory(ctx context.Context, blockCount rpc.DecimalOrHex,
return nil, err
}
defer tx.Rollback()
- oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, tx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, api.feeHistoryCache, api.logger.New("app", "gasPriceOracle"))
+ oracle := api.newGasOracle(tx)
oldest, reward, baseFee, gasUsed, blobBaseFee, blobGasUsedRatio, err := oracle.FeeHistory(ctx, int(blockCount), lastBlock, rewardPercentiles)
if err != nil {
diff --git a/rpc/rpchelper/helper.go b/rpc/rpchelper/helper.go
index 0811d7d36e6..c55522785d6 100644
--- a/rpc/rpchelper/helper.go
+++ b/rpc/rpchelper/helper.go
@@ -120,12 +120,13 @@ func _GetBlockNumber(ctx context.Context, requireCanonical bool, blockNrOrHash r
return 0, common.Hash{}, false, false, err
}
case rpc.PendingBlockNumber:
- pendingBlock := filters.LastPendingBlock()
- if pendingBlock == nil {
- blockNumber = plainStateBlockNumber
- } else {
- return pendingBlock.NumberU64(), pendingBlock.Hash(), false, true, nil
+ if filters != nil {
+ pendingBlock := filters.LastPendingBlock()
+ if pendingBlock != nil {
+ return pendingBlock.NumberU64(), pendingBlock.Hash(), false, true, nil
+ }
}
+ blockNumber = plainStateBlockNumber
case rpc.LatestExecutedBlockNumber:
blockNumber = plainStateBlockNumber
default: