diff options
| author | gary rong <garyrong0905@gmail.com> | 2019-08-08 21:44:11 +0800 | 
|---|---|---|
| committer | Péter Szilágyi <peterke@gmail.com> | 2019-08-08 21:44:11 +0800 | 
| commit | c9cdf144d55f6bbd75314e812eeefc18d7e8c40e (patch) | |
| tree | e392f3130a4ef56398a2dc164f98e82001d0d162 | |
| parent | 081642ed255fd939ec186f97fd60a316b51d8b61 (diff) | |
| download | go-tangerine-c9cdf144d55f6bbd75314e812eeefc18d7e8c40e.tar.gz go-tangerine-c9cdf144d55f6bbd75314e812eeefc18d7e8c40e.tar.zst go-tangerine-c9cdf144d55f6bbd75314e812eeefc18d7e8c40e.zip | |
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
| -rw-r--r-- | core/state/state_object.go | 34 | ||||
| -rw-r--r-- | core/state/statedb.go | 9 | ||||
| -rw-r--r-- | graphql/graphql.go | 4 | ||||
| -rw-r--r-- | internal/ethapi/api.go | 60 | 
4 files changed, 99 insertions, 8 deletions
| diff --git a/core/state/state_object.go b/core/state/state_object.go index 852c340b8..45ae95a2a 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -81,6 +81,7 @@ type stateObject struct {  	originStorage Storage // Storage cache of original entries to dedup rewrites  	dirtyStorage  Storage // Storage entries that need to be flushed to disk +	fakeStorage   Storage // Fake storage which constructed by caller for debugging purpose.  	// Cache flags.  	// 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.  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  	value, dirty := s.dirtyStorage[key]  	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.  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  	value, cached := s.originStorage[key]  	if cached {  		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 {  		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.  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  	prev := s.GetState(db, key)  	if prev == value { @@ -216,6 +230,24 @@ func (s *stateObject) SetState(db Database, key, value common.Hash) {  	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) {  	s.dirtyStorage[key] = value  } diff --git a/core/state/statedb.go b/core/state/statedb.go index 3bb9862ed..b07f08fd2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -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.  // This clears the account balance.  // diff --git a/graphql/graphql.go b/graphql/graphql.go index 771c3c62f..df279f42b 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -817,7 +817,7 @@ func (b *Block) Call(ctx context.Context, args struct {  			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)  	if failed {  		status = 0 @@ -885,7 +885,7 @@ func (p *Pending) Account(ctx context.Context, args struct {  func (p *Pending) Call(ctx context.Context, args struct {  	Data ethapi.CallArgs  }) (*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)  	if failed {  		status = 0 diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index b67017a90..05204e547 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -743,7 +743,21 @@ type CallArgs struct {  	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())  	state, header, err := b.StateAndHeaderByNumber(ctx, blockNr) @@ -761,6 +775,34 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumb  	} else {  		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  	gas := uint64(math.MaxUint64 / 2)  	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. -// 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) { -	result, _, _, err := DoCall(ctx, s.b, args, blockNr, vm.Config{}, 5*time.Second, s.b.RPCGasCap()) +// +// Additionally, the caller can specify a batch of contract for fields overriding. +// +// 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  } @@ -860,7 +910,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.Bl  	executable := func(gas uint64) bool {  		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 {  			return false  		} | 
