diff --git a/eth/api_pre_exec.go b/eth/api_pre_exec.go index 1c841baed7..7acc332761 100644 --- a/eth/api_pre_exec.go +++ b/eth/api_pre_exec.go @@ -2,6 +2,7 @@ package eth import ( "context" + "errors" "fmt" "hash" "math" @@ -18,6 +19,8 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" // "github.com/DeBankDeFi/eth/txtrace" @@ -246,6 +249,7 @@ func (api *PreExecAPI) TraceMany(ctx context.Context, origins []PreArgs) ([]PreR return nil, err } for i := 0; i < len(origins); i++ { + stateOld := state.Copy() origin := origins[i] if origin.Nonce == nil { preResList = append(preResList, PreResult{ @@ -343,7 +347,150 @@ func (api *PreExecAPI) TraceMany(ctx context.Context, origins []PreArgs) ([]PreR Msg: (preRes.Trace)[0].Error, } } + if preRes.Error.Code == 0 { + gasLimit := header.GasLimit + gasCap := api.e.APIBackend.RPCGasCap() + if gasLimit > gasCap { + gasLimit = gasCap + } + usedGas := api.doEstimateGas(ctx, txArgs, header, stateOld, gasLimit) + log.Debug("doEstimateGas", "usedGas", usedGas) + if usedGas > 0 { + preRes.GasUsed = usedGas + } + } preResList = append(preResList, preRes) } return preResList, nil } + +func (api *PreExecAPI) doEstimateGas(ctx context.Context, args ethapi.TransactionArgs, header *types.Header, state *state.StateDB, gasLimit uint64) uint64 { + // Binary search the gas requirement, as it may be higher than the amount used + var ( + lo uint64 = params.TxGas - 1 + hi uint64 + ) + // Use zero address if sender unspecified. + if args.From == nil { + args.From = new(common.Address) + } + // Determine the highest gas limit can be used during the estimation. + if args.Gas != nil && uint64(*args.Gas) >= params.TxGas { + hi = uint64(*args.Gas) + } else { + hi = gasLimit + } + // Normalize the max fee per gas the call is willing to spend. + var feeCap *big.Int + if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { + return 0 + } else if args.GasPrice != nil { + feeCap = args.GasPrice.ToInt() + } else if args.MaxFeePerGas != nil { + feeCap = args.MaxFeePerGas.ToInt() + } else { + feeCap = common.Big0 + } + // Recap the highest gas limit with account's available balance. + if feeCap.BitLen() != 0 { + balance := state.GetBalance(*args.From) // from can't be nil + available := new(big.Int).Set(balance) + if args.Value != nil { + if args.Value.ToInt().Cmp(available) >= 0 { + return 0 + } + available.Sub(available, args.Value.ToInt()) + } + allowance := new(big.Int).Div(available, feeCap) + + // If the allowance is larger than maximum uint64, skip checking + if allowance.IsUint64() && hi > allowance.Uint64() { + transfer := args.Value + if transfer == nil { + transfer = new(hexutil.Big) + } + log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance, + "sent", transfer.ToInt(), "maxFeePerGas", feeCap, "fundable", allowance) + hi = allowance.Uint64() + } + } + // Recap the highest gas allowance with specified gascap. + if gasLimit != 0 && hi > gasLimit { + hi = gasLimit + } + cap := hi + + // 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 := api.doCallCopy(ctx, args, header, state) + log.Debug("doCallCopy", "result", result, "err", err) + if err != nil { + if errors.Is(err, core.ErrIntrinsicGas) { + return true, nil, nil // Special case, raise gas limit + } + return true, nil, err // Bail out + } + return result.Failed(), result, nil + } + + // Execute the binary search and hone in on an executable gas limit + for lo+1 < hi { + mid := (hi + 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 { + return 0 + } + if failed { + lo = mid + } else { + 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 + } + if failed { + if result != nil && result.Err != vm.ErrOutOfGas { + if len(result.Revert()) > 0 { + return 0 + } + return 0 + } + // Otherwise, the specified gas cap is too low + return 0 + } + } + return hi +} + +func (api *PreExecAPI) doCallCopy(ctx context.Context, args ethapi.TransactionArgs, header *types.Header, state *state.StateDB) (*core.ExecutionResult, error) { + stateNew := state.Copy() + // Get a new instance of the EVM. + msg, err := args.ToMessage(api.e.APIBackend.RPCGasCap(), header.BaseFee) + if err != nil { + return nil, err + } + evm, vmError, err := api.e.APIBackend.GetEVM(ctx, msg, stateNew, header, &vm.Config{NoBaseFee: true}) + if err != nil { + return nil, err + } + // Execute the message. + gp := new(core.GasPool).AddGas(math.MaxUint64) + result, err := core.ApplyMessage(evm, msg, gp) + if err := vmError(); err != nil { + return nil, err + } + if err != nil { + return result, fmt.Errorf("err: %w (supplied gas %d)", err, msg.Gas()) + } + return result, nil +}