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()) }