Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BlockOverrides and StateOverrides fields to eth_call #4833

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rosetta/services/call_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ func (c *CallAPIService) call(
"message": errors.WithMessage(err, "invalid parameters").Error(),
})
}
data, err := contractAPI.Call(ctx, args.CallArgs, rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(args.BlockNum)))
data, err := contractAPI.Call(ctx, args.CallArgs, rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(args.BlockNum)), nil, nil)
if err != nil {
return nil, common.NewError(common.ErrCallExecute, map[string]interface{}{
"message": errors.WithMessage(err, "call smart contract error").Error(),
Expand Down
2 changes: 1 addition & 1 deletion rosetta/services/construction_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func (s *ConstructAPI) ConstructionMetadata(
callArgs.To = &contractAddress
}
evmExe, err := rpc.DoEVMCall(
ctx, s.hmy, callArgs, latest, s.evmCallTimeout,
ctx, s.hmy, callArgs, latest, s.evmCallTimeout, nil, nil,
)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
Expand Down
129 changes: 118 additions & 11 deletions rpc/harmony/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math"
"math/big"
"reflect"
"time"

Expand All @@ -14,6 +15,8 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/harmony-one/harmony/common/denominations"
"github.com/harmony-one/harmony/core"
"github.com/harmony-one/harmony/core/state"
"github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/eth/rpc"
"github.com/harmony-one/harmony/hmy"
hmyCommon "github.com/harmony-one/harmony/internal/common"
Expand Down Expand Up @@ -81,6 +84,7 @@ func (s *PublicContractService) wait(limiter *rate.Limiter, ctx context.Context)
// It doesn't make and changes in the state/blockchain and is useful to execute and retrieve values.
func (s *PublicContractService) Call(
ctx context.Context, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash,
overrides *StateOverrides, blockOverrides *BlockOverrides,
) (hexutil.Bytes, error) {
timer := DoMetricRPCRequest(Call)
defer DoRPCRequestDuration(Call, timer)
Expand All @@ -92,7 +96,7 @@ func (s *PublicContractService) Call(
}

// Execute call
result, err := DoEVMCall(ctx, s.hmy, args, blockNrOrHash, s.evmCallTimeout)
result, err := DoEVMCall(ctx, s.hmy, args, blockNrOrHash, s.evmCallTimeout, overrides, blockOverrides)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -158,24 +162,42 @@ func (s *PublicContractService) GetStorageAt(
// DoEVMCall executes an EVM call
func DoEVMCall(
ctx context.Context, hmy *hmy.Harmony, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash,
timeout time.Duration,
timeout time.Duration, overrides *StateOverrides, blockOverrides *BlockOverrides,
) (core.ExecutionResult, error) {
defer func(start time.Time) {
utils.Logger().Debug().
Dur("runtime", time.Since(start)).
Msg("Executing EVM call finished")
}(time.Now())

// Fetch state
// fetch state
state, header, err := hmy.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
DoMetricRPCQueryInfo(DoEvmCall, FailedNumber)
return core.ExecutionResult{}, err
}

// Create new call message
// create new call message
msg := args.ToMessage(hmy.RPCGasCap)

// get a new instance of the EVM.
evm, err := hmy.GetEVM(ctx, msg, state, header)
if err != nil {
DoMetricRPCQueryInfo(DoEvmCall, FailedNumber)
return core.ExecutionResult{}, err
}

// apply block overrides
if blockOverrides != nil {
blockOverrides.Apply(&evm.Context)
}

// apply state overrides
if err := overrides.Apply(*state); err != nil {
DoMetricRPCQueryInfo(DoEvmCall, FailedNumber)
return core.ExecutionResult{}, err
}

// 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
Expand All @@ -189,13 +211,6 @@ func DoEVMCall(
// this makes sure resources are cleaned up.
defer cancel()

// Get a new instance of the EVM.
evm, err := hmy.GetEVM(ctx, msg, state, header)
if err != nil {
DoMetricRPCQueryInfo(DoEvmCall, FailedNumber)
return core.ExecutionResult{}, 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() {
Expand All @@ -221,3 +236,95 @@ func DoEVMCall(
// Response output is the same for all versions
return result, nil
}

// OverrideAccount specifies the fields of an account to override during the execution
// of a message call.
// The fields `state` and `stateDiff` cannot be specified simultaneously. If `state` is set,
// the message execution will use only the data provided in the given state. Conversely,
// if `stateDiff` is set, all the diffs will be applied first, and then the message call
// will be executed.
type OverrideAccount 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"`
}

// StateOverride is the collection of overriden accounts.
type StateOverrides map[common.Address]OverrideAccount

// Apply overrides the fields of specified accounts into the given state.
func (s *StateOverrides) Apply(state state.DB) error {
if s == nil {
return nil
}

for addr, account := range *s {
// nonce
if account.Nonce != nil {
state.SetNonce(addr, uint64(*account.Nonce))
}

// account (contract) code
if account.Code != nil {
state.SetCode(addr, *account.Code, false) // TODO: isValidatorCode set to false
}

// account balance
if account.Balance != nil {
state.SetBalance(addr, (*big.Int)(*account.Balance))
}

if account.State != nil && account.StateDiff != nil {
return 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 k, v := range *account.StateDiff {
state.SetState(addr, k, v)
}
}
}

return nil
}

// BlockOverrides is a set of header fields to override.
type BlockOverrides struct {
Number *hexutil.Big
Difficulty *hexutil.Big // No-op if we're simulating post-merge calls.
Time *hexutil.Uint64
GasLimit *hexutil.Uint64
FeeRecipient *common.Address
PrevRandao *common.Hash
BaseFeePerGas *hexutil.Big // EIP-1559 (not implemented)
BlobBaseFee *hexutil.Big // EIP-4844 (not implemented)
}

// apply overrides the given header fields into the given block context
// difficulty & random not part of vm.Context
func (b *BlockOverrides) Apply(blockCtx *vm.Context) {
if b == nil {
return
}

if b.Number != nil {
blockCtx.BlockNumber = b.Number.ToInt()
}
if b.Time != nil {
blockCtx.Time = big.NewInt(int64(*b.Time))
}
if b.GasLimit != nil {
blockCtx.GasLimit = uint64(*b.GasLimit)
}
if b.FeeRecipient != nil {
blockCtx.Coinbase = *b.FeeRecipient
}
}
2 changes: 1 addition & 1 deletion rpc/harmony/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,7 @@ func EstimateGas(ctx context.Context, hmy *hmy.Harmony, args CallArgs, blockNrOr
executable := func(gas uint64) (bool, *core.ExecutionResult, error) {
args.Gas = (*hexutil.Uint64)(&gas)

result, err := DoEVMCall(ctx, hmy, args, blockNrOrHash, 0)
result, err := DoEVMCall(ctx, hmy, args, blockNrOrHash, 0, nil, nil)
if err != nil {
if errors.Is(err, core.ErrIntrinsicGas) {
return true, nil, nil // Special case, raise gas limit
Expand Down