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

     1  // Copyright 2023 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 core
    18  
    19  import (
    20  	"encoding/hex"
    21  	"fmt"
    22  	"math"
    23  	"math/big"
    24  	"sort"
    25  	"time"
    26  
    27  	"github.com/klaytn/klaytn/common"
    28  	"github.com/klaytn/klaytn/consensus/istanbul"
    29  	"github.com/rcrowley/go-metrics"
    30  )
    31  
    32  type Vrank struct {
    33  	startTime             time.Time
    34  	view                  istanbul.View
    35  	committee             istanbul.Validators
    36  	threshold             time.Duration
    37  	firstCommit           int64
    38  	quorumCommit          int64
    39  	avgCommitWithinQuorum int64
    40  	lastCommit            int64
    41  	commitArrivalTimeMap  map[common.Address]time.Duration
    42  }
    43  
    44  var (
    45  	// VRank metrics
    46  	vrankFirstCommitArrivalTimeGauge           = metrics.NewRegisteredGauge("vrank/first_commit", nil)
    47  	vrankQuorumCommitArrivalTimeGauge          = metrics.NewRegisteredGauge("vrank/quorum_commit", nil)
    48  	vrankAvgCommitArrivalTimeWithinQuorumGauge = metrics.NewRegisteredGauge("vrank/avg_commit_within_quorum", nil)
    49  	vrankLastCommitArrivalTimeGauge            = metrics.NewRegisteredGauge("vrank/last_commit", nil)
    50  
    51  	vrankDefaultThreshold = "300ms" // the time to receive 2f+1 commits in an ideal network
    52  
    53  	vrank *Vrank
    54  )
    55  
    56  const (
    57  	vrankArrivedEarly = iota
    58  	vrankArrivedLate
    59  	vrankNotArrived
    60  )
    61  
    62  const (
    63  	vrankNotArrivedPlaceholder = -1
    64  )
    65  
    66  func NewVrank(view istanbul.View, committee istanbul.Validators) *Vrank {
    67  	threshold, _ := time.ParseDuration(vrankDefaultThreshold)
    68  	return &Vrank{
    69  		startTime:             time.Now(),
    70  		view:                  view,
    71  		committee:             committee,
    72  		threshold:             threshold,
    73  		firstCommit:           int64(0),
    74  		quorumCommit:          int64(0),
    75  		avgCommitWithinQuorum: int64(0),
    76  		lastCommit:            int64(0),
    77  		commitArrivalTimeMap:  make(map[common.Address]time.Duration),
    78  	}
    79  }
    80  
    81  func (v *Vrank) TimeSinceStart() time.Duration {
    82  	return time.Now().Sub(v.startTime)
    83  }
    84  
    85  func (v *Vrank) AddCommit(msg *istanbul.Subject, src istanbul.Validator) {
    86  	if v.isTargetCommit(msg, src) {
    87  		t := v.TimeSinceStart()
    88  		v.commitArrivalTimeMap[src.Address()] = t
    89  	}
    90  }
    91  
    92  func (v *Vrank) HandleCommitted(blockNum *big.Int) {
    93  	if v.view.Sequence.Cmp(blockNum) != 0 {
    94  		return
    95  	}
    96  
    97  	if len(v.commitArrivalTimeMap) != 0 {
    98  		sum := int64(0)
    99  		firstCommitTime := time.Duration(math.MaxInt64)
   100  		quorumCommitTime := time.Duration(0)
   101  		for _, arrivalTime := range v.commitArrivalTimeMap {
   102  			sum += int64(arrivalTime)
   103  			if firstCommitTime > arrivalTime {
   104  				firstCommitTime = arrivalTime
   105  			}
   106  			if quorumCommitTime < arrivalTime {
   107  				quorumCommitTime = arrivalTime
   108  			}
   109  		}
   110  		avg := sum / int64(len(v.commitArrivalTimeMap))
   111  		v.avgCommitWithinQuorum = avg
   112  		v.firstCommit = int64(firstCommitTime)
   113  		v.quorumCommit = int64(quorumCommitTime)
   114  
   115  		if quorumCommitTime != time.Duration(0) && v.threshold > quorumCommitTime {
   116  			v.threshold = quorumCommitTime
   117  		}
   118  	}
   119  }
   120  
   121  func (v *Vrank) Bitmap() string {
   122  	serialized := serialize(v.committee, v.commitArrivalTimeMap)
   123  	assessed := assessBatch(serialized, v.threshold)
   124  	compressed := compress(assessed)
   125  	return hex.EncodeToString(compressed)
   126  }
   127  
   128  func (v *Vrank) LateCommits() []time.Duration {
   129  	serialized := serialize(v.committee, v.commitArrivalTimeMap)
   130  	lateCommits := make([]time.Duration, 0)
   131  	for _, t := range serialized {
   132  		if assess(t, v.threshold) == vrankArrivedLate {
   133  			lateCommits = append(lateCommits, t)
   134  		}
   135  	}
   136  	return lateCommits
   137  }
   138  
   139  // Log logs accumulated data in a compressed form
   140  func (v *Vrank) Log() {
   141  	var (
   142  		lastCommit  = time.Duration(0)
   143  		lateCommits = v.LateCommits()
   144  	)
   145  
   146  	// lastCommit = max(lateCommits)
   147  	for _, t := range lateCommits {
   148  		if lastCommit < t {
   149  			lastCommit = t
   150  		}
   151  	}
   152  	v.lastCommit = int64(lastCommit)
   153  
   154  	v.updateMetrics()
   155  
   156  	logger.Info("VRank", "seq", v.view.Sequence.Int64(),
   157  		"round", v.view.Round.Int64(),
   158  		"bitmap", v.Bitmap(),
   159  		"late", encodeDurationBatch(lateCommits),
   160  	)
   161  }
   162  
   163  func (v *Vrank) updateMetrics() {
   164  	if v.firstCommit != int64(0) {
   165  		vrankFirstCommitArrivalTimeGauge.Update(v.firstCommit)
   166  	}
   167  	if v.quorumCommit != int64(0) {
   168  		vrankQuorumCommitArrivalTimeGauge.Update(v.quorumCommit)
   169  	}
   170  	if v.avgCommitWithinQuorum != int64(0) {
   171  		vrankAvgCommitArrivalTimeWithinQuorumGauge.Update(v.avgCommitWithinQuorum)
   172  	}
   173  	if v.lastCommit != int64(0) {
   174  		vrankLastCommitArrivalTimeGauge.Update(v.lastCommit)
   175  	}
   176  }
   177  
   178  func (v *Vrank) isTargetCommit(msg *istanbul.Subject, src istanbul.Validator) bool {
   179  	if msg.View == nil || msg.View.Sequence == nil || msg.View.Round == nil {
   180  		return false
   181  	}
   182  	if msg.View.Cmp(&v.view) != 0 {
   183  		return false
   184  	}
   185  	_, ok := v.commitArrivalTimeMap[src.Address()]
   186  	if ok {
   187  		return false
   188  	}
   189  	return true
   190  }
   191  
   192  // assess determines if given time is early, late, or not arrived
   193  func assess(t, threshold time.Duration) uint8 {
   194  	if t == vrankNotArrivedPlaceholder {
   195  		return vrankNotArrived
   196  	}
   197  
   198  	if t > threshold {
   199  		return vrankArrivedLate
   200  	} else {
   201  		return vrankArrivedEarly
   202  	}
   203  }
   204  
   205  func assessBatch(ts []time.Duration, threshold time.Duration) []uint8 {
   206  	ret := make([]uint8, len(ts))
   207  	for i, t := range ts {
   208  		ret[i] = assess(t, threshold)
   209  	}
   210  	return ret
   211  }
   212  
   213  // serialize serializes arrivalTime hashmap into array.
   214  // If committee is sorted, we can simply figure out the validator position in the output array
   215  // by sorting the output of `klay.getCommittee()`
   216  func serialize(committee istanbul.Validators, arrivalTimeMap map[common.Address]time.Duration) []time.Duration {
   217  	sortedCommittee := make(istanbul.Validators, len(committee))
   218  	copy(sortedCommittee[:], committee[:])
   219  	sort.Sort(sortedCommittee)
   220  
   221  	serialized := make([]time.Duration, len(sortedCommittee))
   222  	for i, v := range sortedCommittee {
   223  		val, ok := arrivalTimeMap[v.Address()]
   224  		if ok {
   225  			serialized[i] = val
   226  		} else {
   227  			serialized[i] = vrankNotArrivedPlaceholder
   228  		}
   229  
   230  	}
   231  	return serialized
   232  }
   233  
   234  // compress compresses data into 2-bit bitmap
   235  // e.g., [1, 0, 2] => [0b01_00_10_00]
   236  func compress(arr []uint8) []byte {
   237  	zip := func(a, b, c, d uint8) byte {
   238  		a &= 0b11
   239  		b &= 0b11
   240  		c &= 0b11
   241  		d &= 0b11
   242  		return byte(a<<6 | b<<4 | c<<2 | d<<0)
   243  	}
   244  
   245  	// pad zero to make len(arr)%4 == 0
   246  	for len(arr)%4 != 0 {
   247  		arr = append(arr, 0)
   248  	}
   249  
   250  	ret := make([]byte, len(arr)/4)
   251  
   252  	for i := 0; i < len(arr)/4; i++ {
   253  		chunk := arr[4*i : 4*(i+1)]
   254  		ret[i] = zip(chunk[0], chunk[1], chunk[2], chunk[3])
   255  	}
   256  	return ret
   257  }
   258  
   259  // encodeDuration encodes given duration into string
   260  // The returned string is at most 4 bytes
   261  func encodeDuration(d time.Duration) string {
   262  	if d > 10*time.Second {
   263  		return fmt.Sprintf("%.0fs", d.Seconds())
   264  	} else if d > time.Second {
   265  		return fmt.Sprintf("%.1fs", d.Seconds())
   266  	} else {
   267  		return fmt.Sprintf("%d", d.Milliseconds())
   268  	}
   269  }
   270  
   271  func encodeDurationBatch(ds []time.Duration) []string {
   272  	ret := make([]string, len(ds))
   273  	for i, d := range ds {
   274  		ret[i] = encodeDuration(d)
   275  	}
   276  	return ret
   277  }