mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 18:32:23 +00:00
internal/ethapi: add multicall method
This commit is contained in:
parent
6e3aa86a2b
commit
62d33d5a60
2 changed files with 265 additions and 7 deletions
|
|
@ -997,6 +997,10 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash
|
||||||
if err := overrides.Apply(state); err != nil {
|
if err := overrides.Apply(state); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil)
|
||||||
|
if blockOverrides != nil {
|
||||||
|
blockOverrides.Apply(&blockCtx)
|
||||||
|
}
|
||||||
// Setup context so it may be cancelled the call has completed
|
// Setup context so it may be cancelled the call has completed
|
||||||
// or, in case of unmetered gas, setup a context with a timeout.
|
// or, in case of unmetered gas, setup a context with a timeout.
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
|
|
@ -1008,17 +1012,16 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash
|
||||||
// Make sure the context is cancelled when the call has completed
|
// Make sure the context is cancelled when the call has completed
|
||||||
// this makes sure resources are cleaned up.
|
// this makes sure resources are cleaned up.
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
return doCall(ctx, b, args, state, header, timeout, new(core.GasPool).AddGas(globalGasCap), &blockCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, timeout time.Duration, gp *core.GasPool, blockContext *vm.BlockContext) (*core.ExecutionResult, error) {
|
||||||
// Get a new instance of the EVM.
|
// Get a new instance of the EVM.
|
||||||
msg, err := args.ToMessage(globalGasCap, header.BaseFee)
|
msg, err := args.ToMessage(gp.Gas(), header.BaseFee)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil)
|
evm, vmError := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}, blockContext)
|
||||||
if blockOverrides != nil {
|
|
||||||
blockOverrides.Apply(&blockCtx)
|
|
||||||
}
|
|
||||||
evm, vmError := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}, &blockCtx)
|
|
||||||
|
|
||||||
// Wait for the context to be done and cancel the evm. Even if the
|
// Wait for the context to be done and cancel the evm. Even if the
|
||||||
// EVM has finished, cancelling may be done (repeatedly)
|
// EVM has finished, cancelling may be done (repeatedly)
|
||||||
|
|
@ -1028,7 +1031,6 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Execute the message.
|
// Execute the message.
|
||||||
gp := new(core.GasPool).AddGas(math.MaxUint64)
|
|
||||||
result, err := core.ApplyMessage(evm, msg, gp)
|
result, err := core.ApplyMessage(evm, msg, gp)
|
||||||
if err := vmError(); err != nil {
|
if err := vmError(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -1092,6 +1094,90 @@ func (s *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrO
|
||||||
return result.Return(), result.Err
|
return result.Return(), result.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallBatch is a batch of calls to be simulated sequentially.
|
||||||
|
type CallBatch struct {
|
||||||
|
BlockOverrides *BlockOverrides
|
||||||
|
StateOverrides *StateOverride
|
||||||
|
Calls []TransactionArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
type callResult struct {
|
||||||
|
ReturnValue hexutil.Bytes `json:"return"`
|
||||||
|
Logs []*types.Log `json:"logs"`
|
||||||
|
GasUsed hexutil.Uint64 `json:"gasUsed"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multicall executes series of transactions on top of a base state.
|
||||||
|
// The transactions are packed into blocks. For each block, block header
|
||||||
|
// fields can be overridden. The state can also be overridden prior to
|
||||||
|
// execution of each block.
|
||||||
|
//
|
||||||
|
// Note, this function doesn't make any changes in the state/blockchain and is
|
||||||
|
// useful to execute and retrieve values.
|
||||||
|
func (s *BlockChainAPI) Multicall(ctx context.Context, blocks []CallBatch, blockNrOrHash rpc.BlockNumberOrHash) ([][]callResult, error) {
|
||||||
|
state, header, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
|
||||||
|
if state == nil || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup context so it may be cancelled before the calls completed
|
||||||
|
// or, in case of unmetered gas, setup a context with a timeout.
|
||||||
|
var (
|
||||||
|
cancel context.CancelFunc
|
||||||
|
timeout = s.b.RPCEVMTimeout()
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
var (
|
||||||
|
results = make([][]callResult, len(blocks))
|
||||||
|
// Each tx and all the series of txes shouldn't consume more gas than cap
|
||||||
|
globalGasCap = s.b.RPCGasCap()
|
||||||
|
gp = new(core.GasPool).AddGas(globalGasCap)
|
||||||
|
)
|
||||||
|
for bi, block := range blocks {
|
||||||
|
// State overrides are applied prior to execution of a block
|
||||||
|
if err := block.StateOverrides.Apply(state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
blockContext := core.NewEVMBlockContext(header, NewChainContext(ctx, s.b), nil)
|
||||||
|
if block.BlockOverrides != nil {
|
||||||
|
block.BlockOverrides.Apply(&blockContext)
|
||||||
|
}
|
||||||
|
results[bi] = make([]callResult, len(block.Calls))
|
||||||
|
for i, call := range block.Calls {
|
||||||
|
// Hack to get logs from statedb which stores logs by txhash.
|
||||||
|
txhash := common.BigToHash(big.NewInt(int64(i)))
|
||||||
|
state.SetTxContext(txhash, i)
|
||||||
|
result, err := doCall(ctx, s.b, call, state, header, timeout, gp, &blockContext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// If the result contains a revert reason, try to unpack it.
|
||||||
|
if len(result.Revert()) > 0 {
|
||||||
|
result.Err = newRevertError(result)
|
||||||
|
}
|
||||||
|
logs := state.GetLogs(txhash, blockContext.BlockNumber.Uint64(), common.Hash{})
|
||||||
|
// Clear the garbage txhash that was filled in.
|
||||||
|
for _, l := range logs {
|
||||||
|
l.TxHash = common.Hash{}
|
||||||
|
}
|
||||||
|
callRes := callResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas)}
|
||||||
|
if result.Err != nil {
|
||||||
|
callRes.Error = result.Err.Error()
|
||||||
|
}
|
||||||
|
results[bi][i] = callRes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, error) {
|
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
|
// Binary search the gas requirement, as it may be higher than the amount used
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -596,6 +596,178 @@ func TestCall(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMulticall(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// Initialize test accounts
|
||||||
|
var (
|
||||||
|
accounts = newAccounts(3)
|
||||||
|
genBlocks = 10
|
||||||
|
signer = types.HomesteadSigner{}
|
||||||
|
genesis = &core.Genesis{
|
||||||
|
Config: params.TestChainConfig,
|
||||||
|
Alloc: core.GenesisAlloc{
|
||||||
|
accounts[0].addr: {Balance: big.NewInt(params.Ether)},
|
||||||
|
accounts[1].addr: {Balance: big.NewInt(params.Ether)},
|
||||||
|
accounts[2].addr: {Balance: big.NewInt(params.Ether)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
api := NewBlockChainAPI(newTestBackend(t, genBlocks, genesis, 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)
|
||||||
|
}))
|
||||||
|
var (
|
||||||
|
randomAccounts = newAccounts(3)
|
||||||
|
)
|
||||||
|
type res struct {
|
||||||
|
ReturnValue string `json:"return"`
|
||||||
|
Error string
|
||||||
|
Logs string
|
||||||
|
GasUsed string
|
||||||
|
}
|
||||||
|
var testSuite = []struct {
|
||||||
|
blocks []CallBatch
|
||||||
|
expectErr error
|
||||||
|
want [][]res
|
||||||
|
}{
|
||||||
|
// First value transfer OK after state override, second one should succeed
|
||||||
|
// because of first transfer.
|
||||||
|
{
|
||||||
|
blocks: []CallBatch{{
|
||||||
|
StateOverrides: &StateOverride{
|
||||||
|
randomAccounts[0].addr: OverrideAccount{Balance: newRPCBalance(big.NewInt(1000))},
|
||||||
|
},
|
||||||
|
Calls: []TransactionArgs{{
|
||||||
|
From: &randomAccounts[0].addr,
|
||||||
|
To: &randomAccounts[1].addr,
|
||||||
|
Value: (*hexutil.Big)(big.NewInt(1000)),
|
||||||
|
}, {
|
||||||
|
From: &randomAccounts[1].addr,
|
||||||
|
To: &randomAccounts[2].addr,
|
||||||
|
Value: (*hexutil.Big)(big.NewInt(1000)),
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
want: [][]res{{
|
||||||
|
res{
|
||||||
|
ReturnValue: "0x",
|
||||||
|
GasUsed: "0x5208",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
ReturnValue: "0x",
|
||||||
|
GasUsed: "0x5208",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
// Block overrides should work, each call is simulated on a different block number
|
||||||
|
blocks: []CallBatch{{
|
||||||
|
BlockOverrides: &BlockOverrides{
|
||||||
|
Number: (*hexutil.Big)(big.NewInt(10)),
|
||||||
|
},
|
||||||
|
Calls: []TransactionArgs{
|
||||||
|
{
|
||||||
|
From: &accounts[0].addr,
|
||||||
|
Input: &hexutil.Bytes{
|
||||||
|
0x43, // NUMBER
|
||||||
|
0x60, 0x00, 0x52, // MSTORE offset 0
|
||||||
|
0x60, 0x20, 0x60, 0x00, 0xf3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
BlockOverrides: &BlockOverrides{
|
||||||
|
Number: (*hexutil.Big)(big.NewInt(11)),
|
||||||
|
},
|
||||||
|
Calls: []TransactionArgs{{
|
||||||
|
From: &accounts[1].addr,
|
||||||
|
Input: &hexutil.Bytes{
|
||||||
|
0x43, // NUMBER
|
||||||
|
0x60, 0x00, 0x52, // MSTORE offset 0
|
||||||
|
0x60, 0x20, 0x60, 0x00, 0xf3,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
want: [][]res{{
|
||||||
|
res{
|
||||||
|
ReturnValue: "0x000000000000000000000000000000000000000000000000000000000000000a",
|
||||||
|
GasUsed: "0xe891",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
res{
|
||||||
|
ReturnValue: "0x000000000000000000000000000000000000000000000000000000000000000b",
|
||||||
|
GasUsed: "0xe891",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
// Test on solidity storage example. Set value in one call, read in next.
|
||||||
|
{
|
||||||
|
blocks: []CallBatch{{
|
||||||
|
StateOverrides: &StateOverride{
|
||||||
|
randomAccounts[2].addr: OverrideAccount{
|
||||||
|
Code: hex2Bytes("608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea2646970667358221220404e37f487a89a932dca5e77faaf6ca2de3b991f93d230604b1b8daaef64766264736f6c63430008070033"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Calls: []TransactionArgs{{
|
||||||
|
// Set value to 5
|
||||||
|
From: &randomAccounts[0].addr,
|
||||||
|
To: &randomAccounts[2].addr,
|
||||||
|
Input: hex2Bytes("6057361d0000000000000000000000000000000000000000000000000000000000000005"),
|
||||||
|
}, {
|
||||||
|
// Read value
|
||||||
|
From: &randomAccounts[0].addr,
|
||||||
|
To: &randomAccounts[2].addr,
|
||||||
|
Input: hex2Bytes("2e64cec1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
want: [][]res{{{
|
||||||
|
ReturnValue: "0x",
|
||||||
|
GasUsed: "0xaacc",
|
||||||
|
}, {
|
||||||
|
ReturnValue: "0x0000000000000000000000000000000000000000000000000000000000000005",
|
||||||
|
GasUsed: "0x5bb7",
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testSuite {
|
||||||
|
result, err := api.Multicall(context.Background(), tc.blocks, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// Turn result into res-struct
|
||||||
|
var have [][]res
|
||||||
|
resBytes, _ := json.Marshal(result)
|
||||||
|
if err := json.Unmarshal(resBytes, &have); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal result: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(have, tc.want) {
|
||||||
|
t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, have, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
key *ecdsa.PrivateKey
|
key *ecdsa.PrivateKey
|
||||||
addr common.Address
|
addr common.Address
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue