github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/light/detector_test.go (about) 1 package light_test 2 3 import ( 4 "bytes" 5 "context" 6 "testing" 7 "time" 8 9 provider_mocks "github.com/ari-anchor/sei-tendermint/light/provider/mocks" 10 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/mock" 13 "github.com/stretchr/testify/require" 14 15 dbm "github.com/tendermint/tm-db" 16 17 "github.com/ari-anchor/sei-tendermint/libs/log" 18 "github.com/ari-anchor/sei-tendermint/light" 19 "github.com/ari-anchor/sei-tendermint/light/provider" 20 dbs "github.com/ari-anchor/sei-tendermint/light/store/db" 21 "github.com/ari-anchor/sei-tendermint/types" 22 ) 23 24 func TestLightClientAttackEvidence_Lunatic(t *testing.T) { 25 logger := log.NewNopLogger() 26 27 // primary performs a lunatic attack 28 var ( 29 latestHeight = int64(3) 30 valSize = 5 31 divergenceHeight = int64(2) 32 primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight) 33 primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight) 34 ) 35 36 ctx, cancel := context.WithCancel(context.Background()) 37 defer cancel() 38 39 witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(t, latestHeight, valSize, 2, bTime) 40 41 forgedKeys := chainKeys[divergenceHeight-1].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain) 42 forgedVals := forgedKeys.ToValidators(2, 0) 43 44 for height := int64(1); height <= latestHeight; height++ { 45 if height < divergenceHeight { 46 primaryHeaders[height] = witnessHeaders[height] 47 primaryValidators[height] = witnessValidators[height] 48 continue 49 } 50 primaryHeaders[height] = forgedKeys.GenSignedHeader(t, chainID, height, bTime.Add(time.Duration(height)*time.Minute), 51 nil, forgedVals, forgedVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(forgedKeys)) 52 primaryValidators[height] = forgedVals 53 } 54 55 // never called, delete it to make mockery asserts pass 56 delete(witnessHeaders, 2) 57 delete(primaryHeaders, 2) 58 59 mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators) 60 mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators) 61 mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool { 62 evAgainstPrimary := &types.LightClientAttackEvidence{ 63 // after the divergence height the valset doesn't change so we expect the evidence to be for the latest height 64 ConflictingBlock: &types.LightBlock{ 65 SignedHeader: primaryHeaders[latestHeight], 66 ValidatorSet: primaryValidators[latestHeight], 67 }, 68 CommonHeight: 1, 69 } 70 return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash()) 71 })).Return(nil) 72 73 mockPrimary.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool { 74 evAgainstWitness := &types.LightClientAttackEvidence{ 75 // when forming evidence against witness we learn that the canonical chain continued to change validator sets 76 // hence the conflicting block is at 7 77 ConflictingBlock: &types.LightBlock{ 78 SignedHeader: witnessHeaders[divergenceHeight+1], 79 ValidatorSet: witnessValidators[divergenceHeight+1], 80 }, 81 CommonHeight: divergenceHeight - 1, 82 } 83 return bytes.Equal(evidence.Hash(), evAgainstWitness.Hash()) 84 })).Return(nil) 85 86 c, err := light.NewClient( 87 ctx, 88 chainID, 89 light.TrustOptions{ 90 Period: 4 * time.Hour, 91 Height: 1, 92 Hash: primaryHeaders[1].Hash(), 93 }, 94 mockPrimary, 95 []provider.Provider{mockWitness}, 96 dbs.New(dbm.NewMemDB()), 97 light.Logger(logger), 98 ) 99 require.NoError(t, err) 100 101 // Check verification returns an error. 102 _, err = c.VerifyLightBlockAtHeight(ctx, latestHeight, bTime.Add(1*time.Hour)) 103 if assert.Error(t, err) { 104 assert.Equal(t, light.ErrLightClientAttack, err) 105 } 106 107 mockWitness.AssertExpectations(t) 108 mockPrimary.AssertExpectations(t) 109 } 110 111 func TestLightClientAttackEvidence_Equivocation(t *testing.T) { 112 cases := []struct { 113 name string 114 lightOption light.Option 115 unusedWitnessBlockHeights []int64 116 unusedPrimaryBlockHeights []int64 117 latestHeight int64 118 divergenceHeight int64 119 }{ 120 { 121 name: "sequential", 122 lightOption: light.SequentialVerification(), 123 unusedWitnessBlockHeights: []int64{4, 6}, 124 latestHeight: int64(5), 125 divergenceHeight: int64(3), 126 }, 127 { 128 name: "skipping", 129 lightOption: light.SkippingVerification(light.DefaultTrustLevel), 130 unusedWitnessBlockHeights: []int64{2, 4, 6}, 131 unusedPrimaryBlockHeights: []int64{2, 4, 6}, 132 latestHeight: int64(5), 133 divergenceHeight: int64(3), 134 }, 135 } 136 137 bctx, bcancel := context.WithCancel(context.Background()) 138 defer bcancel() 139 140 for _, tc := range cases { 141 testCase := tc 142 t.Run(testCase.name, func(t *testing.T) { 143 ctx, cancel := context.WithCancel(bctx) 144 defer cancel() 145 146 logger := log.NewNopLogger() 147 148 // primary performs an equivocation attack 149 var ( 150 valSize = 5 151 primaryHeaders = make(map[int64]*types.SignedHeader, testCase.latestHeight) 152 // validators don't change in this network (however we still use a map just for convenience) 153 primaryValidators = make(map[int64]*types.ValidatorSet, testCase.latestHeight) 154 ) 155 witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(t, 156 testCase.latestHeight+1, valSize, 2, bTime) 157 for height := int64(1); height <= testCase.latestHeight; height++ { 158 if height < testCase.divergenceHeight { 159 primaryHeaders[height] = witnessHeaders[height] 160 primaryValidators[height] = witnessValidators[height] 161 continue 162 } 163 // we don't have a network partition so we will make 4/5 (greater than 2/3) malicious and vote again for 164 // a different block (which we do by adding txs) 165 primaryHeaders[height] = chainKeys[height].GenSignedHeader(t, chainID, height, 166 bTime.Add(time.Duration(height)*time.Minute), []types.Tx{[]byte("abcd")}, 167 witnessValidators[height], witnessValidators[height+1], hash("app_hash"), 168 hash("cons_hash"), hash("results_hash"), 0, len(chainKeys[height])-1) 169 primaryValidators[height] = witnessValidators[height] 170 } 171 172 for _, height := range testCase.unusedWitnessBlockHeights { 173 delete(witnessHeaders, height) 174 } 175 mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators) 176 177 for _, height := range testCase.unusedPrimaryBlockHeights { 178 delete(primaryHeaders, height) 179 } 180 mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators) 181 182 // Check evidence was sent to both full nodes. 183 // Common height should be set to the height of the divergent header in the instance 184 // of an equivocation attack and the validator sets are the same as what the witness has 185 mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool { 186 evAgainstPrimary := &types.LightClientAttackEvidence{ 187 ConflictingBlock: &types.LightBlock{ 188 SignedHeader: primaryHeaders[testCase.divergenceHeight], 189 ValidatorSet: primaryValidators[testCase.divergenceHeight], 190 }, 191 CommonHeight: testCase.divergenceHeight, 192 } 193 return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash()) 194 })).Return(nil) 195 mockPrimary.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool { 196 evAgainstWitness := &types.LightClientAttackEvidence{ 197 ConflictingBlock: &types.LightBlock{ 198 SignedHeader: witnessHeaders[testCase.divergenceHeight], 199 ValidatorSet: witnessValidators[testCase.divergenceHeight], 200 }, 201 CommonHeight: testCase.divergenceHeight, 202 } 203 return bytes.Equal(evidence.Hash(), evAgainstWitness.Hash()) 204 })).Return(nil) 205 206 c, err := light.NewClient( 207 ctx, 208 chainID, 209 light.TrustOptions{ 210 Period: 4 * time.Hour, 211 Height: 1, 212 Hash: primaryHeaders[1].Hash(), 213 }, 214 mockPrimary, 215 []provider.Provider{mockWitness}, 216 dbs.New(dbm.NewMemDB()), 217 light.Logger(logger), 218 testCase.lightOption, 219 ) 220 require.NoError(t, err) 221 222 // Check verification returns an error. 223 _, err = c.VerifyLightBlockAtHeight(ctx, testCase.latestHeight, bTime.Add(300*time.Second)) 224 if assert.Error(t, err) { 225 assert.Equal(t, light.ErrLightClientAttack, err) 226 } 227 228 mockWitness.AssertExpectations(t) 229 mockPrimary.AssertExpectations(t) 230 }) 231 } 232 } 233 234 func TestLightClientAttackEvidence_ForwardLunatic(t *testing.T) { 235 // primary performs a lunatic attack but changes the time of the header to 236 // something in the future relative to the blockchain 237 var ( 238 latestHeight = int64(10) 239 valSize = 5 240 forgedHeight = int64(12) 241 proofHeight = int64(11) 242 primaryHeaders = make(map[int64]*types.SignedHeader, forgedHeight) 243 primaryValidators = make(map[int64]*types.ValidatorSet, forgedHeight) 244 ) 245 246 ctx, cancel := context.WithCancel(context.Background()) 247 defer cancel() 248 logger := log.NewNopLogger() 249 250 witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(t, latestHeight, valSize, 2, bTime) 251 for _, unusedHeader := range []int64{3, 5, 6, 8} { 252 delete(witnessHeaders, unusedHeader) 253 } 254 255 // primary has the exact same headers except it forges one extra header in the future using keys from 2/5ths of 256 // the validators 257 for h := range witnessHeaders { 258 primaryHeaders[h] = witnessHeaders[h] 259 primaryValidators[h] = witnessValidators[h] 260 } 261 for _, unusedHeader := range []int64{3, 5, 6, 8} { 262 delete(primaryHeaders, unusedHeader) 263 } 264 forgedKeys := chainKeys[latestHeight].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain) 265 primaryValidators[forgedHeight] = forgedKeys.ToValidators(2, 0) 266 primaryHeaders[forgedHeight] = forgedKeys.GenSignedHeader(t, 267 chainID, 268 forgedHeight, 269 bTime.Add(time.Duration(latestHeight+1)*time.Minute), // 11 mins 270 nil, 271 primaryValidators[forgedHeight], 272 primaryValidators[forgedHeight], 273 hash("app_hash"), 274 hash("cons_hash"), 275 hash("results_hash"), 276 0, len(forgedKeys), 277 ) 278 mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators) 279 lastBlock, _ := mockPrimary.LightBlock(ctx, forgedHeight) 280 mockPrimary.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil) 281 mockPrimary.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound) 282 283 mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators) 284 lastBlock, _ = mockWitness.LightBlock(ctx, latestHeight) 285 mockWitness.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil).Once() 286 mockWitness.On("LightBlock", mock.Anything, int64(12)).Return(nil, provider.ErrHeightTooHigh) 287 288 mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool { 289 // Check evidence was sent to the witness against the full node 290 evAgainstPrimary := &types.LightClientAttackEvidence{ 291 ConflictingBlock: &types.LightBlock{ 292 SignedHeader: primaryHeaders[forgedHeight], 293 ValidatorSet: primaryValidators[forgedHeight], 294 }, 295 CommonHeight: latestHeight, 296 } 297 return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash()) 298 })).Return(nil).Twice() 299 300 // In order to perform the attack, the primary needs at least one accomplice as a witness to also 301 // send the forged block 302 accomplice := mockPrimary 303 304 c, err := light.NewClient( 305 ctx, 306 chainID, 307 light.TrustOptions{ 308 Period: 4 * time.Hour, 309 Height: 1, 310 Hash: primaryHeaders[1].Hash(), 311 }, 312 mockPrimary, 313 []provider.Provider{mockWitness, accomplice}, 314 dbs.New(dbm.NewMemDB()), 315 light.Logger(logger), 316 light.MaxClockDrift(1*time.Second), 317 light.MaxBlockLag(1*time.Second), 318 ) 319 require.NoError(t, err) 320 321 // two seconds later, the supporting withness should receive the header that can be used 322 // to prove that there was an attack 323 vals := chainKeys[latestHeight].ToValidators(2, 0) 324 newLb := &types.LightBlock{ 325 SignedHeader: chainKeys[latestHeight].GenSignedHeader(t, 326 chainID, 327 proofHeight, 328 bTime.Add(time.Duration(proofHeight+1)*time.Minute), // 12 mins 329 nil, 330 vals, 331 vals, 332 hash("app_hash"), 333 hash("cons_hash"), 334 hash("results_hash"), 335 0, len(chainKeys), 336 ), 337 ValidatorSet: vals, 338 } 339 go func() { 340 time.Sleep(2 * time.Second) 341 mockWitness.On("LightBlock", mock.Anything, int64(0)).Return(newLb, nil) 342 }() 343 344 // Now assert that verification returns an error. We craft the light clients time to be a little ahead of the chain 345 // to allow a window for the attack to manifest itself. 346 _, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute)) 347 if assert.Error(t, err) { 348 assert.Equal(t, light.ErrLightClientAttack, err) 349 } 350 351 // We attempt the same call but now the supporting witness has a block which should 352 // immediately conflict in time with the primary 353 _, err = c.VerifyLightBlockAtHeight(ctx, forgedHeight, bTime.Add(time.Duration(forgedHeight)*time.Minute)) 354 if assert.Error(t, err) { 355 assert.Equal(t, light.ErrLightClientAttack, err) 356 } 357 358 // Lastly we test the unfortunate case where the light clients supporting witness doesn't update 359 // in enough time 360 mockLaggingWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators) 361 mockLaggingWitness.On("LightBlock", mock.Anything, int64(12)).Return(nil, provider.ErrHeightTooHigh) 362 lastBlock, _ = mockLaggingWitness.LightBlock(ctx, latestHeight) 363 mockLaggingWitness.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil) 364 c, err = light.NewClient( 365 ctx, 366 chainID, 367 light.TrustOptions{ 368 Period: 4 * time.Hour, 369 Height: 1, 370 Hash: primaryHeaders[1].Hash(), 371 }, 372 mockPrimary, 373 []provider.Provider{mockLaggingWitness, accomplice}, 374 dbs.New(dbm.NewMemDB()), 375 light.Logger(logger), 376 light.MaxClockDrift(1*time.Second), 377 light.MaxBlockLag(1*time.Second), 378 ) 379 require.NoError(t, err) 380 381 _, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute)) 382 assert.NoError(t, err) 383 mockPrimary.AssertExpectations(t) 384 } 385 386 // 1. Different nodes therefore a divergent header is produced. 387 // => light client returns an error upon creation because primary and witness 388 // have a different view. 389 func TestClientDivergentTraces1(t *testing.T) { 390 ctx, cancel := context.WithCancel(context.Background()) 391 defer cancel() 392 393 headers, vals, _ := genLightBlocksWithKeys(t, 1, 5, 2, bTime) 394 mockPrimary := mockNodeFromHeadersAndVals(headers, vals) 395 396 firstBlock, err := mockPrimary.LightBlock(ctx, 1) 397 require.NoError(t, err) 398 headers, vals, _ = genLightBlocksWithKeys(t, 1, 5, 2, bTime) 399 mockWitness := mockNodeFromHeadersAndVals(headers, vals) 400 401 logger := log.NewNopLogger() 402 403 _, err = light.NewClient( 404 ctx, 405 chainID, 406 light.TrustOptions{ 407 Height: 1, 408 Hash: firstBlock.Hash(), 409 Period: 4 * time.Hour, 410 }, 411 mockPrimary, 412 []provider.Provider{mockWitness}, 413 dbs.New(dbm.NewMemDB()), 414 light.Logger(logger), 415 ) 416 require.Error(t, err) 417 assert.Contains(t, err.Error(), "does not match primary") 418 mockWitness.AssertExpectations(t) 419 mockPrimary.AssertExpectations(t) 420 } 421 422 // 2. Two out of three nodes don't respond but the third has a header that matches 423 // => verification should be successful and all the witnesses should remain 424 func TestClientDivergentTraces2(t *testing.T) { 425 ctx, cancel := context.WithCancel(context.Background()) 426 defer cancel() 427 logger := log.NewNopLogger() 428 429 headers, vals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime) 430 mockPrimaryNode := mockNodeFromHeadersAndVals(headers, vals) 431 mockDeadNode := &provider_mocks.Provider{} 432 mockDeadNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrNoResponse) 433 firstBlock, err := mockPrimaryNode.LightBlock(ctx, 1) 434 require.NoError(t, err) 435 c, err := light.NewClient( 436 ctx, 437 chainID, 438 light.TrustOptions{ 439 Height: 1, 440 Hash: firstBlock.Hash(), 441 Period: 4 * time.Hour, 442 }, 443 mockPrimaryNode, 444 []provider.Provider{mockDeadNode, mockDeadNode, mockPrimaryNode}, 445 dbs.New(dbm.NewMemDB()), 446 light.Logger(logger), 447 ) 448 require.NoError(t, err) 449 450 _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour)) 451 assert.NoError(t, err) 452 assert.Equal(t, 3, len(c.Witnesses())) 453 mockDeadNode.AssertExpectations(t) 454 mockPrimaryNode.AssertExpectations(t) 455 } 456 457 // 3. witness has the same first header, but different second header 458 // => creation should succeed, but the verification should fail 459 // nolint: dupl 460 func TestClientDivergentTraces3(t *testing.T) { 461 logger := log.NewNopLogger() 462 463 // 464 primaryHeaders, primaryVals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime) 465 mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals) 466 467 ctx, cancel := context.WithCancel(context.Background()) 468 defer cancel() 469 470 firstBlock, err := mockPrimary.LightBlock(ctx, 1) 471 require.NoError(t, err) 472 473 mockHeaders, mockVals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime) 474 mockHeaders[1] = primaryHeaders[1] 475 mockVals[1] = primaryVals[1] 476 mockWitness := mockNodeFromHeadersAndVals(mockHeaders, mockVals) 477 478 c, err := light.NewClient( 479 ctx, 480 chainID, 481 light.TrustOptions{ 482 Height: 1, 483 Hash: firstBlock.Hash(), 484 Period: 4 * time.Hour, 485 }, 486 mockPrimary, 487 []provider.Provider{mockWitness}, 488 dbs.New(dbm.NewMemDB()), 489 light.Logger(logger), 490 ) 491 require.NoError(t, err) 492 493 _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour)) 494 assert.Error(t, err) 495 assert.Equal(t, 1, len(c.Witnesses())) 496 mockWitness.AssertExpectations(t) 497 mockPrimary.AssertExpectations(t) 498 } 499 500 // 4. Witness has a divergent header but can not produce a valid trace to back it up. 501 // It should be ignored 502 // nolint: dupl 503 func TestClientDivergentTraces4(t *testing.T) { 504 logger := log.NewNopLogger() 505 506 // 507 primaryHeaders, primaryVals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime) 508 mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals) 509 510 ctx, cancel := context.WithCancel(context.Background()) 511 defer cancel() 512 513 firstBlock, err := mockPrimary.LightBlock(ctx, 1) 514 require.NoError(t, err) 515 516 witnessHeaders, witnessVals, _ := genLightBlocksWithKeys(t, 2, 5, 2, bTime) 517 primaryHeaders[2] = witnessHeaders[2] 518 primaryVals[2] = witnessVals[2] 519 mockWitness := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals) 520 521 c, err := light.NewClient( 522 ctx, 523 chainID, 524 light.TrustOptions{ 525 Height: 1, 526 Hash: firstBlock.Hash(), 527 Period: 4 * time.Hour, 528 }, 529 mockPrimary, 530 []provider.Provider{mockWitness}, 531 dbs.New(dbm.NewMemDB()), 532 light.Logger(logger), 533 ) 534 require.NoError(t, err) 535 536 _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour)) 537 assert.Error(t, err) 538 assert.Equal(t, 1, len(c.Witnesses())) 539 mockWitness.AssertExpectations(t) 540 mockPrimary.AssertExpectations(t) 541 }