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