diff --git a/core/blockchain.go b/core/blockchain.go index d6bfc4ee8..f1727e39c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1844,3 +1844,8 @@ func (bc *BlockChain) StoreEpochBlockNumber( func (bc *BlockChain) ChainDB() ethdb.Database { return bc.db } + +// GetVMConfig returns the block chain VM config. +func (bc *BlockChain) GetVMConfig() *vm.Config { + return &bc.vmConfig +} diff --git a/core/vm/evm.go b/core/vm/evm.go index acee46efe..15d3138c9 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -169,6 +169,11 @@ func (evm *EVM) Cancel() { atomic.StoreInt32(&evm.abort, 1) } +// Cancelled returns true if Cancel has been called +func (evm *EVM) Cancelled() bool { + return atomic.LoadInt32(&evm.abort) == 1 +} + // Interpreter returns the current interpreter func (evm *EVM) Interpreter() Interpreter { return evm.interpreter diff --git a/hmy/api_backend.go b/hmy/api_backend.go index 143ccd333..b3363c53a 100644 --- a/hmy/api_backend.go +++ b/hmy/api_backend.go @@ -3,9 +3,11 @@ package hmy import ( "context" "errors" + "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/bloombits" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" @@ -16,6 +18,7 @@ import ( "github.com/harmony-one/harmony/core" "github.com/harmony-one/harmony/core/state" "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/core/vm" ) // APIBackend An implementation of internal/hmyapi/Backend. Full client. @@ -206,3 +209,20 @@ func (b *APIBackend) GetBalance(address common.Address) (*hexutil.Big, error) { func (b *APIBackend) NetVersion() uint64 { return b.hmy.NetVersion() } + +// GetEVM returns a new EVM entity +func (b *APIBackend) GetEVM(ctx context.Context, msg core.Message, state *state.DB, header *types.Header) (*vm.EVM, func() error, error) { + // TODO(ricl): The code is borrowed from [go-ethereum](https://github.com/ethereum/go-ethereum/blob/40cdcf8c47ff094775aca08fd5d94051f9cf1dbb/les/api_backend.go#L114) + // [question](https://ethereum.stackexchange.com/q/72977/54923) + // Might need to reconsider the SetBalance behavior + state.SetBalance(msg.From(), math.MaxBig256) + vmError := func() error { return nil } + + context := core.NewEVMContext(msg, header, b.hmy.BlockChain(), nil) + return vm.NewEVM(context, state, b.hmy.blockchain.Config(), *b.hmy.blockchain.GetVMConfig()), vmError, nil +} + +// RPCGasCap returns the gas cap of rpc +func (b *APIBackend) RPCGasCap() *big.Int { + return b.hmy.RPCGasCap // TODO(ricl): should be hmy.config.RPCGasCap +} diff --git a/hmy/backend.go b/hmy/backend.go index c43704eec..16c1067e6 100644 --- a/hmy/backend.go +++ b/hmy/backend.go @@ -33,6 +33,10 @@ type Harmony struct { // aka network version, which is used to identify which network we are using networkID uint64 + // TODO(ricl): put this into config object + // TODO(ricl): this is never set. Will result in nil pointer bug + // RPCGasCap is the global gas cap for eth-call variants. + RPCGasCap *big.Int `toml:",omitempty"` } // NodeAPI is the list of functions from node used to call rpc apis. diff --git a/internal/hmyapi/backend.go b/internal/hmyapi/backend.go index 0d99e1a04..3f7731248 100644 --- a/internal/hmyapi/backend.go +++ b/internal/hmyapi/backend.go @@ -2,6 +2,7 @@ package hmyapi import ( "context" + "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -13,6 +14,7 @@ import ( "github.com/harmony-one/harmony/core" "github.com/harmony-one/harmony/core/state" "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/core/vm" ) // Backend interface provides the common API services (that are provided by @@ -30,7 +32,7 @@ type Backend interface { EventMux() *event.TypeMux AccountManager() *accounts.Manager // ExtRPCEnabled() bool - // RPCGasCap() *big.Int // global gas cap for eth_call over rpc: DoS protection + RPCGasCap() *big.Int // global gas cap for hmy_call over rpc: DoS protection // BlockChain API // SetHead(number uint64) @@ -40,7 +42,7 @@ type Backend interface { GetBlock(ctx context.Context, blockHash common.Hash) (*types.Block, error) GetReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, error) // GetTd(blockHash common.Hash) *big.Int - // GetEVM(ctx context.Context, msg core.Message, state *state.DB, header *types.Header) (*vm.EVM, func() error, error) + GetEVM(ctx context.Context, msg core.Message, state *state.DB, header *types.Header) (*vm.EVM, func() error, error) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription diff --git a/internal/hmyapi/blockchain.go b/internal/hmyapi/blockchain.go index 0223df3bb..d126885a8 100644 --- a/internal/hmyapi/blockchain.go +++ b/internal/hmyapi/blockchain.go @@ -2,10 +2,24 @@ package hmyapi import ( "context" + "fmt" + + "math/big" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" + "github.com/harmony-one/harmony/core" + "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/core/vm" + "github.com/harmony-one/harmony/internal/utils" +) + +const ( + defaultGasPrice = params.GWei ) // PublicBlockChainAPI provides an API to access the Harmony blockchain. @@ -81,3 +95,98 @@ func (s *PublicBlockChainAPI) BlockNumber() hexutil.Uint64 { header, _ := s.b.HeaderByNumber(context.Background(), rpc.LatestBlockNumber) // latest header should always be available return hexutil.Uint64(header.Number.Uint64()) } + +// 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()) + return (hexutil.Bytes)(result), err +} + +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) { + defer func(start time.Time) { + utils.GetLogInstance().Debug("Executing EVM call finished", "runtime", time.Since(start)) + }(time.Now()) + + state, header, err := b.StateAndHeaderByNumber(ctx, blockNr) + if state == nil || err != nil { + return nil, 0, false, err + } + // Set sender address or use a default if none specified + var addr common.Address + if args.From == nil { + // TODO(ricl): this logic was borrowed from [go-ethereum](https://github.com/ethereum/go-ethereum/blob/f578d41ee6b3087f8021fd561a0b5665aea3dba6/internal/ethapi/api.go#L738) + // [question](https://ethereum.stackexchange.com/questions/72979/why-does-the-docall-function-use-the-first-account-by-default) + // Might need to reconsider the logic + if wallets := b.AccountManager().Wallets(); len(wallets) > 0 { + if accounts := wallets[0].Accounts(); len(accounts) > 0 { + addr = accounts[0].Address + } + } + } else { + addr = *args.From + } + // Set default gas & gas price if none were set + gas := uint64(math.MaxUint64 / 2) + if args.Gas != nil { + gas = uint64(*args.Gas) + } + if globalGasCap != nil && globalGasCap.Uint64() < gas { + utils.GetLogInstance().Warn("Caller gas above allowance, capping", "requested", gas, "cap", globalGasCap) + gas = globalGasCap.Uint64() + } + gasPrice := new(big.Int).SetUint64(defaultGasPrice) + if args.GasPrice != nil { + gasPrice = args.GasPrice.ToInt() + } + + value := new(big.Int) + if args.Value != nil { + value = args.Value.ToInt() + } + + var data []byte + if args.Data != nil { + data = []byte(*args.Data) + } + + // Create new call message + msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, data, false) + + // Setup context so it may be cancelled the call has completed + // or, in case of unmetered gas, setup a context with a timeout. + var cancel context.CancelFunc + 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() + + // Get a new instance of the EVM. + evm, vmError, err := b.GetEVM(ctx, msg, state, header) + if err != nil { + return nil, 0, false, err + } + // Wait for the context to be done and cancel the evm. Even if the + // EVM has finished, cancelling may be done (repeatedly) + go func() { + <-ctx.Done() + evm.Cancel() + }() + + // Setup the gas pool (also for unmetered requests) + // and apply the message. + gp := new(core.GasPool).AddGas(math.MaxUint64) + res, gas, failed, err := core.ApplyMessage(evm, msg, gp) + if err := vmError(); err != nil { + return nil, 0, false, err + } + // If the timer caused an abort, return an appropriate error message + if evm.Cancelled() { + return nil, 0, false, fmt.Errorf("execution aborted (timeout = %v)", timeout) + } + return res, gas, failed, err +} diff --git a/internal/hmyapi/types.go b/internal/hmyapi/types.go index ca9a249ef..73009cbbb 100644 --- a/internal/hmyapi/types.go +++ b/internal/hmyapi/types.go @@ -158,3 +158,13 @@ func newRPCTransactionFromBlockIndex(b *types.Block, index uint64) *RPCTransacti } return newRPCTransaction(txs[index], b.Hash(), b.NumberU64(), index) } + +// CallArgs represents the arguments for a call. +type CallArgs struct { + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data *hexutil.Bytes `json:"data"` +}