The core protocol of WoopChain
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
woop/rpc/contract.go

223 lines
6.4 KiB

package rpc
import (
"context"
"fmt"
"math"
"reflect"
"time"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/time/rate"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/woop-chain/woop/common/denominations"
"github.com/woop-chain/woop/core"
"github.com/woop-chain/woop/eth/rpc"
"github.com/woop-chain/woop/wiki"
wikiCommon "github.com/woop-chain/woop/internal/common"
"github.com/woop-chain/woop/internal/utils"
)
const (
defaultGasPrice = denominations.Nano
defaultFromAddress = "0x0000000000000000000000000000000000000000"
)
// PublicContractService provides an API to access Woop's contract services.
// It offers only methods that operate on public data that is freely available to anyone.
type PublicContractService struct {
wiki *wiki.Woop
version Version
// TEMP SOLUTION to rpc node spamming issue
limiterCall *rate.Limiter
evmCallTimeout time.Duration
}
// NewPublicContractAPI creates a new API for the RPC interface
func NewPublicContractAPI(
wiki *wiki.Woop,
version Version,
limiterEnable bool,
limit int,
evmCallTimeout time.Duration,
) rpc.API {
var limiter *rate.Limiter
if limiterEnable {
limiter = rate.NewLimiter(rate.Limit(limit), limit)
}
return rpc.API{
Namespace: version.Namespace(),
Version: APIVersion,
Service: &PublicContractService{
wiki: wiki,
version: version,
limiterCall: limiter,
evmCallTimeout: evmCallTimeout,
},
Public: true,
}
}
func (s *PublicContractService) wait(limiter *rate.Limiter, ctx context.Context) error {
if limiter != nil {
deadlineCtx, cancel := context.WithTimeout(ctx, DefaultRateLimiterWaitTimeout)
defer cancel()
if !limiter.Allow() {
name := reflect.TypeOf(limiter).Elem().Name()
rpcRateLimitCounterVec.With(prometheus.Labels{
"limiter_name": name,
}).Inc()
}
return limiter.Wait(deadlineCtx)
}
return nil
}
// 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 *PublicContractService) Call(
ctx context.Context, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash,
) (hexutil.Bytes, error) {
timer := DoMetricRPCRequest(Call)
defer DoRPCRequestDuration(Call, timer)
err := s.wait(s.limiterCall, ctx)
if err != nil {
DoMetricRPCQueryInfo(Call, RateLimitedNumber)
return nil, err
}
// Execute call
result, err := DoEVMCall(ctx, s.wiki, args, blockNrOrHash, s.evmCallTimeout)
if err != nil {
return nil, err
}
// If the result contains a revert reason, try to unpack and return it.
if len(result.Revert()) > 0 {
return nil, newRevertError(&result)
}
// If VM returns error, still return the ReturnData, which is the contract error message
return result.ReturnData, nil
}
// GetCode returns the code stored at the given address in the state for the given block number.
func (s *PublicContractService) GetCode(
ctx context.Context, addr string, blockNrOrHash rpc.BlockNumberOrHash,
) (hexutil.Bytes, error) {
timer := DoMetricRPCRequest(GetCode)
defer DoRPCRequestDuration(GetCode, timer)
// Fetch state
address, err := wikiCommon.ParseAddr(addr)
if err != nil {
DoMetricRPCQueryInfo(GetCode, FailedNumber)
return nil, err
}
state, _, err := s.wiki.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
DoMetricRPCQueryInfo(GetCode, FailedNumber)
return nil, err
}
code := state.GetCode(address)
// Response output is the same for all versions
return code, state.Error()
}
// GetStorageAt returns the storage from the state at the given address, key and
// block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block
// numbers are also allowed.
func (s *PublicContractService) GetStorageAt(
ctx context.Context, addr string, key string, blockNrOrHash rpc.BlockNumberOrHash,
) (hexutil.Bytes, error) {
timer := DoMetricRPCRequest(GetStorageAt)
defer DoRPCRequestDuration(GetStorageAt, timer)
// Fetch state
state, _, err := s.wiki.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
DoMetricRPCQueryInfo(GetStorageAt, FailedNumber)
return nil, err
}
address, err := wikiCommon.ParseAddr(addr)
if err != nil {
DoMetricRPCQueryInfo(GetStorageAt, FailedNumber)
return nil, err
}
res := state.GetState(address, common.HexToHash(key))
// Response output is the same for all versions
return res[:], state.Error()
}
// DoEVMCall executes an EVM call
func DoEVMCall(
ctx context.Context, wiki *wiki.Woop, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash,
timeout time.Duration,
) (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
state, header, err := wiki.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
DoMetricRPCQueryInfo(DoEvmCall, FailedNumber)
return core.ExecutionResult{}, err
}
// Create new call message
msg := args.ToMessage(wiki.RPCGasCap)
// 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, err := wiki.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() {
<-ctx.Done()
evm.Cancel()
}()
// Setup the gas pool (also for unmetered requests)
// and apply the message.
gp := new(core.GasPool).AddGas(math.MaxUint64)
result, err := core.ApplyMessage(evm, msg, gp)
if err != nil {
DoMetricRPCQueryInfo(DoEvmCall, FailedNumber)
return core.ExecutionResult{}, err
}
// If the timer caused an abort, return an appropriate error message
if evm.Cancelled() {
DoMetricRPCQueryInfo(DoEvmCall, FailedNumber)
return core.ExecutionResult{}, fmt.Errorf("execution aborted (timeout = %v)", timeout)
}
// Response output is the same for all versions
return result, nil
}