github.com/codingfuture/orig-energi3@v0.8.4/core/energi_zerofee.go (about)

     1  // Copyright 2019 The Energi Core Authors
     2  // This file is part of the Energi Core library.
     3  //
     4  // The Energi Core library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The Energi Core library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the Energi Core library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package core
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"math/big"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/ethereum/go-ethereum/accounts/abi"
    27  	"github.com/ethereum/go-ethereum/common"
    28  	"github.com/ethereum/go-ethereum/core/types"
    29  	"github.com/ethereum/go-ethereum/core/vm"
    30  	"github.com/ethereum/go-ethereum/log"
    31  
    32  	energi_abi "energi.world/core/gen3/energi/abi"
    33  	energi_params "energi.world/core/gen3/energi/params"
    34  )
    35  
    36  const (
    37  	ZeroFeeGasLimit uint64 = 500000
    38  )
    39  
    40  var (
    41  	energiClaimID        types.MethodID
    42  	energiVerifyClaimID  types.MethodID
    43  	energiMNHeartbeatID  types.MethodID
    44  	energiMNInvalidateID types.MethodID
    45  	energiCPSignID       types.MethodID
    46  )
    47  
    48  func init() {
    49  	migration_abi, err := abi.JSON(strings.NewReader(energi_abi.Gen2MigrationABI))
    50  	if err != nil {
    51  		panic(err)
    52  	}
    53  
    54  	copy(energiClaimID[:], migration_abi.Methods["claim"].Id())
    55  	copy(energiVerifyClaimID[:], migration_abi.Methods["verifyClaim"].Id())
    56  
    57  	mnreg_abi, err := abi.JSON(strings.NewReader(energi_abi.IMasternodeRegistryV2ABI))
    58  	if err != nil {
    59  		panic(err)
    60  	}
    61  	copy(energiMNHeartbeatID[:], mnreg_abi.Methods["heartbeat"].Id())
    62  	copy(energiMNInvalidateID[:], mnreg_abi.Methods["invalidate"].Id())
    63  
    64  	cpreg_abi, err := abi.JSON(strings.NewReader(energi_abi.ICheckpointRegistryABI))
    65  	if err != nil {
    66  		panic(err)
    67  	}
    68  	copy(energiCPSignID[:], cpreg_abi.Methods["sign"].Id())
    69  }
    70  
    71  /**
    72   * SC-7: Zero-fee transactions
    73   *
    74   * Check if Energi consensus allows the transaction to be processed as
    75   * zero-fee.
    76   */
    77  func IsValidZeroFee(tx *types.Transaction) bool {
    78  	// Skip check for non-zero price
    79  	if tx.Cost().Cmp(common.Big0) != 0 {
    80  		return false
    81  	}
    82  
    83  	if tx.Gas() > ZeroFeeGasLimit {
    84  		log.Trace("Zero-fee gas is over limit", "hash", tx.Hash(), "limit", tx.Gas())
    85  		return false
    86  	}
    87  
    88  	if IsGen2Migration(tx) {
    89  		return true
    90  	}
    91  
    92  	if IsMasternodeCall(tx) {
    93  		return true
    94  	}
    95  
    96  	if IsCheckpointCall(tx) {
    97  		return true
    98  	}
    99  
   100  	return false
   101  }
   102  
   103  func IsGen2Migration(tx *types.Transaction) bool {
   104  	to := tx.To()
   105  
   106  	return (to != nil) &&
   107  		(*to == energi_params.Energi_MigrationContract) &&
   108  		(tx.MethodID() == energiClaimID)
   109  }
   110  
   111  func IsMasternodeCall(tx *types.Transaction) bool {
   112  	to := tx.To()
   113  
   114  	if (to == nil) || (*to != energi_params.Energi_MasternodeRegistry) {
   115  		return false
   116  	}
   117  
   118  	if method := tx.MethodID(); method == energiMNHeartbeatID {
   119  		return true
   120  	} else if method == energiMNInvalidateID {
   121  		return true
   122  	}
   123  
   124  	return false
   125  }
   126  
   127  func IsCheckpointCall(tx *types.Transaction) bool {
   128  	to := tx.To()
   129  
   130  	if (to == nil) || (*to != energi_params.Energi_CheckpointRegistry) {
   131  		return false
   132  	}
   133  
   134  	return tx.MethodID() == energiCPSignID
   135  }
   136  
   137  func IsBlacklisted(db vm.StateDB, addr common.Address) bool {
   138  	return db.GetState(energi_params.Energi_Blacklist, addr.Hash()) != common.Hash{}
   139  }
   140  
   141  func IsWhitelisted(db vm.StateDB, addr common.Address) bool {
   142  	return db.GetState(energi_params.Energi_Whitelist, addr.Hash()) != common.Hash{}
   143  }
   144  
   145  //=============================================================================
   146  
   147  var (
   148  	zfCleanupTimeout        = time.Minute
   149  	zfMinHeartbeatPeriod    = time.Duration(30) * time.Minute
   150  	zfMinInvalidationPeriod = time.Duration(2) * time.Minute
   151  	zfMinCoinClaimPeriod    = time.Duration(3) * time.Minute
   152  	zfMinCheckpointPeriod   = time.Duration(10) * time.Minute
   153  
   154  	ErrZeroFeeDoS = errors.New("zero-fee DoS")
   155  )
   156  
   157  type zeroFeeProtector struct {
   158  	mnHeartbeats    map[common.Address]time.Time
   159  	mnInvalidations map[common.Address]time.Time
   160  	mnCheckpoints   map[common.Address]time.Time
   161  	coinClaims      map[uint32]time.Time
   162  	nextCleanup     time.Time
   163  	timeNow         func() time.Time
   164  }
   165  
   166  func newZeroFeeProtector() *zeroFeeProtector {
   167  	return &zeroFeeProtector{
   168  		mnHeartbeats:    make(map[common.Address]time.Time),
   169  		mnInvalidations: make(map[common.Address]time.Time),
   170  		mnCheckpoints:   make(map[common.Address]time.Time),
   171  		coinClaims:      make(map[uint32]time.Time),
   172  		nextCleanup:     time.Now().Add(zfCleanupTimeout),
   173  		timeNow:         time.Now,
   174  	}
   175  }
   176  
   177  func (z *zeroFeeProtector) cleanupTimeout(
   178  	now time.Time,
   179  	timeMap map[common.Address]time.Time,
   180  	timeout time.Duration,
   181  ) {
   182  	for k, v := range timeMap {
   183  		if now.Sub(v) > timeout {
   184  			delete(timeMap, k)
   185  		}
   186  	}
   187  }
   188  
   189  func (z *zeroFeeProtector) cleanupBySender(
   190  	sender common.Address,
   191  	timeMap map[common.Address]time.Time,
   192  ) {
   193  	if _, ok := timeMap[sender]; ok {
   194  		delete(timeMap, sender)
   195  	}
   196  }
   197  
   198  func (z *zeroFeeProtector) cleanupAllBySender(sender common.Address) {
   199  	z.cleanupBySender(sender, z.mnHeartbeats)
   200  	z.cleanupBySender(sender, z.mnInvalidations)
   201  	z.cleanupBySender(sender, z.mnCheckpoints)
   202  }
   203  
   204  func (z *zeroFeeProtector) cleanupAllByTimeout(now time.Time) {
   205  	if z.nextCleanup.After(now) {
   206  		return
   207  	}
   208  
   209  	z.nextCleanup = now.Add(zfCleanupTimeout)
   210  	//---
   211  
   212  	z.cleanupTimeout(now, z.mnHeartbeats, zfMinHeartbeatPeriod)
   213  	z.cleanupTimeout(now, z.mnInvalidations, zfMinInvalidationPeriod)
   214  	z.cleanupTimeout(now, z.mnCheckpoints, zfMinCheckpointPeriod)
   215  
   216  	for k, v := range z.coinClaims {
   217  		if now.Sub(v) > zfMinCoinClaimPeriod {
   218  			delete(z.coinClaims, k)
   219  		}
   220  	}
   221  }
   222  
   223  func (z *zeroFeeProtector) checkMasternode(
   224  	pool *TxPool,
   225  	sender common.Address,
   226  	now time.Time,
   227  	timeMap map[common.Address]time.Time,
   228  	timeout time.Duration,
   229  ) error {
   230  	if v, ok := timeMap[sender]; ok && now.Sub(v) < timeout {
   231  		log.Debug("ZeroFee DoS by time", "sender", sender, "interval", now.Sub(v))
   232  		return ErrZeroFeeDoS
   233  	}
   234  
   235  	// NOTE: potential issue with nonce gap
   236  	mn_indicator := pool.currentState.GetState(
   237  		energi_params.Energi_MasternodeList, sender.Hash())
   238  	if (mn_indicator == common.Hash{}) {
   239  		log.Debug("ZeroFee DoS by inactive MN", "sender", sender)
   240  		return ErrZeroFeeDoS
   241  	}
   242  
   243  	timeMap[sender] = now
   244  	log.Debug("ZeroFee masternode", "sender", sender, "now", now)
   245  	return nil
   246  }
   247  
   248  func (z *zeroFeeProtector) checkMigration(
   249  	pool *TxPool,
   250  	sender common.Address,
   251  	now time.Time,
   252  	tx *types.Transaction,
   253  ) error {
   254  	callData := tx.Data()
   255  	if len(callData) <= 36 {
   256  		log.Debug("Missing tx data")
   257  		return fmt.Errorf("invalid tx: missing tx data length")
   258  	}
   259  
   260  	item_id := uint32(new(big.Int).SetBytes(callData[4:36]).Uint64())
   261  
   262  	if v, ok := z.coinClaims[item_id]; ok && now.Sub(v) < zfMinCoinClaimPeriod {
   263  		log.Debug("ZeroFee DoS by time", "item_id", item_id, "interval", now.Sub(v))
   264  		return ErrZeroFeeDoS
   265  	}
   266  
   267  	// Check if call is valid
   268  	//---
   269  	copy(callData[:], energiVerifyClaimID[:])
   270  
   271  	msg := types.NewMessage(
   272  		sender,
   273  		tx.To(),
   274  		tx.Nonce(),
   275  		tx.Value(),
   276  		tx.Gas(),
   277  		tx.GasPrice(),
   278  		callData,
   279  		false,
   280  	)
   281  
   282  	// Just in case: safety measure
   283  	statedb := pool.currentState.Copy()
   284  
   285  	bc, ok := pool.chain.(*BlockChain)
   286  	if bc == nil || !ok {
   287  		log.Debug("ZeroFee DoS on missing blockchain")
   288  		return ErrZeroFeeDoS
   289  	}
   290  	vmc := bc.GetVMConfig()
   291  	ctx := NewEVMContext(msg, bc.CurrentHeader(), bc, &sender)
   292  	ctx.GasLimit = ZeroFeeGasLimit
   293  	evm := vm.NewEVM(ctx, statedb, bc.Config(), *vmc)
   294  
   295  	gp := new(GasPool).AddGas(tx.Gas())
   296  	output, _, failed, err := ApplyMessage(evm, msg, gp)
   297  	if failed || err != nil {
   298  		log.Debug("ZeroFee DoS by execution",
   299  			"item", item_id, "err", err, "output", output)
   300  		return ErrZeroFeeDoS
   301  	}
   302  
   303  	if len(output) != len(common.Hash{}) {
   304  		log.Debug("ZeroFee DoS by unpack", "item", item_id, "output", output)
   305  		return ErrZeroFeeDoS
   306  	}
   307  
   308  	amount := new(big.Int).SetBytes(output)
   309  
   310  	if amount.Cmp(common.Big0) <= 0 {
   311  		log.Debug("ZeroFee DoS by already claimed", "item", item_id)
   312  		return ErrZeroFeeDoS
   313  	}
   314  
   315  	//---
   316  	z.coinClaims[item_id] = now
   317  	log.Debug("ZeroFee migration", "item_id", item_id, "now", now)
   318  	return nil
   319  }
   320  
   321  func (z *zeroFeeProtector) checkDoS(pool *TxPool, tx *types.Transaction) error {
   322  	now := z.timeNow()
   323  
   324  	defer z.cleanupAllByTimeout(now)
   325  
   326  	sender, err := types.Sender(pool.signer, tx)
   327  	if err != nil {
   328  		log.Debug("ZeroFee DoS sender error", "err", err)
   329  		return err
   330  	}
   331  
   332  	// NOTE: assumed to be called only on zero fee
   333  	if method := tx.MethodID(); method == energiMNHeartbeatID {
   334  		return z.checkMasternode(pool, sender, now, z.mnHeartbeats, zfMinHeartbeatPeriod)
   335  	} else if method == energiMNInvalidateID {
   336  		return z.checkMasternode(pool, sender, now, z.mnInvalidations, zfMinInvalidationPeriod)
   337  	} else if method == energiClaimID {
   338  		return z.checkMigration(pool, sender, now, tx)
   339  	} else if method == energiCPSignID {
   340  		return z.checkMasternode(pool, sender, now, z.mnCheckpoints, zfMinCheckpointPeriod)
   341  	}
   342  	return nil
   343  }