mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-26 06:36:43 +00:00
371 lines
9.4 KiB
Go
371 lines
9.4 KiB
Go
package ethapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/common/math"
|
|
"github.com/ethereum/go-ethereum/core"
|
|
"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/metrics"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
)
|
|
|
|
type multiCallResp struct {
|
|
Results []*callResult `json:"results"`
|
|
Stats *multiCallStats `json:"stats"`
|
|
}
|
|
|
|
type callResult struct {
|
|
Code int `json:"code"`
|
|
Err string `json:"err"`
|
|
FromCache bool `json:"fromCache"`
|
|
Result hexutil.Bytes `json:"result"`
|
|
GasUsed int64 `json:"gasUsed"`
|
|
TimeCost float64 `json:"timeCost"`
|
|
}
|
|
|
|
type multiCallStats struct {
|
|
BlockNum int64 `json:"blockNum"`
|
|
BlockHash common.Hash `json:"blockHash"`
|
|
BlockTime int64 `json:"blockTime"`
|
|
Success bool `json:"success"`
|
|
CacheEnabled bool `json:"cacheEnabled"`
|
|
}
|
|
|
|
const (
|
|
singleCallTimeout = 5 * time.Second
|
|
multiCallLimit = 50
|
|
|
|
// client param error
|
|
errCodeTxArgs = -40000
|
|
errNativeMethodNotFound = -40001
|
|
errNativeMethodInput = -40002
|
|
errNativeMethodInputAddress = -40003
|
|
|
|
// evm processing error
|
|
errNativeMethodOutput = -40010
|
|
errNativeMethodStateError = -40011
|
|
errMessageExecuting = -40012
|
|
errEVMCancelled = -40013
|
|
errEVMReverted = -40014
|
|
errEVMFastFailed = -40015
|
|
|
|
// internal error
|
|
errUnderlyingDB = -40020
|
|
errLoadingState = -40021
|
|
)
|
|
|
|
var (
|
|
ethMultiCallCacheHit = metrics.GetOrRegisterMeter("rpc/ethmulticall/cache/hit", nil)
|
|
ethMultiCallCacheCount = metrics.GetOrRegisterMeter("rpc/ethmulticall/cache/count", nil)
|
|
)
|
|
|
|
const (
|
|
nativeAddr = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
|
)
|
|
|
|
var (
|
|
// copied from: accounts/abi/abi_test.go
|
|
Uint8, _ = abi.NewType("uint8", "", nil)
|
|
Uint256, _ = abi.NewType("uint256", "", nil)
|
|
String, _ = abi.NewType("string", "", nil)
|
|
Address, _ = abi.NewType("address", "", nil)
|
|
|
|
erc20ABI = abi.ABI{
|
|
Methods: map[string]abi.Method{
|
|
"name": funcName,
|
|
"symbol": funcSymbol,
|
|
"decimals": funcDecimals,
|
|
"totalSupply": funcTotalSupply,
|
|
"balanceOf": funcBalanceOf,
|
|
},
|
|
}
|
|
|
|
funcName = abi.NewMethod("name", "name", abi.Function, "", false, false,
|
|
[]abi.Argument{},
|
|
[]abi.Argument{
|
|
{Name: "", Type: String, Indexed: false},
|
|
},
|
|
)
|
|
funcSymbol = abi.NewMethod("symbol", "symbol", abi.Function, "", false, false,
|
|
[]abi.Argument{},
|
|
[]abi.Argument{
|
|
{Name: "", Type: String, Indexed: false},
|
|
},
|
|
)
|
|
funcDecimals = abi.NewMethod("decimals", "decimals", abi.Function, "", false, false,
|
|
[]abi.Argument{},
|
|
[]abi.Argument{
|
|
{Name: "", Type: Uint8, Indexed: false},
|
|
},
|
|
)
|
|
funcTotalSupply = abi.NewMethod("totalSupply", "totalSupply", abi.Function, "", false, false,
|
|
[]abi.Argument{},
|
|
[]abi.Argument{
|
|
{Name: "", Type: Uint256, Indexed: false},
|
|
},
|
|
)
|
|
funcBalanceOf = abi.NewMethod("balanceOf", "balanceOf", abi.Function, "", false, false,
|
|
[]abi.Argument{
|
|
{Name: "", Type: Address, Indexed: false},
|
|
},
|
|
[]abi.Argument{
|
|
{Name: "", Type: Uint256, Indexed: false},
|
|
},
|
|
)
|
|
)
|
|
|
|
func ethCallCacheKey(b Backend, blockHash common.Hash, to *common.Address, input []byte) string {
|
|
var sb strings.Builder
|
|
|
|
h := sha256.New()
|
|
h.Write(input)
|
|
bs := h.Sum(nil)
|
|
|
|
sb.Grow(len(bs) + len(to.Bytes()) + len(blockHash))
|
|
sb.Write(blockHash[:])
|
|
sb.Write(bytes.ToLower(to.Bytes()))
|
|
sb.Write(bs)
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func handleNative(ctx context.Context, state *state.StateDB, msg types.Message) ([]byte, int, error) {
|
|
data := msg.Data()
|
|
method, err := erc20ABI.MethodById(data)
|
|
if err != nil {
|
|
return nil, errNativeMethodNotFound, err
|
|
}
|
|
switch method.Name {
|
|
case "name", "symbol":
|
|
res, err := method.Outputs.Pack("ETH")
|
|
if err != nil {
|
|
return nil, errNativeMethodOutput, err
|
|
}
|
|
return res, 0, nil
|
|
case "decimals":
|
|
res, err := method.Outputs.Pack(uint8(18))
|
|
if err != nil {
|
|
return nil, errNativeMethodOutput, err
|
|
}
|
|
return res, 0, nil
|
|
case "totalSupply":
|
|
res, err := method.Outputs.Pack(big.NewInt(1_000_000_000_000_000_000)) // 1 ETH
|
|
if err != nil {
|
|
return nil, errNativeMethodOutput, err
|
|
}
|
|
return res, 0, nil
|
|
case "balanceOf":
|
|
inputs, err := method.Inputs.Unpack(data[4:])
|
|
if err != nil || len(inputs) == 0 {
|
|
return nil, errNativeMethodInput, err
|
|
}
|
|
address, ok := inputs[0].(common.Address)
|
|
if !ok {
|
|
return nil, errNativeMethodInputAddress, fmt.Errorf("input address error")
|
|
}
|
|
balance, err := method.Outputs.Pack(state.GetBalance(address))
|
|
if err != nil {
|
|
return nil, errNativeMethodOutput, err
|
|
}
|
|
if state.Error() != nil {
|
|
return nil, errNativeMethodStateError, state.Error()
|
|
}
|
|
return balance, 0, nil
|
|
default:
|
|
return nil, errNativeMethodNotFound, fmt.Errorf("method not found")
|
|
}
|
|
}
|
|
|
|
func doOneCall(ctx context.Context, b Backend, state *state.StateDB, header *types.Header, arg TransactionArgs, disableCache bool) (*callResult, error) {
|
|
var err error
|
|
var result = &callResult{}
|
|
|
|
start := time.Now()
|
|
|
|
if !disableCache {
|
|
// try load result from cache
|
|
cacheKey := ethCallCacheKey(b, header.Hash(), arg.To, arg.data())
|
|
ethMultiCallCacheCount.Mark(1)
|
|
if r, ok := b.GetCallCache(cacheKey); ok {
|
|
ethMultiCallCacheHit.Mark(1)
|
|
if res, ok := r.(*callResult); ok {
|
|
res.FromCache = true
|
|
return res, nil
|
|
}
|
|
}
|
|
defer func() {
|
|
// `err` here specifics to non-evm error. Evm internal error won't prevent
|
|
// caching the result
|
|
if err == nil {
|
|
b.SetCallCache(cacheKey, result, int64(len(result.Result)))
|
|
}
|
|
}()
|
|
}
|
|
// make sure this will be called prior to the SetCallCache defer func on returning
|
|
defer func() {
|
|
result.TimeCost = time.Since(start).Seconds()
|
|
}()
|
|
|
|
msg, err := arg.ToMessage(b.RPCGasCap(), header.BaseFee)
|
|
if err != nil {
|
|
result.Code = errCodeTxArgs
|
|
result.Err = err.Error()
|
|
return result, err
|
|
}
|
|
|
|
// skip EVM if requests for native token
|
|
if strings.ToLower(msg.To().Hex()) == nativeAddr {
|
|
res, code, err := handleNative(ctx, state, msg)
|
|
if err != nil {
|
|
result.Code = code
|
|
result.Err = err.Error()
|
|
}
|
|
result.Result = res
|
|
return result, err
|
|
}
|
|
|
|
// Get a new instance of the EVM.
|
|
evm, _, _ := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}) // never return error
|
|
|
|
// 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(math.MaxUint64)
|
|
evmRet, err := core.ApplyMessage(evm, msg, gp)
|
|
if err != nil {
|
|
result.Code = errMessageExecuting
|
|
result.Err = err.Error()
|
|
return result, err
|
|
}
|
|
|
|
// If the timer caused an abort, return an appropriate error message
|
|
if evm.Cancelled() {
|
|
result.Code = errEVMCancelled
|
|
result.Err = fmt.Sprintf("execution cancelled, timeout(%v)", singleCallTimeout)
|
|
// if execution time is close to timeout, not proper
|
|
if time.Since(start) < singleCallTimeout {
|
|
result.Code = errEVMFastFailed
|
|
result.Err = "fast failed"
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
if evmRet.Err != nil {
|
|
e := evmRet.Err
|
|
if len(evmRet.Revert()) > 0 {
|
|
e = newRevertError(evmRet)
|
|
}
|
|
result.Code = errEVMReverted
|
|
result.Err = e.Error()
|
|
return result, e
|
|
}
|
|
|
|
result.Result = evmRet.Return()
|
|
result.GasUsed = int64(evmRet.UsedGas)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *BlockChainAPI) MultiCall(ctx context.Context, args []TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, pfastFail, puseParallel, pdisableCache *bool, overrides *StateOverride) (resp *multiCallResp, err error) {
|
|
|
|
// maximum calls check
|
|
if len(args) > multiCallLimit {
|
|
return nil, fmt.Errorf("calls exceed limit, expected: <%v, actual: %v", multiCallLimit, len(args))
|
|
}
|
|
|
|
setb := func(p *bool, d bool) bool {
|
|
if p == nil {
|
|
return d
|
|
}
|
|
return *p
|
|
}
|
|
|
|
fastFail := setb(pfastFail, true)
|
|
useParallel := setb(puseParallel, true)
|
|
disableCache := setb(pdisableCache, false)
|
|
|
|
// check block & state
|
|
state, header, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
|
|
if state == nil || err != nil {
|
|
return nil, err
|
|
}
|
|
if err := overrides.Apply(state); err != nil {
|
|
return nil, err
|
|
}
|
|
blockTime := header.Time
|
|
|
|
ret := make([]*callResult, len(args))
|
|
stats := &multiCallStats{
|
|
BlockNum: header.Number.Int64(),
|
|
BlockHash: header.Hash(),
|
|
BlockTime: int64(blockTime),
|
|
Success: true,
|
|
CacheEnabled: !disableCache,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, singleCallTimeout)
|
|
defer cancel()
|
|
|
|
if useParallel {
|
|
// run in parallel
|
|
var wg sync.WaitGroup
|
|
for i, arg := range args {
|
|
wg.Add(1)
|
|
go func(i int, arg TransactionArgs) {
|
|
defer wg.Done()
|
|
|
|
// state is not reentrancy in concurrent scenarios, so use a copy
|
|
r, _ := doOneCall(ctx, s.b, state.Copy(), header, arg, disableCache)
|
|
ret[i] = r
|
|
if r.Err != "" {
|
|
stats.Success = false
|
|
if fastFail {
|
|
cancel()
|
|
}
|
|
return
|
|
}
|
|
}(i, arg)
|
|
}
|
|
wg.Wait()
|
|
|
|
return &multiCallResp{Results: ret, Stats: stats}, nil
|
|
}
|
|
|
|
// run in sequence
|
|
failedOnce := false
|
|
for i, arg := range args {
|
|
if failedOnce {
|
|
ret[i] = nil
|
|
continue
|
|
}
|
|
|
|
r, _ := doOneCall(ctx, s.b, state, header, arg, disableCache)
|
|
ret[i] = r
|
|
if r.Err != "" {
|
|
stats.Success = false
|
|
if fastFail {
|
|
failedOnce = true
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
return &multiCallResp{Results: ret, Stats: stats}, nil
|
|
}
|