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