github.com/klaytn/klaytn@v1.10.2/reward/staking_manager.go (about)

     1  // Copyright 2019 The klaytn Authors
     2  // This file is part of the klaytn library.
     3  //
     4  // The klaytn 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 klaytn 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 klaytn library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package reward
    18  
    19  import (
    20  	"errors"
    21  	"sync"
    22  
    23  	"github.com/klaytn/klaytn/blockchain"
    24  	"github.com/klaytn/klaytn/blockchain/state"
    25  	"github.com/klaytn/klaytn/blockchain/types"
    26  	"github.com/klaytn/klaytn/common"
    27  	"github.com/klaytn/klaytn/event"
    28  	"github.com/klaytn/klaytn/params"
    29  )
    30  
    31  const (
    32  	chainHeadChanSize = 100
    33  )
    34  
    35  // blockChain is an interface for blockchain.Blockchain used in reward package.
    36  type blockChain interface {
    37  	SubscribeChainHeadEvent(ch chan<- blockchain.ChainHeadEvent) event.Subscription
    38  	GetBlockByNumber(number uint64) *types.Block
    39  	StateAt(root common.Hash) (*state.StateDB, error)
    40  	Config() *params.ChainConfig
    41  
    42  	blockchain.ChainContext
    43  }
    44  
    45  type StakingManager struct {
    46  	addressBookConnector *addressBookConnector
    47  	stakingInfoCache     *stakingInfoCache
    48  	stakingInfoDB        stakingInfoDB
    49  	governanceHelper     governanceHelper
    50  	blockchain           blockChain
    51  	chainHeadChan        chan blockchain.ChainHeadEvent
    52  	chainHeadSub         event.Subscription
    53  }
    54  
    55  var (
    56  	// variables for sole StakingManager
    57  	once           sync.Once
    58  	stakingManager *StakingManager
    59  
    60  	// errors for staking manager
    61  	ErrStakingManagerNotSet = errors.New("staking manager is not set")
    62  	ErrChainHeadChanNotSet  = errors.New("chain head channel is not set")
    63  )
    64  
    65  // NewStakingManager creates and returns StakingManager.
    66  //
    67  // On the first call, a StakingManager is created with given parameters.
    68  // From next calls, the existing StakingManager is returned. (Parameters
    69  // from the next calls will not affect.)
    70  func NewStakingManager(bc blockChain, gh governanceHelper, db stakingInfoDB) *StakingManager {
    71  	if bc != nil && gh != nil {
    72  		// this is only called once
    73  		once.Do(func() {
    74  			stakingManager = &StakingManager{
    75  				addressBookConnector: newAddressBookConnector(bc, gh),
    76  				stakingInfoCache:     newStakingInfoCache(),
    77  				stakingInfoDB:        db,
    78  				governanceHelper:     gh,
    79  				blockchain:           bc,
    80  				chainHeadChan:        make(chan blockchain.ChainHeadEvent, chainHeadChanSize),
    81  			}
    82  
    83  			// Before migration, staking information of current and before should be stored in DB.
    84  			//
    85  			// Staking information from block of StakingUpdateInterval ahead is needed to create a block.
    86  			// If there is no staking info in either cache, db or state trie, the node cannot make a block.
    87  			// The information in state trie is deleted after state trie migration.
    88  			blockchain.RegisterMigrationPrerequisites(func(blockNum uint64) error {
    89  				if err := CheckStakingInfoStored(blockNum); err != nil {
    90  					return err
    91  				}
    92  				return CheckStakingInfoStored(blockNum + params.StakingUpdateInterval())
    93  			})
    94  		})
    95  	} else {
    96  		logger.Error("unable to set StakingManager", "blockchain", bc, "governanceHelper", gh)
    97  	}
    98  
    99  	return stakingManager
   100  }
   101  
   102  func GetStakingManager() *StakingManager {
   103  	return stakingManager
   104  }
   105  
   106  // GetStakingInfo returns a stakingInfo on the staking block of the given block number.
   107  // Note that staking block is the block on which the associated staking information is stored and used during an interval.
   108  func GetStakingInfo(blockNum uint64) *StakingInfo {
   109  	stakingBlockNumber := params.CalcStakingBlockNumber(blockNum)
   110  	logger.Debug("Staking information is requested", "blockNum", blockNum, "staking block number", stakingBlockNumber)
   111  	return GetStakingInfoOnStakingBlock(stakingBlockNumber)
   112  }
   113  
   114  // GetStakingInfoOnStakingBlock returns a corresponding StakingInfo for a staking block number.
   115  // If the given number is not on the staking block, it returns nil.
   116  //
   117  // Fixup for Gini coefficients:
   118  // Klaytn core stores Gini: -1 in its database.
   119  // We ensure GetStakingInfoOnStakingBlock() to always return meaningful Gini.
   120  // - If cache hit                               -> fillMissingGini -> modifies cached in-memory object
   121  // - If db hit                                  -> fillMissingGini -> write to cache
   122  // - If read contract -> write to db (gini: -1) -> fillMissingGini -> write to cache
   123  func GetStakingInfoOnStakingBlock(stakingBlockNumber uint64) *StakingInfo {
   124  	if stakingManager == nil {
   125  		logger.Error("unable to GetStakingInfo", "err", ErrStakingManagerNotSet)
   126  		return nil
   127  	}
   128  
   129  	// shortcut if given block is not on staking update interval
   130  	if !params.IsStakingUpdateInterval(stakingBlockNumber) {
   131  		return nil
   132  	}
   133  
   134  	// Get staking info from cache
   135  	if cachedStakingInfo := stakingManager.stakingInfoCache.get(stakingBlockNumber); cachedStakingInfo != nil {
   136  		logger.Debug("StakingInfoCache hit.", "staking block number", stakingBlockNumber, "stakingInfo", cachedStakingInfo)
   137  		// Fill in Gini coeff if not set. Modifies the cached object.
   138  		if err := fillMissingGiniCoefficient(cachedStakingInfo, stakingBlockNumber); err != nil {
   139  			logger.Warn("Cannot fill in gini coefficient", "staking block number", stakingBlockNumber, "err", err)
   140  		}
   141  		return cachedStakingInfo
   142  	}
   143  
   144  	// Get staking info from DB
   145  	if storedStakingInfo, err := getStakingInfoFromDB(stakingBlockNumber); storedStakingInfo != nil && err == nil {
   146  		logger.Debug("StakingInfoDB hit.", "staking block number", stakingBlockNumber, "stakingInfo", storedStakingInfo)
   147  		// Fill in Gini coeff before adding to cache.
   148  		if err := fillMissingGiniCoefficient(storedStakingInfo, stakingBlockNumber); err != nil {
   149  			logger.Warn("Cannot fill in gini coefficient", "staking block number", stakingBlockNumber, "err", err)
   150  		}
   151  		stakingManager.stakingInfoCache.add(storedStakingInfo)
   152  		return storedStakingInfo
   153  	} else {
   154  		logger.Debug("failed to get stakingInfo from DB", "err", err, "staking block number", stakingBlockNumber)
   155  	}
   156  
   157  	// Calculate staking info from block header and updates it to cache and db
   158  	calcStakingInfo, err := updateStakingInfo(stakingBlockNumber)
   159  	if calcStakingInfo == nil {
   160  		logger.Error("failed to update stakingInfo", "staking block number", stakingBlockNumber, "err", err)
   161  		return nil
   162  	}
   163  
   164  	logger.Debug("Get stakingInfo from header.", "staking block number", stakingBlockNumber, "stakingInfo", calcStakingInfo)
   165  	return calcStakingInfo
   166  }
   167  
   168  // updateStakingInfo updates staking info in cache and db created from given block number.
   169  func updateStakingInfo(blockNum uint64) (*StakingInfo, error) {
   170  	if stakingManager == nil {
   171  		return nil, ErrStakingManagerNotSet
   172  	}
   173  
   174  	stakingInfo, err := stakingManager.addressBookConnector.getStakingInfoFromAddressBook(blockNum)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	// Add to DB before setting Gini; DB will contain {Gini: -1}
   180  	if err := AddStakingInfoToDB(stakingInfo); err != nil {
   181  		logger.Debug("failed to write staking info to db", "err", err, "stakingInfo", stakingInfo)
   182  		return stakingInfo, err
   183  	}
   184  
   185  	// Fill in Gini coeff before adding to cache
   186  	if err := fillMissingGiniCoefficient(stakingInfo, blockNum); err != nil {
   187  		logger.Warn("Cannot fill in gini coefficient", "blockNum", blockNum, "err", err)
   188  	}
   189  
   190  	// Add to cache after setting Gini
   191  	stakingManager.stakingInfoCache.add(stakingInfo)
   192  
   193  	logger.Info("Add a new stakingInfo to stakingInfoCache and stakingInfoDB", "blockNum", blockNum)
   194  	logger.Debug("Added stakingInfo", "stakingInfo", stakingInfo)
   195  	return stakingInfo, nil
   196  }
   197  
   198  // CheckStakingInfoStored makes sure the given staking info is stored in cache and DB
   199  func CheckStakingInfoStored(blockNum uint64) error {
   200  	if stakingManager == nil {
   201  		return ErrStakingManagerNotSet
   202  	}
   203  
   204  	stakingBlockNumber := params.CalcStakingBlockNumber(blockNum)
   205  
   206  	// skip checking if staking info is stored in DB
   207  	if _, err := getStakingInfoFromDB(stakingBlockNumber); err == nil {
   208  		return nil
   209  	}
   210  
   211  	// update staking info in DB and cache from address book
   212  	_, err := updateStakingInfo(stakingBlockNumber)
   213  	return err
   214  }
   215  
   216  // Fill in StakingInfo.Gini value if not set.
   217  func fillMissingGiniCoefficient(stakingInfo *StakingInfo, number uint64) error {
   218  	if !stakingInfo.UseGini {
   219  		return nil
   220  	}
   221  	if stakingInfo.Gini >= 0 {
   222  		return nil
   223  	}
   224  
   225  	// We reach here if UseGini == true && Gini == -1. There are two such cases.
   226  	// - Gini was never been calculated, so it is DefaultGiniCoefficient.
   227  	// - Gini was calculated but there was no eligible node, so Gini = -1.
   228  	// For the second case, in theory we won't have to recalculalte Gini,
   229  	// but there is no way to distinguish both. So we just recalculate.
   230  	pset, err := stakingManager.governanceHelper.EffectiveParams(number)
   231  	if err != nil {
   232  		return err
   233  	}
   234  	minStaking := pset.MinimumStakeBig().Uint64()
   235  
   236  	c := stakingInfo.GetConsolidatedStakingInfo()
   237  	if c == nil {
   238  		return errors.New("Cannot create ConsolidatedStakingInfo")
   239  	}
   240  
   241  	stakingInfo.Gini = c.CalcGiniCoefficientMinStake(minStaking)
   242  	logger.Debug("Calculated missing Gini for stored StakingInfo", "number", number, "gini", stakingInfo.Gini)
   243  	return nil
   244  }
   245  
   246  // StakingManagerSubscribe setups a channel to listen chain head event and starts a goroutine to update staking cache.
   247  func StakingManagerSubscribe() {
   248  	if stakingManager == nil {
   249  		logger.Warn("unable to subscribe; this can slow down node", "err", ErrStakingManagerNotSet)
   250  		return
   251  	}
   252  
   253  	stakingManager.chainHeadSub = stakingManager.blockchain.SubscribeChainHeadEvent(stakingManager.chainHeadChan)
   254  
   255  	go handleChainHeadEvent()
   256  }
   257  
   258  func handleChainHeadEvent() {
   259  	if stakingManager == nil {
   260  		logger.Warn("unable to start chain head event", "err", ErrStakingManagerNotSet)
   261  		return
   262  	} else if stakingManager.chainHeadSub == nil {
   263  		logger.Info("unable to start chain head event", "err", ErrChainHeadChanNotSet)
   264  		return
   265  	}
   266  
   267  	defer StakingManagerUnsubscribe()
   268  
   269  	logger.Info("Start listening chain head event to update stakingInfoCache.")
   270  
   271  	for {
   272  		// A real event arrived, process interesting content
   273  		select {
   274  		// Handle ChainHeadEvent
   275  		case ev := <-stakingManager.chainHeadChan:
   276  			pset, err := stakingManager.governanceHelper.EffectiveParams(ev.Block.NumberU64() + 1)
   277  			if err != nil {
   278  				logger.Error("unable to fetch parameters at", "blockNum", ev.Block.NumberU64()+1)
   279  				continue
   280  			}
   281  			if pset.Policy() == params.WeightedRandom {
   282  				// check and update if staking info is not valid before for the next update interval blocks
   283  				stakingInfo := GetStakingInfo(ev.Block.NumberU64() + pset.StakeUpdateInterval())
   284  				if stakingInfo == nil {
   285  					logger.Error("unable to fetch staking info", "blockNum", ev.Block.NumberU64())
   286  				}
   287  			}
   288  		case <-stakingManager.chainHeadSub.Err():
   289  			return
   290  		}
   291  	}
   292  }
   293  
   294  // StakingManagerUnsubscribe can unsubscribe a subscription on chain head event.
   295  func StakingManagerUnsubscribe() {
   296  	if stakingManager == nil {
   297  		logger.Warn("unable to start chain head event", "err", ErrStakingManagerNotSet)
   298  		return
   299  	} else if stakingManager.chainHeadSub == nil {
   300  		logger.Info("unable to start chain head event", "err", ErrChainHeadChanNotSet)
   301  		return
   302  	}
   303  
   304  	stakingManager.chainHeadSub.Unsubscribe()
   305  }
   306  
   307  // TODO-Klaytn-Reward the following methods are used for testing purpose, it needs to be moved into test files.
   308  // Unlike NewStakingManager(), SetTestStakingManager*() do not trigger once.Do().
   309  // This way you can avoid irreversible side effects during tests.
   310  
   311  // SetTestStakingManagerWithChain sets a full-featured staking manager with blockchain, database and cache.
   312  // Note that this method is used only for testing purpose.
   313  func SetTestStakingManagerWithChain(bc blockChain, gh governanceHelper, db stakingInfoDB) {
   314  	SetTestStakingManager(&StakingManager{
   315  		addressBookConnector: newAddressBookConnector(bc, gh),
   316  		stakingInfoCache:     newStakingInfoCache(),
   317  		stakingInfoDB:        db,
   318  		governanceHelper:     gh,
   319  		blockchain:           bc,
   320  		chainHeadChan:        make(chan blockchain.ChainHeadEvent, chainHeadChanSize),
   321  	})
   322  }
   323  
   324  // SetTestStakingManagerWithDB sets the staking manager with the given database.
   325  // Note that this method is used only for testing purpose.
   326  func SetTestStakingManagerWithDB(testDB stakingInfoDB) {
   327  	SetTestStakingManager(&StakingManager{
   328  		stakingInfoDB: testDB,
   329  	})
   330  }
   331  
   332  // SetTestStakingManagerWithStakingInfoCache sets the staking manager with the given test staking information.
   333  // Note that this method is used only for testing purpose.
   334  func SetTestStakingManagerWithStakingInfoCache(testInfo *StakingInfo) {
   335  	cache := newStakingInfoCache()
   336  	cache.add(testInfo)
   337  	SetTestStakingManager(&StakingManager{
   338  		stakingInfoCache: cache,
   339  	})
   340  }
   341  
   342  // SetTestStakingManager sets the staking manager for testing purpose.
   343  // Note that this method is used only for testing purpose.
   344  func SetTestStakingManager(sm *StakingManager) {
   345  	stakingManager = sm
   346  }