From 9d99fc3c876f2661d668fdddeaa585ddf142e6a1 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Wed, 17 Jun 2026 21:48:57 +0200
Subject: [PATCH 01/14] impl eth_fillTransaction()
---
cmd/rpcdaemon/README.md | 1 +
rpc/ethapi/api.go | 32 ++++
rpc/jsonrpc/eth_api.go | 1 +
rpc/jsonrpc/eth_fill_transaction.go | 194 +++++++++++++++++++++++
rpc/jsonrpc/eth_fill_transaction_test.go | 98 ++++++++++++
5 files changed, 326 insertions(+)
create mode 100644 rpc/jsonrpc/eth_fill_transaction.go
create mode 100644 rpc/jsonrpc/eth_fill_transaction_test.go
diff --git a/cmd/rpcdaemon/README.md b/cmd/rpcdaemon/README.md
index 38d426b6054..6513c6c1874 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 | |
| | | |
| eth_getProof | Yes | Limited to last 100000 blocks |
| | | |
diff --git a/rpc/ethapi/api.go b/rpc/ethapi/api.go
index c3eba4e8f92..8be2d53f02a 100644
--- a/rpc/ethapi/api.go
+++ b/rpc/ethapi/api.go
@@ -20,6 +20,7 @@
package ethapi
import (
+ "encoding/json"
"errors"
"fmt"
"math/big"
@@ -495,6 +496,37 @@ 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"`
+}
+
+// MarshalJSON omits block-placement and sender fields that are meaningless for
+// an unsigned, unsubmitted transaction.
+func (r SignTransactionResult) MarshalJSON() ([]byte, error) {
+ 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)
+ }
+ 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/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..dcd6f566a2d
--- /dev/null
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -0,0 +1,194 @@
+// Copyright 2024 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"
+
+ "google.golang.org/grpc"
+
+ "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/types"
+ "github.com/erigontech/erigon/node/ethconfig"
+ "github.com/erigontech/erigon/node/gointerfaces"
+ "github.com/erigontech/erigon/node/gointerfaces/txpoolproto"
+ "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")
+ }
+
+ 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 {
+ reply, err2 := api.txPool.Nonce(ctx, &txpoolproto.NonceRequest{
+ Address: gointerfaces.ConvertAddressToH160(*args.From),
+ }, &grpc.EmptyCallOption{})
+ if err2 == nil && reply != nil && reply.Found {
+ nonce = reply.Nonce + 1
+ } else {
+ latestBlock := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
+ if count, err3 := api.GetTransactionCount(ctx, *args.From, &latestBlock); err3 == nil && count != nil {
+ 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.To == nil {
+ 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 err := api.fillFeeDefaults(ctx, &args, head, dbTx); err != nil {
+ return nil, err
+ }
+
+ if args.BlobVersionedHashes != nil && args.MaxFeePerBlobGas == nil && head.ExcessBlobGas != nil {
+ nextBlockTime := head.Time + cc.SecondsPerSlot()
+ blobFee, err := misc.GetBlobGasPrice(cc, *head.ExcessBlobGas, nextBlockTime)
+ if err != nil {
+ return nil, err
+ }
+ args.MaxFeePerBlobGas = (*hexutil.Big)(new(big.Int).Lsh(blobFee.ToBig(), 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, nil, nil, nil)
+ if err != nil {
+ return nil, err
+ }
+ args.Gas = &estimated
+ }
+
+ baseFee := head.BaseFee
+
+ txn, err := args.ToTransaction(api.GasCap, 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) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs, head *types.Header, dbTx kv.TemporalTx) error {
+ isPostLondon := head.BaseFee != nil
+ oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
+
+ if !isPostLondon {
+ 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 := oracle.SuggestTipCap(ctx)
+ if err != nil {
+ return err
+ }
+ args.GasPrice = (*hexutil.Big)(price.ToBig())
+ }
+ return nil
+ }
+
+ // Post-London: if both EIP-1559 fields already set, just sanity-check them.
+ 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
+ }
+
+ // GasPrice is already set: the caller wants a legacy transaction.
+ if args.GasPrice != nil {
+ return nil
+ }
+
+ tip, err := oracle.SuggestTipCap(ctx)
+ if err != nil {
+ return err
+ }
+ if args.MaxPriorityFeePerGas == nil {
+ 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 {
+ 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..d31ca24ee4c
--- /dev/null
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -0,0 +1,98 @@
+// Copyright 2024 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"
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/erigontech/erigon/cmd/rpcdaemon/rpcdaemontest"
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/hexutil"
+ "github.com/erigontech/erigon/rpc/ethapi"
+)
+
+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")
+}
From 5badd3002b271d1149442589251a732e483ac4eb Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Wed, 17 Jun 2026 21:50:18 +0200
Subject: [PATCH 02/14] dave temporray rpc branch
---
.github/workflows/scripts/rpc_version.env | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/scripts/rpc_version.env b/.github/workflows/scripts/rpc_version.env
index e0533b9bf48..ef21739d24a 100644
--- a/.github/workflows/scripts/rpc_version.env
+++ b/.github/workflows/scripts/rpc_version.env
@@ -1 +1 @@
-RPC_VERSION=v2.14.1
+RPC_VERSION=lupin012/add_test_eth_fillTransaction
From f43f0aa1ff1d6d28b741869d13f284762e7fd9be Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Wed, 17 Jun 2026 22:38:12 +0200
Subject: [PATCH 03/14] rpc/ethapi, rpc/jsonrpc: align eth_fillTransaction JSON
format with Geth
SignTransactionResult.MarshalJSON now explicitly emits gasPrice:null for
EIP-1559 transactions, maxFeePerGas:null/maxPriorityFeePerGas:null for
legacy transactions, and normalises null v/r/s to "0x0" for unsigned
transactions. Adds blob/authorizationList validation checks to
FillTransaction and two unit tests that pin the serialisation contract.
---
rpc/ethapi/api.go | 15 ++++-
rpc/ethapi/api_test.go | 88 +++++++++++++++++++++++++++++
rpc/jsonrpc/eth_fill_transaction.go | 17 ++++++
3 files changed, 118 insertions(+), 2 deletions(-)
diff --git a/rpc/ethapi/api.go b/rpc/ethapi/api.go
index 8be2d53f02a..d1bd96a61b6 100644
--- a/rpc/ethapi/api.go
+++ b/rpc/ethapi/api.go
@@ -502,8 +502,7 @@ type SignTransactionResult struct {
Tx *RPCTransaction `json:"tx"`
}
-// MarshalJSON omits block-placement and sender fields that are meaningless for
-// an unsigned, unsubmitted transaction.
+// MarshalJSON strips block-placement/sender fields and normalises fee and signature fields to match Geth.
func (r SignTransactionResult) MarshalJSON() ([]byte, error) {
type plain struct {
Raw hexutil.Bytes `json:"raw"`
@@ -520,6 +519,18 @@ func (r SignTransactionResult) MarshalJSON() ([]byte, error) {
for _, k := range []string{"blockHash", "blockNumber", "blockTimestamp", "transactionIndex", "from"} {
delete(m, k)
}
+ nullJSON := json.RawMessage("null")
+ for _, k := range []string{"gasPrice", "maxFeePerGas", "maxPriorityFeePerGas"} {
+ if _, ok := m[k]; !ok {
+ m[k] = nullJSON
+ }
+ }
+ 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
diff --git a/rpc/ethapi/api_test.go b/rpc/ethapi/api_test.go
index d8397746e8e..2da66193df4 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: gasPrice must be explicit null.
+ gasPrice, ok := fields["gasPrice"]
+ require.True(t, ok, "gasPrice must be present")
+ require.Equal(t, "null", string(gasPrice))
+
+ // 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 stripped.
+ 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))
+
+ // maxFeePerGas and maxPriorityFeePerGas must be explicit null.
+ maxFee, ok := fields["maxFeePerGas"]
+ require.True(t, ok, "maxFeePerGas must be present")
+ require.Equal(t, "null", string(maxFee))
+ maxPrio, ok := fields["maxPriorityFeePerGas"]
+ require.True(t, ok, "maxPriorityFeePerGas must be present")
+ require.Equal(t, "null", string(maxPrio))
+
+ // 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 TestNewRPCTransaction_AccessList_AllZeroSig(t *testing.T) {
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
chainID := uint256.NewInt(1)
diff --git a/rpc/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index dcd6f566a2d..4a48071ba61 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -30,6 +30,7 @@ import (
"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/node/gointerfaces"
@@ -86,11 +87,24 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
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 && len(args.BlobVersionedHashes) == 0 {
+ return nil, errors.New("need at least 1 blob for a blob transaction")
+ }
+ if len(args.BlobVersionedHashes) > params.MaxBlobsPerTxn {
+ return nil, fmt.Errorf("too many blobs in transaction (have=%d, max=%d)", len(args.BlobVersionedHashes), 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 {
@@ -170,6 +184,9 @@ func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs,
// GasPrice is already set: the caller wants a legacy transaction.
if args.GasPrice != nil {
+ if args.GasPrice.ToInt().Sign() == 0 {
+ return errors.New("gasPrice must be non-zero after london fork")
+ }
return nil
}
From 7b11cb330a272e87aa97f5a556e84c326b791f4b Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Wed, 17 Jun 2026 22:45:14 +0200
Subject: [PATCH 04/14] rpc/jsonrpc: defer oracle construction to branches that
use it
fillFeeDefaults constructed NewOracle unconditionally, wasting allocs on
the three fast-exit paths (pre-London with gasPrice set, post-London with
all EIP-1559 fields supplied, post-London legacy gasPrice path). Move
oracle construction to the two branches that actually call SuggestTipCap.
Also remove the redundant isPostLondon variable.
---
rpc/jsonrpc/eth_fill_transaction.go | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/rpc/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index 4a48071ba61..22a9d800ad3 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -154,14 +154,12 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
}
func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs, head *types.Header, dbTx kv.TemporalTx) error {
- isPostLondon := head.BaseFee != nil
- oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
-
- if !isPostLondon {
+ 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 {
+ oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
price, err := oracle.SuggestTipCap(ctx)
if err != nil {
return err
@@ -171,7 +169,6 @@ func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs,
return nil
}
- // Post-London: if both EIP-1559 fields already set, just sanity-check them.
if args.GasPrice == nil && args.MaxFeePerGas != nil && args.MaxPriorityFeePerGas != nil {
if args.MaxFeePerGas.ToInt().Sign() == 0 {
return errors.New("maxFeePerGas must be non-zero")
@@ -182,7 +179,6 @@ func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs,
return nil
}
- // GasPrice is already set: the caller wants a legacy transaction.
if args.GasPrice != nil {
if args.GasPrice.ToInt().Sign() == 0 {
return errors.New("gasPrice must be non-zero after london fork")
@@ -190,6 +186,7 @@ func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs,
return nil
}
+ oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
tip, err := oracle.SuggestTipCap(ctx)
if err != nil {
return err
From 7b9413f4e6745f3ee1e2c00270a46668c3afe486 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Thu, 18 Jun 2026 08:55:18 +0200
Subject: [PATCH 05/14] rpc/jsonrpc: fix two bugs in FillTransaction found in
PR review
- Propagate GetTransactionCount error in nonce fallback instead of
silently leaving nonce at 0, which would produce an incorrect contract
address for deployments.
- Return a clear error for blob transactions on pre-Cancun chains
(ExcessBlobGas == nil) instead of panicking when ToTransaction
dereferences a nil MaxFeePerBlobGas.
---
rpc/jsonrpc/eth_fill_transaction.go | 11 +++++++++--
rpc/jsonrpc/eth_fill_transaction_test.go | 19 +++++++++++++++++++
2 files changed, 28 insertions(+), 2 deletions(-)
diff --git a/rpc/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index 22a9d800ad3..2fe440eeb8a 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -75,7 +75,11 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
nonce = reply.Nonce + 1
} else {
latestBlock := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
- if count, err3 := api.GetTransactionCount(ctx, *args.From, &latestBlock); err3 == nil && count != nil {
+ count, err3 := api.GetTransactionCount(ctx, *args.From, &latestBlock)
+ if err3 != nil {
+ return nil, err3
+ }
+ if count != nil {
nonce = uint64(*count)
}
}
@@ -111,7 +115,10 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
return nil, err
}
- if args.BlobVersionedHashes != nil && args.MaxFeePerBlobGas == nil && head.ExcessBlobGas != nil {
+ if args.BlobVersionedHashes != nil && args.MaxFeePerBlobGas == nil {
+ if head.ExcessBlobGas == nil {
+ return nil, errors.New("blob transactions not supported before Cancun")
+ }
nextBlockTime := head.Time + cc.SecondsPerSlot()
blobFee, err := misc.GetBlobGasPrice(cc, *head.ExcessBlobGas, nextBlockTime)
if err != nil {
diff --git a/rpc/jsonrpc/eth_fill_transaction_test.go b/rpc/jsonrpc/eth_fill_transaction_test.go
index d31ca24ee4c..c1a4bf05b04 100644
--- a/rpc/jsonrpc/eth_fill_transaction_test.go
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -96,3 +96,22 @@ func TestFillTransactionContractCreationNoData(t *testing.T) {
require.Error(t, err)
require.Contains(t, err.Error(), "contract creation")
}
+
+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")
+}
From 4f26a44af90b76974c878e010df7e6b8f8c59754 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Sat, 20 Jun 2026 17:16:32 +0200
Subject: [PATCH 06/14] rpc: eth_fillTransaction review fixes
- validate maxFeePerBlobGas != 0 if specified (matches Geth)
- extract newGasOracle helper to avoid duplicating oracle construction in fillFeeDefaults
- add tests for From=nil (nonce defaults to 0), explicit nonce preservation, explicit gas preservation
Co-Authored-By: Claude Sonnet 4.6
---
rpc/jsonrpc/eth_fill_transaction.go | 13 +++++--
rpc/jsonrpc/eth_fill_transaction_test.go | 48 ++++++++++++++++++++++++
2 files changed, 57 insertions(+), 4 deletions(-)
diff --git a/rpc/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index 2fe440eeb8a..41a1270164e 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -45,6 +45,9 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
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 {
@@ -160,14 +163,17 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
}, nil
}
+func (api *APIImpl) newGasOracle(dbTx kv.TemporalTx) *gasprice.Oracle {
+ return gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, 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 {
- oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
- price, err := oracle.SuggestTipCap(ctx)
+ price, err := api.newGasOracle(dbTx).SuggestTipCap(ctx)
if err != nil {
return err
}
@@ -193,8 +199,7 @@ func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs,
return nil
}
- oracle := gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
- tip, err := oracle.SuggestTipCap(ctx)
+ tip, err := api.newGasOracle(dbTx).SuggestTipCap(ctx)
if err != nil {
return err
}
diff --git a/rpc/jsonrpc/eth_fill_transaction_test.go b/rpc/jsonrpc/eth_fill_transaction_test.go
index c1a4bf05b04..dcba299bd86 100644
--- a/rpc/jsonrpc/eth_fill_transaction_test.go
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -97,6 +97,54 @@ func TestFillTransactionContractCreationNoData(t *testing.T) {
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(42)
+
+ 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.
From 5f41116095c77066398ebabd241e2e03436dd26b Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Sun, 21 Jun 2026 09:52:06 +0200
Subject: [PATCH 07/14] rpc: eth_fillTransaction review fixes and
simplifications
- preserve user-provided gas (pass 0 instead of api.GasCap to ToTransaction)
- warn on txpool nonce fallback instead of silently using on-chain nonce
- improve error message when only maxFeePerGas is provided and oracle tip exceeds it
- add README note about blob sidecar not supported
- add tests for pool error fallback, gas cap bypass, pool nonce, fee filling, gas price post-London
- avoid calling gas oracle when MaxPriorityFeePerGas is already set
- merge blob-hash guards into single block; drop redundant proto nil guard; inline baseFee
---
cmd/rpcdaemon/README.md | 2 +-
rpc/jsonrpc/eth_fill_transaction.go | 34 +++--
rpc/jsonrpc/eth_fill_transaction_test.go | 155 +++++++++++++++++++++++
3 files changed, 176 insertions(+), 15 deletions(-)
diff --git a/cmd/rpcdaemon/README.md b/cmd/rpcdaemon/README.md
index 6513c6c1874..a0c24e1cb70 100644
--- a/cmd/rpcdaemon/README.md
+++ b/cmd/rpcdaemon/README.md
@@ -310,7 +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 | |
+| 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/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index 41a1270164e..c2d5fa10835 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -74,9 +74,12 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
reply, err2 := api.txPool.Nonce(ctx, &txpoolproto.NonceRequest{
Address: gointerfaces.ConvertAddressToH160(*args.From),
}, &grpc.EmptyCallOption{})
- if err2 == nil && reply != nil && reply.Found {
+ if err2 == nil && reply.Found {
nonce = reply.Nonce + 1
} else {
+ if err2 != nil {
+ api.logger.Warn("eth_fillTransaction: txpool nonce lookup failed, falling back to on-chain nonce", "err", err2)
+ }
latestBlock := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
count, err3 := api.GetTransactionCount(ctx, *args.From, &latestBlock)
if err3 != nil {
@@ -94,11 +97,12 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
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 && len(args.BlobVersionedHashes) == 0 {
- return nil, errors.New("need at least 1 blob for a blob transaction")
- }
- if len(args.BlobVersionedHashes) > params.MaxBlobsPerTxn {
- return nil, fmt.Errorf("too many blobs in transaction (have=%d, max=%d)", len(args.BlobVersionedHashes), params.MaxBlobsPerTxn)
+ 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 {
@@ -145,9 +149,7 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
args.Gas = &estimated
}
- baseFee := head.BaseFee
-
- txn, err := args.ToTransaction(api.GasCap, baseFee)
+ txn, err := args.ToTransaction(0, head.BaseFee)
if err != nil {
return nil, err
}
@@ -199,11 +201,12 @@ func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs,
return nil
}
- tip, err := api.newGasOracle(dbTx).SuggestTipCap(ctx)
- if err != nil {
- return err
- }
- if args.MaxPriorityFeePerGas == 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 {
@@ -214,6 +217,9 @@ func (api *APIImpl) fillFeeDefaults(ctx context.Context, args *ethapi.CallArgs,
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
index dcba299bd86..736ef76e31f 100644
--- a/rpc/jsonrpc/eth_fill_transaction_test.go
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -18,17 +18,63 @@ 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/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)
@@ -163,3 +209,112 @@ func TestFillTransactionBlobPreCancun(t *testing.T) {
require.Error(t, err)
require.Contains(t, err.Error(), "Cancun")
}
+
+func TestFillTransactionPoolErrorFallsBackToOnChainNonce(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ api := newEthApiForTest(newBaseApiForTest(m), m.DB, errPoolClient{}, nil)
+
+ from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
+ to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
+
+ // Pool error must not surface as a FillTransaction error; on-chain nonce is used.
+ result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{From: &from, To: &to})
+ require.NoError(t, err, "pool gRPC error must not propagate to the caller")
+ require.NotNil(t, result)
+}
+
+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")
+ // GasCap in newEthApiForTest is 5_000_000; pass double to trigger the cap.
+ 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")
+}
From 1aab299c9d4b31b25d765db197d13efc186da23f Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Mon, 22 Jun 2026 20:17:14 +0200
Subject: [PATCH 08/14] rpc/ethapi, rpc/jsonrpc, rpc/rpchelper:
eth_fillTransaction nonce and type-1 divergence
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two fixes for yperbasis's review of PR #21870:
1. gasPrice + accessList type: document the deliberate divergence from Geth
in ToTransaction — Erigon preserves type-1 when an explicit accessList is
given alongside gasPrice; Geth silently drops it to LegacyTx (type 0).
Add TestFillTransactionGasPriceWithAccessListIsTypeOne to pin the behavior.
2. Nonce filling: replace the inline txpool→warn→fallback block with a
single GetTransactionCount(pending) call. Propagates pool errors to the
caller instead of silently falling back to the on-chain nonce, matching
Geth's behavior. Also fix _GetBlockNumber to guard LastPendingBlock behind
a nil check on filters (consistent with the existing nil guard for WithOverlay).
---
rpc/ethapi/api.go | 2 ++
rpc/jsonrpc/eth_fill_transaction.go | 28 ++++++------------------
rpc/jsonrpc/eth_fill_transaction_test.go | 28 +++++++++++++++++++-----
rpc/rpchelper/helper.go | 11 +++++-----
4 files changed, 38 insertions(+), 31 deletions(-)
diff --git a/rpc/ethapi/api.go b/rpc/ethapi/api.go
index d1bd96a61b6..52c3935ee9c 100644
--- a/rpc/ethapi/api.go
+++ b/rpc/ethapi/api.go
@@ -287,6 +287,8 @@ func (args *CallArgs) ToTransaction(globalGasCap uint64, baseFee *uint256.Int) (
TipCap: *msg.TipCap(),
AccessList: al,
}
+ // Deliberate divergence from Geth: an explicit accessList is preserved as type 1 even when gasPrice
+ // is also set; Geth silently drops the access list and returns a LegacyTx (type 0).
case args.AccessList != nil:
al := types.AccessList{}
if args.AccessList != nil {
diff --git a/rpc/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index c2d5fa10835..bae7194df7b 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -23,8 +23,6 @@ import (
"fmt"
"math/big"
- "google.golang.org/grpc"
-
"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/hexutil"
"github.com/erigontech/erigon/db/kv"
@@ -33,8 +31,6 @@ import (
"github.com/erigontech/erigon/execution/protocol/params"
"github.com/erigontech/erigon/execution/types"
"github.com/erigontech/erigon/node/ethconfig"
- "github.com/erigontech/erigon/node/gointerfaces"
- "github.com/erigontech/erigon/node/gointerfaces/txpoolproto"
"github.com/erigontech/erigon/rpc"
"github.com/erigontech/erigon/rpc/ethapi"
"github.com/erigontech/erigon/rpc/gasprice"
@@ -71,23 +67,13 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
if args.Nonce == nil {
var nonce uint64
if args.From != nil {
- reply, err2 := api.txPool.Nonce(ctx, &txpoolproto.NonceRequest{
- Address: gointerfaces.ConvertAddressToH160(*args.From),
- }, &grpc.EmptyCallOption{})
- if err2 == nil && reply.Found {
- nonce = reply.Nonce + 1
- } else {
- if err2 != nil {
- api.logger.Warn("eth_fillTransaction: txpool nonce lookup failed, falling back to on-chain nonce", "err", err2)
- }
- latestBlock := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
- count, err3 := api.GetTransactionCount(ctx, *args.From, &latestBlock)
- if err3 != nil {
- return nil, err3
- }
- if count != nil {
- nonce = uint64(*count)
- }
+ pendingBlock := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
+ count, err := api.GetTransactionCount(ctx, *args.From, &pendingBlock)
+ if err != nil {
+ return nil, err
+ }
+ if count != nil {
+ nonce = uint64(*count)
}
}
args.Nonce = (*hexutil.Uint64)(&nonce)
diff --git a/rpc/jsonrpc/eth_fill_transaction_test.go b/rpc/jsonrpc/eth_fill_transaction_test.go
index 736ef76e31f..f454e4a720f 100644
--- a/rpc/jsonrpc/eth_fill_transaction_test.go
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -210,17 +210,35 @@ func TestFillTransactionBlobPreCancun(t *testing.T) {
require.Contains(t, err.Error(), "Cancun")
}
-func TestFillTransactionPoolErrorFallsBackToOnChainNonce(t *testing.T) {
+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")
- // Pool error must not surface as a FillTransaction error; on-chain nonce is used.
- result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{From: &from, To: &to})
- require.NoError(t, err, "pool gRPC error must not propagate to the caller")
- require.NotNil(t, result)
+ _, 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,
+ "gasPrice + explicit accessList must produce type-1 tx (Erigon diverges from Geth here)")
}
func TestFillTransactionUserGasAboveCapPreserved(t *testing.T) {
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:
From 51895b65a5441694b2d5ed57fbbaf0d86b4034d7 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Mon, 22 Jun 2026 20:27:32 +0200
Subject: [PATCH 09/14] rpc/ethapi: replace SignTransactionResult.MarshalJSON
denylist with typed allowlist
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace the marshal→map→strip→re-marshal approach with a dedicated
signedTxJSON struct. New fields added to RPCTransaction will not leak
into the output since only explicitly listed fields are marshalled.
---
rpc/ethapi/api.go | 89 +++++++++++++++++++++++++++++++----------------
1 file changed, 59 insertions(+), 30 deletions(-)
diff --git a/rpc/ethapi/api.go b/rpc/ethapi/api.go
index 52c3935ee9c..df429e5e89d 100644
--- a/rpc/ethapi/api.go
+++ b/rpc/ethapi/api.go
@@ -504,40 +504,69 @@ type SignTransactionResult struct {
Tx *RPCTransaction `json:"tx"`
}
-// MarshalJSON strips block-placement/sender fields and normalises fee and signature fields to match Geth.
+// signedTxJSON is the allowlist output shape for SignTransactionResult.Tx; block-placement and sender fields are absent.
+type signedTxJSON struct {
+ Gas hexutil.Uint64 `json:"gas"`
+ GasPrice *hexutil.Big `json:"gasPrice"`
+ MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"`
+ MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"`
+ Hash common.Hash `json:"hash"`
+ Input hexutil.Bytes `json:"input"`
+ Nonce hexutil.Uint64 `json:"nonce"`
+ To *common.Address `json:"to"`
+ Value *hexutil.Big `json:"value"`
+ Type hexutil.Uint64 `json:"type"`
+ Accesses *types.AccessList `json:"accessList,omitempty"`
+ ChainID *hexutil.Big `json:"chainId,omitempty"`
+ MaxFeePerBlobGas *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
+ BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
+ Authorizations *[]types.JsonAuthorization `json:"authorizationList,omitempty"`
+ V *hexutil.Big `json:"v"`
+ YParity *hexutil.Big `json:"yParity,omitempty"`
+ R *hexutil.Big `json:"r"`
+ S *hexutil.Big `json:"s"`
+}
+
func (r SignTransactionResult) MarshalJSON() ([]byte, error) {
- 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
+ zero := (*hexutil.Big)(new(big.Int))
+ vv, rv, sv := r.Tx.V, r.Tx.R, r.Tx.S
+ if vv == nil {
+ vv = zero
}
- var m map[string]json.RawMessage
- if err := json.Unmarshal(txBytes, &m); err != nil {
- return nil, err
+ if rv == nil {
+ rv = zero
}
- for _, k := range []string{"blockHash", "blockNumber", "blockTimestamp", "transactionIndex", "from"} {
- delete(m, k)
+ if sv == nil {
+ sv = zero
}
- nullJSON := json.RawMessage("null")
- for _, k := range []string{"gasPrice", "maxFeePerGas", "maxPriorityFeePerGas"} {
- if _, ok := m[k]; !ok {
- m[k] = nullJSON
- }
- }
- 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})
+ type plain struct {
+ Raw hexutil.Bytes `json:"raw"`
+ Tx signedTxJSON `json:"tx"`
+ }
+ return json.Marshal(plain{
+ Raw: r.Raw,
+ Tx: signedTxJSON{
+ Gas: r.Tx.Gas,
+ GasPrice: r.Tx.GasPrice,
+ MaxPriorityFeePerGas: r.Tx.MaxPriorityFeePerGas,
+ MaxFeePerGas: r.Tx.MaxFeePerGas,
+ Hash: r.Tx.Hash,
+ Input: r.Tx.Input,
+ Nonce: r.Tx.Nonce,
+ To: r.Tx.To,
+ Value: r.Tx.Value,
+ Type: r.Tx.Type,
+ Accesses: r.Tx.Accesses,
+ ChainID: r.Tx.ChainID,
+ MaxFeePerBlobGas: r.Tx.MaxFeePerBlobGas,
+ BlobVersionedHashes: r.Tx.BlobVersionedHashes,
+ Authorizations: r.Tx.Authorizations,
+ V: vv,
+ YParity: r.Tx.YParity,
+ R: rv,
+ S: sv,
+ },
+ })
}
// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
From 39bb6a8d088c5061a7c94cb193aa70d414154956 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Tue, 23 Jun 2026 20:47:41 +0200
Subject: [PATCH 10/14] rpc/ethapi, rpc/jsonrpc: address yperbasis round-2
review on eth_fillTransaction
- Hoist head.ExcessBlobGas == nil check outside MaxFeePerBlobGas == nil guard
so pre-Cancun blob requests error even when maxFeePerBlobGas is explicit.
- Fix TestFillTransactionExplicitNoncePreserved: use nonce 7 (not 42, which
collides with the DB state of the test sender and made the test vacuous).
- Fix TestFillTransactionUserGasAboveCapPreserved comment: FillTransaction
passes globalGasCap=0 so no capping can occur on that path.
- Add nil guard on r.Tx in SignTransactionResult.MarshalJSON to prevent panic
when the type is used with a nil transaction.
- Add TestFillTransactionBlobPreCancunExplicitBlobFee and
TestSignTransactionResultMarshalJSON_NilTx.
---
rpc/ethapi/api.go | 3 +++
rpc/ethapi/api_test.go | 6 ++++++
rpc/jsonrpc/eth_fill_transaction.go | 14 +++++++------
rpc/jsonrpc/eth_fill_transaction_test.go | 25 ++++++++++++++++++++++--
4 files changed, 40 insertions(+), 8 deletions(-)
diff --git a/rpc/ethapi/api.go b/rpc/ethapi/api.go
index df429e5e89d..e823b50f054 100644
--- a/rpc/ethapi/api.go
+++ b/rpc/ethapi/api.go
@@ -528,6 +528,9 @@ type signedTxJSON struct {
}
func (r SignTransactionResult) MarshalJSON() ([]byte, error) {
+ if r.Tx == nil {
+ return nil, errors.New("nil transaction")
+ }
zero := (*hexutil.Big)(new(big.Int))
vv, rv, sv := r.Tx.V, r.Tx.R, r.Tx.S
if vv == nil {
diff --git a/rpc/ethapi/api_test.go b/rpc/ethapi/api_test.go
index 2da66193df4..f52cbed2f36 100644
--- a/rpc/ethapi/api_test.go
+++ b/rpc/ethapi/api_test.go
@@ -176,6 +176,12 @@ func TestSignTransactionResultMarshalJSON_Legacy(t *testing.T) {
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_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index bae7194df7b..ab9a02e3cf5 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -108,16 +108,18 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
return nil, err
}
- if args.BlobVersionedHashes != nil && args.MaxFeePerBlobGas == nil {
+ if args.BlobVersionedHashes != nil {
if head.ExcessBlobGas == nil {
return nil, errors.New("blob transactions not supported before Cancun")
}
- nextBlockTime := head.Time + cc.SecondsPerSlot()
- blobFee, err := misc.GetBlobGasPrice(cc, *head.ExcessBlobGas, nextBlockTime)
- if err != nil {
- return nil, err
+ if args.MaxFeePerBlobGas == nil {
+ nextBlockTime := head.Time + cc.SecondsPerSlot()
+ blobFee, err := misc.GetBlobGasPrice(cc, *head.ExcessBlobGas, nextBlockTime)
+ if err != nil {
+ return nil, err
+ }
+ args.MaxFeePerBlobGas = (*hexutil.Big)(new(big.Int).Lsh(blobFee.ToBig(), 1))
}
- args.MaxFeePerBlobGas = (*hexutil.Big)(new(big.Int).Lsh(blobFee.ToBig(), 1))
}
chainIDBig := cc.ChainID.ToBig()
diff --git a/rpc/jsonrpc/eth_fill_transaction_test.go b/rpc/jsonrpc/eth_fill_transaction_test.go
index f454e4a720f..17dc27f00ae 100644
--- a/rpc/jsonrpc/eth_fill_transaction_test.go
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -163,7 +163,7 @@ func TestFillTransactionExplicitNoncePreserved(t *testing.T) {
var from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
var to = common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
- nonce := hexutil.Uint64(42)
+ nonce := hexutil.Uint64(7)
result, err := api.FillTransaction(context.Background(), ethapi.CallArgs{
From: &from,
@@ -210,6 +210,27 @@ func TestFillTransactionBlobPreCancun(t *testing.T) {
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)
@@ -247,7 +268,7 @@ func TestFillTransactionUserGasAboveCapPreserved(t *testing.T) {
from := common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
to := common.HexToAddress("0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e")
- // GasCap in newEthApiForTest is 5_000_000; pass double to trigger the cap.
+ // 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 29c1cb45cea4ab0f8a165245e02ee0b4ffe3ad94 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Wed, 24 Jun 2026 16:54:05 +0200
Subject: [PATCH 11/14] rpc/ethapi, rpc/jsonrpc: address yperbasis round-3
review on eth_fillTransaction
- Remove signedTxJSON; marshal RPCTransaction via map and strip placement/sender
fields so new fields flow through automatically; inapplicable fee fields are
absent (not null), matching Geth
- Estimate gas against explicit latest block reference (matching Geth)
- Consolidate newGasOracle helper; route GasPrice/MaxPriorityFeePerGas/FeeHistory
through it with feeHistoryCache wired in
- Fix MaxFeePerBlobGas default: use CalcExcessBlobGas for next-block excess
instead of head.ExcessBlobGas directly
- Add TestFillTransactionBlobFeeUsesNextBlockExcess (RED before fix, GREEN after)
---
rpc/ethapi/api.go | 85 +++++++-----------------
rpc/ethapi/api_test.go | 21 +++---
rpc/jsonrpc/eth_fill_transaction.go | 13 ++--
rpc/jsonrpc/eth_fill_transaction_test.go | 71 +++++++++++++++++++-
rpc/jsonrpc/eth_system.go | 7 +-
5 files changed, 110 insertions(+), 87 deletions(-)
diff --git a/rpc/ethapi/api.go b/rpc/ethapi/api.go
index e823b50f054..a287144c7ef 100644
--- a/rpc/ethapi/api.go
+++ b/rpc/ethapi/api.go
@@ -287,8 +287,7 @@ func (args *CallArgs) ToTransaction(globalGasCap uint64, baseFee *uint256.Int) (
TipCap: *msg.TipCap(),
AccessList: al,
}
- // Deliberate divergence from Geth: an explicit accessList is preserved as type 1 even when gasPrice
- // is also set; Geth silently drops the access list and returns a LegacyTx (type 0).
+ // 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 {
@@ -504,72 +503,36 @@ type SignTransactionResult struct {
Tx *RPCTransaction `json:"tx"`
}
-// signedTxJSON is the allowlist output shape for SignTransactionResult.Tx; block-placement and sender fields are absent.
-type signedTxJSON struct {
- Gas hexutil.Uint64 `json:"gas"`
- GasPrice *hexutil.Big `json:"gasPrice"`
- MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"`
- MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"`
- Hash common.Hash `json:"hash"`
- Input hexutil.Bytes `json:"input"`
- Nonce hexutil.Uint64 `json:"nonce"`
- To *common.Address `json:"to"`
- Value *hexutil.Big `json:"value"`
- Type hexutil.Uint64 `json:"type"`
- Accesses *types.AccessList `json:"accessList,omitempty"`
- ChainID *hexutil.Big `json:"chainId,omitempty"`
- MaxFeePerBlobGas *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
- BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
- Authorizations *[]types.JsonAuthorization `json:"authorizationList,omitempty"`
- V *hexutil.Big `json:"v"`
- YParity *hexutil.Big `json:"yParity,omitempty"`
- R *hexutil.Big `json:"r"`
- S *hexutil.Big `json:"s"`
-}
-
func (r SignTransactionResult) MarshalJSON() ([]byte, error) {
if r.Tx == nil {
return nil, errors.New("nil transaction")
}
- zero := (*hexutil.Big)(new(big.Int))
- vv, rv, sv := r.Tx.V, r.Tx.R, r.Tx.S
- if vv == nil {
- vv = zero
+ 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
}
- if rv == nil {
- rv = zero
+ for _, k := range []string{"blockHash", "blockNumber", "blockTimestamp", "transactionIndex", "from"} {
+ delete(m, k)
}
- if sv == nil {
- sv = zero
+ zeroHex := json.RawMessage(`"0x0"`)
+ for _, k := range []string{"v", "r", "s"} {
+ if v, ok := m[k]; !ok || string(v) == "null" {
+ m[k] = zeroHex
+ }
}
- type plain struct {
- Raw hexutil.Bytes `json:"raw"`
- Tx signedTxJSON `json:"tx"`
- }
- return json.Marshal(plain{
- Raw: r.Raw,
- Tx: signedTxJSON{
- Gas: r.Tx.Gas,
- GasPrice: r.Tx.GasPrice,
- MaxPriorityFeePerGas: r.Tx.MaxPriorityFeePerGas,
- MaxFeePerGas: r.Tx.MaxFeePerGas,
- Hash: r.Tx.Hash,
- Input: r.Tx.Input,
- Nonce: r.Tx.Nonce,
- To: r.Tx.To,
- Value: r.Tx.Value,
- Type: r.Tx.Type,
- Accesses: r.Tx.Accesses,
- ChainID: r.Tx.ChainID,
- MaxFeePerBlobGas: r.Tx.MaxFeePerBlobGas,
- BlobVersionedHashes: r.Tx.BlobVersionedHashes,
- Authorizations: r.Tx.Authorizations,
- V: vv,
- YParity: r.Tx.YParity,
- R: rv,
- S: sv,
- },
- })
+ 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
diff --git a/rpc/ethapi/api_test.go b/rpc/ethapi/api_test.go
index f52cbed2f36..d32c1a468f6 100644
--- a/rpc/ethapi/api_test.go
+++ b/rpc/ethapi/api_test.go
@@ -121,10 +121,9 @@ func TestSignTransactionResultMarshalJSON_EIP1559(t *testing.T) {
fields := txFields(t, SignTransactionResult{Raw: buf.Bytes(), Tx: NewRPCTransaction(tx, common.Hash{}, 0, 0, 0, nil)})
- // EIP-1559: gasPrice must be explicit null.
- gasPrice, ok := fields["gasPrice"]
- require.True(t, ok, "gasPrice must be present")
- require.Equal(t, "null", string(gasPrice))
+ // EIP-1559 without known baseFee: gasPrice is absent (computeGasPrice returns nil).
+ _, ok := fields["gasPrice"]
+ require.False(t, ok, "gasPrice must be absent when baseFee is unknown")
// maxFeePerGas and maxPriorityFeePerGas must be set.
require.NotEqual(t, "null", string(fields["maxFeePerGas"]))
@@ -135,7 +134,7 @@ func TestSignTransactionResultMarshalJSON_EIP1559(t *testing.T) {
require.Equal(t, `"0x0"`, string(fields["r"]))
require.Equal(t, `"0x0"`, string(fields["s"]))
- // block-placement and sender fields must be stripped.
+ // 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)
@@ -162,13 +161,11 @@ func TestSignTransactionResultMarshalJSON_Legacy(t *testing.T) {
require.True(t, ok, "gasPrice must be present")
require.NotEqual(t, "null", string(gasPrice))
- // maxFeePerGas and maxPriorityFeePerGas must be explicit null.
- maxFee, ok := fields["maxFeePerGas"]
- require.True(t, ok, "maxFeePerGas must be present")
- require.Equal(t, "null", string(maxFee))
- maxPrio, ok := fields["maxPriorityFeePerGas"]
- require.True(t, ok, "maxPriorityFeePerGas must be present")
- require.Equal(t, "null", string(maxPrio))
+ // Legacy: maxFeePerGas and maxPriorityFeePerGas are inapplicable and must be absent.
+ _, ok = fields["maxFeePerGas"]
+ require.False(t, ok, "maxFeePerGas must be absent for legacy tx")
+ _, ok = fields["maxPriorityFeePerGas"]
+ require.False(t, ok, "maxPriorityFeePerGas must be absent for legacy tx")
// unsigned tx: v/r/s must be "0x0", not null.
require.Equal(t, `"0x0"`, string(fields["v"]))
diff --git a/rpc/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index ab9a02e3cf5..ff815022e9d 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Erigon Authors
+// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
@@ -72,9 +72,7 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
if err != nil {
return nil, err
}
- if count != nil {
- nonce = uint64(*count)
- }
+ nonce = uint64(*count)
}
args.Nonce = (*hexutil.Uint64)(&nonce)
}
@@ -114,7 +112,8 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
}
if args.MaxFeePerBlobGas == nil {
nextBlockTime := head.Time + cc.SecondsPerSlot()
- blobFee, err := misc.GetBlobGasPrice(cc, *head.ExcessBlobGas, nextBlockTime)
+ nextExcessBlobGas := misc.CalcExcessBlobGas(cc, head, nextBlockTime)
+ blobFee, err := misc.GetBlobGasPrice(cc, nextExcessBlobGas, nextBlockTime)
if err != nil {
return nil, err
}
@@ -130,7 +129,7 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
}
if args.Gas == nil {
- estimated, err := api.EstimateGas(ctx, &args, nil, nil, nil)
+ estimated, err := api.EstimateGas(ctx, &args, &latestNumOrHash, nil, nil)
if err != nil {
return nil, err
}
@@ -154,7 +153,7 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
}
func (api *APIImpl) newGasOracle(dbTx kv.TemporalTx) *gasprice.Oracle {
- return gasprice.NewOracle(NewGasPriceOracleBackend(api.db, dbTx, api.BaseAPI), ethconfig.Defaults.GPO, api.gasCache, nil, api.logger.New("app", "gasPriceOracle"))
+ 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 {
diff --git a/rpc/jsonrpc/eth_fill_transaction_test.go b/rpc/jsonrpc/eth_fill_transaction_test.go
index 17dc27f00ae..2e8ffadbc68 100644
--- a/rpc/jsonrpc/eth_fill_transaction_test.go
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Erigon Authors
+// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
@@ -31,6 +31,7 @@ import (
"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"
@@ -258,8 +259,7 @@ func TestFillTransactionGasPriceWithAccessListIsTypeOne(t *testing.T) {
AccessList: &al,
})
require.NoError(t, err)
- require.Equal(t, hexutil.Uint64(types.AccessListTxType), result.Tx.Type,
- "gasPrice + explicit accessList must produce type-1 tx (Erigon diverges from Geth here)")
+ require.Equal(t, hexutil.Uint64(types.AccessListTxType), result.Tx.Type)
}
func TestFillTransactionUserGasAboveCapPreserved(t *testing.T) {
@@ -357,3 +357,68 @@ func TestFillTransactionGasPricePostLondon(t *testing.T) {
require.NoError(t, err)
require.Equal(t, hexutil.Uint64(types.LegacyTxType), result.Tx.Type, "explicit gasPrice must produce a legacy tx")
}
+
+func TestFillTransactionBlobFeeUsesNextBlockExcess(t *testing.T) {
+ // Genesis has a large ExcessBlobGas but zero BlobGasUsed.
+ // CalcExcessBlobGas returns ExcessBlobGas - targetBlobGas, not ExcessBlobGas itself.
+ // FillTransaction must use the former (next block's excess), not the latter.
+ 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()
+ nextTime := head.Time + cancunCfg.SecondsPerSlot()
+ nextExcess := misc.CalcExcessBlobGas(cancunCfg, head, nextTime)
+ correctFee, err := misc.GetBlobGasPrice(cancunCfg, nextExcess, nextTime)
+ require.NoError(t, err)
+ expectedMaxFeePerBlobGas := new(big.Int).Lsh(correctFee.ToBig(), 1)
+ require.Equal(t, expectedMaxFeePerBlobGas, result.Tx.MaxFeePerBlobGas.ToInt())
+
+ // Sanity: the old approach (using head.ExcessBlobGas directly) gives a different, larger value.
+ oldFee, err := misc.GetBlobGasPrice(cancunCfg, headExcessBlobGas, nextTime)
+ require.NoError(t, err)
+ oldMaxFeePerBlobGas := new(big.Int).Lsh(oldFee.ToBig(), 1)
+ require.NotEqual(t, expectedMaxFeePerBlobGas, oldMaxFeePerBlobGas,
+ "test scenario must differentiate CalcExcessBlobGas from head.ExcessBlobGas")
+}
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 {
From 902178d0b09b9f89c7c4c584198bce8ec0f7f0f6 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Wed, 24 Jun 2026 17:18:01 +0200
Subject: [PATCH 12/14] rpc/ethapi: inject null for inapplicable fee fields in
SignTransactionResult
EIP-1559 txs lack gasPrice; legacy txs lack maxFeePerGas/maxPriorityFeePerGas.
RPCTransaction omits nil fields, but Geth emits null for them. Inject null
in SignTransactionResult.MarshalJSON to match Geth's eth_fillTransaction output.
---
rpc/ethapi/api.go | 6 ++++++
rpc/ethapi/api_test.go | 13 +++++--------
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/rpc/ethapi/api.go b/rpc/ethapi/api.go
index a287144c7ef..f293bad8824 100644
--- a/rpc/ethapi/api.go
+++ b/rpc/ethapi/api.go
@@ -522,6 +522,12 @@ func (r SignTransactionResult) MarshalJSON() ([]byte, error) {
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" {
diff --git a/rpc/ethapi/api_test.go b/rpc/ethapi/api_test.go
index d32c1a468f6..833f0ef44d5 100644
--- a/rpc/ethapi/api_test.go
+++ b/rpc/ethapi/api_test.go
@@ -121,9 +121,8 @@ func TestSignTransactionResultMarshalJSON_EIP1559(t *testing.T) {
fields := txFields(t, SignTransactionResult{Raw: buf.Bytes(), Tx: NewRPCTransaction(tx, common.Hash{}, 0, 0, 0, nil)})
- // EIP-1559 without known baseFee: gasPrice is absent (computeGasPrice returns nil).
- _, ok := fields["gasPrice"]
- require.False(t, ok, "gasPrice must be absent when baseFee is unknown")
+ // 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"]))
@@ -161,11 +160,9 @@ func TestSignTransactionResultMarshalJSON_Legacy(t *testing.T) {
require.True(t, ok, "gasPrice must be present")
require.NotEqual(t, "null", string(gasPrice))
- // Legacy: maxFeePerGas and maxPriorityFeePerGas are inapplicable and must be absent.
- _, ok = fields["maxFeePerGas"]
- require.False(t, ok, "maxFeePerGas must be absent for legacy tx")
- _, ok = fields["maxPriorityFeePerGas"]
- require.False(t, ok, "maxPriorityFeePerGas must be absent for legacy tx")
+ // 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"]))
From 081cf78814d85c1aa75c76c274c9e98571e5d134 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Fri, 26 Jun 2026 14:10:11 +0200
Subject: [PATCH 13/14] update tag rpc version
---
.github/workflows/scripts/rpc_version.env | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/scripts/rpc_version.env b/.github/workflows/scripts/rpc_version.env
index 7a1567570da..11116da10a6 100644
--- a/.github/workflows/scripts/rpc_version.env
+++ b/.github/workflows/scripts/rpc_version.env
@@ -1,3 +1,3 @@
-RPC_VERSION=lupin012/add_test_eth_fillTransaction
+RPC_VERSION=v2.18.0
From 760acdb6df790355233e25f066995113c3829758 Mon Sep 17 00:00:00 2001
From: lupin012 <58134934+lupin012@users.noreply.github.com.>
Date: Fri, 26 Jun 2026 18:52:16 +0200
Subject: [PATCH 14/14] =?UTF-8?q?rpc/jsonrpc:=20align=20blob=20fee=20defau?=
=?UTF-8?q?lt=20to=20Geth=20=E2=80=94=20use=20head=20ExcessBlobGas=20direc?=
=?UTF-8?q?tly;=20keep=202=C3=97=20fee=20headroom=20on=20maxFeePerBlobGas?=
=?UTF-8?q?=20as=20Geth=20does?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rpc/jsonrpc/eth_fill_transaction.go | 7 +++----
rpc/jsonrpc/eth_fill_transaction_test.go | 20 ++++----------------
2 files changed, 7 insertions(+), 20 deletions(-)
diff --git a/rpc/jsonrpc/eth_fill_transaction.go b/rpc/jsonrpc/eth_fill_transaction.go
index ff815022e9d..86f92520c9e 100644
--- a/rpc/jsonrpc/eth_fill_transaction.go
+++ b/rpc/jsonrpc/eth_fill_transaction.go
@@ -111,13 +111,12 @@ func (api *APIImpl) FillTransaction(ctx context.Context, args ethapi.CallArgs) (
return nil, errors.New("blob transactions not supported before Cancun")
}
if args.MaxFeePerBlobGas == nil {
- nextBlockTime := head.Time + cc.SecondsPerSlot()
- nextExcessBlobGas := misc.CalcExcessBlobGas(cc, head, nextBlockTime)
- blobFee, err := misc.GetBlobGasPrice(cc, nextExcessBlobGas, nextBlockTime)
+ blobFee, err := misc.GetBlobGasPrice(cc, *head.ExcessBlobGas, head.Time)
if err != nil {
return nil, err
}
- args.MaxFeePerBlobGas = (*hexutil.Big)(new(big.Int).Lsh(blobFee.ToBig(), 1))
+ b := blobFee.ToBig()
+ args.MaxFeePerBlobGas = (*hexutil.Big)(b.Lsh(b, 1))
}
}
diff --git a/rpc/jsonrpc/eth_fill_transaction_test.go b/rpc/jsonrpc/eth_fill_transaction_test.go
index 2e8ffadbc68..17777ba9c76 100644
--- a/rpc/jsonrpc/eth_fill_transaction_test.go
+++ b/rpc/jsonrpc/eth_fill_transaction_test.go
@@ -358,10 +358,7 @@ func TestFillTransactionGasPricePostLondon(t *testing.T) {
require.Equal(t, hexutil.Uint64(types.LegacyTxType), result.Tx.Type, "explicit gasPrice must produce a legacy tx")
}
-func TestFillTransactionBlobFeeUsesNextBlockExcess(t *testing.T) {
- // Genesis has a large ExcessBlobGas but zero BlobGasUsed.
- // CalcExcessBlobGas returns ExcessBlobGas - targetBlobGas, not ExcessBlobGas itself.
- // FillTransaction must use the former (next block's excess), not the latter.
+func TestFillTransactionBlobFeeUsesHeadExcess(t *testing.T) {
const headExcessBlobGas = uint64(10_000_000)
cancunCfg := &chain.Config{
@@ -408,17 +405,8 @@ func TestFillTransactionBlobFeeUsesNextBlockExcess(t *testing.T) {
require.NotNil(t, result.Tx.MaxFeePerBlobGas)
head := m.Genesis.HeaderNoCopy()
- nextTime := head.Time + cancunCfg.SecondsPerSlot()
- nextExcess := misc.CalcExcessBlobGas(cancunCfg, head, nextTime)
- correctFee, err := misc.GetBlobGasPrice(cancunCfg, nextExcess, nextTime)
+ expectedFee, err := misc.GetBlobGasPrice(cancunCfg, *head.ExcessBlobGas, head.Time)
require.NoError(t, err)
- expectedMaxFeePerBlobGas := new(big.Int).Lsh(correctFee.ToBig(), 1)
- require.Equal(t, expectedMaxFeePerBlobGas, result.Tx.MaxFeePerBlobGas.ToInt())
-
- // Sanity: the old approach (using head.ExcessBlobGas directly) gives a different, larger value.
- oldFee, err := misc.GetBlobGasPrice(cancunCfg, headExcessBlobGas, nextTime)
- require.NoError(t, err)
- oldMaxFeePerBlobGas := new(big.Int).Lsh(oldFee.ToBig(), 1)
- require.NotEqual(t, expectedMaxFeePerBlobGas, oldMaxFeePerBlobGas,
- "test scenario must differentiate CalcExcessBlobGas from head.ExcessBlobGas")
+ b := expectedFee.ToBig()
+ require.Equal(t, b.Lsh(b, 1), result.Tx.MaxFeePerBlobGas.ToInt())
}