bitbucket.org/number571/tendermint@v0.8.14/store/store_test.go (about) 1 package store 2 3 import ( 4 "fmt" 5 "os" 6 "runtime/debug" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 dbm "github.com/tendermint/tm-db" 14 15 cfg "bitbucket.org/number571/tendermint/config" 16 "bitbucket.org/number571/tendermint/crypto" 17 "bitbucket.org/number571/tendermint/libs/log" 18 tmrand "bitbucket.org/number571/tendermint/libs/rand" 19 tmtime "bitbucket.org/number571/tendermint/libs/time" 20 sm "bitbucket.org/number571/tendermint/state" 21 "bitbucket.org/number571/tendermint/state/test/factory" 22 "bitbucket.org/number571/tendermint/types" 23 "bitbucket.org/number571/tendermint/version" 24 ) 25 26 // A cleanupFunc cleans up any config / test files created for a particular 27 // test. 28 type cleanupFunc func() 29 30 // make a Commit with a single vote containing just the height and a timestamp 31 func makeTestCommit(height int64, timestamp time.Time) *types.Commit { 32 commitSigs := []types.CommitSig{{ 33 BlockIDFlag: types.BlockIDFlagCommit, 34 ValidatorAddress: tmrand.Bytes(crypto.AddressSize), 35 Timestamp: timestamp, 36 Signature: []byte("Signature"), 37 }} 38 return types.NewCommit( 39 height, 40 0, 41 types.BlockID{ 42 Hash: crypto.CRandBytes(32), 43 PartSetHeader: types.PartSetHeader{Hash: crypto.CRandBytes(32), Total: 2}, 44 }, 45 commitSigs) 46 } 47 48 func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFunc) { 49 config := cfg.ResetTestRoot("blockchain_reactor_test") 50 blockDB := dbm.NewMemDB() 51 state, err := sm.MakeGenesisStateFromFile(config.GenesisFile()) 52 if err != nil { 53 panic(fmt.Errorf("error constructing state from genesis file: %w", err)) 54 } 55 return state, NewBlockStore(blockDB), func() { os.RemoveAll(config.RootDir) } 56 } 57 58 func freshBlockStore() (*BlockStore, dbm.DB) { 59 db := dbm.NewMemDB() 60 return NewBlockStore(db), db 61 } 62 63 var ( 64 state sm.State 65 block *types.Block 66 partSet *types.PartSet 67 part1 *types.Part 68 part2 *types.Part 69 seenCommit1 *types.Commit 70 ) 71 72 func TestMain(m *testing.M) { 73 var cleanup cleanupFunc 74 state, _, cleanup = makeStateAndBlockStore(log.NewNopLogger()) 75 block = factory.MakeBlock(state, 1, new(types.Commit)) 76 partSet = block.MakePartSet(2) 77 part1 = partSet.GetPart(0) 78 part2 = partSet.GetPart(1) 79 seenCommit1 = makeTestCommit(10, tmtime.Now()) 80 code := m.Run() 81 cleanup() 82 os.Exit(code) 83 } 84 85 // TODO: This test should be simplified ... 86 func TestBlockStoreSaveLoadBlock(t *testing.T) { 87 state, bs, cleanup := makeStateAndBlockStore(log.NewNopLogger()) 88 defer cleanup() 89 require.Equal(t, bs.Base(), int64(0), "initially the base should be zero") 90 require.Equal(t, bs.Height(), int64(0), "initially the height should be zero") 91 92 // check there are no blocks at various heights 93 noBlockHeights := []int64{0, -1, 100, 1000, 2} 94 for i, height := range noBlockHeights { 95 if g := bs.LoadBlock(height); g != nil { 96 t.Errorf("#%d: height(%d) got a block; want nil", i, height) 97 } 98 } 99 100 // save a block 101 block := factory.MakeBlock(state, bs.Height()+1, new(types.Commit)) 102 validPartSet := block.MakePartSet(2) 103 seenCommit := makeTestCommit(10, tmtime.Now()) 104 bs.SaveBlock(block, partSet, seenCommit) 105 require.EqualValues(t, 1, bs.Base(), "expecting the new height to be changed") 106 require.EqualValues(t, block.Header.Height, bs.Height(), "expecting the new height to be changed") 107 108 incompletePartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 2}) 109 uncontiguousPartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 0}) 110 _, err := uncontiguousPartSet.AddPart(part2) 111 require.Error(t, err) 112 113 header1 := types.Header{ 114 Version: version.Consensus{Block: version.BlockProtocol}, 115 Height: 1, 116 ChainID: "block_test", 117 Time: tmtime.Now(), 118 ProposerAddress: tmrand.Bytes(crypto.AddressSize), 119 } 120 121 // End of setup, test data 122 commitAtH10 := makeTestCommit(10, tmtime.Now()) 123 tuples := []struct { 124 block *types.Block 125 parts *types.PartSet 126 seenCommit *types.Commit 127 wantPanic string 128 wantErr bool 129 130 corruptBlockInDB bool 131 corruptCommitInDB bool 132 corruptSeenCommitInDB bool 133 eraseCommitInDB bool 134 eraseSeenCommitInDB bool 135 }{ 136 { 137 block: newBlock(header1, commitAtH10), 138 parts: validPartSet, 139 seenCommit: seenCommit1, 140 }, 141 142 { 143 block: nil, 144 wantPanic: "only save a non-nil block", 145 }, 146 147 { 148 block: newBlock( // New block at height 5 in empty block store is fine 149 types.Header{ 150 Version: version.Consensus{Block: version.BlockProtocol}, 151 Height: 5, 152 ChainID: "block_test", 153 Time: tmtime.Now(), 154 ProposerAddress: tmrand.Bytes(crypto.AddressSize)}, 155 makeTestCommit(5, tmtime.Now()), 156 ), 157 parts: validPartSet, 158 seenCommit: makeTestCommit(5, tmtime.Now()), 159 }, 160 161 { 162 block: newBlock(header1, commitAtH10), 163 parts: incompletePartSet, 164 wantPanic: "only save complete block", // incomplete parts 165 }, 166 167 { 168 block: newBlock(header1, commitAtH10), 169 parts: validPartSet, 170 seenCommit: seenCommit1, 171 corruptCommitInDB: true, // Corrupt the DB's commit entry 172 wantPanic: "error reading block commit", 173 }, 174 175 { 176 block: newBlock(header1, commitAtH10), 177 parts: validPartSet, 178 seenCommit: seenCommit1, 179 wantPanic: "unmarshal to tmproto.BlockMeta", 180 corruptBlockInDB: true, // Corrupt the DB's block entry 181 }, 182 183 { 184 block: newBlock(header1, commitAtH10), 185 parts: validPartSet, 186 seenCommit: seenCommit1, 187 188 // Expecting no error and we want a nil back 189 eraseSeenCommitInDB: true, 190 }, 191 192 { 193 block: newBlock(header1, commitAtH10), 194 parts: validPartSet, 195 seenCommit: seenCommit1, 196 197 corruptSeenCommitInDB: true, 198 wantPanic: "error reading block seen commit", 199 }, 200 201 { 202 block: newBlock(header1, commitAtH10), 203 parts: validPartSet, 204 seenCommit: seenCommit1, 205 206 // Expecting no error and we want a nil back 207 eraseCommitInDB: true, 208 }, 209 } 210 211 type quad struct { 212 block *types.Block 213 commit *types.Commit 214 meta *types.BlockMeta 215 216 seenCommit *types.Commit 217 } 218 219 for i, tuple := range tuples { 220 tuple := tuple 221 bs, db := freshBlockStore() 222 // SaveBlock 223 res, err, panicErr := doFn(func() (interface{}, error) { 224 bs.SaveBlock(tuple.block, tuple.parts, tuple.seenCommit) 225 if tuple.block == nil { 226 return nil, nil 227 } 228 229 if tuple.corruptBlockInDB { 230 err := db.Set(blockMetaKey(tuple.block.Height), []byte("block-bogus")) 231 require.NoError(t, err) 232 } 233 bBlock := bs.LoadBlock(tuple.block.Height) 234 bBlockMeta := bs.LoadBlockMeta(tuple.block.Height) 235 236 if tuple.eraseSeenCommitInDB { 237 err := db.Delete(seenCommitKey(tuple.block.Height)) 238 require.NoError(t, err) 239 } 240 if tuple.corruptSeenCommitInDB { 241 err := db.Set(seenCommitKey(tuple.block.Height), []byte("bogus-seen-commit")) 242 require.NoError(t, err) 243 } 244 bSeenCommit := bs.LoadSeenCommit(tuple.block.Height) 245 246 commitHeight := tuple.block.Height - 1 247 if tuple.eraseCommitInDB { 248 err := db.Delete(blockCommitKey(commitHeight)) 249 require.NoError(t, err) 250 } 251 if tuple.corruptCommitInDB { 252 err := db.Set(blockCommitKey(commitHeight), []byte("foo-bogus")) 253 require.NoError(t, err) 254 } 255 bCommit := bs.LoadBlockCommit(commitHeight) 256 return &quad{block: bBlock, seenCommit: bSeenCommit, commit: bCommit, 257 meta: bBlockMeta}, nil 258 }) 259 260 if subStr := tuple.wantPanic; subStr != "" { 261 if panicErr == nil { 262 t.Errorf("#%d: want a non-nil panic", i) 263 } else if got := fmt.Sprintf("%#v", panicErr); !strings.Contains(got, subStr) { 264 t.Errorf("#%d:\n\tgotErr: %q\nwant substring: %q", i, got, subStr) 265 } 266 continue 267 } 268 269 if tuple.wantErr { 270 if err == nil { 271 t.Errorf("#%d: got nil error", i) 272 } 273 continue 274 } 275 276 assert.Nil(t, panicErr, "#%d: unexpected panic", i) 277 assert.Nil(t, err, "#%d: expecting a non-nil error", i) 278 qua, ok := res.(*quad) 279 if !ok || qua == nil { 280 t.Errorf("#%d: got nil quad back; gotType=%T", i, res) 281 continue 282 } 283 if tuple.eraseSeenCommitInDB { 284 assert.Nil(t, qua.seenCommit, 285 "erased the seenCommit in the DB hence we should get back a nil seenCommit") 286 } 287 if tuple.eraseCommitInDB { 288 assert.Nil(t, qua.commit, 289 "erased the commit in the DB hence we should get back a nil commit") 290 } 291 } 292 } 293 294 func TestLoadBaseMeta(t *testing.T) { 295 config := cfg.ResetTestRoot("blockchain_reactor_test") 296 defer os.RemoveAll(config.RootDir) 297 state, err := sm.MakeGenesisStateFromFile(config.GenesisFile()) 298 require.NoError(t, err) 299 bs := NewBlockStore(dbm.NewMemDB()) 300 301 for h := int64(1); h <= 10; h++ { 302 block := factory.MakeBlock(state, h, new(types.Commit)) 303 partSet := block.MakePartSet(2) 304 seenCommit := makeTestCommit(h, tmtime.Now()) 305 bs.SaveBlock(block, partSet, seenCommit) 306 } 307 308 pruned, err := bs.PruneBlocks(4) 309 require.NoError(t, err) 310 assert.EqualValues(t, 3, pruned) 311 312 baseBlock := bs.LoadBaseMeta() 313 assert.EqualValues(t, 4, baseBlock.Header.Height) 314 assert.EqualValues(t, 4, bs.Base()) 315 } 316 317 func TestLoadBlockPart(t *testing.T) { 318 bs, db := freshBlockStore() 319 height, index := int64(10), 1 320 loadPart := func() (interface{}, error) { 321 part := bs.LoadBlockPart(height, index) 322 return part, nil 323 } 324 325 // Initially no contents. 326 // 1. Requesting for a non-existent block shouldn't fail 327 res, _, panicErr := doFn(loadPart) 328 require.Nil(t, panicErr, "a non-existent block part shouldn't cause a panic") 329 require.Nil(t, res, "a non-existent block part should return nil") 330 331 // 2. Next save a corrupted block then try to load it 332 err := db.Set(blockPartKey(height, index), []byte("Tendermint")) 333 require.NoError(t, err) 334 res, _, panicErr = doFn(loadPart) 335 require.NotNil(t, panicErr, "expecting a non-nil panic") 336 require.Contains(t, panicErr.Error(), "unmarshal to tmproto.Part failed") 337 338 // 3. A good block serialized and saved to the DB should be retrievable 339 pb1, err := part1.ToProto() 340 require.NoError(t, err) 341 err = db.Set(blockPartKey(height, index), mustEncode(pb1)) 342 require.NoError(t, err) 343 gotPart, _, panicErr := doFn(loadPart) 344 require.Nil(t, panicErr, "an existent and proper block should not panic") 345 require.Nil(t, res, "a properly saved block should return a proper block") 346 require.Equal(t, gotPart.(*types.Part), part1, 347 "expecting successful retrieval of previously saved block") 348 } 349 350 func TestPruneBlocks(t *testing.T) { 351 config := cfg.ResetTestRoot("blockchain_reactor_test") 352 defer os.RemoveAll(config.RootDir) 353 state, err := sm.MakeGenesisStateFromFile(config.GenesisFile()) 354 require.NoError(t, err) 355 db := dbm.NewMemDB() 356 bs := NewBlockStore(db) 357 assert.EqualValues(t, 0, bs.Base()) 358 assert.EqualValues(t, 0, bs.Height()) 359 assert.EqualValues(t, 0, bs.Size()) 360 361 _, err = bs.PruneBlocks(0) 362 require.Error(t, err) 363 364 // make more than 1000 blocks, to test batch deletions 365 for h := int64(1); h <= 1500; h++ { 366 block := factory.MakeBlock(state, h, new(types.Commit)) 367 partSet := block.MakePartSet(2) 368 seenCommit := makeTestCommit(h, tmtime.Now()) 369 bs.SaveBlock(block, partSet, seenCommit) 370 } 371 372 assert.EqualValues(t, 1, bs.Base()) 373 assert.EqualValues(t, 1500, bs.Height()) 374 assert.EqualValues(t, 1500, bs.Size()) 375 376 prunedBlock := bs.LoadBlock(1199) 377 378 // Check that basic pruning works 379 pruned, err := bs.PruneBlocks(1200) 380 require.NoError(t, err) 381 assert.EqualValues(t, 1199, pruned) 382 assert.EqualValues(t, 1200, bs.Base()) 383 assert.EqualValues(t, 1500, bs.Height()) 384 assert.EqualValues(t, 301, bs.Size()) 385 386 require.NotNil(t, bs.LoadBlock(1200)) 387 require.Nil(t, bs.LoadBlock(1199)) 388 require.Nil(t, bs.LoadBlockByHash(prunedBlock.Hash())) 389 require.Nil(t, bs.LoadBlockCommit(1199)) 390 require.Nil(t, bs.LoadBlockMeta(1199)) 391 require.Nil(t, bs.LoadBlockPart(1199, 1)) 392 393 for i := int64(1); i < 1200; i++ { 394 require.Nil(t, bs.LoadBlock(i)) 395 } 396 for i := int64(1200); i <= 1500; i++ { 397 require.NotNil(t, bs.LoadBlock(i)) 398 } 399 400 // Pruning below the current base should not error 401 _, err = bs.PruneBlocks(1199) 402 require.NoError(t, err) 403 404 // Pruning to the current base should work 405 pruned, err = bs.PruneBlocks(1200) 406 require.NoError(t, err) 407 assert.EqualValues(t, 0, pruned) 408 409 // Pruning again should work 410 pruned, err = bs.PruneBlocks(1300) 411 require.NoError(t, err) 412 assert.EqualValues(t, 100, pruned) 413 assert.EqualValues(t, 1300, bs.Base()) 414 415 // Pruning beyond the current height should error 416 _, err = bs.PruneBlocks(1501) 417 require.Error(t, err) 418 419 // Pruning to the current height should work 420 pruned, err = bs.PruneBlocks(1500) 421 require.NoError(t, err) 422 assert.EqualValues(t, 200, pruned) 423 assert.Nil(t, bs.LoadBlock(1499)) 424 assert.NotNil(t, bs.LoadBlock(1500)) 425 assert.Nil(t, bs.LoadBlock(1501)) 426 } 427 428 func TestLoadBlockMeta(t *testing.T) { 429 bs, db := freshBlockStore() 430 height := int64(10) 431 loadMeta := func() (interface{}, error) { 432 meta := bs.LoadBlockMeta(height) 433 return meta, nil 434 } 435 436 // Initially no contents. 437 // 1. Requesting for a non-existent blockMeta shouldn't fail 438 res, _, panicErr := doFn(loadMeta) 439 require.Nil(t, panicErr, "a non-existent blockMeta shouldn't cause a panic") 440 require.Nil(t, res, "a non-existent blockMeta should return nil") 441 442 // 2. Next save a corrupted blockMeta then try to load it 443 err := db.Set(blockMetaKey(height), []byte("Tendermint-Meta")) 444 require.NoError(t, err) 445 res, _, panicErr = doFn(loadMeta) 446 require.NotNil(t, panicErr, "expecting a non-nil panic") 447 require.Contains(t, panicErr.Error(), "unmarshal to tmproto.BlockMeta") 448 449 // 3. A good blockMeta serialized and saved to the DB should be retrievable 450 meta := &types.BlockMeta{Header: types.Header{ 451 Version: version.Consensus{ 452 Block: version.BlockProtocol, App: 0}, Height: 1, ProposerAddress: tmrand.Bytes(crypto.AddressSize)}} 453 pbm := meta.ToProto() 454 err = db.Set(blockMetaKey(height), mustEncode(pbm)) 455 require.NoError(t, err) 456 gotMeta, _, panicErr := doFn(loadMeta) 457 require.Nil(t, panicErr, "an existent and proper block should not panic") 458 require.Nil(t, res, "a properly saved blockMeta should return a proper blocMeta ") 459 pbmeta := meta.ToProto() 460 if gmeta, ok := gotMeta.(*types.BlockMeta); ok { 461 pbgotMeta := gmeta.ToProto() 462 require.Equal(t, mustEncode(pbmeta), mustEncode(pbgotMeta), 463 "expecting successful retrieval of previously saved blockMeta") 464 } 465 } 466 467 func TestBlockFetchAtHeight(t *testing.T) { 468 state, bs, cleanup := makeStateAndBlockStore(log.NewNopLogger()) 469 defer cleanup() 470 require.Equal(t, bs.Height(), int64(0), "initially the height should be zero") 471 block := factory.MakeBlock(state, bs.Height()+1, new(types.Commit)) 472 473 partSet := block.MakePartSet(2) 474 seenCommit := makeTestCommit(10, tmtime.Now()) 475 bs.SaveBlock(block, partSet, seenCommit) 476 require.Equal(t, bs.Height(), block.Header.Height, "expecting the new height to be changed") 477 478 blockAtHeight := bs.LoadBlock(bs.Height()) 479 b1, err := block.ToProto() 480 require.NoError(t, err) 481 b2, err := blockAtHeight.ToProto() 482 require.NoError(t, err) 483 bz1 := mustEncode(b1) 484 bz2 := mustEncode(b2) 485 require.Equal(t, bz1, bz2) 486 require.Equal(t, block.Hash(), blockAtHeight.Hash(), 487 "expecting a successful load of the last saved block") 488 489 blockAtHeightPlus1 := bs.LoadBlock(bs.Height() + 1) 490 require.Nil(t, blockAtHeightPlus1, "expecting an unsuccessful load of Height()+1") 491 blockAtHeightPlus2 := bs.LoadBlock(bs.Height() + 2) 492 require.Nil(t, blockAtHeightPlus2, "expecting an unsuccessful load of Height()+2") 493 } 494 495 func TestSeenAndCanonicalCommit(t *testing.T) { 496 bs, _ := freshBlockStore() 497 height := int64(2) 498 loadCommit := func() (interface{}, error) { 499 meta := bs.LoadSeenCommit(height) 500 return meta, nil 501 } 502 503 // Initially no contents. 504 // 1. Requesting for a non-existent blockMeta shouldn't fail 505 res, _, panicErr := doFn(loadCommit) 506 require.Nil(t, panicErr, "a non-existent blockMeta shouldn't cause a panic") 507 require.Nil(t, res, "a non-existent blockMeta should return nil") 508 509 // produce a few blocks and check that the correct seen and cannoncial commits 510 // are persisted. 511 for h := int64(3); h <= 5; h++ { 512 c1 := bs.LoadSeenCommit(h) 513 require.Nil(t, c1) 514 c2 := bs.LoadBlockCommit(h - 1) 515 require.Nil(t, c2) 516 blockCommit := makeTestCommit(h-1, tmtime.Now()) 517 block := factory.MakeBlock(state, h, blockCommit) 518 partSet := block.MakePartSet(2) 519 seenCommit := makeTestCommit(h, tmtime.Now()) 520 bs.SaveBlock(block, partSet, seenCommit) 521 c3 := bs.LoadSeenCommit(h) 522 require.Equal(t, seenCommit.Hash(), c3.Hash()) 523 // the previous seen commit should be removed 524 c4 := bs.LoadSeenCommit(h - 1) 525 require.Nil(t, c4) 526 c5 := bs.LoadBlockCommit(h) 527 require.Nil(t, c5) 528 c6 := bs.LoadBlockCommit(h - 1) 529 require.Equal(t, blockCommit.Hash(), c6.Hash()) 530 } 531 532 } 533 534 func doFn(fn func() (interface{}, error)) (res interface{}, err error, panicErr error) { 535 defer func() { 536 if r := recover(); r != nil { 537 switch e := r.(type) { 538 case error: 539 panicErr = e 540 case string: 541 panicErr = fmt.Errorf("%s", e) 542 default: 543 if st, ok := r.(fmt.Stringer); ok { 544 panicErr = fmt.Errorf("%s", st) 545 } else { 546 panicErr = fmt.Errorf("%s", debug.Stack()) 547 } 548 } 549 } 550 }() 551 552 res, err = fn() 553 return res, err, panicErr 554 } 555 556 func newBlock(hdr types.Header, lastCommit *types.Commit) *types.Block { 557 return &types.Block{ 558 Header: hdr, 559 LastCommit: lastCommit, 560 } 561 }