github.com/soomindae/tendermint@v0.0.5-0.20210528140126-84a0c70c8162/light/detector_test.go (about) 1 package light_test 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/stretchr/testify/assert" 8 "github.com/stretchr/testify/require" 9 10 dbm "github.com/soomindae/tm-db" 11 12 "github.com/soomindae/tendermint/libs/log" 13 "github.com/soomindae/tendermint/light" 14 "github.com/soomindae/tendermint/light/provider" 15 mockp "github.com/soomindae/tendermint/light/provider/mock" 16 dbs "github.com/soomindae/tendermint/light/store/db" 17 "github.com/soomindae/tendermint/types" 18 ) 19 20 func TestLightClientAttackEvidence_Lunatic(t *testing.T) { 21 // primary performs a lunatic attack 22 var ( 23 latestHeight = int64(10) 24 valSize = 5 25 divergenceHeight = int64(6) 26 primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight) 27 primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight) 28 ) 29 30 witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight, valSize, 2, bTime) 31 witness := mockp.New(chainID, witnessHeaders, witnessValidators) 32 forgedKeys := chainKeys[divergenceHeight-1].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain) 33 forgedVals := forgedKeys.ToValidators(2, 0) 34 35 for height := int64(1); height <= latestHeight; height++ { 36 if height < divergenceHeight { 37 primaryHeaders[height] = witnessHeaders[height] 38 primaryValidators[height] = witnessValidators[height] 39 continue 40 } 41 primaryHeaders[height] = forgedKeys.GenSignedHeader(chainID, height, bTime.Add(time.Duration(height)*time.Minute), 42 nil, forgedVals, forgedVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(forgedKeys)) 43 primaryValidators[height] = forgedVals 44 } 45 primary := mockp.New(chainID, primaryHeaders, primaryValidators) 46 47 c, err := light.NewClient( 48 ctx, 49 chainID, 50 light.TrustOptions{ 51 Period: 4 * time.Hour, 52 Height: 1, 53 Hash: primaryHeaders[1].Hash(), 54 }, 55 primary, 56 []provider.Provider{witness}, 57 dbs.New(dbm.NewMemDB(), chainID), 58 light.Logger(log.TestingLogger()), 59 light.MaxRetryAttempts(1), 60 ) 61 require.NoError(t, err) 62 63 // Check verification returns an error. 64 _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour)) 65 if assert.Error(t, err) { 66 assert.Equal(t, light.ErrLightClientAttack, err) 67 } 68 69 // Check evidence was sent to both full nodes. 70 evAgainstPrimary := &types.LightClientAttackEvidence{ 71 // after the divergence height the valset doesn't change so we expect the evidence to be for height 10 72 ConflictingBlock: &types.LightBlock{ 73 SignedHeader: primaryHeaders[10], 74 ValidatorSet: primaryValidators[10], 75 }, 76 CommonHeight: 4, 77 } 78 assert.True(t, witness.HasEvidence(evAgainstPrimary)) 79 80 evAgainstWitness := &types.LightClientAttackEvidence{ 81 // when forming evidence against witness we learn that the canonical chain continued to change validator sets 82 // hence the conflicting block is at 7 83 ConflictingBlock: &types.LightBlock{ 84 SignedHeader: witnessHeaders[7], 85 ValidatorSet: witnessValidators[7], 86 }, 87 CommonHeight: 4, 88 } 89 assert.True(t, primary.HasEvidence(evAgainstWitness)) 90 } 91 92 func TestLightClientAttackEvidence_Equivocation(t *testing.T) { 93 verificationOptions := map[string]light.Option{ 94 "sequential": light.SequentialVerification(), 95 "skipping": light.SkippingVerification(light.DefaultTrustLevel), 96 } 97 98 for s, verificationOption := range verificationOptions { 99 t.Log("==> verification", s) 100 101 // primary performs an equivocation attack 102 var ( 103 latestHeight = int64(10) 104 valSize = 5 105 divergenceHeight = int64(6) 106 primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight) 107 primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight) 108 ) 109 // validators don't change in this network (however we still use a map just for convenience) 110 witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight+2, valSize, 2, bTime) 111 witness := mockp.New(chainID, witnessHeaders, witnessValidators) 112 113 for height := int64(1); height <= latestHeight; height++ { 114 if height < divergenceHeight { 115 primaryHeaders[height] = witnessHeaders[height] 116 primaryValidators[height] = witnessValidators[height] 117 continue 118 } 119 // we don't have a network partition so we will make 4/5 (greater than 2/3) malicious and vote again for 120 // a different block (which we do by adding txs) 121 primaryHeaders[height] = chainKeys[height].GenSignedHeader(chainID, height, 122 bTime.Add(time.Duration(height)*time.Minute), []types.Tx{[]byte("abcd")}, 123 witnessValidators[height], witnessValidators[height+1], hash("app_hash"), 124 hash("cons_hash"), hash("results_hash"), 0, len(chainKeys[height])-1) 125 primaryValidators[height] = witnessValidators[height] 126 } 127 primary := mockp.New(chainID, primaryHeaders, primaryValidators) 128 129 c, err := light.NewClient( 130 ctx, 131 chainID, 132 light.TrustOptions{ 133 Period: 4 * time.Hour, 134 Height: 1, 135 Hash: primaryHeaders[1].Hash(), 136 }, 137 primary, 138 []provider.Provider{witness}, 139 dbs.New(dbm.NewMemDB(), chainID), 140 light.Logger(log.TestingLogger()), 141 light.MaxRetryAttempts(1), 142 verificationOption, 143 ) 144 require.NoError(t, err) 145 146 // Check verification returns an error. 147 _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour)) 148 if assert.Error(t, err) { 149 assert.Equal(t, light.ErrLightClientAttack, err) 150 } 151 152 // Check evidence was sent to both full nodes. 153 // Common height should be set to the height of the divergent header in the instance 154 // of an equivocation attack and the validator sets are the same as what the witness has 155 evAgainstPrimary := &types.LightClientAttackEvidence{ 156 ConflictingBlock: &types.LightBlock{ 157 SignedHeader: primaryHeaders[divergenceHeight], 158 ValidatorSet: primaryValidators[divergenceHeight], 159 }, 160 CommonHeight: divergenceHeight, 161 } 162 assert.True(t, witness.HasEvidence(evAgainstPrimary)) 163 164 evAgainstWitness := &types.LightClientAttackEvidence{ 165 ConflictingBlock: &types.LightBlock{ 166 SignedHeader: witnessHeaders[divergenceHeight], 167 ValidatorSet: witnessValidators[divergenceHeight], 168 }, 169 CommonHeight: divergenceHeight, 170 } 171 assert.True(t, primary.HasEvidence(evAgainstWitness)) 172 } 173 } 174 175 func TestLightClientAttackEvidence_ForwardLunatic(t *testing.T) { 176 // primary performs a lunatic attack but changes the time of the header to 177 // something in the future relative to the blockchain 178 var ( 179 latestHeight = int64(10) 180 valSize = 5 181 forgedHeight = int64(12) 182 proofHeight = int64(11) 183 primaryHeaders = make(map[int64]*types.SignedHeader, forgedHeight) 184 primaryValidators = make(map[int64]*types.ValidatorSet, forgedHeight) 185 ) 186 187 witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight, valSize, 2, bTime) 188 189 // primary has the exact same headers except it forges one extra header in the future using keys from 2/5ths of 190 // the validators 191 for h := range witnessHeaders { 192 primaryHeaders[h] = witnessHeaders[h] 193 primaryValidators[h] = witnessValidators[h] 194 } 195 forgedKeys := chainKeys[latestHeight].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain) 196 primaryValidators[forgedHeight] = forgedKeys.ToValidators(2, 0) 197 primaryHeaders[forgedHeight] = forgedKeys.GenSignedHeader( 198 chainID, 199 forgedHeight, 200 bTime.Add(time.Duration(latestHeight+1)*time.Minute), // 11 mins 201 nil, 202 primaryValidators[forgedHeight], 203 primaryValidators[forgedHeight], 204 hash("app_hash"), 205 hash("cons_hash"), 206 hash("results_hash"), 207 0, len(forgedKeys), 208 ) 209 210 witness := mockp.New(chainID, witnessHeaders, witnessValidators) 211 primary := mockp.New(chainID, primaryHeaders, primaryValidators) 212 213 laggingWitness := witness.Copy(chainID) 214 215 // In order to perform the attack, the primary needs at least one accomplice as a witness to also 216 // send the forged block 217 accomplice := primary 218 219 c, err := light.NewClient( 220 ctx, 221 chainID, 222 light.TrustOptions{ 223 Period: 4 * time.Hour, 224 Height: 1, 225 Hash: primaryHeaders[1].Hash(), 226 }, 227 primary, 228 []provider.Provider{witness, accomplice}, 229 dbs.New(dbm.NewMemDB(), chainID), 230 light.Logger(log.TestingLogger()), 231 light.MaxClockDrift(1*time.Second), 232 light.MaxBlockLag(1*time.Second), 233 ) 234 require.NoError(t, err) 235 236 // two seconds later, the supporting withness should receive the header that can be used 237 // to prove that there was an attack 238 vals := chainKeys[latestHeight].ToValidators(2, 0) 239 newLb := &types.LightBlock{ 240 SignedHeader: chainKeys[latestHeight].GenSignedHeader( 241 chainID, 242 proofHeight, 243 bTime.Add(time.Duration(proofHeight+1)*time.Minute), // 12 mins 244 nil, 245 vals, 246 vals, 247 hash("app_hash"), 248 hash("cons_hash"), 249 hash("results_hash"), 250 0, len(chainKeys), 251 ), 252 ValidatorSet: vals, 253 } 254 go func() { 255 time.Sleep(2 * time.Second) 256 witness.AddLightBlock(newLb) 257 }() 258 259 // Now assert that verification returns an error. We craft the light clients time to be a little ahead of the chain 260 // to allow a window for the attack to manifest itself. 261 _, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute)) 262 if assert.Error(t, err) { 263 assert.Equal(t, light.ErrLightClientAttack, err) 264 } 265 266 // Check evidence was sent to the witness against the full node 267 evAgainstPrimary := &types.LightClientAttackEvidence{ 268 ConflictingBlock: &types.LightBlock{ 269 SignedHeader: primaryHeaders[forgedHeight], 270 ValidatorSet: primaryValidators[forgedHeight], 271 }, 272 CommonHeight: latestHeight, 273 } 274 assert.True(t, witness.HasEvidence(evAgainstPrimary)) 275 276 // We attempt the same call but now the supporting witness has a block which should 277 // immediately conflict in time with the primary 278 _, err = c.VerifyLightBlockAtHeight(ctx, forgedHeight, bTime.Add(time.Duration(forgedHeight)*time.Minute)) 279 if assert.Error(t, err) { 280 assert.Equal(t, light.ErrLightClientAttack, err) 281 } 282 assert.True(t, witness.HasEvidence(evAgainstPrimary)) 283 284 // Lastly we test the unfortunate case where the light clients supporting witness doesn't update 285 // in enough time 286 c, err = light.NewClient( 287 ctx, 288 chainID, 289 light.TrustOptions{ 290 Period: 4 * time.Hour, 291 Height: 1, 292 Hash: primaryHeaders[1].Hash(), 293 }, 294 primary, 295 []provider.Provider{laggingWitness, accomplice}, 296 dbs.New(dbm.NewMemDB(), chainID), 297 light.Logger(log.TestingLogger()), 298 light.MaxClockDrift(1*time.Second), 299 light.MaxBlockLag(1*time.Second), 300 ) 301 require.NoError(t, err) 302 303 _, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute)) 304 assert.NoError(t, err) 305 306 } 307 308 // 1. Different nodes therefore a divergent header is produced. 309 // => light client returns an error upon creation because primary and witness 310 // have a different view. 311 func TestClientDivergentTraces1(t *testing.T) { 312 primary := mockp.New(genMockNode(chainID, 10, 5, 2, bTime)) 313 firstBlock, err := primary.LightBlock(ctx, 1) 314 require.NoError(t, err) 315 witness := mockp.New(genMockNode(chainID, 10, 5, 2, bTime)) 316 317 _, err = light.NewClient( 318 ctx, 319 chainID, 320 light.TrustOptions{ 321 Height: 1, 322 Hash: firstBlock.Hash(), 323 Period: 4 * time.Hour, 324 }, 325 primary, 326 []provider.Provider{witness}, 327 dbs.New(dbm.NewMemDB(), chainID), 328 light.Logger(log.TestingLogger()), 329 light.MaxRetryAttempts(1), 330 ) 331 require.Error(t, err) 332 assert.Contains(t, err.Error(), "does not match primary") 333 } 334 335 // 2. Two out of three nodes don't respond but the third has a header that matches 336 // => verification should be successful and all the witnesses should remain 337 func TestClientDivergentTraces2(t *testing.T) { 338 primary := mockp.New(genMockNode(chainID, 10, 5, 2, bTime)) 339 firstBlock, err := primary.LightBlock(ctx, 1) 340 require.NoError(t, err) 341 c, err := light.NewClient( 342 ctx, 343 chainID, 344 light.TrustOptions{ 345 Height: 1, 346 Hash: firstBlock.Hash(), 347 Period: 4 * time.Hour, 348 }, 349 primary, 350 []provider.Provider{deadNode, deadNode, primary}, 351 dbs.New(dbm.NewMemDB(), chainID), 352 light.Logger(log.TestingLogger()), 353 light.MaxRetryAttempts(1), 354 ) 355 require.NoError(t, err) 356 357 _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour)) 358 assert.NoError(t, err) 359 assert.Equal(t, 3, len(c.Witnesses())) 360 } 361 362 // 3. witness has the same first header, but different second header 363 // => creation should succeed, but the verification should fail 364 func TestClientDivergentTraces3(t *testing.T) { 365 _, primaryHeaders, primaryVals := genMockNode(chainID, 10, 5, 2, bTime) 366 primary := mockp.New(chainID, primaryHeaders, primaryVals) 367 368 firstBlock, err := primary.LightBlock(ctx, 1) 369 require.NoError(t, err) 370 371 _, mockHeaders, mockVals := genMockNode(chainID, 10, 5, 2, bTime) 372 mockHeaders[1] = primaryHeaders[1] 373 mockVals[1] = primaryVals[1] 374 witness := mockp.New(chainID, mockHeaders, mockVals) 375 376 c, err := light.NewClient( 377 ctx, 378 chainID, 379 light.TrustOptions{ 380 Height: 1, 381 Hash: firstBlock.Hash(), 382 Period: 4 * time.Hour, 383 }, 384 primary, 385 []provider.Provider{witness}, 386 dbs.New(dbm.NewMemDB(), chainID), 387 light.Logger(log.TestingLogger()), 388 light.MaxRetryAttempts(1), 389 ) 390 require.NoError(t, err) 391 392 _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour)) 393 assert.Error(t, err) 394 assert.Equal(t, 1, len(c.Witnesses())) 395 } 396 397 // 4. Witness has a divergent header but can not produce a valid trace to back it up. 398 // It should be ignored 399 func TestClientDivergentTraces4(t *testing.T) { 400 _, primaryHeaders, primaryVals := genMockNode(chainID, 10, 5, 2, bTime) 401 primary := mockp.New(chainID, primaryHeaders, primaryVals) 402 403 firstBlock, err := primary.LightBlock(ctx, 1) 404 require.NoError(t, err) 405 406 _, mockHeaders, mockVals := genMockNode(chainID, 10, 5, 2, bTime) 407 witness := primary.Copy(chainID) 408 witness.AddLightBlock(&types.LightBlock{ 409 SignedHeader: mockHeaders[10], 410 ValidatorSet: mockVals[10], 411 }) 412 413 c, err := light.NewClient( 414 ctx, 415 chainID, 416 light.TrustOptions{ 417 Height: 1, 418 Hash: firstBlock.Hash(), 419 Period: 4 * time.Hour, 420 }, 421 primary, 422 []provider.Provider{witness}, 423 dbs.New(dbm.NewMemDB(), chainID), 424 light.Logger(log.TestingLogger()), 425 ) 426 require.NoError(t, err) 427 428 _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour)) 429 assert.Error(t, err) 430 assert.Equal(t, 1, len(c.Witnesses())) 431 }