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