diff --git a/core/state/state_object.go b/core/state/state_object.go index 852c340b82..4594f22e24 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,29 @@ func (s *stateObject) SetState(db Database, key, value common.Hash) { s.setState(key, value) } +// SetStatesForDebug 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) SetStatesForDebug(storage map[common.Hash]common.Hash) { + // Clean the fake storage if debugging is done. + if storage == nil { + s.fakeStorage = nil + return + } + // 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 3bb9862edd..e2ff98bf10 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -386,6 +386,13 @@ func (self *StateDB) SetState(addr common.Address, key, value common.Hash) { } } +func (self *StateDB) SetStateForDebug(addr common.Address, storage map[common.Hash]common.Hash) { + stateObject := self.GetOrNewStateObject(addr) + if stateObject != nil { + stateObject.SetStatesForDebug(storage) + } +} + // Suicide marks the given account as suicided. // This clears the account balance. // diff --git a/graphql/graphql.go b/graphql/graphql.go index 771c3c62f6..df279f42b1 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 a00598f820..40596b65be 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -755,7 +755,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, accounts 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) @@ -773,6 +787,37 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumb } else { addr = *args.From } + // Override the fields of specified contracts before execution. + if accounts != nil { + for addr, account := range accounts { + // 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, errors.New("can't use state and stateDiff at the same time") + } + // Replace entire state if caller requires. + if account.State != nil { + state.SetStateForDebug(addr, *account.State) + defer state.SetStateForDebug(addr, nil) // Clean up the "fake" storage + } + // 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 { @@ -839,9 +884,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 } @@ -872,7 +925,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 }