github.com/electroneum/electroneum-sc@v0.0.0-20230105223411-3bc1d078281e/consensus/istanbul/backend/snapshot_test.go (about) 1 // Copyright 2017 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum 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 go-ethereum 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 go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package backend 18 19 import ( 20 "bytes" 21 "crypto/ecdsa" 22 "math/big" 23 "reflect" 24 "testing" 25 26 "github.com/electroneum/electroneum-sc/common" 27 "github.com/electroneum/electroneum-sc/consensus/istanbul" 28 istanbulcommon "github.com/electroneum/electroneum-sc/consensus/istanbul/common" 29 qbftengine "github.com/electroneum/electroneum-sc/consensus/istanbul/engine" 30 "github.com/electroneum/electroneum-sc/consensus/istanbul/testutils" 31 "github.com/electroneum/electroneum-sc/consensus/istanbul/validator" 32 "github.com/electroneum/electroneum-sc/core/rawdb" 33 "github.com/electroneum/electroneum-sc/core/types" 34 "github.com/electroneum/electroneum-sc/crypto" 35 ) 36 37 type testerVote struct { 38 validator string 39 voted string 40 auth bool 41 } 42 43 // testerAccountPool is a pool to maintain currently active tester accounts, 44 // mapped from textual names used in the tests below to actual Ethereum private 45 // keys capable of signing transactions. 46 type testerAccountPool struct { 47 accounts map[string]*ecdsa.PrivateKey 48 } 49 50 func newTesterAccountPool() *testerAccountPool { 51 return &testerAccountPool{ 52 accounts: make(map[string]*ecdsa.PrivateKey), 53 } 54 } 55 56 func (ap *testerAccountPool) writeValidatorVote(header *types.Header, validator string, recipientAddress string, authorize bool) error { 57 return qbftengine.ApplyHeaderQBFTExtra( 58 header, 59 qbftengine.WriteVote(ap.address(recipientAddress), authorize), 60 ) 61 } 62 63 func (ap *testerAccountPool) address(account string) common.Address { 64 // Ensure we have a persistent key for the account 65 if ap.accounts[account] == nil { 66 ap.accounts[account], _ = crypto.GenerateKey() 67 } 68 // Resolve and return the Ethereum address 69 return crypto.PubkeyToAddress(ap.accounts[account].PublicKey) 70 } 71 72 // Tests that voting is evaluated correctly for various simple and complex scenarios. 73 func TestVoting(t *testing.T) { 74 // Define the various voting scenarios to test 75 tests := []struct { 76 epoch uint64 77 validators []string 78 votes []testerVote 79 results []string 80 }{ 81 { 82 // Single validator, no votes cast 83 validators: []string{"A"}, 84 votes: []testerVote{{validator: "A"}}, 85 results: []string{"A"}, 86 }, { 87 // Single validator, voting to add two others (only accept first, second needs 2 votes) 88 validators: []string{"A"}, 89 votes: []testerVote{ 90 {validator: "A", voted: "B", auth: true}, 91 {validator: "B"}, 92 {validator: "A", voted: "C", auth: true}, 93 }, 94 results: []string{"A", "B"}, 95 }, { 96 // Two validators, voting to add three others (only accept first two, third needs 3 votes already) 97 validators: []string{"A", "B"}, 98 votes: []testerVote{ 99 {validator: "A", voted: "C", auth: true}, 100 {validator: "B", voted: "C", auth: true}, 101 {validator: "A", voted: "D", auth: true}, 102 {validator: "B", voted: "D", auth: true}, 103 {validator: "C"}, 104 {validator: "A", voted: "E", auth: true}, 105 {validator: "B", voted: "E", auth: true}, 106 }, 107 results: []string{"A", "B", "C", "D"}, 108 }, { 109 // Single validator, dropping itself (weird, but one less cornercase by explicitly allowing this) 110 validators: []string{"A"}, 111 votes: []testerVote{ 112 {validator: "A", voted: "A", auth: false}, 113 }, 114 results: []string{}, 115 }, { 116 // Two validators, actually needing mutual consent to drop either of them (not fulfilled) 117 validators: []string{"A", "B"}, 118 votes: []testerVote{ 119 {validator: "A", voted: "B", auth: false}, 120 }, 121 results: []string{"A", "B"}, 122 }, { 123 // Two validators, actually needing mutual consent to drop either of them (fulfilled) 124 validators: []string{"A", "B"}, 125 votes: []testerVote{ 126 {validator: "A", voted: "B", auth: false}, 127 {validator: "B", voted: "B", auth: false}, 128 }, 129 results: []string{"A"}, 130 }, { 131 // Three validators, two of them deciding to drop the third 132 validators: []string{"A", "B", "C"}, 133 votes: []testerVote{ 134 {validator: "A", voted: "C", auth: false}, 135 {validator: "B", voted: "C", auth: false}, 136 }, 137 results: []string{"A", "B"}, 138 }, { 139 // Four validators, consensus of two not being enough to drop anyone 140 validators: []string{"A", "B", "C", "D"}, 141 votes: []testerVote{ 142 {validator: "A", voted: "C", auth: false}, 143 {validator: "B", voted: "C", auth: false}, 144 }, 145 results: []string{"A", "B", "C", "D"}, 146 }, { 147 // Four validators, consensus of three already being enough to drop someone 148 validators: []string{"A", "B", "C", "D"}, 149 votes: []testerVote{ 150 {validator: "A", voted: "D", auth: false}, 151 {validator: "B", voted: "D", auth: false}, 152 {validator: "C", voted: "D", auth: false}, 153 }, 154 results: []string{"A", "B", "C"}, 155 }, { 156 // Authorizations are counted once per validator per target 157 validators: []string{"A", "B"}, 158 votes: []testerVote{ 159 {validator: "A", voted: "C", auth: true}, 160 {validator: "B"}, 161 {validator: "A", voted: "C", auth: true}, 162 {validator: "B"}, 163 {validator: "A", voted: "C", auth: true}, 164 }, 165 results: []string{"A", "B"}, 166 }, { 167 // Authorizing multiple accounts concurrently is permitted 168 validators: []string{"A", "B"}, 169 votes: []testerVote{ 170 {validator: "A", voted: "C", auth: true}, 171 {validator: "B"}, 172 {validator: "A", voted: "D", auth: true}, 173 {validator: "B"}, 174 {validator: "A"}, 175 {validator: "B", voted: "D", auth: true}, 176 {validator: "A"}, 177 {validator: "B", voted: "C", auth: true}, 178 }, 179 results: []string{"A", "B", "C", "D"}, 180 }, { 181 // Deauthorizations are counted once per validator per target 182 validators: []string{"A", "B"}, 183 votes: []testerVote{ 184 {validator: "A", voted: "B", auth: false}, 185 {validator: "B"}, 186 {validator: "A", voted: "B", auth: false}, 187 {validator: "B"}, 188 {validator: "A", voted: "B", auth: false}, 189 }, 190 results: []string{"A", "B"}, 191 }, { 192 // Deauthorizing multiple accounts concurrently is permitted 193 validators: []string{"A", "B", "C", "D"}, 194 votes: []testerVote{ 195 {validator: "A", voted: "C", auth: false}, 196 {validator: "B"}, 197 {validator: "C"}, 198 {validator: "A", voted: "D", auth: false}, 199 {validator: "B"}, 200 {validator: "C"}, 201 {validator: "A"}, 202 {validator: "B", voted: "D", auth: false}, 203 {validator: "C", voted: "D", auth: false}, 204 {validator: "A"}, 205 {validator: "B", voted: "C", auth: false}, 206 }, 207 results: []string{"A", "B"}, 208 }, { 209 // Votes from deauthorized validators are discarded immediately (deauth votes) 210 validators: []string{"A", "B", "C"}, 211 votes: []testerVote{ 212 {validator: "C", voted: "B", auth: false}, 213 {validator: "A", voted: "C", auth: false}, 214 {validator: "B", voted: "C", auth: false}, 215 {validator: "A", voted: "B", auth: false}, 216 }, 217 results: []string{"A", "B"}, 218 }, { 219 // Votes from deauthorized validators are discarded immediately (auth votes) 220 validators: []string{"A", "B", "C"}, 221 votes: []testerVote{ 222 {validator: "C", voted: "B", auth: false}, 223 {validator: "A", voted: "C", auth: false}, 224 {validator: "B", voted: "C", auth: false}, 225 {validator: "A", voted: "B", auth: false}, 226 }, 227 results: []string{"A", "B"}, 228 }, { 229 // Cascading changes are not allowed, only the the account being voted on may change 230 validators: []string{"A", "B", "C", "D"}, 231 votes: []testerVote{ 232 {validator: "A", voted: "C", auth: false}, 233 {validator: "B"}, 234 {validator: "C"}, 235 {validator: "A", voted: "D", auth: false}, 236 {validator: "B", voted: "C", auth: false}, 237 {validator: "C"}, 238 {validator: "A"}, 239 {validator: "B", voted: "D", auth: false}, 240 {validator: "C", voted: "D", auth: false}, 241 }, 242 results: []string{"A", "B", "C"}, 243 }, { 244 // Changes reaching consensus out of bounds (via a deauth) execute on touch 245 validators: []string{"A", "B", "C", "D"}, 246 votes: []testerVote{ 247 {validator: "A", voted: "C", auth: false}, 248 {validator: "B"}, 249 {validator: "C"}, 250 {validator: "A", voted: "D", auth: false}, 251 {validator: "B", voted: "C", auth: false}, 252 {validator: "C"}, 253 {validator: "A"}, 254 {validator: "B", voted: "D", auth: false}, 255 {validator: "C", voted: "D", auth: false}, 256 {validator: "A"}, 257 {validator: "C", voted: "C", auth: true}, 258 }, 259 results: []string{"A", "B"}, 260 }, { 261 // Changes reaching consensus out of bounds (via a deauth) may go out of consensus on first touch 262 validators: []string{"A", "B", "C", "D"}, 263 votes: []testerVote{ 264 {validator: "A", voted: "C", auth: false}, 265 {validator: "B"}, 266 {validator: "C"}, 267 {validator: "A", voted: "D", auth: false}, 268 {validator: "B", voted: "C", auth: false}, 269 {validator: "C"}, 270 {validator: "A"}, 271 {validator: "B", voted: "D", auth: false}, 272 {validator: "C", voted: "D", auth: false}, 273 {validator: "A"}, 274 {validator: "B", voted: "C", auth: true}, 275 }, 276 results: []string{"A", "B", "C"}, 277 }, { 278 // Ensure that pending votes don't survive authorization status changes. This 279 // corner case can only appear if a validator is quickly added, remove and then 280 // readded (or the inverse), while one of the original voters dropped. If a 281 // past vote is left cached in the system somewhere, this will interfere with 282 // the final validator outcome. 283 validators: []string{"A", "B", "C", "D", "E"}, 284 votes: []testerVote{ 285 {validator: "A", voted: "F", auth: true}, // Authorize F, 3 votes needed 286 {validator: "B", voted: "F", auth: true}, 287 {validator: "C", voted: "F", auth: true}, 288 {validator: "D", voted: "F", auth: false}, // Deauthorize F, 4 votes needed (leave A's previous vote "unchanged") 289 {validator: "E", voted: "F", auth: false}, 290 {validator: "B", voted: "F", auth: false}, 291 {validator: "C", voted: "F", auth: false}, 292 {validator: "D", voted: "F", auth: true}, // Almost authorize F, 2/3 votes needed 293 {validator: "E", voted: "F", auth: true}, 294 {validator: "B", voted: "A", auth: false}, // Deauthorize A, 3 votes needed 295 {validator: "C", voted: "A", auth: false}, 296 {validator: "D", voted: "A", auth: false}, 297 {validator: "B", voted: "F", auth: true}, // Finish authorizing F, 3/3 votes needed 298 }, 299 results: []string{"B", "C", "D", "E", "F"}, 300 }, { 301 // Epoch transitions reset all votes to allow chain checkpointing 302 epoch: 3, 303 validators: []string{"A", "B"}, 304 votes: []testerVote{ 305 {validator: "A", voted: "C", auth: true}, 306 {validator: "B"}, 307 {validator: "A"}, // Checkpoint block, (don't vote here, it's validated outside of snapshots) 308 {validator: "B", voted: "C", auth: true}, 309 }, 310 results: []string{"A", "B"}, 311 }, 312 } 313 314 // Run through the scenarios and test them 315 for i, tt := range tests { 316 // Create the account pool and generate the initial set of validators 317 accounts := newTesterAccountPool() 318 319 validators := make([]common.Address, len(tt.validators)) 320 for j, validator := range tt.validators { 321 validators[j] = accounts.address(validator) 322 } 323 for j := 0; j < len(validators); j++ { 324 for k := j + 1; k < len(validators); k++ { 325 if bytes.Compare(validators[j][:], validators[k][:]) > 0 { 326 validators[j], validators[k] = validators[k], validators[j] 327 } 328 } 329 } 330 331 genesis := testutils.Genesis(validators) 332 config := istanbul.Config{ 333 RequestTimeout: istanbul.DefaultConfig.RequestTimeout, 334 BlockPeriod: istanbul.DefaultConfig.BlockPeriod, 335 ProposerPolicy: istanbul.DefaultConfig.ProposerPolicy, 336 Epoch: istanbul.DefaultConfig.Epoch, 337 AllowedFutureBlockTime: istanbul.DefaultConfig.AllowedFutureBlockTime, 338 } 339 if tt.epoch != 0 { 340 config.Epoch = tt.epoch 341 } 342 343 chain, backend := newBlockchainFromConfig( 344 genesis, 345 []*ecdsa.PrivateKey{accounts.accounts[tt.validators[0]]}, 346 config, 347 ) 348 349 // Assemble a chain of headers from the cast votes 350 headers := make([]*types.Header, len(tt.votes)) 351 for j, vote := range tt.votes { 352 headers[j] = &types.Header{ 353 Number: big.NewInt(int64(j) + 1), 354 Time: uint64(int64(j) * int64(config.BlockPeriod)), 355 Coinbase: accounts.address(vote.validator), 356 Difficulty: istanbulcommon.DefaultDifficulty, 357 MixDigest: types.IstanbulDigest, 358 } 359 _ = qbftengine.ApplyHeaderQBFTExtra( 360 headers[j], 361 qbftengine.WriteValidators(validators), 362 ) 363 364 if j > 0 { 365 headers[j].ParentHash = headers[j-1].Hash() 366 } 367 368 copy(headers[j].Extra, genesis.ExtraData) 369 370 if len(vote.voted) > 0 { 371 if err := accounts.writeValidatorVote(headers[j], vote.validator, vote.voted, vote.auth); err != nil { 372 t.Errorf("Error writeValidatorVote test: %d, validator: %s, voteType: %v (err=%v)", j, vote.voted, vote.auth, err) 373 } 374 } 375 } 376 377 // Pass all the headers through clique and ensure tallying succeeds 378 head := headers[len(headers)-1] 379 380 snap, err := backend.snapshot(chain, head.Number.Uint64(), head.Hash(), headers) 381 if err != nil { 382 t.Errorf("test %d: failed to create voting snapshot: %v", i, err) 383 backend.Stop() 384 continue 385 } 386 // Verify the final list of validators against the expected ones 387 validators = make([]common.Address, len(tt.results)) 388 for j, validator := range tt.results { 389 validators[j] = accounts.address(validator) 390 } 391 for j := 0; j < len(validators); j++ { 392 for k := j + 1; k < len(validators); k++ { 393 if bytes.Compare(validators[j][:], validators[k][:]) > 0 { 394 validators[j], validators[k] = validators[k], validators[j] 395 } 396 } 397 } 398 result := snap.validators() 399 if len(result) != len(validators) { 400 t.Errorf("test %d: validators mismatch: have %x, want %x", i, result, validators) 401 backend.Stop() 402 continue 403 } 404 for j := 0; j < len(result); j++ { 405 if !bytes.Equal(result[j][:], validators[j][:]) { 406 t.Errorf("test %d, validator %d: validator mismatch: have %x, want %x", i, j, result[j], validators[j]) 407 } 408 } 409 backend.Stop() 410 } 411 } 412 413 func TestSaveAndLoad(t *testing.T) { 414 snap := &Snapshot{ 415 Epoch: 5, 416 Number: 10, 417 Hash: common.HexToHash("1234567890"), 418 Votes: []*Vote{ 419 { 420 Validator: common.StringToAddress("1234567891"), 421 Block: 15, 422 Address: common.StringToAddress("1234567892"), 423 Authorize: false, 424 }, 425 }, 426 Tally: map[common.Address]Tally{ 427 common.StringToAddress("1234567893"): { 428 Authorize: false, 429 Votes: 20, 430 }, 431 }, 432 ValSet: validator.NewSet([]common.Address{ 433 common.StringToAddress("1234567894"), 434 common.StringToAddress("1234567895"), 435 }, istanbul.NewRoundRobinProposerPolicy()), 436 } 437 db := rawdb.NewMemoryDatabase() 438 err := snap.store(db) 439 if err != nil { 440 t.Errorf("store snapshot failed: %v", err) 441 } 442 443 snap1, err := loadSnapshot(snap.Epoch, db, snap.Hash) 444 if err != nil { 445 t.Errorf("load snapshot failed: %v", err) 446 } 447 if snap.Epoch != snap1.Epoch { 448 t.Errorf("epoch mismatch: have %v, want %v", snap1.Epoch, snap.Epoch) 449 } 450 if snap.Hash != snap1.Hash { 451 t.Errorf("hash mismatch: have %v, want %v", snap1.Number, snap.Number) 452 } 453 if !reflect.DeepEqual(snap.Votes, snap.Votes) { 454 t.Errorf("votes mismatch: have %v, want %v", snap1.Votes, snap.Votes) 455 } 456 if !reflect.DeepEqual(snap.Tally, snap.Tally) { 457 t.Errorf("tally mismatch: have %v, want %v", snap1.Tally, snap.Tally) 458 } 459 if !reflect.DeepEqual(snap.ValSet, snap.ValSet) { 460 t.Errorf("validator set mismatch: have %v, want %v", snap1.ValSet, snap.ValSet) 461 } 462 }