diff --git a/rosetta/services/call_service.go b/rosetta/services/call_service.go index fd128a845e..f13996f431 100644 --- a/rosetta/services/call_service.go +++ b/rosetta/services/call_service.go @@ -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(), diff --git a/rosetta/services/construction_check.go b/rosetta/services/construction_check.go index b88e3bf6c5..b073ae187b 100644 --- a/rosetta/services/construction_check.go +++ b/rosetta/services/construction_check.go @@ -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{}{ diff --git a/rpc/harmony/contract.go b/rpc/harmony/contract.go index abcb4f9418..000e938218 100644 --- a/rpc/harmony/contract.go +++ b/rpc/harmony/contract.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "math/big" "reflect" "time" @@ -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" @@ -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) @@ -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 } @@ -158,7 +162,7 @@ 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(). @@ -166,16 +170,34 @@ func DoEVMCall( 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 @@ -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() { @@ -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 + } +} diff --git a/rpc/harmony/transaction.go b/rpc/harmony/transaction.go index 5191566a36..1b88ce709e 100644 --- a/rpc/harmony/transaction.go +++ b/rpc/harmony/transaction.go @@ -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