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 }