forked from cerc-io/laconicd-deprecated
ee806fc41f
* tests(json-rpc): wip evm_backend unit test setup * tests(json-rpc): wip evm_backend unit test setup * fix viper * wip query client mock * fix first backend test except error message * clean up * wip Context with Height * fix JSON RPC backend test setup * typo * refactor folder structure * tests(json-rpc):add BlockBloom tests * tests(json-rpc): remove unused malleate * tests(json-rpc): add BaseFee tests * refactor query tests * add client mock * add GetTendermintBlockByNumber tests * refactor mock tests * refactor * wip backend EthBlockFromTendermint test * wip backend EthBlockFromTendermint test * refactor backend EthBlockFromTendermint test * add TestGetTendermintBlockResultByNumber * add GetBlockByNumber tests * refactor mocks * fix spelling * add more tests and address comments
409 lines
12 KiB
Go
409 lines
12 KiB
Go
package backend
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
|
|
|
abci "github.com/tendermint/tendermint/abci/types"
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
tmrpctypes "github.com/tendermint/tendermint/rpc/core/types"
|
|
|
|
"github.com/evmos/ethermint/rpc/types"
|
|
evmtypes "github.com/evmos/ethermint/x/evm/types"
|
|
)
|
|
|
|
// ExceedBlockGasLimitError defines the error message when tx execution exceeds the block gas limit.
|
|
// The tx fee is deducted in ante handler, so it shouldn't be ignored in JSON-RPC API.
|
|
const ExceedBlockGasLimitError = "out of gas in location: block gas meter; gasWanted:"
|
|
|
|
type txGasAndReward struct {
|
|
gasUsed uint64
|
|
reward *big.Int
|
|
}
|
|
|
|
type sortGasAndReward []txGasAndReward
|
|
|
|
func (s sortGasAndReward) Len() int { return len(s) }
|
|
func (s sortGasAndReward) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
|
|
func (s sortGasAndReward) Less(i, j int) bool {
|
|
return s[i].reward.Cmp(s[j].reward) < 0
|
|
}
|
|
|
|
// SetTxDefaults populates tx message with default values in case they are not
|
|
// provided on the args
|
|
func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) {
|
|
if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) {
|
|
return args, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
|
|
}
|
|
|
|
head := b.CurrentHeader()
|
|
if head == nil {
|
|
return args, errors.New("latest header is nil")
|
|
}
|
|
|
|
// If user specifies both maxPriorityfee and maxFee, then we do not
|
|
// need to consult the chain for defaults. It's definitely a London tx.
|
|
if args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil {
|
|
// In this clause, user left some fields unspecified.
|
|
if head.BaseFee != nil && args.GasPrice == nil {
|
|
if args.MaxPriorityFeePerGas == nil {
|
|
tip, err := b.SuggestGasTipCap(head.BaseFee)
|
|
if err != nil {
|
|
return args, err
|
|
}
|
|
args.MaxPriorityFeePerGas = (*hexutil.Big)(tip)
|
|
}
|
|
|
|
if args.MaxFeePerGas == nil {
|
|
gasFeeCap := new(big.Int).Add(
|
|
(*big.Int)(args.MaxPriorityFeePerGas),
|
|
new(big.Int).Mul(head.BaseFee, big.NewInt(2)),
|
|
)
|
|
args.MaxFeePerGas = (*hexutil.Big)(gasFeeCap)
|
|
}
|
|
|
|
if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 {
|
|
return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas)
|
|
}
|
|
|
|
} else {
|
|
if args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil {
|
|
return args, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet")
|
|
}
|
|
|
|
if args.GasPrice == nil {
|
|
price, err := b.SuggestGasTipCap(head.BaseFee)
|
|
if err != nil {
|
|
return args, err
|
|
}
|
|
if head.BaseFee != nil {
|
|
// The legacy tx gas price suggestion should not add 2x base fee
|
|
// because all fees are consumed, so it would result in a spiral
|
|
// upwards.
|
|
price.Add(price, head.BaseFee)
|
|
}
|
|
args.GasPrice = (*hexutil.Big)(price)
|
|
}
|
|
}
|
|
} else {
|
|
// Both maxPriorityfee and maxFee set by caller. Sanity-check their internal relation
|
|
if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 {
|
|
return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas)
|
|
}
|
|
}
|
|
|
|
if args.Value == nil {
|
|
args.Value = new(hexutil.Big)
|
|
}
|
|
if args.Nonce == nil {
|
|
// get the nonce from the account retriever
|
|
// ignore error in case tge account doesn't exist yet
|
|
nonce, _ := b.getAccountNonce(*args.From, true, 0, b.logger)
|
|
args.Nonce = (*hexutil.Uint64)(&nonce)
|
|
}
|
|
|
|
if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
|
|
return args, errors.New("both 'data' and 'input' are set and not equal. Please use 'input' to pass transaction call data")
|
|
}
|
|
|
|
if args.To == nil {
|
|
// Contract creation
|
|
var input []byte
|
|
if args.Data != nil {
|
|
input = *args.Data
|
|
} else if args.Input != nil {
|
|
input = *args.Input
|
|
}
|
|
|
|
if len(input) == 0 {
|
|
return args, errors.New("contract creation without any data provided")
|
|
}
|
|
}
|
|
|
|
if args.Gas == nil {
|
|
// For backwards-compatibility reason, we try both input and data
|
|
// but input is preferred.
|
|
input := args.Input
|
|
if input == nil {
|
|
input = args.Data
|
|
}
|
|
|
|
callArgs := evmtypes.TransactionArgs{
|
|
From: args.From,
|
|
To: args.To,
|
|
Gas: args.Gas,
|
|
GasPrice: args.GasPrice,
|
|
MaxFeePerGas: args.MaxFeePerGas,
|
|
MaxPriorityFeePerGas: args.MaxPriorityFeePerGas,
|
|
Value: args.Value,
|
|
Data: input,
|
|
AccessList: args.AccessList,
|
|
}
|
|
|
|
blockNr := types.NewBlockNumber(big.NewInt(0))
|
|
estimated, err := b.EstimateGas(callArgs, &blockNr)
|
|
if err != nil {
|
|
return args, err
|
|
}
|
|
args.Gas = &estimated
|
|
b.logger.Debug("estimate gas usage automatically", "gas", args.Gas)
|
|
}
|
|
|
|
if args.ChainID == nil {
|
|
args.ChainID = (*hexutil.Big)(b.chainID)
|
|
}
|
|
|
|
return args, nil
|
|
}
|
|
|
|
// getAccountNonce returns the account nonce for the given account address.
|
|
// If the pending value is true, it will iterate over the mempool (pending)
|
|
// txs in order to compute and return the pending tx sequence.
|
|
// Todo: include the ability to specify a blockNumber
|
|
func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height int64, logger log.Logger) (uint64, error) {
|
|
queryClient := authtypes.NewQueryClient(b.clientCtx)
|
|
res, err := queryClient.Account(types.ContextWithHeight(height), &authtypes.QueryAccountRequest{Address: sdk.AccAddress(accAddr.Bytes()).String()})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var acc authtypes.AccountI
|
|
if err := b.clientCtx.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
nonce := acc.GetSequence()
|
|
|
|
if !pending {
|
|
return nonce, nil
|
|
}
|
|
|
|
// the account retriever doesn't include the uncommitted transactions on the nonce so we need to
|
|
// to manually add them.
|
|
pendingTxs, err := b.PendingTransactions()
|
|
if err != nil {
|
|
logger.Error("failed to fetch pending transactions", "error", err.Error())
|
|
return nonce, nil
|
|
}
|
|
|
|
// add the uncommitted txs to the nonce counter
|
|
// only supports `MsgEthereumTx` style tx
|
|
for _, tx := range pendingTxs {
|
|
for _, msg := range (*tx).GetMsgs() {
|
|
ethMsg, ok := msg.(*evmtypes.MsgEthereumTx)
|
|
if !ok {
|
|
// not ethereum tx
|
|
break
|
|
}
|
|
|
|
sender, err := ethMsg.GetSender(b.chainID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if sender == accAddr {
|
|
nonce++
|
|
}
|
|
}
|
|
}
|
|
|
|
return nonce, nil
|
|
}
|
|
|
|
// output: targetOneFeeHistory
|
|
func (b *Backend) processBlock(
|
|
tendermintBlock *tmrpctypes.ResultBlock,
|
|
ethBlock *map[string]interface{},
|
|
rewardPercentiles []float64,
|
|
tendermintBlockResult *tmrpctypes.ResultBlockResults,
|
|
targetOneFeeHistory *types.OneFeeHistory,
|
|
) error {
|
|
blockHeight := tendermintBlock.Block.Height
|
|
blockBaseFee, err := b.BaseFee(tendermintBlockResult)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// set basefee
|
|
targetOneFeeHistory.BaseFee = blockBaseFee
|
|
|
|
// set gas used ratio
|
|
gasLimitUint64, ok := (*ethBlock)["gasLimit"].(hexutil.Uint64)
|
|
if !ok {
|
|
return fmt.Errorf("invalid gas limit type: %T", (*ethBlock)["gasLimit"])
|
|
}
|
|
|
|
gasUsedBig, ok := (*ethBlock)["gasUsed"].(*hexutil.Big)
|
|
if !ok {
|
|
return fmt.Errorf("invalid gas used type: %T", (*ethBlock)["gasUsed"])
|
|
}
|
|
|
|
gasusedfloat, _ := new(big.Float).SetInt(gasUsedBig.ToInt()).Float64()
|
|
|
|
if gasLimitUint64 <= 0 {
|
|
return fmt.Errorf("gasLimit of block height %d should be bigger than 0 , current gaslimit %d", blockHeight, gasLimitUint64)
|
|
}
|
|
|
|
gasUsedRatio := gasusedfloat / float64(gasLimitUint64)
|
|
blockGasUsed := gasusedfloat
|
|
targetOneFeeHistory.GasUsedRatio = gasUsedRatio
|
|
|
|
rewardCount := len(rewardPercentiles)
|
|
targetOneFeeHistory.Reward = make([]*big.Int, rewardCount)
|
|
for i := 0; i < rewardCount; i++ {
|
|
targetOneFeeHistory.Reward[i] = big.NewInt(0)
|
|
}
|
|
|
|
// check tendermintTxs
|
|
tendermintTxs := tendermintBlock.Block.Txs
|
|
tendermintTxResults := tendermintBlockResult.TxsResults
|
|
tendermintTxCount := len(tendermintTxs)
|
|
|
|
var sorter sortGasAndReward
|
|
|
|
for i := 0; i < tendermintTxCount; i++ {
|
|
eachTendermintTx := tendermintTxs[i]
|
|
eachTendermintTxResult := tendermintTxResults[i]
|
|
|
|
tx, err := b.clientCtx.TxConfig.TxDecoder()(eachTendermintTx)
|
|
if err != nil {
|
|
b.logger.Debug("failed to decode transaction in block", "height", blockHeight, "error", err.Error())
|
|
continue
|
|
}
|
|
txGasUsed := uint64(eachTendermintTxResult.GasUsed)
|
|
for _, msg := range tx.GetMsgs() {
|
|
ethMsg, ok := msg.(*evmtypes.MsgEthereumTx)
|
|
if !ok {
|
|
continue
|
|
}
|
|
tx := ethMsg.AsTransaction()
|
|
reward := tx.EffectiveGasTipValue(blockBaseFee)
|
|
if reward == nil {
|
|
reward = big.NewInt(0)
|
|
}
|
|
sorter = append(sorter, txGasAndReward{gasUsed: txGasUsed, reward: reward})
|
|
}
|
|
}
|
|
|
|
// return an all zero row if there are no transactions to gather data from
|
|
ethTxCount := len(sorter)
|
|
if ethTxCount == 0 {
|
|
return nil
|
|
}
|
|
|
|
sort.Sort(sorter)
|
|
|
|
var txIndex int
|
|
sumGasUsed := sorter[0].gasUsed
|
|
|
|
for i, p := range rewardPercentiles {
|
|
thresholdGasUsed := uint64(blockGasUsed * p / 100)
|
|
for sumGasUsed < thresholdGasUsed && txIndex < ethTxCount-1 {
|
|
txIndex++
|
|
sumGasUsed += sorter[txIndex].gasUsed
|
|
}
|
|
targetOneFeeHistory.Reward[i] = sorter[txIndex].reward
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AllTxLogsFromEvents parses all ethereum logs from cosmos events
|
|
func AllTxLogsFromEvents(events []abci.Event) ([][]*ethtypes.Log, error) {
|
|
allLogs := make([][]*ethtypes.Log, 0, 4)
|
|
for _, event := range events {
|
|
if event.Type != evmtypes.EventTypeTxLog {
|
|
continue
|
|
}
|
|
|
|
logs, err := ParseTxLogsFromEvent(event)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allLogs = append(allLogs, logs)
|
|
}
|
|
return allLogs, nil
|
|
}
|
|
|
|
// TxLogsFromEvents parses ethereum logs from cosmos events for specific msg index
|
|
func TxLogsFromEvents(events []abci.Event, msgIndex int) ([]*ethtypes.Log, error) {
|
|
for _, event := range events {
|
|
if event.Type != evmtypes.EventTypeTxLog {
|
|
continue
|
|
}
|
|
|
|
if msgIndex > 0 {
|
|
// not the eth tx we want
|
|
msgIndex--
|
|
continue
|
|
}
|
|
|
|
return ParseTxLogsFromEvent(event)
|
|
}
|
|
return nil, fmt.Errorf("eth tx logs not found for message index %d", msgIndex)
|
|
}
|
|
|
|
// ParseTxLogsFromEvent parse tx logs from one event
|
|
func ParseTxLogsFromEvent(event abci.Event) ([]*ethtypes.Log, error) {
|
|
logs := make([]*evmtypes.Log, 0, len(event.Attributes))
|
|
for _, attr := range event.Attributes {
|
|
if !bytes.Equal(attr.Key, []byte(evmtypes.AttributeKeyTxLog)) {
|
|
continue
|
|
}
|
|
|
|
var log evmtypes.Log
|
|
if err := json.Unmarshal(attr.Value, &log); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logs = append(logs, &log)
|
|
}
|
|
return evmtypes.LogsToEthereum(logs), nil
|
|
}
|
|
|
|
// TxExceedBlockGasLimit returns true if the tx exceeds block gas limit.
|
|
func TxExceedBlockGasLimit(res *abci.ResponseDeliverTx) bool {
|
|
return strings.Contains(res.Log, ExceedBlockGasLimitError)
|
|
}
|
|
|
|
// TxSuccessOrExceedsBlockGasLimit returnsrue if the transaction was successful
|
|
// or if it failed with an ExceedBlockGasLimit error
|
|
func TxSuccessOrExceedsBlockGasLimit(res *abci.ResponseDeliverTx) bool {
|
|
return res.Code == 0 || TxExceedBlockGasLimit(res)
|
|
}
|
|
|
|
// ShouldIgnoreGasUsed returns true if the gasUsed in result should be ignored
|
|
// workaround for issue: https://github.com/cosmos/cosmos-sdk/issues/10832
|
|
func ShouldIgnoreGasUsed(res *abci.ResponseDeliverTx) bool {
|
|
return res.GetCode() == 11 && strings.Contains(res.GetLog(), "no block gas left to run tx: out of gas")
|
|
}
|
|
|
|
// GetLogsFromBlockResults returns the list of event logs from the tendermint block result response
|
|
func GetLogsFromBlockResults(blockRes *tmrpctypes.ResultBlockResults) ([][]*ethtypes.Log, error) {
|
|
blockLogs := [][]*ethtypes.Log{}
|
|
for _, txResult := range blockRes.TxsResults {
|
|
logs, err := AllTxLogsFromEvents(txResult.Events)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blockLogs = append(blockLogs, logs...)
|
|
}
|
|
|
|
return blockLogs, nil
|
|
}
|