graphql, internal/ethapi: support overriding accounts in eth_call (#19917)

* graphql, internal/ethapi: extend eth_call

This PR offers the third option parameter for eth_call API.
Caller can specify a batch of contracts for overriding the
original account metadata(nonce, balance, code, state).
It has a few advantages:

* It's friendly for debugging
* It's can make on-chain contract lighter for getting rid of
  state access functions

* core, internal: address comments
ChrisChinchilla-patch-3
gary rong 5 years ago committed by Péter Szilágyi
parent 081642ed25
commit c9cdf144d5
  1. 34
      core/state/state_object.go
  2. 9
      core/state/statedb.go
  3. 4
      graphql/graphql.go
  4. 60
      internal/ethapi/api.go

@ -81,6 +81,7 @@ type stateObject struct {
originStorage Storage // Storage cache of original entries to dedup rewrites originStorage Storage // Storage cache of original entries to dedup rewrites
dirtyStorage Storage // Storage entries that need to be flushed to disk dirtyStorage Storage // Storage entries that need to be flushed to disk
fakeStorage Storage // Fake storage which constructed by caller for debugging purpose.
// Cache flags. // Cache flags.
// When an object is marked suicided it will be delete from the trie // When an object is marked suicided it will be delete from the trie
@ -163,6 +164,10 @@ func (s *stateObject) getTrie(db Database) Trie {
// GetState retrieves a value from the account storage trie. // GetState retrieves a value from the account storage trie.
func (s *stateObject) GetState(db Database, key common.Hash) common.Hash { func (s *stateObject) GetState(db Database, key common.Hash) common.Hash {
// If the fake storage is set, only lookup the state here(in the debugging mode)
if s.fakeStorage != nil {
return s.fakeStorage[key]
}
// If we have a dirty value for this state entry, return it // If we have a dirty value for this state entry, return it
value, dirty := s.dirtyStorage[key] value, dirty := s.dirtyStorage[key]
if dirty { if dirty {
@ -174,12 +179,16 @@ func (s *stateObject) GetState(db Database, key common.Hash) common.Hash {
// GetCommittedState retrieves a value from the committed account storage trie. // GetCommittedState retrieves a value from the committed account storage trie.
func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Hash { func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Hash {
// If the fake storage is set, only lookup the state here(in the debugging mode)
if s.fakeStorage != nil {
return s.fakeStorage[key]
}
// If we have the original value cached, return that // If we have the original value cached, return that
value, cached := s.originStorage[key] value, cached := s.originStorage[key]
if cached { if cached {
return value return value
} }
// Track the amount of time wasted on reading the storge trie // Track the amount of time wasted on reading the storage trie
if metrics.EnabledExpensive { if metrics.EnabledExpensive {
defer func(start time.Time) { s.db.StorageReads += time.Since(start) }(time.Now()) defer func(start time.Time) { s.db.StorageReads += time.Since(start) }(time.Now())
} }
@ -202,6 +211,11 @@ func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Has
// SetState updates a value in account storage. // SetState updates a value in account storage.
func (s *stateObject) SetState(db Database, key, value common.Hash) { func (s *stateObject) SetState(db Database, key, value common.Hash) {
// If the fake storage is set, put the temporary state update here.
if s.fakeStorage != nil {
s.fakeStorage[key] = value
return
}
// If the new value is the same as old, don't set // If the new value is the same as old, don't set
prev := s.GetState(db, key) prev := s.GetState(db, key)
if prev == value { if prev == value {
@ -216,6 +230,24 @@ func (s *stateObject) SetState(db Database, key, value common.Hash) {
s.setState(key, value) s.setState(key, value)
} }
// SetStorage replaces the entire state storage with the given one.
//
// After this function is called, all original state will be ignored and state
// lookup only happens in the fake state storage.
//
// Note this function should only be used for debugging purpose.
func (s *stateObject) SetStorage(storage map[common.Hash]common.Hash) {
// Allocate fake storage if it's nil.
if s.fakeStorage == nil {
s.fakeStorage = make(Storage)
}
for key, value := range storage {
s.fakeStorage[key] = value
}
// Don't bother journal since this function should only be used for
// debugging and the `fake` storage won't be committed to database.
}
func (s *stateObject) setState(key, value common.Hash) { func (s *stateObject) setState(key, value common.Hash) {
s.dirtyStorage[key] = value s.dirtyStorage[key] = value
} }

@ -386,6 +386,15 @@ func (self *StateDB) SetState(addr common.Address, key, value common.Hash) {
} }
} }
// SetStorage replaces the entire storage for the specified account with given
// storage. This function should only be used for debugging.
func (self *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common.Hash) {
stateObject := self.GetOrNewStateObject(addr)
if stateObject != nil {
stateObject.SetStorage(storage)
}
}
// Suicide marks the given account as suicided. // Suicide marks the given account as suicided.
// This clears the account balance. // This clears the account balance.
// //

@ -817,7 +817,7 @@ func (b *Block) Call(ctx context.Context, args struct {
return nil, err return nil, err
} }
} }
result, gas, failed, err := ethapi.DoCall(ctx, b.backend, args.Data, *b.num, vm.Config{}, 5*time.Second, b.backend.RPCGasCap()) result, gas, failed, err := ethapi.DoCall(ctx, b.backend, args.Data, *b.num, nil, vm.Config{}, 5*time.Second, b.backend.RPCGasCap())
status := hexutil.Uint64(1) status := hexutil.Uint64(1)
if failed { if failed {
status = 0 status = 0
@ -885,7 +885,7 @@ func (p *Pending) Account(ctx context.Context, args struct {
func (p *Pending) Call(ctx context.Context, args struct { func (p *Pending) Call(ctx context.Context, args struct {
Data ethapi.CallArgs Data ethapi.CallArgs
}) (*CallResult, error) { }) (*CallResult, error) {
result, gas, failed, err := ethapi.DoCall(ctx, p.backend, args.Data, rpc.PendingBlockNumber, vm.Config{}, 5*time.Second, p.backend.RPCGasCap()) result, gas, failed, err := ethapi.DoCall(ctx, p.backend, args.Data, rpc.PendingBlockNumber, nil, vm.Config{}, 5*time.Second, p.backend.RPCGasCap())
status := hexutil.Uint64(1) status := hexutil.Uint64(1)
if failed { if failed {
status = 0 status = 0

@ -743,7 +743,21 @@ type CallArgs struct {
Data *hexutil.Bytes `json:"data"` Data *hexutil.Bytes `json:"data"`
} }
func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber, vmCfg vm.Config, timeout time.Duration, globalGasCap *big.Int) ([]byte, uint64, bool, error) { // account indicates the overriding fields of account during the execution of
// a message call.
// Note, state and stateDiff can't be specified at the same time. If state is
// set, message execution will only use the data in the given state. Otherwise
// if statDiff is set, all diff will be applied first and then execute the call
// message.
type account struct {
Nonce *hexutil.Uint64 `json:"nonce"`
Code *hexutil.Bytes `json:"code"`
Balance **hexutil.Big `json:"balance"`
State *map[common.Hash]common.Hash `json:"state"`
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
}
func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber, overrides map[common.Address]account, vmCfg vm.Config, timeout time.Duration, globalGasCap *big.Int) ([]byte, uint64, bool, error) {
defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())
state, header, err := b.StateAndHeaderByNumber(ctx, blockNr) state, header, err := b.StateAndHeaderByNumber(ctx, blockNr)
@ -761,6 +775,34 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumb
} else { } else {
addr = *args.From addr = *args.From
} }
// Override the fields of specified contracts before execution.
for addr, account := range overrides {
// Override account nonce.
if account.Nonce != nil {
state.SetNonce(addr, uint64(*account.Nonce))
}
// Override account(contract) code.
if account.Code != nil {
state.SetCode(addr, *account.Code)
}
// Override account balance.
if account.Balance != nil {
state.SetBalance(addr, (*big.Int)(*account.Balance))
}
if account.State != nil && account.StateDiff != nil {
return nil, 0, false, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
}
// Replace entire state if caller requires.
if account.State != nil {
state.SetStorage(addr, *account.State)
}
// Apply state diff into specified accounts.
if account.StateDiff != nil {
for key, value := range *account.StateDiff {
state.SetState(addr, key, value)
}
}
}
// Set default gas & gas price if none were set // Set default gas & gas price if none were set
gas := uint64(math.MaxUint64 / 2) gas := uint64(math.MaxUint64 / 2)
if args.Gas != nil { if args.Gas != nil {
@ -827,9 +869,17 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumb
} }
// Call executes the given transaction on the state for the given block number. // Call executes the given transaction on the state for the given block number.
// It doesn't make and changes in the state/blockchain and is useful to execute and retrieve values. //
func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { // Additionally, the caller can specify a batch of contract for fields overriding.
result, _, _, err := DoCall(ctx, s.b, args, blockNr, vm.Config{}, 5*time.Second, s.b.RPCGasCap()) //
// Note, this function doesn't make and changes in the state/blockchain and is
// useful to execute and retrieve values.
func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber, overrides *map[common.Address]account) (hexutil.Bytes, error) {
var accounts map[common.Address]account
if overrides != nil {
accounts = *overrides
}
result, _, _, err := DoCall(ctx, s.b, args, blockNr, accounts, vm.Config{}, 5*time.Second, s.b.RPCGasCap())
return (hexutil.Bytes)(result), err return (hexutil.Bytes)(result), err
} }
@ -860,7 +910,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.Bl
executable := func(gas uint64) bool { executable := func(gas uint64) bool {
args.Gas = (*hexutil.Uint64)(&gas) args.Gas = (*hexutil.Uint64)(&gas)
_, _, failed, err := DoCall(ctx, b, args, rpc.PendingBlockNumber, vm.Config{}, 0, gasCap) _, _, failed, err := DoCall(ctx, b, args, rpc.PendingBlockNumber, nil, vm.Config{}, 0, gasCap)
if err != nil || failed { if err != nil || failed {
return false return false
} }

Loading…
Cancel
Save