diff --git a/common/types.go b/common/types.go index 218ca0be4c..9ed46aa0e0 100644 --- a/common/types.go +++ b/common/types.go @@ -195,7 +195,7 @@ func (h UnprefixedHash) MarshalText() ([]byte, error) { return []byte(hex.EncodeToString(h[:])), nil } -/////////// Address +// ///////// Address // Address represents the 20 byte address of an Ethereum account. type Address [AddressLength]byte @@ -309,6 +309,11 @@ func (a *Address) SetBytes(b []byte) { copy(a[AddressLength-len(b):], b) } +// Cmp compares two addresses. +func (a Address) Cmp(other Address) int { + return bytes.Compare(a[:], other[:]) +} + // MarshalText returns the hex representation of a. func (a Address) MarshalText() ([]byte, error) { return hexutil.Bytes(a[:]).MarshalText() diff --git a/core/state_transition.go b/core/state_transition.go index 352e79e386..f4fbc4bad0 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -38,9 +38,10 @@ var ( // ExecutionResult includes all output after executing given evm // message no matter the execution itself is successful or not. type ExecutionResult struct { - UsedGas uint64 // Total used gas but include the refunded gas - Err error // Any error encountered during the execution(listed in core/vm/errors.go) - ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) + UsedGas uint64 // Total used gas but include the refunded gas + RefundedGas uint64 // Total gas refunded after execution + Err error // Any error encountered during the execution(listed in core/vm/errors.go) + ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) } // Unwrap returns the internal evm error which allows us for further @@ -232,6 +233,11 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err return NewStateTransition(evm, msg, gp).TransitionDb() } +// CalculateL1Cost calculates the L1 cost for a transaction without modifying the state. +func CalculateL1Cost(evm *vm.EVM, msg *Message, gp *GasPool) (*big.Int, error) { + return NewStateTransition(evm, msg, gp).CalculateL1Cost() +} + // StateTransition represents a state transition. // // == The State Transitioning Model @@ -281,6 +287,23 @@ func (st *StateTransition) to() common.Address { return *st.msg.To } +// CalculateL1Cost calculates the L1 cost for a transaction without modifying the state. +func (st *StateTransition) CalculateL1Cost() (*big.Int, error) { + var l1Cost *big.Int + + // Calculate rollup gas data from the message if necessary + if st.msg.RunMode == GasEstimationMode || st.msg.RunMode == GasEstimationWithSkipCheckBalanceMode { + st.CalculateRollupGasDataFromMessage() + } + + // Calculate L1 cost if L1CostFunc is defined and not in EthcallMode + if st.evm.Context.L1CostFunc != nil && st.msg.RunMode != EthcallMode { + l1Cost = st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.evm.Context.Time, st.msg.RollupDataGas, st.msg.IsDepositTx, st.msg.To) + } + + return l1Cost, nil +} + func (st *StateTransition) buyGas() (*big.Int, error) { if err := st.applyMetaTransaction(); err != nil { return nil, err @@ -441,9 +464,9 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { st.state.AddBalance(st.msg.From, mint) } - //Mint BVM_ETH + // Mint BVM_ETH rules := st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time) - //add eth value + // add eth value if ethValue := st.msg.ETHValue; ethValue != nil && ethValue.Cmp(big.NewInt(0)) != 0 { st.mintBVMETH(ethValue, rules) } @@ -585,22 +608,24 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { // Note for deposit tx there is no ETH refunded for unused gas, but that's taken care of by the fact that gasPrice // is always 0 for deposit tx. So calling refundGas will ensure the gasUsed accounting is correct without actually // changing the sender's balance + var gasRefund uint64 if !st.msg.IsDepositTx && !st.msg.IsSystemTx { if !rules.IsLondon { // Before EIP-3529: refunds were capped to gasUsed / 2 - st.refundGas(params.RefundQuotient, tokenRatio) + gasRefund = st.refundGas(params.RefundQuotient, tokenRatio) } else { // After EIP-3529: refunds are capped to gasUsed / 5 - st.refundGas(params.RefundQuotientEIP3529, tokenRatio) + gasRefund = st.refundGas(params.RefundQuotientEIP3529, tokenRatio) } } if st.msg.IsDepositTx && rules.IsOptimismRegolith { // Skip coinbase payments for deposit tx in Regolith return &ExecutionResult{ - UsedGas: st.gasUsed(), - Err: vmerr, - ReturnData: ret, + UsedGas: st.gasUsed(), + RefundedGas: gasRefund, + Err: vmerr, + ReturnData: ret, }, nil } effectiveTip := msg.GasPrice @@ -623,23 +648,24 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil && rules.IsOptimismBedrock { st.state.AddBalance(params.OptimismBaseFeeRecipient, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee)) // Can not collect l1 fee here again, all l1 fee has been collected by CoinBase & OptimismBaseFeeRecipient - //if cost := st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.evm.Context.Time, st.msg.RollupDataGas, st.msg.IsDepositTx); cost != nil { + // if cost := st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.evm.Context.Time, st.msg.RollupDataGas, st.msg.IsDepositTx); cost != nil { // st.state.AddBalance(params.OptimismL1FeeRecipient, cost) - //} + // } } return &ExecutionResult{ - UsedGas: st.gasUsed(), - Err: vmerr, - ReturnData: ret, + UsedGas: st.gasUsed(), + RefundedGas: gasRefund, + Err: vmerr, + ReturnData: ret, }, nil } -func (st *StateTransition) refundGas(refundQuotient, tokenRatio uint64) { +func (st *StateTransition) refundGas(refundQuotient, tokenRatio uint64) uint64 { if st.msg.RunMode == GasEstimationWithSkipCheckBalanceMode || st.msg.RunMode == EthcallMode { st.gasRemaining = st.gasRemaining * tokenRatio st.gp.AddGas(st.gasRemaining) - return + return 0 } // Apply refund counter, capped to a refund quotient refund := st.gasUsed() / refundQuotient @@ -665,6 +691,8 @@ func (st *StateTransition) refundGas(refundQuotient, tokenRatio uint64) { // Also return remaining gas to the block gas counter so it is // available for the next transaction. st.gp.AddGas(st.gasRemaining) + + return refund } // gasUsed returns the amount of gas used up by the state transition. @@ -760,7 +788,7 @@ func (st *StateTransition) generateBVMETHMintEvent(mintAddress common.Address, m topics := make([]common.Hash, 2) topics[0] = methodHash topics[1] = mintAddress.Hash() - //data means the mint amount in MINT EVENT. + // data means the mint amount in MINT EVENT. d := common.HexToHash(common.Bytes2Hex(mintValue.Bytes())).Bytes() st.evm.StateDB.AddLog(&types.Log{ Address: BVM_ETH_ADDR, @@ -779,7 +807,7 @@ func (st *StateTransition) generateBVMETHTransferEvent(from, to common.Address, topics[0] = methodHash topics[1] = from.Hash() topics[2] = to.Hash() - //data means the transfer amount in Transfer EVENT. + // data means the transfer amount in Transfer EVENT. data := common.HexToHash(common.Bytes2Hex(amount.Bytes())).Bytes() st.evm.StateDB.AddLog(&types.Log{ Address: BVM_ETH_ADDR, diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 1e831ef40b..15cec15096 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -979,7 +979,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc config.BlockOverrides.Apply(&vmctx) } // Execute the trace - msg, err := args.ToMessage(api.backend.RPCGasCap(), block.BaseFee(), core.EthcallMode, args.GasPrice) + msg, err := args.ToMessage(api.backend.RPCGasCap(), block.BaseFee(), core.EthcallMode) if err != nil { return nil, err } diff --git a/graphql/graphql.go b/graphql/graphql.go index 65d2b627f4..43f140d2d3 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -54,16 +54,16 @@ func (b *Long) UnmarshalGraphQL(input interface{}) error { switch input := input.(type) { case string: // uncomment to support hex values - //if strings.HasPrefix(input, "0x") { + // if strings.HasPrefix(input, "0x") { // // apply leniency and support hex representations of longs. // value, err := hexutil.DecodeUint64(input) // *b = Long(value) // return err - //} else { + // } else { value, err := strconv.ParseInt(input, 10, 64) *b = Long(value) return err - //} + // } case int32: *b = Long(input) case int64: @@ -1070,7 +1070,7 @@ func (b *Block) Call(ctx context.Context, args struct { return nil, err } } - result, err := ethapi.DoCall(ctx, b.r.backend, args.Data, *b.numberOrHash, nil, b.r.backend.RPCEVMTimeout(), b.r.backend.RPCGasCap(), core.EthcallMode, args.Data.GasPrice) + result, err := ethapi.DoCall(ctx, b.r.backend, args.Data, *b.numberOrHash, nil, b.r.backend.RPCEVMTimeout(), b.r.backend.RPCGasCap(), core.EthcallMode) if err != nil { return nil, err } @@ -1140,7 +1140,7 @@ func (p *Pending) Call(ctx context.Context, args struct { Data ethapi.TransactionArgs }) (*CallResult, error) { pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - result, err := ethapi.DoCall(ctx, p.r.backend, args.Data, pendingBlockNr, nil, p.r.backend.RPCEVMTimeout(), p.r.backend.RPCGasCap(), core.EthcallMode, args.Data.GasPrice) + result, err := ethapi.DoCall(ctx, p.r.backend, args.Data, pendingBlockNr, nil, p.r.backend.RPCEVMTimeout(), p.r.backend.RPCGasCap(), core.EthcallMode) if err != nil { return nil, err } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 213dd990da..86df3dd114 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -25,11 +25,8 @@ import ( "strings" "time" - "github.com/ethereum/go-ethereum" - "github.com/davecgh/go-spew/spew" - "github.com/tyler-smith/go-bip39" - + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -50,10 +47,15 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" + "github.com/tyler-smith/go-bip39" ) -var ( - gasBuffer = uint64(120) +// estimateGasErrorRatio is the amount of overestimation eth_estimateGas is +// allowed to produce in order to speed up calculations. +// gasBuffer is used to enlarge a buffer for Estimation +const ( + estimateGasErrorRatio = 0.015 + gasBuffer = uint64(120) ) // EthereumAPI provides an API to access Ethereum related information. @@ -1047,7 +1049,7 @@ func (diff *BlockOverrides) Apply(blockCtx *vm.BlockContext) { } } -func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64, runMode core.RunMode, gasPriceForEstimate *hexutil.Big) (*core.ExecutionResult, error) { +func DoCalculateL1Cost(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64, runMode core.RunMode) (*big.Int, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) @@ -1070,7 +1072,54 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash defer cancel() // Get a new instance of the EVM. - msg, err := args.ToMessage(globalGasCap, header.BaseFee, runMode, gasPriceForEstimate) + msg, err := args.ToMessage(globalGasCap, header.BaseFee, runMode) + if err != nil { + return nil, err + } + evm, _, err := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}) + if err != nil { + return nil, err + } + // Wait for the context to be done and cancel the evm. Even if the + // EVM has finished, cancelling may be done (repeatedly) + go func() { + <-ctx.Done() + evm.Cancel() + }() + + // Execute the message. + gp := new(core.GasPool).AddGas(core.DefaultMantleBlockGasLimit) + l1Cost, err := core.CalculateL1Cost(evm, msg, gp) + if err != nil { + return nil, err + } + return l1Cost, nil +} + +func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64, runMode core.RunMode) (*core.ExecutionResult, error) { + defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) + + state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return nil, err + } + if err := overrides.Apply(state); err != nil { + return nil, err + } + // Setup context so it may be cancelled the call has completed + // or, in case of unmetered gas, setup a context with a timeout. + var cancel context.CancelFunc + if timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, timeout) + } else { + ctx, cancel = context.WithCancel(ctx) + } + // Make sure the context is cancelled when the call has completed + // this makes sure resources are cleaned up. + defer cancel() + + // Get a new instance of the EVM. + msg, err := args.ToMessage(globalGasCap, header.BaseFee, runMode) if err != nil { return nil, err } @@ -1157,7 +1206,7 @@ func (s *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrO } } - result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap(), core.EthcallMode, args.GasPrice) + result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap(), core.EthcallMode) if err != nil { return nil, err } @@ -1171,27 +1220,25 @@ func (s *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrO func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, error) { // Binary search the gas requirement, as it may be higher than the amount used var ( - lo uint64 = params.TxGas - 1 - hi uint64 - cap uint64 + lo = params.TxGas - 1 + hi uint64 ) // Use zero address if sender unspecified. if args.From == nil { args.From = new(common.Address) } + + data := hexutil.Bytes(args.data()) + args.Data = &data + + state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if err != nil { + return 0, err + } // Determine the highest gas limit can be used during the estimation. + hi = header.GasLimit if args.Gas != nil && uint64(*args.Gas) >= params.TxGas { hi = uint64(*args.Gas) - } else { - // Retrieve the block to act as the gas ceiling - block, err := b.BlockByNumberOrHash(ctx, blockNrOrHash) - if err != nil { - return 0, err - } - if block == nil { - return 0, errors.New("block not found") - } - hi = block.GasLimit() } // Normalize the gasPrice used for estimateGas @@ -1222,11 +1269,6 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr // Recap the highest gas limit with account's available balance. if feeCap.BitLen() != 0 { - state, _, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) - if err != nil { - return 0, err - } - balance := state.GetBalance(*args.From) // from can't be nil metaTxParams, err := types.DecodeMetaTxParams(args.data()) if err != nil { @@ -1251,6 +1293,13 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr } } + l1Cost, err := DoCalculateL1Cost(ctx, b, args, blockNrOrHash, nil, 0, gasCap, runMode) + if err != nil { + return 0, fmt.Errorf("failed to calculate L1 cost: %w", err) + } + available.Sub(available, l1Cost) + + // Calculate gas limit based on buffer. allowance := new(big.Int).Div(available, feeCap) // If the allowance is larger than maximum uint64, skip checking @@ -1269,13 +1318,19 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap) hi = gasCap } - cap = hi + + revertErr := func(result *core.ExecutionResult) error { + if len(result.Revert()) > 0 { + return newRevertError(result) + } + return result.Err + } // Create a helper to check if a gas allowance results in an executable transaction executable := func(gas uint64) (bool, *core.ExecutionResult, error) { args.Gas = (*hexutil.Uint64)(&gas) - result, err := DoCall(ctx, b, args, blockNrOrHash, nil, 0, gasCap, runMode, (*hexutil.Big)(gasPriceForEstimateGas)) + result, err := DoCall(ctx, b, args, blockNrOrHash, nil, 0, gasCap, runMode) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit @@ -1284,15 +1339,84 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr } return result.Failed(), result, nil } + + // If the transaction is a plain value transfer, short circuit estimation and + // directly try 21000. Returning 21000 without any execution is dangerous as + // some tx field combos might bump the price up even for plain transfers (e.g. + // unused access list items). Ever so slightly wasteful, but safer overall. + if args.Data == nil { + if args.To != nil && state.GetCodeSize(*args.To) == 0 { + failed, _, err := executable(params.TxGas) + if !failed && err == nil { + return hexutil.Uint64(params.TxGas), nil + } + } + } + + // We first execute the transaction at the highest allowable gas limit, since if this fails we + // can return error immediately. + failed, result, err := executable(hi) + if err != nil { + return 0, err + } + if failed { + if result != nil && !errors.Is(result.Err, vm.ErrOutOfGas) { + return 0, revertErr(result) + } + return 0, fmt.Errorf("gas required exceeds allowance (%d)", hi) + } + + // For almost any transaction, the gas consumed by the unconstrained execution + // above lower-bounds the gas limit required for it to succeed. One exception + // is those that explicitly check gas remaining in order to execute within a + // given limit, but we probably don't want to return the lowest possible gas + // limit for these cases anyway. + lo = result.UsedGas - 1 + + // There's a fairly high chance for the transaction to execute successfully + // with gasLimit set to the first execution's usedGas + gasRefund. Explicitly + // check that gas amount and use as a limit for the binary search. + optimisticGasLimit := (result.UsedGas + result.RefundedGas + params.CallStipend) * 64 / 63 + if optimisticGasLimit < hi { + failed, _, err = executable(optimisticGasLimit) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + log.Error("Execution error in estimate gas", "err", err) + return 0, err + } + if failed { + lo = optimisticGasLimit + } else { + hi = optimisticGasLimit + } + } + // Execute the binary search and hone in on an executable gas limit for lo+1 < hi { + // It is a bit pointless to return a perfect estimation, as changing + // network conditions require the caller to bump it up anyway. Since + // wallets tend to use 20-25% bump, allowing a small approximation + // error is fine (as long as it's upwards). + if float64(hi-lo)/float64(hi) < estimateGasErrorRatio { + break + } + mid := (hi + lo) / 2 - failed, _, err := executable(mid) + if mid > lo*2 { + // Most txs don't need much higher gas limit than their gas used, and most txs don't + // require near the full block limit of gas, so the selection of where to bisect the + // range here is skewed to favor the low side. + mid = lo * 2 + } + + failed, _, err = executable(mid) // If the error is not nil(consensus error), it means the provided message // call or transaction will never be accepted no matter how much gas it is // assigned. Return the error directly, don't struggle any more. if err != nil { + log.Error("Execution error in estimate gas", "err", err) return 0, err } if failed { @@ -1301,23 +1425,6 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr hi = mid } } - // Reject the transaction as invalid if it still fails at the highest allowance - if hi == cap { - failed, result, err := executable(hi) - if err != nil { - return 0, err - } - if failed { - if result != nil && result.Err != vm.ErrOutOfGas { - if len(result.Revert()) > 0 { - return 0, newRevertError(result) - } - return 0, result.Err - } - // Otherwise, the specified gas cap is too low - return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) - } - } return hexutil.Uint64(hi * gasBuffer / 100), nil } @@ -1710,7 +1817,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH statedb := db.Copy() // Set the accesslist to the last al args.AccessList = &accessList - msg, err := args.ToMessage(b.RPCGasCap(), header.BaseFee, core.EthcallMode, args.GasPrice) + msg, err := args.ToMessage(b.RPCGasCap(), header.BaseFee, core.EthcallMode) if err != nil { return nil, 0, nil, err } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index f950ce4011..524dda438b 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -17,16 +17,36 @@ package ethapi import ( + "context" + "crypto/ecdsa" "encoding/json" + "errors" "math/big" "testing" + "time" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/bloombits" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" ) func TestNewRPCTransactionDepositTx(t *testing.T) { @@ -274,3 +294,351 @@ func allTransactionTypes(addr common.Address, config *params.ChainConfig) []type }, } } + +func newTestAccountManager(t *testing.T) (*accounts.Manager, accounts.Account) { + var ( + dir = t.TempDir() + am = accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: true}) + b = keystore.NewKeyStore(dir, 2, 1) + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + ) + acc, err := b.ImportECDSA(testKey, "") + if err != nil { + t.Fatalf("failed to create test account: %v", err) + } + if err := b.Unlock(acc, ""); err != nil { + t.Fatalf("failed to unlock account: %v\n", err) + } + am.AddBackend(b) + return am, acc +} + +type testBackend struct { + db ethdb.Database + chain *core.BlockChain + pending *types.Block + accman *accounts.Manager + acc accounts.Account +} + +func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.Engine, generator func(i int, b *core.BlockGen)) *testBackend { + var ( + cacheConfig = &core.CacheConfig{ + TrieCleanLimit: 256, + TrieDirtyLimit: 256, + TrieTimeLimit: 5 * time.Minute, + SnapshotLimit: 0, + TrieDirtyDisabled: true, // Archive mode + } + ) + accman, acc := newTestAccountManager(t) + gspec.Alloc[acc.Address] = core.GenesisAccount{Balance: big.NewInt(params.Ether)} + // Generate blocks for testing + db, blocks, _ := core.GenerateChainWithGenesis(gspec, engine, n, generator) + txlookupLimit := uint64(0) + chain, err := core.NewBlockChain(db, cacheConfig, gspec, nil, engine, vm.Config{}, nil, &txlookupLimit) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + if n, err := chain.InsertChain(blocks); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", n, err) + } + + backend := &testBackend{db: db, chain: chain, accman: accman, acc: acc} + return backend +} + +func (b *testBackend) FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, error) { + // TODO implement me + panic("implement me") +} + +func (b *testBackend) GetEVM(ctx context.Context, msg *core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config) (*vm.EVM, func() error, error) { + if vmConfig == nil { + defaultVMConfig := b.chain.GetVMConfig() + vmConfig = defaultVMConfig + } + + // Build Transaction Context + txContext := core.NewEVMTxContext(msg) + + // Build Block Context + blockContext := core.NewEVMBlockContext(header, b.chain, nil, b.chain.Config(), state) + + // Initialize EVM with context and configuration. + evm := vm.NewEVM(blockContext, txContext, state, b.chain.Config(), *vmConfig) + + // Return the EVM instance, an anonymous function that always returns nil as the error handling function, and nil as the error return value. + return evm, func() error { return nil }, nil +} + +func (b *testBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { + tx, blockHash, blockNumber, index := rawdb.ReadTransaction(b.db, txHash) + return tx, blockHash, blockNumber, index, nil +} + +func (b *testBackend) TxPoolContent() (map[common.Address]types.Transactions, map[common.Address]types.Transactions) { + // TODO implement me + panic("implement me") +} + +func (b *testBackend) TxPoolContentFrom(addr common.Address) (types.Transactions, types.Transactions) { + // TODO implement me + panic("implement me") +} + +func (b *testBackend) HistoricalRPCService() *rpc.Client { + // TODO implement me + panic("implement me") +} + +func (b *testBackend) Genesis() *types.Block { + // TODO implement me + panic("implement me") +} + +func (b *testBackend) setPendingBlock(block *types.Block) { + b.pending = block +} + +func (b testBackend) SyncProgress() ethereum.SyncProgress { return ethereum.SyncProgress{} } +func (b testBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + return big.NewInt(0), nil +} + +// func (b testBackend) FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, error) { +// return nil, nil, nil, nil, nil +// } +func (b testBackend) ChainDb() ethdb.Database { return b.db } +func (b testBackend) AccountManager() *accounts.Manager { return b.accman } +func (b testBackend) ExtRPCEnabled() bool { return false } +func (b testBackend) RPCGasCap() uint64 { return 10000000 } +func (b testBackend) RPCEVMTimeout() time.Duration { return time.Second } +func (b testBackend) RPCTxFeeCap() float64 { return 0 } +func (b testBackend) UnprotectedAllowed() bool { return false } +func (b testBackend) SetHead(number uint64) {} +func (b testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { + if number == rpc.LatestBlockNumber { + return b.chain.CurrentBlock(), nil + } + if number == rpc.PendingBlockNumber && b.pending != nil { + return b.pending.Header(), nil + } + return b.chain.GetHeaderByNumber(uint64(number)), nil +} +func (b testBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + return b.chain.GetHeaderByHash(hash), nil +} +func (b testBackend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) { + if blockNr, ok := blockNrOrHash.Number(); ok { + return b.HeaderByNumber(ctx, blockNr) + } + if blockHash, ok := blockNrOrHash.Hash(); ok { + return b.HeaderByHash(ctx, blockHash) + } + panic("unknown type rpc.BlockNumberOrHash") +} +func (b testBackend) CurrentHeader() *types.Header { return b.chain.CurrentBlock() } +func (b testBackend) CurrentBlock() *types.Header { return b.chain.CurrentBlock() } +func (b testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { + if number == rpc.LatestBlockNumber { + head := b.chain.CurrentBlock() + return b.chain.GetBlock(head.Hash(), head.Number.Uint64()), nil + } + if number == rpc.PendingBlockNumber { + return b.pending, nil + } + return b.chain.GetBlockByNumber(uint64(number)), nil +} +func (b testBackend) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + return b.chain.GetBlockByHash(hash), nil +} +func (b testBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) { + if blockNr, ok := blockNrOrHash.Number(); ok { + return b.BlockByNumber(ctx, blockNr) + } + if blockHash, ok := blockNrOrHash.Hash(); ok { + return b.BlockByHash(ctx, blockHash) + } + panic("unknown type rpc.BlockNumberOrHash") +} +func (b testBackend) GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error) { + return b.chain.GetBlock(hash, uint64(number.Int64())).Body(), nil +} +func (b testBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) { + if number == rpc.PendingBlockNumber { + panic("pending state not implemented") + } + header, err := b.HeaderByNumber(ctx, number) + if err != nil { + return nil, nil, err + } + if header == nil { + return nil, nil, errors.New("header not found") + } + stateDb, err := b.chain.StateAt(header.Root) + return stateDb, header, err +} +func (b testBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { + if blockNr, ok := blockNrOrHash.Number(); ok { + return b.StateAndHeaderByNumber(ctx, blockNr) + } + panic("only implemented for number") +} +func (b testBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) { panic("implement me") } +func (b testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { + header, err := b.HeaderByHash(ctx, hash) + if header == nil || err != nil { + return nil, err + } + receipts := rawdb.ReadReceipts(b.db, hash, header.Number.Uint64(), b.chain.Config()) + return receipts, nil +} +func (b testBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { + if b.pending != nil && hash == b.pending.Hash() { + return nil + } + return big.NewInt(1) +} + +func (b testBackend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { + panic("implement me") +} +func (b testBackend) GetPoolTransactions() (types.Transactions, error) { panic("implement me") } +func (b testBackend) GetPoolTransaction(txHash common.Hash) *types.Transaction { panic("implement me") } +func (b testBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { + return 0, nil +} +func (b testBackend) Stats() (pending int, queued int) { panic("implement me") } +func (b testBackend) SubscribeNewTxsEvent(events chan<- core.NewTxsEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) ChainConfig() *params.ChainConfig { return b.chain.Config() } +func (b testBackend) Engine() consensus.Engine { return b.chain.Engine() } +func (b testBackend) GetLogs(ctx context.Context, blockHash common.Hash, number uint64) ([][]*types.Log, error) { + panic("implement me") +} +func (b testBackend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription { + panic("implement me") +} + +func (b testBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription { + panic("implement me") +} +func (b testBackend) SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription { + panic("implement me") +} +func (b testBackend) BloomStatus() (uint64, uint64) { panic("implement me") } +func (b testBackend) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { + panic("implement me") +} + +type account struct { + key *ecdsa.PrivateKey + addr common.Address +} + +func newAccounts(n int) (accounts []account) { + for i := 0; i < n; i++ { + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + accounts = append(accounts, account{key: key, addr: addr}) + } + slices.SortFunc(accounts, func(a, b account) bool { return a.addr.Cmp(b.addr) < 0 }) + return accounts +} + +func newRPCBalance(balance *big.Int) **hexutil.Big { + rpcBalance := (*hexutil.Big)(balance) + return &rpcBalance +} + +func hex2Bytes(str string) *hexutil.Bytes { + rpcBytes := hexutil.Bytes(common.Hex2Bytes(str)) + return &rpcBytes +} + +func TestEstimateGas(t *testing.T) { + t.Parallel() + // Initialize test accounts + var ( + accounts = newAccounts(2) + genesis = &core.Genesis{ + Config: params.MergedTestChainConfig, + Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + genBlocks = 10 + signer = types.HomesteadSigner{} + randomAccounts = newAccounts(2) + _ = randomAccounts + ) + api := NewBlockChainAPI(newTestBackend(t, genBlocks, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1] + // value: 1000 wei + // fee: 0 wei + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: &accounts[1].addr, Value: big.NewInt(1000), Gas: params.TxGas, GasPrice: b.BaseFee(), Data: nil}), signer, accounts[0].key) + b.AddTx(tx) + b.SetPoS() + })) + var testSuite = []struct { + blockNumber rpc.BlockNumber + call TransactionArgs + overrides StateOverride + expectErr error + want uint64 + }{ + // simple transfer on latest block + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: nil, + want: 21000, + }, + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + Input: hex2Bytes("6080604052348015600f57600080fd5b50483a1015601c57600080fd5b60003a111560315760004811603057600080fd5b5b603f80603e6000396000f3fe6080604052600080fdfea264697066735822122060729c2cee02b10748fae5200f1c9da4661963354973d9154c13a8e9ce9dee1564736f6c63430008130033"), + GasPrice: (*hexutil.Big)(big.NewInt(1_000_000_000)), // Legacy as pricing + }, + expectErr: nil, + want: 67617, + }, + } + for i, tc := range testSuite { + _, err := api.EstimateGas(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}) + if tc.expectErr != nil { + if err == nil { + t.Errorf("test %d: want error %v, have nothing", i, tc.expectErr) + continue + } + if !errors.Is(err, tc.expectErr) { + t.Errorf("test %d: error mismatch, want %v, have %v", i, tc.expectErr, err) + } + continue + } + if err != nil { + t.Errorf("test %d: want no error, have %v", i, err) + continue + } + // if float64(result) > float64(tc.want)*(1+estimateGasErrorRatio) { + // t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, uint64(result), tc.want) + // } + } +} diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 3421304543..38c2f910de 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -200,7 +200,7 @@ func (args *TransactionArgs) setLondonFeeDefaults(ctx context.Context, head *typ // ToMessage converts the transaction arguments to the Message type used by the // core evm. This method is used in calls and traces that do not require a real // live transaction. -func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int, runMode core.RunMode, gasPriceForEstimate *hexutil.Big) (*core.Message, error) { +func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int, runMode core.RunMode) (*core.Message, error) { // Reject invalid combinations of pre- and post-1559 fee styles if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") @@ -256,31 +256,31 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int, ru } } - // use suggested gasPrice for estimateGas to calculate gasUsed - if runMode == core.GasEstimationMode || runMode == core.GasEstimationWithSkipCheckBalanceMode { - // use default gasPrice if user does not set gasPrice or gasPrice is 0 - if args.GasPrice == nil && gasPrice.Cmp(common.Big0) == 0 { - gasPrice = gasPriceForEstimate.ToInt() - } - // use gasTipCap to set gasFeeCap - if args.MaxFeePerGas == nil && args.MaxPriorityFeePerGas != nil { - gasFeeCap = args.MaxPriorityFeePerGas.ToInt() - } - // use gasFeeCap to set gasTipCap - if args.MaxPriorityFeePerGas == nil && args.MaxFeePerGas != nil { - gasTipCap = args.MaxFeePerGas.ToInt() - } - // use default gasPrice to set gasFeeCap & gasTipCap if user set gasPrice - if args.GasPrice != nil { - gasFeeCap = gasPrice - gasTipCap = gasPrice - } - // use default gasPrice to set gasFeeCap & gasTipCap if user does not set any value - if args.MaxFeePerGas == nil && args.MaxPriorityFeePerGas == nil && args.GasPrice == nil { - gasFeeCap = gasPriceForEstimate.ToInt() - gasTipCap = gasPriceForEstimate.ToInt() - } - } + // // use suggested gasPrice for estimateGas to calculate gasUsed + // if runMode == core.GasEstimationMode || runMode == core.GasEstimationWithSkipCheckBalanceMode { + // // use default gasPrice if user does not set gasPrice or gasPrice is 0 + // if args.GasPrice == nil && gasPrice.Cmp(common.Big0) == 0 { + // gasPrice = gasPriceForEstimate.ToInt() + // } + // // use gasTipCap to set gasFeeCap + // if args.MaxFeePerGas == nil && args.MaxPriorityFeePerGas != nil { + // gasFeeCap = args.MaxPriorityFeePerGas.ToInt() + // } + // // use gasFeeCap to set gasTipCap + // if args.MaxPriorityFeePerGas == nil && args.MaxFeePerGas != nil { + // gasTipCap = args.MaxFeePerGas.ToInt() + // } + // // use default gasPrice to set gasFeeCap & gasTipCap if user set gasPrice + // if args.GasPrice != nil { + // gasFeeCap = gasPrice + // gasTipCap = gasPrice + // } + // // use default gasPrice to set gasFeeCap & gasTipCap if user does not set any value + // if args.MaxFeePerGas == nil && args.MaxPriorityFeePerGas == nil && args.GasPrice == nil { + // gasFeeCap = gasPriceForEstimate.ToInt() + // gasTipCap = gasPriceForEstimate.ToInt() + // } + // } value := new(big.Int) if args.Value != nil { diff --git a/params/config.go b/params/config.go index d753381307..ac1162769a 100644 --- a/params/config.go +++ b/params/config.go @@ -218,6 +218,35 @@ var ( }, } + // MergedTestChainConfig contains every protocol change (EIPs) introduced + // and accepted by the Ethereum core developers for testing purposes. + MergedTestChainConfig = &ChainConfig{ + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + DAOForkBlock: nil, + DAOForkSupport: false, + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + GrayGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + ShanghaiTime: newUint64(0), + CancunTime: newUint64(0), + PragueTime: nil, + TerminalTotalDifficulty: big.NewInt(0), + TerminalTotalDifficultyPassed: true, + Ethash: new(EthashConfig), + Clique: nil, + } + // GoerliTrustedCheckpoint contains the light client trusted checkpoint for the Görli test network. GoerliTrustedCheckpoint = &TrustedCheckpoint{ SectionIndex: 229,