package store
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
dbm "github.com/tendermint/tm-db"
|
|
|
|
tmsync "github.com/tendermint/tendermint/libs/sync"
|
|
tmstore "github.com/tendermint/tendermint/proto/tendermint/store"
|
|
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
/*
|
|
BlockStore is a simple low level store for blocks.
|
|
|
|
There are three types of information stored:
|
|
- BlockMeta: Meta information about each block
|
|
- Block part: Parts of each block, aggregated w/ PartSet
|
|
- Commit: The commit part of each block, for gossiping precommit votes
|
|
|
|
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.
|
|
*/
|
|
type BlockStore struct {
|
|
db dbm.DB
|
|
|
|
mtx tmsync.RWMutex
|
|
base int64
|
|
height int64
|
|
}
|
|
|
|
// NewBlockStore returns a new BlockStore with the given DB,
|
|
// initialized to the last height that was committed to the DB.
|
|
func NewBlockStore(db dbm.DB) *BlockStore {
|
|
bs := LoadBlockStoreState(db)
|
|
return &BlockStore{
|
|
base: bs.Base,
|
|
height: bs.Height,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
var blockMeta = bs.LoadBlockMeta(height)
|
|
if blockMeta == nil {
|
|
return nil
|
|
}
|
|
|
|
pbb := new(tmproto.Block)
|
|
buf := []byte{}
|
|
for i := 0; i < int(blockMeta.BlockID.PartSetHeader.Total); i++ {
|
|
part := bs.LoadBlockPart(height, i)
|
|
buf = append(buf, part.Bytes...)
|
|
}
|
|
err := proto.Unmarshal(buf, pbb)
|
|
if err != nil {
|
|
// NOTE: The existence of meta should imply the existence of the
|
|
// block. So, make sure meta is only saved after blocks are saved.
|
|
panic(fmt.Sprintf("Error reading block: %v", err))
|
|
}
|
|
|
|
block, err := types.BlockFromProto(pbb)
|
|
if err != nil {
|
|
panic(fmt.Errorf("error from proto block: %w", err))
|
|
}
|
|
|
|
return block
|
|
}
|
|
|
|
// LoadBlockByHash returns the block with the given hash.
|
|
// If no block is found for that hash, it returns nil.
|
|
// Panics if it fails to parse height associated with the given hash.
|
|
func (bs *BlockStore) LoadBlockByHash(hash []byte) *types.Block {
|
|
bz, err := bs.db.Get(calcBlockHashKey(hash))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(bz) == 0 {
|
|
return nil
|
|
}
|
|
|
|
s := string(bz)
|
|
height, err := strconv.ParseInt(s, 10, 64)
|
|
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to extract height from %s: %v", s, err))
|
|
}
|
|
return bs.LoadBlock(height)
|
|
}
|
|
|
|
// LoadBlockPart returns the Part at the given index
|
|
// from the block at the given height.
|
|
// If no part is found for the given height and index, it returns nil.
|
|
func (bs *BlockStore) LoadBlockPart(height int64, index int) *types.Part {
|
|
var pbpart = new(tmproto.Part)
|
|
|
|
bz, err := bs.db.Get(calcBlockPartKey(height, index))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(bz) == 0 {
|
|
return nil
|
|
}
|
|
|
|
err = proto.Unmarshal(bz, pbpart)
|
|
if err != nil {
|
|
panic(fmt.Errorf("unmarshal to tmproto.Part failed: %w", err))
|
|
}
|
|
part, err := types.PartFromProto(pbpart)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Error reading block part: %v", err))
|
|
}
|
|
|
|
return part
|
|
}
|
|
|
|
// LoadBlockMeta returns the BlockMeta for the given height.
|
|
// If no block is found for the given height, it returns nil.
|
|
func (bs *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta {
|
|
var pbbm = new(tmproto.BlockMeta)
|
|
bz, err := bs.db.Get(calcBlockMetaKey(height))
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if len(bz) == 0 {
|
|
return nil
|
|
}
|
|
|
|
err = proto.Unmarshal(bz, pbbm)
|
|
if err != nil {
|
|
panic(fmt.Errorf("unmarshal to tmproto.BlockMeta: %w", err))
|
|
}
|
|
|
|
blockMeta, err := types.BlockMetaFromProto(pbbm)
|
|
if err != nil {
|
|
panic(fmt.Errorf("error from proto blockMeta: %w", err))
|
|
}
|
|
|
|
return blockMeta
|
|
}
|
|
|
|
// LoadBlockCommit returns the Commit for the given height.
|
|
// This commit consists of the +2/3 and other Precommit-votes for block at `height`,
|
|
// and it comes from the block.LastCommit for `height+1`.
|
|
// If no commit is found for the given height, it returns nil.
|
|
func (bs *BlockStore) LoadBlockCommit(height int64) *types.Commit {
|
|
var pbc = new(tmproto.Commit)
|
|
bz, err := bs.db.Get(calcBlockCommitKey(height))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(bz) == 0 {
|
|
return nil
|
|
}
|
|
err = proto.Unmarshal(bz, pbc)
|
|
if err != nil {
|
|
panic(fmt.Errorf("error reading block commit: %w", err))
|
|
}
|
|
commit, err := types.CommitFromProto(pbc)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Error reading block commit: %v", err))
|
|
}
|
|
return commit
|
|
}
|
|
|
|
// LoadSeenCommit returns the locally seen Commit for the given height.
|
|
// This is useful when we've seen a commit, but there has not yet been
|
|
// a new block at `height + 1` that includes this commit in its block.LastCommit.
|
|
func (bs *BlockStore) LoadSeenCommit(height int64) *types.Commit {
|
|
var pbc = new(tmproto.Commit)
|
|
bz, err := bs.db.Get(calcSeenCommitKey(height))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(bz) == 0 {
|
|
return nil
|
|
}
|
|
err = proto.Unmarshal(bz, pbc)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("error reading block seen commit: %v", err))
|
|
}
|
|
|
|
commit, err := types.CommitFromProto(pbc)
|
|
if err != nil {
|
|
panic(fmt.Errorf("error from proto commit: %w", err))
|
|
}
|
|
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 dbm.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
|
|
}
|
|
if err := batch.Delete(calcBlockMetaKey(h)); err != nil {
|
|
return 0, err
|
|
}
|
|
if err := batch.Delete(calcBlockHashKey(meta.BlockID.Hash)); err != nil {
|
|
return 0, err
|
|
}
|
|
if err := batch.Delete(calcBlockCommitKey(h)); err != nil {
|
|
return 0, err
|
|
}
|
|
if err := batch.Delete(calcSeenCommitKey(h)); err != nil {
|
|
return 0, err
|
|
}
|
|
for p := 0; p < int(meta.BlockID.PartSetHeader.Total); p++ {
|
|
if err := batch.Delete(calcBlockPartKey(h, p)); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
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.
|
|
// If all the nodes restart after committing a block,
|
|
// we need this to reload the precommits to catch-up nodes to the
|
|
// most recent height. Otherwise they'd stall at H-1.
|
|
func (bs *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) {
|
|
if block == nil {
|
|
panic("BlockStore can only save a non-nil block")
|
|
}
|
|
|
|
height := block.Height
|
|
hash := block.Hash()
|
|
|
|
if g, w := height, bs.Height()+1; bs.Base() > 0 && g != w {
|
|
panic(fmt.Sprintf("BlockStore can only save contiguous blocks. Wanted %v, got %v", w, g))
|
|
}
|
|
if !blockParts.IsComplete() {
|
|
panic("BlockStore can only save complete block part sets")
|
|
}
|
|
|
|
// Save block meta
|
|
blockMeta := types.NewBlockMeta(block, blockParts)
|
|
pbm := blockMeta.ToProto()
|
|
if pbm == nil {
|
|
panic("nil blockmeta")
|
|
}
|
|
metaBytes := mustEncode(pbm)
|
|
if err := bs.db.Set(calcBlockMetaKey(height), metaBytes); err != nil {
|
|
panic(err)
|
|
}
|
|
if err := bs.db.Set(calcBlockHashKey(hash), []byte(fmt.Sprintf("%d", height))); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Save block parts
|
|
for i := 0; i < int(blockParts.Total()); i++ {
|
|
part := blockParts.GetPart(i)
|
|
bs.saveBlockPart(height, i, part)
|
|
}
|
|
|
|
// Save block commit (duplicate and separate from the Block)
|
|
pbc := block.LastCommit.ToProto()
|
|
blockCommitBytes := mustEncode(pbc)
|
|
if err := bs.db.Set(calcBlockCommitKey(height-1), blockCommitBytes); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Save seen commit (seen +2/3 precommits for block)
|
|
// NOTE: we can delete this at a later height
|
|
pbsc := seenCommit.ToProto()
|
|
seenCommitBytes := mustEncode(pbsc)
|
|
if err := bs.db.Set(calcSeenCommitKey(height), seenCommitBytes); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Done!
|
|
bs.mtx.Lock()
|
|
bs.height = height
|
|
if bs.base == 0 {
|
|
bs.base = height
|
|
}
|
|
bs.mtx.Unlock()
|
|
|
|
// Save new BlockStoreState descriptor. This also flushes the database.
|
|
bs.saveState()
|
|
}
|
|
|
|
func (bs *BlockStore) saveBlockPart(height int64, index int, part *types.Part) {
|
|
pbp, err := part.ToProto()
|
|
if err != nil {
|
|
panic(fmt.Errorf("unable to make part into proto: %w", err))
|
|
}
|
|
partBytes := mustEncode(pbp)
|
|
if err := bs.db.Set(calcBlockPartKey(height, index), partBytes); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (bs *BlockStore) saveState() {
|
|
bs.mtx.RLock()
|
|
bss := tmstore.BlockStoreState{
|
|
Base: bs.base,
|
|
Height: bs.height,
|
|
}
|
|
bs.mtx.RUnlock()
|
|
SaveBlockStoreState(&bss, bs.db)
|
|
}
|
|
|
|
// SaveSeenCommit saves a seen commit, used by e.g. the state sync reactor when bootstrapping node.
|
|
func (bs *BlockStore) SaveSeenCommit(height int64, seenCommit *types.Commit) error {
|
|
pbc := seenCommit.ToProto()
|
|
seenCommitBytes, err := proto.Marshal(pbc)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to marshal commit: %w", err)
|
|
}
|
|
return bs.db.Set(calcSeenCommitKey(height), seenCommitBytes)
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
func calcBlockMetaKey(height int64) []byte {
|
|
return []byte(fmt.Sprintf("H:%v", height))
|
|
}
|
|
|
|
func calcBlockPartKey(height int64, partIndex int) []byte {
|
|
return []byte(fmt.Sprintf("P:%v:%v", height, partIndex))
|
|
}
|
|
|
|
func calcBlockCommitKey(height int64) []byte {
|
|
return []byte(fmt.Sprintf("C:%v", height))
|
|
}
|
|
|
|
func calcSeenCommitKey(height int64) []byte {
|
|
return []byte(fmt.Sprintf("SC:%v", height))
|
|
}
|
|
|
|
func calcBlockHashKey(hash []byte) []byte {
|
|
return []byte(fmt.Sprintf("BH:%x", hash))
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
var blockStoreKey = []byte("blockStore")
|
|
|
|
// SaveBlockStoreState persists the blockStore state to the database.
|
|
func SaveBlockStoreState(bsj *tmstore.BlockStoreState, db dbm.DB) {
|
|
bytes, err := proto.Marshal(bsj)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Could not marshal state bytes: %v", err))
|
|
}
|
|
if err := db.SetSync(blockStoreKey, bytes); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// LoadBlockStoreState returns the BlockStoreState as loaded from disk.
|
|
// If no BlockStoreState was previously persisted, it returns the zero value.
|
|
func LoadBlockStoreState(db dbm.DB) tmstore.BlockStoreState {
|
|
bytes, err := db.Get(blockStoreKey)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if len(bytes) == 0 {
|
|
return tmstore.BlockStoreState{
|
|
Base: 0,
|
|
Height: 0,
|
|
}
|
|
}
|
|
|
|
var bsj tmstore.BlockStoreState
|
|
if err := proto.Unmarshal(bytes, &bsj); 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
|
|
}
|
|
|
|
//mustEncode proto encodes a proto.message and panics if fails
|
|
func mustEncode(pb proto.Message) []byte {
|
|
bz, err := proto.Marshal(pb)
|
|
if err != nil {
|
|
panic(fmt.Errorf("unable to marshal: %w", err))
|
|
}
|
|
return bz
|
|
}
|