github.com/klaytn/klaytn@v1.10.2/consensus/istanbul/core/core.go (about)

     1  // Modifications Copyright 2018 The klaytn Authors
     2  // Copyright 2017 The go-ethereum Authors
     3  // This file is part of the go-ethereum library.
     4  //
     5  // The go-ethereum library is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Lesser General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // The go-ethereum library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  // GNU Lesser General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public License
    16  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    17  //
    18  // This file is derived from quorum/consensus/istanbul/core/core.go (2018/06/04).
    19  // Modified and improved for the klaytn development.
    20  
    21  package core
    22  
    23  import (
    24  	"bytes"
    25  	"math"
    26  	"math/big"
    27  	"sync"
    28  	"sync/atomic"
    29  	"time"
    30  
    31  	"github.com/klaytn/klaytn/blockchain/types"
    32  	"github.com/klaytn/klaytn/common"
    33  	"github.com/klaytn/klaytn/common/prque"
    34  	"github.com/klaytn/klaytn/consensus/istanbul"
    35  	"github.com/klaytn/klaytn/event"
    36  	"github.com/klaytn/klaytn/log"
    37  	"github.com/rcrowley/go-metrics"
    38  )
    39  
    40  var logger = log.NewModuleLogger(log.ConsensusIstanbulCore)
    41  
    42  // New creates an Istanbul consensus core
    43  func New(backend istanbul.Backend, config *istanbul.Config) Engine {
    44  	c := &core{
    45  		config:             config,
    46  		address:            backend.Address(),
    47  		state:              StateAcceptRequest,
    48  		handlerWg:          new(sync.WaitGroup),
    49  		logger:             logger.NewWith("address", backend.Address()),
    50  		backend:            backend,
    51  		backlogs:           make(map[common.Address]*prque.Prque),
    52  		backlogsMu:         new(sync.Mutex),
    53  		pendingRequests:    prque.New(),
    54  		pendingRequestsMu:  new(sync.Mutex),
    55  		consensusTimestamp: time.Time{},
    56  
    57  		roundMeter:         metrics.NewRegisteredMeter("consensus/istanbul/core/round", nil),
    58  		currentRoundGauge:  metrics.NewRegisteredGauge("consensus/istanbul/core/currentRound", nil),
    59  		sequenceMeter:      metrics.NewRegisteredMeter("consensus/istanbul/core/sequence", nil),
    60  		consensusTimeGauge: metrics.NewRegisteredGauge("consensus/istanbul/core/timer", nil),
    61  		councilSizeGauge:   metrics.NewRegisteredGauge("consensus/istanbul/core/councilSize", nil),
    62  		committeeSizeGauge: metrics.NewRegisteredGauge("consensus/istanbul/core/committeeSize", nil),
    63  		hashLockGauge:      metrics.NewRegisteredGauge("consensus/istanbul/core/hashLock", nil),
    64  	}
    65  	c.validateFn = c.checkValidatorSignature
    66  	return c
    67  }
    68  
    69  // ----------------------------------------------------------------------------
    70  
    71  type core struct {
    72  	config  *istanbul.Config
    73  	address common.Address
    74  	state   State
    75  	logger  log.Logger
    76  
    77  	backend               istanbul.Backend
    78  	events                *event.TypeMuxSubscription
    79  	finalCommittedSub     *event.TypeMuxSubscription
    80  	timeoutSub            *event.TypeMuxSubscription
    81  	futurePreprepareTimer *time.Timer
    82  
    83  	valSet                istanbul.ValidatorSet
    84  	waitingForRoundChange bool
    85  	validateFn            func([]byte, []byte) (common.Address, error)
    86  
    87  	backlogs   map[common.Address]*prque.Prque
    88  	backlogsMu *sync.Mutex
    89  
    90  	current   *roundState
    91  	handlerWg *sync.WaitGroup
    92  
    93  	roundChangeSet    *roundChangeSet
    94  	roundChangeTimer  atomic.Value //*time.Timer
    95  	pendingRequests   *prque.Prque
    96  	pendingRequestsMu *sync.Mutex
    97  
    98  	consensusTimestamp time.Time
    99  	// the meter to record the round change rate
   100  	roundMeter metrics.Meter
   101  	// the gauge to record the current round
   102  	currentRoundGauge metrics.Gauge
   103  	// the meter to record the sequence update rate
   104  	sequenceMeter metrics.Meter
   105  	// the gauge to record consensus duration (from accepting a preprepare to final committed stage)
   106  	consensusTimeGauge metrics.Gauge
   107  	// the gauge to record hashLock status (1 if hash-locked. 0 otherwise)
   108  	hashLockGauge metrics.Gauge
   109  
   110  	councilSizeGauge   metrics.Gauge
   111  	committeeSizeGauge metrics.Gauge
   112  }
   113  
   114  func (c *core) finalizeMessage(msg *message) ([]byte, error) {
   115  	var err error
   116  	// Add sender address
   117  	msg.Address = c.Address()
   118  
   119  	// Add proof of consensus
   120  	msg.CommittedSeal = []byte{}
   121  	// Assign the CommittedSeal if it's a COMMIT message and proposal is not nil
   122  	if msg.Code == msgCommit && c.current.Proposal() != nil {
   123  		seal := PrepareCommittedSeal(c.current.Proposal().Hash())
   124  		msg.CommittedSeal, err = c.backend.Sign(seal)
   125  		if err != nil {
   126  			return nil, err
   127  		}
   128  	}
   129  
   130  	// Sign message
   131  	data, err := msg.PayloadNoSig()
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	msg.Signature, err = c.backend.Sign(data)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	// Convert to payload
   141  	payload, err := msg.Payload()
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	return payload, nil
   147  }
   148  
   149  func (c *core) broadcast(msg *message) {
   150  	logger := c.logger.NewWith("state", c.state)
   151  
   152  	payload, err := c.finalizeMessage(msg)
   153  	if err != nil {
   154  		logger.Error("Failed to finalize message", "msg", msg, "err", err)
   155  		return
   156  	}
   157  
   158  	// Broadcast payload
   159  	if err = c.backend.Broadcast(msg.Hash, c.valSet, payload); err != nil {
   160  		logger.Error("Failed to broadcast message", "msg", msg, "err", err)
   161  		return
   162  	}
   163  }
   164  
   165  func (c *core) currentView() *istanbul.View {
   166  	return &istanbul.View{
   167  		Sequence: new(big.Int).Set(c.current.Sequence()),
   168  		Round:    new(big.Int).Set(c.current.Round()),
   169  	}
   170  }
   171  
   172  func (c *core) isProposer() bool {
   173  	v := c.valSet
   174  	if v == nil {
   175  		return false
   176  	}
   177  	return v.IsProposer(c.backend.Address())
   178  }
   179  
   180  func (c *core) commit() {
   181  	c.setState(StateCommitted)
   182  
   183  	proposal := c.current.Proposal()
   184  	if proposal != nil {
   185  		committedSeals := make([][]byte, c.current.Commits.Size())
   186  		for i, v := range c.current.Commits.Values() {
   187  			committedSeals[i] = make([]byte, types.IstanbulExtraSeal)
   188  			copy(committedSeals[i][:], v.CommittedSeal[:])
   189  		}
   190  
   191  		if err := c.backend.Commit(proposal, committedSeals); err != nil {
   192  			c.current.UnlockHash() // Unlock block when insertion fails
   193  			c.sendNextRoundChange("commit failure")
   194  			return
   195  		}
   196  	} else {
   197  		// TODO-Klaytn never happen, but if proposal is nil, mining is not working.
   198  		logger.Error("istanbul.core current.Proposal is NULL")
   199  		c.current.UnlockHash() // Unlock block when insertion fails
   200  		c.sendNextRoundChange("commit failure. proposal is nil")
   201  		return
   202  	}
   203  }
   204  
   205  // startNewRound starts a new round. if round equals to 0, it means to starts a new sequence
   206  func (c *core) startNewRound(round *big.Int) {
   207  	var logger log.Logger
   208  	if c.current == nil {
   209  		logger = c.logger.NewWith("old_round", -1, "old_seq", 0)
   210  	} else {
   211  		logger = c.logger.NewWith("old_round", c.current.Round(), "old_seq", c.current.Sequence())
   212  	}
   213  
   214  	roundChange := false
   215  	// Try to get last proposal
   216  	lastProposal, lastProposer := c.backend.LastProposal()
   217  	//if c.valSet != nil && c.valSet.IsSubSet() {
   218  	//	c.current = nil
   219  	//} else {
   220  	if c.current == nil {
   221  		logger.Trace("Start to the initial round")
   222  	} else if lastProposal.Number().Cmp(c.current.Sequence()) >= 0 {
   223  		diff := new(big.Int).Sub(lastProposal.Number(), c.current.Sequence())
   224  		c.sequenceMeter.Mark(new(big.Int).Add(diff, common.Big1).Int64())
   225  
   226  		if !c.consensusTimestamp.IsZero() {
   227  			c.consensusTimeGauge.Update(int64(time.Since(c.consensusTimestamp)))
   228  			c.consensusTimestamp = time.Time{}
   229  		}
   230  		logger.Trace("Catch up latest proposal", "number", lastProposal.Number().Uint64(), "hash", lastProposal.Hash())
   231  	} else if lastProposal.Number().Cmp(big.NewInt(c.current.Sequence().Int64()-1)) == 0 {
   232  		if round.Cmp(common.Big0) == 0 {
   233  			// same seq and round, don't need to start new round
   234  			return
   235  		} else if round.Cmp(c.current.Round()) < 0 {
   236  			logger.Warn("New round should not be smaller than current round", "seq", lastProposal.Number().Int64(), "new_round", round, "old_round", c.current.Round())
   237  			return
   238  		}
   239  		roundChange = true
   240  	} else {
   241  		logger.Warn("New sequence should be larger than current sequence", "new_seq", lastProposal.Number().Int64())
   242  		return
   243  	}
   244  	//}
   245  
   246  	var newView *istanbul.View
   247  	if roundChange {
   248  		newView = &istanbul.View{
   249  			Sequence: new(big.Int).Set(c.current.Sequence()),
   250  			Round:    new(big.Int).Set(round),
   251  		}
   252  	} else {
   253  		newView = &istanbul.View{
   254  			Sequence: new(big.Int).Add(lastProposal.Number(), common.Big1),
   255  			Round:    new(big.Int),
   256  		}
   257  		c.valSet = c.backend.Validators(lastProposal)
   258  
   259  		councilSize := int64(c.valSet.Size())
   260  		committeeSize := int64(c.valSet.SubGroupSize())
   261  		if committeeSize > councilSize {
   262  			committeeSize = councilSize
   263  		}
   264  		c.councilSizeGauge.Update(councilSize)
   265  		c.committeeSizeGauge.Update(committeeSize)
   266  	}
   267  	c.backend.SetCurrentView(newView)
   268  
   269  	// Update logger
   270  	logger = logger.NewWith("old_proposer", c.valSet.GetProposer())
   271  	// Clear invalid ROUND CHANGE messages
   272  	c.roundChangeSet = newRoundChangeSet(c.valSet)
   273  	// New snapshot for new round
   274  	c.updateRoundState(newView, c.valSet, roundChange)
   275  	// Calculate new proposer
   276  	c.valSet.CalcProposer(lastProposer, newView.Round.Uint64())
   277  	c.waitingForRoundChange = false
   278  	c.setState(StateAcceptRequest)
   279  	if roundChange && c.isProposer() && c.current != nil {
   280  		// If it is locked, propose the old proposal
   281  		// If we have pending request, propose pending request
   282  		if c.current.IsHashLocked() {
   283  			r := &istanbul.Request{
   284  				Proposal: c.current.Proposal(), // c.current.Proposal would be the locked proposal by previous proposer, see updateRoundState
   285  			}
   286  			c.sendPreprepare(r)
   287  		} else if c.current.pendingRequest != nil {
   288  			c.sendPreprepare(c.current.pendingRequest)
   289  		}
   290  	}
   291  	c.newRoundChangeTimer()
   292  
   293  	logger.Debug("New round", "new_round", newView.Round, "new_seq", newView.Sequence, "new_proposer", c.valSet.GetProposer(), "isProposer", c.isProposer())
   294  	logger.Trace("New round", "new_round", newView.Round, "new_seq", newView.Sequence, "size", c.valSet.Size(), "valSet", c.valSet.List())
   295  }
   296  
   297  func (c *core) catchUpRound(view *istanbul.View) {
   298  	logger := c.logger.NewWith("old_round", c.current.Round(), "old_seq", c.current.Sequence(), "old_proposer", c.valSet.GetProposer())
   299  
   300  	if view.Round.Cmp(c.current.Round()) > 0 {
   301  		c.roundMeter.Mark(new(big.Int).Sub(view.Round, c.current.Round()).Int64())
   302  	}
   303  	c.waitingForRoundChange = true
   304  
   305  	// Need to keep block locked for round catching up
   306  	c.updateRoundState(view, c.valSet, true)
   307  	c.roundChangeSet.Clear(view.Round)
   308  
   309  	c.newRoundChangeTimer()
   310  	logger.Warn("[RC] Catch up round", "new_round", view.Round, "new_seq", view.Sequence, "new_proposer", c.valSet.GetProposer())
   311  }
   312  
   313  // updateRoundState updates round state by checking if locking block is necessary
   314  func (c *core) updateRoundState(view *istanbul.View, validatorSet istanbul.ValidatorSet, roundChange bool) {
   315  	// Lock only if both roundChange is true and it is locked
   316  	if roundChange && c.current != nil {
   317  		if c.current.IsHashLocked() {
   318  			c.current = newRoundState(view, validatorSet, c.current.GetLockedHash(), c.current.Preprepare, c.current.pendingRequest, c.backend.HasBadProposal)
   319  		} else {
   320  			c.current = newRoundState(view, validatorSet, common.Hash{}, nil, c.current.pendingRequest, c.backend.HasBadProposal)
   321  		}
   322  	} else {
   323  		c.current = newRoundState(view, validatorSet, common.Hash{}, nil, nil, c.backend.HasBadProposal)
   324  	}
   325  	c.currentRoundGauge.Update(c.current.round.Int64())
   326  	if c.current.IsHashLocked() {
   327  		c.hashLockGauge.Update(1)
   328  	} else {
   329  		c.hashLockGauge.Update(0)
   330  	}
   331  }
   332  
   333  func (c *core) setState(state State) {
   334  	if c.state != state {
   335  		c.state = state
   336  	}
   337  	if state == StateAcceptRequest {
   338  		c.processPendingRequests()
   339  	}
   340  	c.processBacklog()
   341  }
   342  
   343  func (c *core) Address() common.Address {
   344  	return c.address
   345  }
   346  
   347  func (c *core) stopFuturePreprepareTimer() {
   348  	if c.futurePreprepareTimer != nil {
   349  		c.futurePreprepareTimer.Stop()
   350  	}
   351  }
   352  
   353  func (c *core) stopTimer() {
   354  	c.stopFuturePreprepareTimer()
   355  
   356  	if c.roundChangeTimer.Load() != nil {
   357  		c.roundChangeTimer.Load().(*time.Timer).Stop()
   358  	}
   359  }
   360  
   361  func (c *core) newRoundChangeTimer() {
   362  	c.stopTimer()
   363  
   364  	// TODO-Klaytn-Istanbul: Replace &istanbul.DefaultConfig.Timeout to c.config.Timeout
   365  	// set timeout based on the round number
   366  	timeout := time.Duration(atomic.LoadUint64(&istanbul.DefaultConfig.Timeout)) * time.Millisecond
   367  	round := c.current.Round().Uint64()
   368  	if round > 0 {
   369  		timeout += time.Duration(math.Pow(2, float64(round))) * time.Second
   370  	}
   371  
   372  	current := c.current
   373  	proposer := c.valSet.GetProposer()
   374  
   375  	c.roundChangeTimer.Store(time.AfterFunc(timeout, func() {
   376  		var loc, proposerStr string
   377  
   378  		if round == 0 {
   379  			loc = "startNewRound"
   380  		} else {
   381  			loc = "catchUpRound"
   382  		}
   383  		if proposer == nil {
   384  			proposerStr = ""
   385  		} else {
   386  			proposerStr = proposer.String()
   387  		}
   388  
   389  		if c.backend.NodeType() == common.CONSENSUSNODE {
   390  			// Write log messages for validator activities analysis
   391  			preparesSize := current.Prepares.Size()
   392  			commitsSize := current.Commits.Size()
   393  			logger.Warn("[RC] timeoutEvent Sent!", "set by", loc, "sequence",
   394  				current.sequence, "round", current.round, "proposer", proposerStr, "preprepare is nil?",
   395  				current.Preprepare == nil, "len(prepares)", preparesSize, "len(commits)", commitsSize)
   396  
   397  			if preparesSize > 0 {
   398  				logger.Warn("[RC] Prepares:", "messages", current.Prepares.GetMessages())
   399  			}
   400  			if commitsSize > 0 {
   401  				logger.Warn("[RC] Commits:", "messages", current.Commits.GetMessages())
   402  			}
   403  		}
   404  
   405  		c.sendEvent(timeoutEvent{&istanbul.View{
   406  			Sequence: current.sequence,
   407  			Round:    new(big.Int).Add(current.round, common.Big1),
   408  		}})
   409  	}))
   410  
   411  	logger.Debug("New RoundChangeTimer Set", "seq", c.current.Sequence(), "round", round, "timeout", timeout)
   412  }
   413  
   414  func (c *core) checkValidatorSignature(data []byte, sig []byte) (common.Address, error) {
   415  	return istanbul.CheckValidatorSignature(c.valSet, data, sig)
   416  }
   417  
   418  // PrepareCommittedSeal returns a committed seal for the given hash
   419  func PrepareCommittedSeal(hash common.Hash) []byte {
   420  	var buf bytes.Buffer
   421  	buf.Write(hash.Bytes())
   422  	buf.Write([]byte{byte(msgCommit)})
   423  	return buf.Bytes()
   424  }
   425  
   426  // Minimum required number of consensus messages to proceed
   427  func requiredMessageCount(valSet istanbul.ValidatorSet) int {
   428  	var size uint64
   429  	if valSet.IsSubSet() {
   430  		size = valSet.SubGroupSize()
   431  	} else {
   432  		size = valSet.Size()
   433  	}
   434  	switch size {
   435  	// in the certain cases we must receive the messages from all consensus nodes to ensure finality...
   436  	case 1, 2, 3:
   437  		return int(size)
   438  	case 6:
   439  		return 4 // when the number of valSet is 6 and return value is 2*F+1, the return value(int 3) is not safe. It should return 4 or more.
   440  	default:
   441  		return 2*valSet.F() + 1
   442  	}
   443  }