mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-03 11:45:18 +00:00
add support for block pruning via ABCI Commit response (#4588)
* Added BlockStore.DeleteBlock() * Added initial block pruner prototype * wip * Added BlockStore.PruneBlocks() * Added consensus setting for block pruning * Added BlockStore base * Error on replay if base does not have blocks * Handle missing blocks when sending VoteSetMaj23Message * Error message tweak * Properly update blockstore state * Error message fix again * blockchain: ignore peer missing blocks * Added FIXME * Added test for block replay with truncated history * Handle peer base in blockchain reactor * Improved replay error handling * Added tests for Store.PruneBlocks() * Fix non-RPC handling of truncated block history * Panic on missing block meta in needProofBlock() * Updated changelog * Handle truncated block history in RPC layer * Added info about earliest block in /status RPC * Reorder height and base in blockchain reactor messages * Updated changelog * Fix tests * Appease linter * Minor review fixes * Non-empty BlockStores should always have base > 0 * Update code to assume base > 0 invariant * Added blockstore tests for pruning to 0 * Make sure we don't prune below the current base * Added BlockStore.Size() * config: added retain_blocks recommendations * Update v1 blockchain reactor to handle blockstore base * Added state database pruning * Propagate errors on missing validator sets * Comment tweaks * Improved error message Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com> * use ABCI field ResponseCommit.retain_height instead of retain-blocks config option * remove State.RetainHeight, return value instead * fix minor issues * rename pruneHeights() to pruneBlocks() * noop to fix GitHub borkage Co-authored-by: Anton Kaliaev <anton.kalyaev@gmail.com>
This commit is contained in:
117
store/store.go
117
store/store.go
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
db "github.com/tendermint/tm-db"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
@@ -24,6 +25,8 @@ Currently the precommit signatures are duplicated in the Block parts as
|
||||
well as the Commit. In the future this may change, perhaps by moving
|
||||
the Commit data outside the Block. (TODO)
|
||||
|
||||
The store can be assumed to contain all contiguous blocks between base and height (inclusive).
|
||||
|
||||
// NOTE: BlockStore methods will panic if they encounter errors
|
||||
// deserializing loaded data, indicating probable corruption on disk.
|
||||
*/
|
||||
@@ -31,6 +34,7 @@ type BlockStore struct {
|
||||
db dbm.DB
|
||||
|
||||
mtx sync.RWMutex
|
||||
base int64
|
||||
height int64
|
||||
}
|
||||
|
||||
@@ -39,18 +43,36 @@ type BlockStore struct {
|
||||
func NewBlockStore(db dbm.DB) *BlockStore {
|
||||
bsjson := LoadBlockStoreStateJSON(db)
|
||||
return &BlockStore{
|
||||
base: bsjson.Base,
|
||||
height: bsjson.Height,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Height returns the last known contiguous block height.
|
||||
// Base returns the first known contiguous block height, or 0 for empty block stores.
|
||||
func (bs *BlockStore) Base() int64 {
|
||||
bs.mtx.RLock()
|
||||
defer bs.mtx.RUnlock()
|
||||
return bs.base
|
||||
}
|
||||
|
||||
// Height returns the last known contiguous block height, or 0 for empty block stores.
|
||||
func (bs *BlockStore) Height() int64 {
|
||||
bs.mtx.RLock()
|
||||
defer bs.mtx.RUnlock()
|
||||
return bs.height
|
||||
}
|
||||
|
||||
// Size returns the number of blocks in the block store.
|
||||
func (bs *BlockStore) Size() int64 {
|
||||
bs.mtx.RLock()
|
||||
defer bs.mtx.RUnlock()
|
||||
if bs.height == 0 {
|
||||
return 0
|
||||
}
|
||||
return bs.height - bs.base + 1
|
||||
}
|
||||
|
||||
// LoadBlock returns the block with the given height.
|
||||
// If no block is found for that height, it returns nil.
|
||||
func (bs *BlockStore) LoadBlock(height int64) *types.Block {
|
||||
@@ -171,6 +193,74 @@ func (bs *BlockStore) LoadSeenCommit(height int64) *types.Commit {
|
||||
return commit
|
||||
}
|
||||
|
||||
// PruneBlocks removes block up to (but not including) a height. It returns number of blocks pruned.
|
||||
func (bs *BlockStore) PruneBlocks(height int64) (uint64, error) {
|
||||
if height <= 0 {
|
||||
return 0, fmt.Errorf("height must be greater than 0")
|
||||
}
|
||||
bs.mtx.RLock()
|
||||
if height > bs.height {
|
||||
bs.mtx.RUnlock()
|
||||
return 0, fmt.Errorf("cannot prune beyond the latest height %v", bs.height)
|
||||
}
|
||||
base := bs.base
|
||||
bs.mtx.RUnlock()
|
||||
if height < base {
|
||||
return 0, fmt.Errorf("cannot prune to height %v, it is lower than base height %v",
|
||||
height, base)
|
||||
}
|
||||
|
||||
pruned := uint64(0)
|
||||
batch := bs.db.NewBatch()
|
||||
defer batch.Close()
|
||||
flush := func(batch db.Batch, base int64) error {
|
||||
// We can't trust batches to be atomic, so update base first to make sure noone
|
||||
// tries to access missing blocks.
|
||||
bs.mtx.Lock()
|
||||
bs.base = base
|
||||
bs.mtx.Unlock()
|
||||
bs.saveState()
|
||||
|
||||
err := batch.WriteSync()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prune up to height %v: %w", base, err)
|
||||
}
|
||||
batch.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
for h := base; h < height; h++ {
|
||||
meta := bs.LoadBlockMeta(h)
|
||||
if meta == nil { // assume already deleted
|
||||
continue
|
||||
}
|
||||
batch.Delete(calcBlockMetaKey(h))
|
||||
batch.Delete(calcBlockHashKey(meta.BlockID.Hash))
|
||||
batch.Delete(calcBlockCommitKey(h))
|
||||
batch.Delete(calcSeenCommitKey(h))
|
||||
for p := 0; p < meta.BlockID.PartsHeader.Total; p++ {
|
||||
batch.Delete(calcBlockPartKey(h, p))
|
||||
}
|
||||
pruned++
|
||||
|
||||
// flush every 1000 blocks to avoid batches becoming too large
|
||||
if pruned%1000 == 0 && pruned > 0 {
|
||||
err := flush(batch, h)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
batch = bs.db.NewBatch()
|
||||
defer batch.Close()
|
||||
}
|
||||
}
|
||||
|
||||
err := flush(batch, height)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return pruned, nil
|
||||
}
|
||||
|
||||
// SaveBlock persists the given block, blockParts, and seenCommit to the underlying db.
|
||||
// blockParts: Must be parts of the block
|
||||
// seenCommit: The +2/3 precommits that were seen which committed at height.
|
||||
@@ -213,14 +303,17 @@ func (bs *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, s
|
||||
seenCommitBytes := cdc.MustMarshalBinaryBare(seenCommit)
|
||||
bs.db.Set(calcSeenCommitKey(height), seenCommitBytes)
|
||||
|
||||
// Save new BlockStoreStateJSON descriptor
|
||||
BlockStoreStateJSON{Height: height}.Save(bs.db)
|
||||
|
||||
// Done!
|
||||
bs.mtx.Lock()
|
||||
bs.height = height
|
||||
if bs.base == 0 && height == 1 {
|
||||
bs.base = 1
|
||||
}
|
||||
bs.mtx.Unlock()
|
||||
|
||||
// Save new BlockStoreStateJSON descriptor
|
||||
bs.saveState()
|
||||
|
||||
// Flush
|
||||
bs.db.SetSync(nil, nil)
|
||||
}
|
||||
@@ -233,6 +326,16 @@ func (bs *BlockStore) saveBlockPart(height int64, index int, part *types.Part) {
|
||||
bs.db.Set(calcBlockPartKey(height, index), partBytes)
|
||||
}
|
||||
|
||||
func (bs *BlockStore) saveState() {
|
||||
bs.mtx.RLock()
|
||||
bsJSON := BlockStoreStateJSON{
|
||||
Base: bs.base,
|
||||
Height: bs.height,
|
||||
}
|
||||
bs.mtx.RUnlock()
|
||||
bsJSON.Save(bs.db)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
func calcBlockMetaKey(height int64) []byte {
|
||||
@@ -261,6 +364,7 @@ var blockStoreKey = []byte("blockStore")
|
||||
|
||||
// BlockStoreStateJSON is the block store state JSON structure.
|
||||
type BlockStoreStateJSON struct {
|
||||
Base int64 `json:"base"`
|
||||
Height int64 `json:"height"`
|
||||
}
|
||||
|
||||
@@ -282,6 +386,7 @@ func LoadBlockStoreStateJSON(db dbm.DB) BlockStoreStateJSON {
|
||||
}
|
||||
if len(bytes) == 0 {
|
||||
return BlockStoreStateJSON{
|
||||
Base: 0,
|
||||
Height: 0,
|
||||
}
|
||||
}
|
||||
@@ -290,5 +395,9 @@ func LoadBlockStoreStateJSON(db dbm.DB) BlockStoreStateJSON {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not unmarshal bytes: %X", bytes))
|
||||
}
|
||||
// Backwards compatibility with persisted data from before Base existed.
|
||||
if bsj.Height > 0 && bsj.Base == 0 {
|
||||
bsj.Base = 1
|
||||
}
|
||||
return bsj
|
||||
}
|
||||
|
||||
@@ -65,20 +65,39 @@ func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFu
|
||||
|
||||
func TestLoadBlockStoreStateJSON(t *testing.T) {
|
||||
db := db.NewMemDB()
|
||||
bsj := &BlockStoreStateJSON{Base: 100, Height: 1000}
|
||||
bsj.Save(db)
|
||||
|
||||
retrBSJ := LoadBlockStoreStateJSON(db)
|
||||
assert.Equal(t, *bsj, retrBSJ, "expected the retrieved DBs to match")
|
||||
}
|
||||
|
||||
func TestLoadBlockStoreStateJSON_Empty(t *testing.T) {
|
||||
db := db.NewMemDB()
|
||||
|
||||
bsj := &BlockStoreStateJSON{}
|
||||
bsj.Save(db)
|
||||
|
||||
retrBSJ := LoadBlockStoreStateJSON(db)
|
||||
assert.Equal(t, BlockStoreStateJSON{}, retrBSJ, "expected the retrieved DBs to match")
|
||||
}
|
||||
|
||||
func TestLoadBlockStoreStateJSON_NoBase(t *testing.T) {
|
||||
db := db.NewMemDB()
|
||||
|
||||
bsj := &BlockStoreStateJSON{Height: 1000}
|
||||
bsj.Save(db)
|
||||
|
||||
retrBSJ := LoadBlockStoreStateJSON(db)
|
||||
|
||||
assert.Equal(t, *bsj, retrBSJ, "expected the retrieved DBs to match")
|
||||
assert.Equal(t, BlockStoreStateJSON{Base: 1, Height: 1000}, retrBSJ, "expected the retrieved DBs to match")
|
||||
}
|
||||
|
||||
func TestNewBlockStore(t *testing.T) {
|
||||
db := db.NewMemDB()
|
||||
err := db.Set(blockStoreKey, []byte(`{"height": "10000"}`))
|
||||
err := db.Set(blockStoreKey, []byte(`{"base": "100", "height": "10000"}`))
|
||||
require.NoError(t, err)
|
||||
bs := NewBlockStore(db)
|
||||
require.Equal(t, int64(100), bs.Base(), "failed to properly parse blockstore")
|
||||
require.Equal(t, int64(10000), bs.Height(), "failed to properly parse blockstore")
|
||||
|
||||
panicCausers := []struct {
|
||||
@@ -140,6 +159,7 @@ func TestMain(m *testing.M) {
|
||||
func TestBlockStoreSaveLoadBlock(t *testing.T) {
|
||||
state, bs, cleanup := makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer)))
|
||||
defer cleanup()
|
||||
require.Equal(t, bs.Base(), int64(0), "initially the base should be zero")
|
||||
require.Equal(t, bs.Height(), int64(0), "initially the height should be zero")
|
||||
|
||||
// check there are no blocks at various heights
|
||||
@@ -155,7 +175,8 @@ func TestBlockStoreSaveLoadBlock(t *testing.T) {
|
||||
validPartSet := block.MakePartSet(2)
|
||||
seenCommit := makeTestCommit(10, tmtime.Now())
|
||||
bs.SaveBlock(block, partSet, seenCommit)
|
||||
require.Equal(t, bs.Height(), block.Header.Height, "expecting the new height to be changed")
|
||||
require.EqualValues(t, 1, bs.Base(), "expecting the new height to be changed")
|
||||
require.EqualValues(t, block.Header.Height, bs.Height(), "expecting the new height to be changed")
|
||||
|
||||
incompletePartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 2})
|
||||
uncontiguousPartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 0})
|
||||
@@ -364,6 +385,92 @@ func TestLoadBlockPart(t *testing.T) {
|
||||
"expecting successful retrieval of previously saved block")
|
||||
}
|
||||
|
||||
func TestPruneBlocks(t *testing.T) {
|
||||
config := cfg.ResetTestRoot("blockchain_reactor_test")
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
state, err := sm.LoadStateFromDBOrGenesisFile(dbm.NewMemDB(), config.GenesisFile())
|
||||
require.NoError(t, err)
|
||||
db := dbm.NewMemDB()
|
||||
bs := NewBlockStore(db)
|
||||
assert.EqualValues(t, 0, bs.Base())
|
||||
assert.EqualValues(t, 0, bs.Height())
|
||||
assert.EqualValues(t, 0, bs.Size())
|
||||
|
||||
// pruning an empty store should error, even when pruning to 0
|
||||
_, err = bs.PruneBlocks(1)
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = bs.PruneBlocks(0)
|
||||
require.Error(t, err)
|
||||
|
||||
// make more than 1000 blocks, to test batch deletions
|
||||
for h := int64(1); h <= 1500; h++ {
|
||||
block := makeBlock(h, state, new(types.Commit))
|
||||
partSet := block.MakePartSet(2)
|
||||
seenCommit := makeTestCommit(h, tmtime.Now())
|
||||
bs.SaveBlock(block, partSet, seenCommit)
|
||||
}
|
||||
|
||||
assert.EqualValues(t, 1, bs.Base())
|
||||
assert.EqualValues(t, 1500, bs.Height())
|
||||
assert.EqualValues(t, 1500, bs.Size())
|
||||
|
||||
prunedBlock := bs.LoadBlock(1199)
|
||||
|
||||
// Check that basic pruning works
|
||||
pruned, err := bs.PruneBlocks(1200)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1199, pruned)
|
||||
assert.EqualValues(t, 1200, bs.Base())
|
||||
assert.EqualValues(t, 1500, bs.Height())
|
||||
assert.EqualValues(t, 301, bs.Size())
|
||||
assert.EqualValues(t, BlockStoreStateJSON{
|
||||
Base: 1200,
|
||||
Height: 1500,
|
||||
}, LoadBlockStoreStateJSON(db))
|
||||
|
||||
require.NotNil(t, bs.LoadBlock(1200))
|
||||
require.Nil(t, bs.LoadBlock(1199))
|
||||
require.Nil(t, bs.LoadBlockByHash(prunedBlock.Hash()))
|
||||
require.Nil(t, bs.LoadBlockCommit(1199))
|
||||
require.Nil(t, bs.LoadBlockMeta(1199))
|
||||
require.Nil(t, bs.LoadBlockPart(1199, 1))
|
||||
|
||||
for i := int64(1); i < 1200; i++ {
|
||||
require.Nil(t, bs.LoadBlock(i))
|
||||
}
|
||||
for i := int64(1200); i <= 1500; i++ {
|
||||
require.NotNil(t, bs.LoadBlock(i))
|
||||
}
|
||||
|
||||
// Pruning below the current base should error
|
||||
_, err = bs.PruneBlocks(1199)
|
||||
require.Error(t, err)
|
||||
|
||||
// Pruning to the current base should work
|
||||
pruned, err = bs.PruneBlocks(1200)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 0, pruned)
|
||||
|
||||
// Pruning again should work
|
||||
pruned, err = bs.PruneBlocks(1300)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 100, pruned)
|
||||
assert.EqualValues(t, 1300, bs.Base())
|
||||
|
||||
// Pruning beyond the current height should error
|
||||
_, err = bs.PruneBlocks(1501)
|
||||
require.Error(t, err)
|
||||
|
||||
// Pruning to the current height should work
|
||||
pruned, err = bs.PruneBlocks(1500)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 200, pruned)
|
||||
assert.Nil(t, bs.LoadBlock(1499))
|
||||
assert.NotNil(t, bs.LoadBlock(1500))
|
||||
assert.Nil(t, bs.LoadBlock(1501))
|
||||
}
|
||||
|
||||
func TestLoadBlockMeta(t *testing.T) {
|
||||
bs, db := freshBlockStore()
|
||||
height := int64(10)
|
||||
|
||||
Reference in New Issue
Block a user