Skip to content

Commit

Permalink
Create access list (erigontech#3048)
Browse files Browse the repository at this point in the history
* Create access list

* CreateAccessList compiles

* fixes

Co-authored-by: Alexey Sharp <alexeysharp@Alexeys-iMac.local>
  • Loading branch information
AlexeyAkhunov and Alexey Sharp authored Nov 29, 2021
1 parent 57d641b commit 0e6e47e
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 431 deletions.
1 change: 1 addition & 0 deletions cmd/rpcdaemon/commands/eth_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type EthAPI interface {
Sign(ctx context.Context, _ common.Address, _ hexutil.Bytes) (hexutil.Bytes, error)
SignTransaction(_ context.Context, txObject interface{}) (common.Hash, error)
GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNr rpc.BlockNumber) (*interface{}, error)
CreateAccessList(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, args ethapi.CallArgs) (*accessListResult, error)

// Mining related (see ./eth_mining.go)
Coinbase(ctx context.Context) (common.Address, error)
Expand Down
130 changes: 130 additions & 0 deletions cmd/rpcdaemon/commands/eth_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"fmt"
"math/big"

"github.com/holiman/uint256"
"github.com/ledgerwatch/erigon-lib/gointerfaces"
txpool_proto "github.com/ledgerwatch/erigon-lib/gointerfaces/txpool"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/common/hexutil"
Expand All @@ -14,13 +17,16 @@ import (
"github.com/ledgerwatch/erigon/core/state"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/core/vm"
"github.com/ledgerwatch/erigon/crypto"
"github.com/ledgerwatch/erigon/eth/tracers/logger"
"github.com/ledgerwatch/erigon/ethdb"
"github.com/ledgerwatch/erigon/internal/ethapi"
"github.com/ledgerwatch/erigon/params"
"github.com/ledgerwatch/erigon/rpc"
"github.com/ledgerwatch/erigon/turbo/rpchelper"
"github.com/ledgerwatch/erigon/turbo/transactions"
"github.com/ledgerwatch/log/v3"
"google.golang.org/grpc"
)

// Call implements eth_call. Executes a new message call immediately without creating a transaction on the block chain.
Expand Down Expand Up @@ -276,3 +282,127 @@ func (api *APIImpl) GetProof(ctx context.Context, address common.Address, storag
var stub interface{}
return &stub, fmt.Errorf(NotImplemented, "eth_getProof")
}

// accessListResult returns an optional accesslist
// Its the result of the `debug_createAccessList` RPC call.
// It contains an error if the transaction itself failed.
type accessListResult struct {
Accesslist *types.AccessList `json:"accessList"`
Error string `json:"error,omitempty"`
GasUsed hexutil.Uint64 `json:"gasUsed"`
}

// CreateAccessList implements eth_createAccessList. It creates an access list for the given transaction.
// If the accesslist creation fails an error is returned.
// If the transaction itself fails, an vmErr is returned.
func (api *APIImpl) CreateAccessList(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, args ethapi.CallArgs) (*accessListResult, error) {
tx, err := api.db.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()

chainConfig, err := api.chainConfig(tx)
if err != nil {
return nil, err
}
contractHasTEVM := func(contractHash common.Hash) (bool, error) { return false, nil }
if api.TevmEnabled {
contractHasTEVM = ethdb.GetHasTEVM(tx)
}
blockNumber, hash, err := rpchelper.GetCanonicalBlockNumber(blockNrOrHash, tx, api.filters) // DoCall cannot be executed on non-canonical blocks
if err != nil {
return nil, err
}
block, err := api.BaseAPI.blockWithSenders(tx, hash, blockNumber)
if err != nil {
return nil, err
}
if block == nil {
return nil, nil
}
var stateReader state.StateReader
if num, ok := blockNrOrHash.Number(); ok && num == rpc.LatestBlockNumber {
cacheView, err := api.stateCache.View(ctx, tx)
if err != nil {
return nil, err
}
stateReader = state.NewCachedReader2(cacheView, tx)
} else {
stateReader = state.NewPlainState(tx, blockNumber)
}
state := state.New(stateReader)

header := block.Header()
// If the gas amount is not set, extract this as it will depend on access
// lists and we'll need to reestimate every time
nogas := args.Gas == nil

var to common.Address
if args.To != nil {
to = *args.To
} else {
// Require nonce to calculate address of created contract
if args.Nonce == nil {
var nonce uint64
reply, err := api.txPool.Nonce(ctx, &txpool_proto.NonceRequest{
Address: gointerfaces.ConvertAddressToH160(*args.From),
}, &grpc.EmptyCallOption{})
if err != nil {
return nil, err
}
if reply.Found {
nonce = reply.Nonce + 1
}
args.Nonce = (*hexutil.Uint64)(&nonce)
}
to = crypto.CreateAddress(*args.From, uint64(*args.Nonce))
}
// Retrieve the precompiles since they don't need to be added to the access list
precompiles := vm.ActivePrecompiles(chainConfig.Rules(blockNumber))

// Create an initial tracer
prevTracer := logger.NewAccessListTracer(nil, *args.From, to, precompiles)
if args.AccessList != nil {
prevTracer = logger.NewAccessListTracer(*args.AccessList, *args.From, to, precompiles)
}
for {
// Retrieve the current access list to expand
accessList := prevTracer.AccessList()
log.Trace("Creating access list", "input", accessList)

// If no gas amount was specified, each unique access list needs it's own
// gas calculation. This is quite expensive, but we need to be accurate
// and it's convered by the sender only anyway.
if nogas {
args.Gas = nil
}
// Set the accesslist to the last al
args.AccessList = &accessList
baseFee, _ := uint256.FromBig(header.BaseFee)
msg, err := args.ToMessage(api.GasCap, baseFee)
if err != nil {
return nil, err
}

// Apply the transaction with the access list tracer
tracer := logger.NewAccessListTracer(accessList, *args.From, to, precompiles)
config := vm.Config{Tracer: tracer, Debug: true, NoBaseFee: true}
blockCtx, txCtx := transactions.GetEvmContext(msg, header, blockNrOrHash.RequireCanonical, tx, contractHasTEVM)

evm := vm.NewEVM(blockCtx, txCtx, state, chainConfig, config)
gp := new(core.GasPool).AddGas(msg.Gas())
res, err := core.ApplyMessage(evm, msg, gp, true /* refunds */, false /* gasBailout */)
if err != nil {
return nil, err
}
if tracer.Equal(prevTracer) {
var errString string
if res.Err != nil {
errString = res.Err.Error()
}
return &accessListResult{Accesslist: &accessList, Error: errString, GasUsed: (hexutil.Uint64)(res.UsedGas)}, nil
}
prevTracer = tracer
}
}
192 changes: 192 additions & 0 deletions eth/tracers/logger/access_list_tracer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright 2021 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library 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.
//
// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package logger

import (
"math/big"
"time"

"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/core/vm"
"github.com/ledgerwatch/erigon/core/vm/stack"
)

// accessList is an accumulator for the set of accounts and storage slots an EVM
// contract execution touches.
type accessList map[common.Address]accessListSlots

// accessListSlots is an accumulator for the set of storage slots within a single
// contract that an EVM contract execution touches.
type accessListSlots map[common.Hash]struct{}

// newAccessList creates a new accessList.
func newAccessList() accessList {
return make(map[common.Address]accessListSlots)
}

// addAddress adds an address to the accesslist.
func (al accessList) addAddress(address common.Address) {
// Set address if not previously present
if _, present := al[address]; !present {
al[address] = make(map[common.Hash]struct{})
}
}

// addSlot adds a storage slot to the accesslist.
func (al accessList) addSlot(address common.Address, slot common.Hash) {
// Set address if not previously present
al.addAddress(address)

// Set the slot on the surely existent storage set
al[address][slot] = struct{}{}
}

// equal checks if the content of the current access list is the same as the
// content of the other one.
func (al accessList) equal(other accessList) bool {
// Cross reference the accounts first
if len(al) != len(other) {
return false
}
for addr := range al {
if _, ok := other[addr]; !ok {
return false
}
}
for addr := range other {
if _, ok := al[addr]; !ok {
return false
}
}
// Accounts match, cross reference the storage slots too
for addr, slots := range al {
otherslots := other[addr]

if len(slots) != len(otherslots) {
return false
}
for hash := range slots {
if _, ok := otherslots[hash]; !ok {
return false
}
}
for hash := range otherslots {
if _, ok := slots[hash]; !ok {
return false
}
}
}
return true
}

// accesslist converts the accesslist to a types.AccessList.
func (al accessList) accessList() types.AccessList {
acl := make(types.AccessList, 0, len(al))
for addr, slots := range al {
tuple := types.AccessTuple{Address: addr, StorageKeys: []common.Hash{}}
for slot := range slots {
tuple.StorageKeys = append(tuple.StorageKeys, slot)
}
acl = append(acl, tuple)
}
return acl
}

// AccessListTracer is a tracer that accumulates touched accounts and storage
// slots into an internal set.
type AccessListTracer struct {
excl map[common.Address]struct{} // Set of account to exclude from the list
list accessList // Set of accounts and storage slots touched
}

// NewAccessListTracer creates a new tracer that can generate AccessLists.
// An optional AccessList can be specified to occupy slots and addresses in
// the resulting accesslist.
func NewAccessListTracer(acl types.AccessList, from, to common.Address, precompiles []common.Address) *AccessListTracer {
excl := map[common.Address]struct{}{
from: {}, to: {},
}
for _, addr := range precompiles {
excl[addr] = struct{}{}
}
list := newAccessList()
for _, al := range acl {
if _, ok := excl[al.Address]; !ok {
list.addAddress(al.Address)
}
for _, slot := range al.StorageKeys {
list.addSlot(al.Address, slot)
}
}
return &AccessListTracer{
excl: excl,
list: list,
}
}

func (a *AccessListTracer) CaptureStart(depth int, from common.Address, to common.Address, precompile bool, create bool, callType vm.CallType, input []byte, gas uint64, value *big.Int, code []byte) error {
return nil
}

// CaptureState captures all opcodes that touch storage or addresses and adds them to the accesslist.
func (a *AccessListTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, memory *vm.Memory, stack *stack.Stack, rData []byte, contract *vm.Contract, depth int, err error) error {
stackData := stack.Data
stackLen := len(stackData)
if (op == vm.SLOAD || op == vm.SSTORE) && stackLen >= 1 {
slot := common.Hash(stackData[stackLen-1].Bytes32())
a.list.addSlot(contract.Address(), slot)
}
if (op == vm.EXTCODECOPY || op == vm.EXTCODEHASH || op == vm.EXTCODESIZE || op == vm.BALANCE || op == vm.SELFDESTRUCT) && stackLen >= 1 {
addr := common.Address(stackData[stackLen-1].Bytes20())
if _, ok := a.excl[addr]; !ok {
a.list.addAddress(addr)
}
}
if (op == vm.DELEGATECALL || op == vm.CALL || op == vm.STATICCALL || op == vm.CALLCODE) && stackLen >= 5 {
addr := common.Address(stackData[stackLen-2].Bytes20())
if _, ok := a.excl[addr]; !ok {
a.list.addAddress(addr)
}
}
return nil
}

func (*AccessListTracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, memory *vm.Memory, stack *stack.Stack, contract *vm.Contract, depth int, err error) error {
return nil
}
func (*AccessListTracer) CaptureEnd(depth int, output []byte, startGas, endGas uint64, t time.Duration, err error) error {
return nil
}
func (*AccessListTracer) CaptureSelfDestruct(from common.Address, to common.Address, value *big.Int) {
}
func (*AccessListTracer) CaptureAccountRead(account common.Address) error {
return nil
}
func (*AccessListTracer) CaptureAccountWrite(account common.Address) error {
return nil
}

// AccessList returns the current accesslist maintained by the tracer.
func (a *AccessListTracer) AccessList() types.AccessList {
return a.list.accessList()
}

// Equal returns if the content of two access list traces are equal.
func (a *AccessListTracer) Equal(other *AccessListTracer) bool {
return a.list.equal(other.list)
}
Loading

0 comments on commit 0e6e47e

Please sign in to comment.