github.com/evdatsion/aphelion-dpos-bft@v0.32.1/blockchain/store_test.go (about) 1 package blockchain 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "runtime/debug" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 cfg "github.com/evdatsion/aphelion-dpos-bft/config" 15 cmn "github.com/evdatsion/aphelion-dpos-bft/libs/common" 16 "github.com/evdatsion/aphelion-dpos-bft/libs/db" 17 dbm "github.com/evdatsion/aphelion-dpos-bft/libs/db" 18 "github.com/evdatsion/aphelion-dpos-bft/libs/log" 19 sm "github.com/evdatsion/aphelion-dpos-bft/state" 20 21 "github.com/evdatsion/aphelion-dpos-bft/types" 22 tmtime "github.com/evdatsion/aphelion-dpos-bft/types/time" 23 ) 24 25 // A cleanupFunc cleans up any config / test files created for a particular 26 // test. 27 type cleanupFunc func() 28 29 // make a Commit with a single vote containing just the height and a timestamp 30 func makeTestCommit(height int64, timestamp time.Time) *types.Commit { 31 commitSigs := []*types.CommitSig{{Height: height, Timestamp: timestamp}} 32 return types.NewCommit(types.BlockID{}, commitSigs) 33 } 34 35 func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFunc) { 36 config := cfg.ResetTestRoot("blockchain_reactor_test") 37 // blockDB := dbm.NewDebugDB("blockDB", dbm.NewMemDB()) 38 // stateDB := dbm.NewDebugDB("stateDB", dbm.NewMemDB()) 39 blockDB := dbm.NewMemDB() 40 stateDB := dbm.NewMemDB() 41 state, err := sm.LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) 42 if err != nil { 43 panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) 44 } 45 return state, NewBlockStore(blockDB), func() { os.RemoveAll(config.RootDir) } 46 } 47 48 func TestLoadBlockStoreStateJSON(t *testing.T) { 49 db := db.NewMemDB() 50 51 bsj := &BlockStoreStateJSON{Height: 1000} 52 bsj.Save(db) 53 54 retrBSJ := LoadBlockStoreStateJSON(db) 55 56 assert.Equal(t, *bsj, retrBSJ, "expected the retrieved DBs to match") 57 } 58 59 func TestNewBlockStore(t *testing.T) { 60 db := db.NewMemDB() 61 db.Set(blockStoreKey, []byte(`{"height": "10000"}`)) 62 bs := NewBlockStore(db) 63 require.Equal(t, int64(10000), bs.Height(), "failed to properly parse blockstore") 64 65 panicCausers := []struct { 66 data []byte 67 wantErr string 68 }{ 69 {[]byte("artful-doger"), "not unmarshal bytes"}, 70 {[]byte(" "), "unmarshal bytes"}, 71 } 72 73 for i, tt := range panicCausers { 74 // Expecting a panic here on trying to parse an invalid blockStore 75 _, _, panicErr := doFn(func() (interface{}, error) { 76 db.Set(blockStoreKey, tt.data) 77 _ = NewBlockStore(db) 78 return nil, nil 79 }) 80 require.NotNil(t, panicErr, "#%d panicCauser: %q expected a panic", i, tt.data) 81 assert.Contains(t, fmt.Sprintf("%#v", panicErr), tt.wantErr, "#%d data: %q", i, tt.data) 82 } 83 84 db.Set(blockStoreKey, nil) 85 bs = NewBlockStore(db) 86 assert.Equal(t, bs.Height(), int64(0), "expecting nil bytes to be unmarshaled alright") 87 } 88 89 func freshBlockStore() (*BlockStore, db.DB) { 90 db := db.NewMemDB() 91 return NewBlockStore(db), db 92 } 93 94 var ( 95 state sm.State 96 block *types.Block 97 partSet *types.PartSet 98 part1 *types.Part 99 part2 *types.Part 100 seenCommit1 *types.Commit 101 ) 102 103 func TestMain(m *testing.M) { 104 var cleanup cleanupFunc 105 state, _, cleanup = makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer))) 106 block = makeBlock(1, state, new(types.Commit)) 107 partSet = block.MakePartSet(2) 108 part1 = partSet.GetPart(0) 109 part2 = partSet.GetPart(1) 110 seenCommit1 = makeTestCommit(10, tmtime.Now()) 111 code := m.Run() 112 cleanup() 113 os.Exit(code) 114 } 115 116 // TODO: This test should be simplified ... 117 118 func TestBlockStoreSaveLoadBlock(t *testing.T) { 119 state, bs, cleanup := makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer))) 120 defer cleanup() 121 require.Equal(t, bs.Height(), int64(0), "initially the height should be zero") 122 123 // check there are no blocks at various heights 124 noBlockHeights := []int64{0, -1, 100, 1000, 2} 125 for i, height := range noBlockHeights { 126 if g := bs.LoadBlock(height); g != nil { 127 t.Errorf("#%d: height(%d) got a block; want nil", i, height) 128 } 129 } 130 131 // save a block 132 block := makeBlock(bs.Height()+1, state, new(types.Commit)) 133 validPartSet := block.MakePartSet(2) 134 seenCommit := makeTestCommit(10, tmtime.Now()) 135 bs.SaveBlock(block, partSet, seenCommit) 136 require.Equal(t, bs.Height(), block.Header.Height, "expecting the new height to be changed") 137 138 incompletePartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 2}) 139 uncontiguousPartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 0}) 140 uncontiguousPartSet.AddPart(part2) 141 142 header1 := types.Header{ 143 Height: 1, 144 NumTxs: 100, 145 ChainID: "block_test", 146 Time: tmtime.Now(), 147 } 148 header2 := header1 149 header2.Height = 4 150 151 // End of setup, test data 152 153 commitAtH10 := makeTestCommit(10, tmtime.Now()) 154 tuples := []struct { 155 block *types.Block 156 parts *types.PartSet 157 seenCommit *types.Commit 158 wantErr bool 159 wantPanic string 160 161 corruptBlockInDB bool 162 corruptCommitInDB bool 163 corruptSeenCommitInDB bool 164 eraseCommitInDB bool 165 eraseSeenCommitInDB bool 166 }{ 167 { 168 block: newBlock(header1, commitAtH10), 169 parts: validPartSet, 170 seenCommit: seenCommit1, 171 }, 172 173 { 174 block: nil, 175 wantPanic: "only save a non-nil block", 176 }, 177 178 { 179 block: newBlock(header2, commitAtH10), 180 parts: uncontiguousPartSet, 181 wantPanic: "only save contiguous blocks", // and incomplete and uncontiguous parts 182 }, 183 184 { 185 block: newBlock(header1, commitAtH10), 186 parts: incompletePartSet, 187 wantPanic: "only save complete block", // incomplete parts 188 }, 189 190 { 191 block: newBlock(header1, commitAtH10), 192 parts: validPartSet, 193 seenCommit: seenCommit1, 194 corruptCommitInDB: true, // Corrupt the DB's commit entry 195 wantPanic: "unmarshal to types.Commit failed", 196 }, 197 198 { 199 block: newBlock(header1, commitAtH10), 200 parts: validPartSet, 201 seenCommit: seenCommit1, 202 wantPanic: "unmarshal to types.BlockMeta failed", 203 corruptBlockInDB: true, // Corrupt the DB's block entry 204 }, 205 206 { 207 block: newBlock(header1, commitAtH10), 208 parts: validPartSet, 209 seenCommit: seenCommit1, 210 211 // Expecting no error and we want a nil back 212 eraseSeenCommitInDB: true, 213 }, 214 215 { 216 block: newBlock(header1, commitAtH10), 217 parts: validPartSet, 218 seenCommit: seenCommit1, 219 220 corruptSeenCommitInDB: true, 221 wantPanic: "unmarshal to types.Commit failed", 222 }, 223 224 { 225 block: newBlock(header1, commitAtH10), 226 parts: validPartSet, 227 seenCommit: seenCommit1, 228 229 // Expecting no error and we want a nil back 230 eraseCommitInDB: true, 231 }, 232 } 233 234 type quad struct { 235 block *types.Block 236 commit *types.Commit 237 meta *types.BlockMeta 238 239 seenCommit *types.Commit 240 } 241 242 for i, tuple := range tuples { 243 bs, db := freshBlockStore() 244 // SaveBlock 245 res, err, panicErr := doFn(func() (interface{}, error) { 246 bs.SaveBlock(tuple.block, tuple.parts, tuple.seenCommit) 247 if tuple.block == nil { 248 return nil, nil 249 } 250 251 if tuple.corruptBlockInDB { 252 db.Set(calcBlockMetaKey(tuple.block.Height), []byte("block-bogus")) 253 } 254 bBlock := bs.LoadBlock(tuple.block.Height) 255 bBlockMeta := bs.LoadBlockMeta(tuple.block.Height) 256 257 if tuple.eraseSeenCommitInDB { 258 db.Delete(calcSeenCommitKey(tuple.block.Height)) 259 } 260 if tuple.corruptSeenCommitInDB { 261 db.Set(calcSeenCommitKey(tuple.block.Height), []byte("bogus-seen-commit")) 262 } 263 bSeenCommit := bs.LoadSeenCommit(tuple.block.Height) 264 265 commitHeight := tuple.block.Height - 1 266 if tuple.eraseCommitInDB { 267 db.Delete(calcBlockCommitKey(commitHeight)) 268 } 269 if tuple.corruptCommitInDB { 270 db.Set(calcBlockCommitKey(commitHeight), []byte("foo-bogus")) 271 } 272 bCommit := bs.LoadBlockCommit(commitHeight) 273 return &quad{block: bBlock, seenCommit: bSeenCommit, commit: bCommit, 274 meta: bBlockMeta}, nil 275 }) 276 277 if subStr := tuple.wantPanic; subStr != "" { 278 if panicErr == nil { 279 t.Errorf("#%d: want a non-nil panic", i) 280 } else if got := fmt.Sprintf("%#v", panicErr); !strings.Contains(got, subStr) { 281 t.Errorf("#%d:\n\tgotErr: %q\nwant substring: %q", i, got, subStr) 282 } 283 continue 284 } 285 286 if tuple.wantErr { 287 if err == nil { 288 t.Errorf("#%d: got nil error", i) 289 } 290 continue 291 } 292 293 assert.Nil(t, panicErr, "#%d: unexpected panic", i) 294 assert.Nil(t, err, "#%d: expecting a non-nil error", i) 295 qua, ok := res.(*quad) 296 if !ok || qua == nil { 297 t.Errorf("#%d: got nil quad back; gotType=%T", i, res) 298 continue 299 } 300 if tuple.eraseSeenCommitInDB { 301 assert.Nil(t, qua.seenCommit, 302 "erased the seenCommit in the DB hence we should get back a nil seenCommit") 303 } 304 if tuple.eraseCommitInDB { 305 assert.Nil(t, qua.commit, 306 "erased the commit in the DB hence we should get back a nil commit") 307 } 308 } 309 } 310 311 func TestLoadBlockPart(t *testing.T) { 312 bs, db := freshBlockStore() 313 height, index := int64(10), 1 314 loadPart := func() (interface{}, error) { 315 part := bs.LoadBlockPart(height, index) 316 return part, nil 317 } 318 319 // Initially no contents. 320 // 1. Requesting for a non-existent block shouldn't fail 321 res, _, panicErr := doFn(loadPart) 322 require.Nil(t, panicErr, "a non-existent block part shouldn't cause a panic") 323 require.Nil(t, res, "a non-existent block part should return nil") 324 325 // 2. Next save a corrupted block then try to load it 326 db.Set(calcBlockPartKey(height, index), []byte("Tendermint")) 327 res, _, panicErr = doFn(loadPart) 328 require.NotNil(t, panicErr, "expecting a non-nil panic") 329 require.Contains(t, panicErr.Error(), "unmarshal to types.Part failed") 330 331 // 3. A good block serialized and saved to the DB should be retrievable 332 db.Set(calcBlockPartKey(height, index), cdc.MustMarshalBinaryBare(part1)) 333 gotPart, _, panicErr := doFn(loadPart) 334 require.Nil(t, panicErr, "an existent and proper block should not panic") 335 require.Nil(t, res, "a properly saved block should return a proper block") 336 require.Equal(t, gotPart.(*types.Part), part1, 337 "expecting successful retrieval of previously saved block") 338 } 339 340 func TestLoadBlockMeta(t *testing.T) { 341 bs, db := freshBlockStore() 342 height := int64(10) 343 loadMeta := func() (interface{}, error) { 344 meta := bs.LoadBlockMeta(height) 345 return meta, nil 346 } 347 348 // Initially no contents. 349 // 1. Requesting for a non-existent blockMeta shouldn't fail 350 res, _, panicErr := doFn(loadMeta) 351 require.Nil(t, panicErr, "a non-existent blockMeta shouldn't cause a panic") 352 require.Nil(t, res, "a non-existent blockMeta should return nil") 353 354 // 2. Next save a corrupted blockMeta then try to load it 355 db.Set(calcBlockMetaKey(height), []byte("Tendermint-Meta")) 356 res, _, panicErr = doFn(loadMeta) 357 require.NotNil(t, panicErr, "expecting a non-nil panic") 358 require.Contains(t, panicErr.Error(), "unmarshal to types.BlockMeta") 359 360 // 3. A good blockMeta serialized and saved to the DB should be retrievable 361 meta := &types.BlockMeta{} 362 db.Set(calcBlockMetaKey(height), cdc.MustMarshalBinaryBare(meta)) 363 gotMeta, _, panicErr := doFn(loadMeta) 364 require.Nil(t, panicErr, "an existent and proper block should not panic") 365 require.Nil(t, res, "a properly saved blockMeta should return a proper blocMeta ") 366 require.Equal(t, cdc.MustMarshalBinaryBare(meta), cdc.MustMarshalBinaryBare(gotMeta), 367 "expecting successful retrieval of previously saved blockMeta") 368 } 369 370 func TestBlockFetchAtHeight(t *testing.T) { 371 state, bs, cleanup := makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer))) 372 defer cleanup() 373 require.Equal(t, bs.Height(), int64(0), "initially the height should be zero") 374 block := makeBlock(bs.Height()+1, state, new(types.Commit)) 375 376 partSet := block.MakePartSet(2) 377 seenCommit := makeTestCommit(10, tmtime.Now()) 378 bs.SaveBlock(block, partSet, seenCommit) 379 require.Equal(t, bs.Height(), block.Header.Height, "expecting the new height to be changed") 380 381 blockAtHeight := bs.LoadBlock(bs.Height()) 382 bz1 := cdc.MustMarshalBinaryBare(block) 383 bz2 := cdc.MustMarshalBinaryBare(blockAtHeight) 384 require.Equal(t, bz1, bz2) 385 require.Equal(t, block.Hash(), blockAtHeight.Hash(), 386 "expecting a successful load of the last saved block") 387 388 blockAtHeightPlus1 := bs.LoadBlock(bs.Height() + 1) 389 require.Nil(t, blockAtHeightPlus1, "expecting an unsuccessful load of Height()+1") 390 blockAtHeightPlus2 := bs.LoadBlock(bs.Height() + 2) 391 require.Nil(t, blockAtHeightPlus2, "expecting an unsuccessful load of Height()+2") 392 } 393 394 func doFn(fn func() (interface{}, error)) (res interface{}, err error, panicErr error) { 395 defer func() { 396 if r := recover(); r != nil { 397 switch e := r.(type) { 398 case error: 399 panicErr = e 400 case string: 401 panicErr = fmt.Errorf("%s", e) 402 default: 403 if st, ok := r.(fmt.Stringer); ok { 404 panicErr = fmt.Errorf("%s", st) 405 } else { 406 panicErr = fmt.Errorf("%s", debug.Stack()) 407 } 408 } 409 } 410 }() 411 412 res, err = fn() 413 return res, err, panicErr 414 } 415 416 func newBlock(hdr types.Header, lastCommit *types.Commit) *types.Block { 417 return &types.Block{ 418 Header: hdr, 419 LastCommit: lastCommit, 420 } 421 }