light: implement light block (#5298)

This commit is contained in:
Callum Waters
2020-09-01 17:45:55 +02:00
committed by GitHub
parent b6a5f7b126
commit 2b58a62721
25 changed files with 1343 additions and 1196 deletions

View File

@@ -6,7 +6,6 @@ import (
"regexp"
"strconv"
"github.com/gogo/protobuf/proto"
dbm "github.com/tendermint/tm-db"
tmsync "github.com/tendermint/tendermint/libs/sync"
@@ -40,30 +39,22 @@ func New(db dbm.DB, prefix string) store.Store {
return &dbs{db: db, prefix: prefix, size: size}
}
// SaveSignedHeaderAndValidatorSet persists SignedHeader and ValidatorSet to
// the db.
// SaveLightBlock persists LightBlock to the db.
//
// Safe for concurrent use by multiple goroutines.
func (s *dbs) SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error {
if sh.Height <= 0 {
func (s *dbs) SaveLightBlock(lb *types.LightBlock) error {
if lb.Height <= 0 {
panic("negative or zero height")
}
pbsh := sh.ToProto()
shBz, err := proto.Marshal(pbsh)
lbpb, err := lb.ToProto()
if err != nil {
return fmt.Errorf("marshalling SignedHeader: %w", err)
return fmt.Errorf("unable to convert light block to protobuf: %w", err)
}
pbvs, err := valSet.ToProto()
lbBz, err := lbpb.Marshal()
if err != nil {
return fmt.Errorf("unable to transition validator set to protobuf: %w", err)
}
valSetBz, err := proto.Marshal(pbvs)
if err != nil {
return fmt.Errorf("marshalling validator set: %w", err)
return fmt.Errorf("marshalling LightBlock: %w", err)
}
s.mtx.Lock()
@@ -71,10 +62,7 @@ func (s *dbs) SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *ty
b := s.db.NewBatch()
defer b.Close()
if err = b.Set(s.shKey(sh.Height), shBz); err != nil {
return err
}
if err = b.Set(s.vsKey(sh.Height), valSetBz); err != nil {
if err = b.Set(s.lbKey(lb.Height), lbBz); err != nil {
return err
}
if err = b.Set(sizeKey, marshalSize(s.size+1)); err != nil {
@@ -88,11 +76,11 @@ func (s *dbs) SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *ty
return nil
}
// DeleteSignedHeaderAndValidatorSet deletes SignedHeader and ValidatorSet from
// DeleteLightBlockAndValidatorSet deletes the LightBlock from
// the db.
//
// Safe for concurrent use by multiple goroutines.
func (s *dbs) DeleteSignedHeaderAndValidatorSet(height int64) error {
func (s *dbs) DeleteLightBlock(height int64) error {
if height <= 0 {
panic("negative or zero height")
}
@@ -102,10 +90,7 @@ func (s *dbs) DeleteSignedHeaderAndValidatorSet(height int64) error {
b := s.db.NewBatch()
defer b.Close()
if err := b.Delete(s.shKey(height)); err != nil {
return err
}
if err := b.Delete(s.vsKey(height)); err != nil {
if err := b.Delete(s.lbKey(height)); err != nil {
return err
}
if err := b.Set(sizeKey, marshalSize(s.size-1)); err != nil {
@@ -119,73 +104,43 @@ func (s *dbs) DeleteSignedHeaderAndValidatorSet(height int64) error {
return nil
}
// SignedHeader loads SignedHeader at the given height.
// LightBlock retrieves the LightBlock at the given height.
//
// Safe for concurrent use by multiple goroutines.
func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) {
func (s *dbs) LightBlock(height int64) (*types.LightBlock, error) {
if height <= 0 {
panic("negative or zero height")
}
bz, err := s.db.Get(s.shKey(height))
bz, err := s.db.Get(s.lbKey(height))
if err != nil {
panic(err)
}
if len(bz) == 0 {
return nil, store.ErrSignedHeaderNotFound
return nil, store.ErrLightBlockNotFound
}
var pbsh tmproto.SignedHeader
err = proto.Unmarshal(bz, &pbsh)
var lbpb tmproto.LightBlock
err = lbpb.Unmarshal(bz)
if err != nil {
return nil, err
return nil, fmt.Errorf("unmarshal error: %w", err)
}
signedHeader, err := types.SignedHeaderFromProto(&pbsh)
lightBlock, err := types.LightBlockFromProto(&lbpb)
if err != nil {
return nil, err
return nil, fmt.Errorf("proto conversion error: %w", err)
}
return signedHeader, err
return lightBlock, err
}
// ValidatorSet loads ValidatorSet at the given height.
// LastLightBlockHeight returns the last LightBlock height stored.
//
// Safe for concurrent use by multiple goroutines.
func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) {
if height <= 0 {
panic("negative or zero height")
}
bz, err := s.db.Get(s.vsKey(height))
if err != nil {
panic(err)
}
if len(bz) == 0 {
return nil, store.ErrValidatorSetNotFound
}
var pbvs tmproto.ValidatorSet
err = proto.Unmarshal(bz, &pbvs)
if err != nil {
return nil, err
}
valSet, err := types.ValidatorSetFromProto(&pbvs)
if err != nil {
return nil, err
}
return valSet, err
}
// LastSignedHeaderHeight returns the last SignedHeader height stored.
//
// Safe for concurrent use by multiple goroutines.
func (s *dbs) LastSignedHeaderHeight() (int64, error) {
func (s *dbs) LastLightBlockHeight() (int64, error) {
itr, err := s.db.ReverseIterator(
s.shKey(1),
append(s.shKey(1<<63-1), byte(0x00)),
s.lbKey(1),
append(s.lbKey(1<<63-1), byte(0x00)),
)
if err != nil {
panic(err)
@@ -194,7 +149,7 @@ func (s *dbs) LastSignedHeaderHeight() (int64, error) {
for itr.Valid() {
key := itr.Key()
_, height, ok := parseShKey(key)
_, height, ok := parseLbKey(key)
if ok {
return height, nil
}
@@ -204,13 +159,13 @@ func (s *dbs) LastSignedHeaderHeight() (int64, error) {
return -1, itr.Error()
}
// FirstSignedHeaderHeight returns the first SignedHeader height stored.
// FirstLightBlockHeight returns the first LightBlock height stored.
//
// Safe for concurrent use by multiple goroutines.
func (s *dbs) FirstSignedHeaderHeight() (int64, error) {
func (s *dbs) FirstLightBlockHeight() (int64, error) {
itr, err := s.db.Iterator(
s.shKey(1),
append(s.shKey(1<<63-1), byte(0x00)),
s.lbKey(1),
append(s.lbKey(1<<63-1), byte(0x00)),
)
if err != nil {
panic(err)
@@ -219,7 +174,7 @@ func (s *dbs) FirstSignedHeaderHeight() (int64, error) {
for itr.Valid() {
key := itr.Key()
_, height, ok := parseShKey(key)
_, height, ok := parseLbKey(key)
if ok {
return height, nil
}
@@ -229,18 +184,18 @@ func (s *dbs) FirstSignedHeaderHeight() (int64, error) {
return -1, itr.Error()
}
// SignedHeaderBefore iterates over headers until it finds a header before
// the given height. It returns ErrSignedHeaderNotFound if no such header exists.
// LightBlockBefore iterates over light blocks until it finds a block before
// the given height. It returns ErrLightBlockNotFound if no such block exists.
//
// Safe for concurrent use by multiple goroutines.
func (s *dbs) SignedHeaderBefore(height int64) (*types.SignedHeader, error) {
func (s *dbs) LightBlockBefore(height int64) (*types.LightBlock, error) {
if height <= 0 {
panic("negative or zero height")
}
itr, err := s.db.ReverseIterator(
s.shKey(1),
s.shKey(height),
s.lbKey(1),
s.lbKey(height),
)
if err != nil {
panic(err)
@@ -249,9 +204,9 @@ func (s *dbs) SignedHeaderBefore(height int64) (*types.SignedHeader, error) {
for itr.Valid() {
key := itr.Key()
_, existingHeight, ok := parseShKey(key)
_, existingHeight, ok := parseLbKey(key)
if ok {
return s.SignedHeader(existingHeight)
return s.LightBlock(existingHeight)
}
itr.Next()
}
@@ -259,7 +214,7 @@ func (s *dbs) SignedHeaderBefore(height int64) (*types.SignedHeader, error) {
return nil, err
}
return nil, store.ErrSignedHeaderNotFound
return nil, store.ErrLightBlockNotFound
}
// Prune prunes header & validator set pairs until there are only size pairs
@@ -279,8 +234,8 @@ func (s *dbs) Prune(size uint16) error {
// 2) Iterate over headers and perform a batch operation.
itr, err := s.db.Iterator(
s.shKey(1),
append(s.shKey(1<<63-1), byte(0x00)),
s.lbKey(1),
append(s.lbKey(1<<63-1), byte(0x00)),
)
if err != nil {
return err
@@ -293,12 +248,9 @@ func (s *dbs) Prune(size uint16) error {
pruned := 0
for itr.Valid() && numToPrune > 0 {
key := itr.Key()
_, height, ok := parseShKey(key)
_, height, ok := parseLbKey(key)
if ok {
if err = b.Delete(s.shKey(height)); err != nil {
return err
}
if err = b.Delete(s.vsKey(height)); err != nil {
if err = b.Delete(s.lbKey(height)); err != nil {
return err
}
}
@@ -337,15 +289,11 @@ func (s *dbs) Size() uint16 {
return s.size
}
func (s *dbs) shKey(height int64) []byte {
return []byte(fmt.Sprintf("sh/%s/%020d", s.prefix, height))
func (s *dbs) lbKey(height int64) []byte {
return []byte(fmt.Sprintf("lb/%s/%020d", s.prefix, height))
}
func (s *dbs) vsKey(height int64) []byte {
return []byte(fmt.Sprintf("vs/%s/%020d", s.prefix, height))
}
var keyPattern = regexp.MustCompile(`^(sh|vs)/([^/]*)/([0-9]+)$`)
var keyPattern = regexp.MustCompile(`^(lb)/([^/]*)/([0-9]+)$`)
func parseKey(key []byte) (part string, prefix string, height int64, ok bool) {
submatch := keyPattern.FindSubmatch(key)
@@ -362,10 +310,10 @@ func parseKey(key []byte) (part string, prefix string, height int64, ok bool) {
return
}
func parseShKey(key []byte) (prefix string, height int64, ok bool) {
func parseLbKey(key []byte) (prefix string, height int64, ok bool) {
var part string
part, prefix, height, ok = parseKey(key)
if part != "sh" {
if part != "lb" {
return "", 0, false
}
return

View File

@@ -3,6 +3,7 @@ package db
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -10,91 +11,78 @@ import (
dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/tmhash"
tmrand "github.com/tendermint/tendermint/libs/rand"
"github.com/tendermint/tendermint/types"
)
func TestLast_FirstSignedHeaderHeight(t *testing.T) {
dbStore := New(dbm.NewMemDB(), "TestLast_FirstSignedHeaderHeight")
vals, _ := types.RandValidatorSet(10, 100)
func TestLast_FirstLightBlockHeight(t *testing.T) {
dbStore := New(dbm.NewMemDB(), "TestLast_FirstLightBlockHeight")
// Empty store
height, err := dbStore.LastSignedHeaderHeight()
height, err := dbStore.LastLightBlockHeight()
require.NoError(t, err)
assert.EqualValues(t, -1, height)
height, err = dbStore.FirstSignedHeaderHeight()
height, err = dbStore.FirstLightBlockHeight()
require.NoError(t, err)
assert.EqualValues(t, -1, height)
// 1 key
err = dbStore.SaveSignedHeaderAndValidatorSet(
&types.SignedHeader{Header: &types.Header{Height: 1}}, vals)
err = dbStore.SaveLightBlock(randLightBlock(int64(1)))
require.NoError(t, err)
height, err = dbStore.LastSignedHeaderHeight()
height, err = dbStore.LastLightBlockHeight()
require.NoError(t, err)
assert.EqualValues(t, 1, height)
height, err = dbStore.FirstSignedHeaderHeight()
height, err = dbStore.FirstLightBlockHeight()
require.NoError(t, err)
assert.EqualValues(t, 1, height)
}
func Test_SaveSignedHeaderAndValidatorSet(t *testing.T) {
dbStore := New(dbm.NewMemDB(), "Test_SaveSignedHeaderAndValidatorSet")
vals, _ := types.RandValidatorSet(10, 100)
func Test_SaveLightBlock(t *testing.T) {
dbStore := New(dbm.NewMemDB(), "Test_SaveLightBlockAndValidatorSet")
// Empty store
h, err := dbStore.SignedHeader(1)
h, err := dbStore.LightBlock(1)
require.Error(t, err)
assert.Nil(t, h)
valSet, err := dbStore.ValidatorSet(1)
require.Error(t, err)
assert.Nil(t, valSet)
// 1 key
pa := vals.Validators[0].Address
err = dbStore.SaveSignedHeaderAndValidatorSet(
&types.SignedHeader{Header: &types.Header{Height: 1, ProposerAddress: pa}}, vals)
err = dbStore.SaveLightBlock(randLightBlock(1))
require.NoError(t, err)
h, err = dbStore.SignedHeader(1)
size := dbStore.Size()
assert.Equal(t, uint16(1), size)
t.Log(size)
h, err = dbStore.LightBlock(1)
require.NoError(t, err)
assert.NotNil(t, h)
valSet, err = dbStore.ValidatorSet(1)
require.NoError(t, err)
assert.NotNil(t, valSet)
// Empty store
err = dbStore.DeleteSignedHeaderAndValidatorSet(1)
err = dbStore.DeleteLightBlock(1)
require.NoError(t, err)
h, err = dbStore.SignedHeader(1)
h, err = dbStore.LightBlock(1)
require.Error(t, err)
assert.Nil(t, h)
valSet, err = dbStore.ValidatorSet(1)
require.Error(t, err)
assert.Nil(t, valSet)
}
func Test_SignedHeaderBefore(t *testing.T) {
dbStore := New(dbm.NewMemDB(), "Test_SignedHeaderBefore")
valSet, _ := types.RandValidatorSet(10, 100)
pa := valSet.Proposer.Address
func Test_LightBlockBefore(t *testing.T) {
dbStore := New(dbm.NewMemDB(), "Test_LightBlockBefore")
assert.Panics(t, func() {
_, _ = dbStore.SignedHeaderBefore(0)
_, _ = dbStore.SignedHeaderBefore(100)
_, _ = dbStore.LightBlockBefore(0)
_, _ = dbStore.LightBlockBefore(100)
})
err := dbStore.SaveSignedHeaderAndValidatorSet(
&types.SignedHeader{Header: &types.Header{Height: 2, ProposerAddress: pa}}, valSet)
err := dbStore.SaveLightBlock(randLightBlock(int64(2)))
require.NoError(t, err)
h, err := dbStore.SignedHeaderBefore(3)
h, err := dbStore.LightBlockBefore(3)
require.NoError(t, err)
if assert.NotNil(t, h) {
assert.EqualValues(t, 2, h.Height)
@@ -103,7 +91,6 @@ func Test_SignedHeaderBefore(t *testing.T) {
func Test_Prune(t *testing.T) {
dbStore := New(dbm.NewMemDB(), "Test_Prune")
valSet, _ := types.RandValidatorSet(10, 100)
// Empty store
assert.EqualValues(t, 0, dbStore.Size())
@@ -111,8 +98,7 @@ func Test_Prune(t *testing.T) {
require.NoError(t, err)
// One header
err = dbStore.SaveSignedHeaderAndValidatorSet(
&types.SignedHeader{Header: &types.Header{Height: 2}}, valSet)
err = dbStore.SaveLightBlock(randLightBlock(2))
require.NoError(t, err)
assert.EqualValues(t, 1, dbStore.Size())
@@ -127,8 +113,7 @@ func Test_Prune(t *testing.T) {
// Multiple headers
for i := 1; i <= 10; i++ {
err = dbStore.SaveSignedHeaderAndValidatorSet(
&types.SignedHeader{Header: &types.Header{Height: int64(i)}}, valSet)
err = dbStore.SaveLightBlock(randLightBlock(int64(i)))
require.NoError(t, err)
}
@@ -143,7 +128,6 @@ func Test_Prune(t *testing.T) {
func Test_Concurrency(t *testing.T) {
dbStore := New(dbm.NewMemDB(), "Test_Prune")
vals, _ := types.RandValidatorSet(10, 100)
var wg sync.WaitGroup
for i := 1; i <= 100; i++ {
@@ -151,24 +135,19 @@ func Test_Concurrency(t *testing.T) {
go func(i int64) {
defer wg.Done()
err := dbStore.SaveSignedHeaderAndValidatorSet(
&types.SignedHeader{Header: &types.Header{Height: i,
ProposerAddress: tmrand.Bytes(crypto.AddressSize)}}, vals)
err := dbStore.SaveLightBlock(randLightBlock(i))
require.NoError(t, err)
_, err = dbStore.SignedHeader(i)
_, err = dbStore.LightBlock(i)
if err != nil {
t.Log(err)
}
_, err = dbStore.ValidatorSet(i)
if err != nil {
t.Log(err) // could not find validator set
}
_, err = dbStore.LastSignedHeaderHeight()
_, err = dbStore.LastLightBlockHeight()
if err != nil {
t.Log(err)
}
_, err = dbStore.FirstSignedHeaderHeight()
_, err = dbStore.FirstLightBlockHeight()
if err != nil {
t.Log(err)
}
@@ -179,7 +158,7 @@ func Test_Concurrency(t *testing.T) {
}
_ = dbStore.Size()
err = dbStore.DeleteSignedHeaderAndValidatorSet(1)
err = dbStore.DeleteLightBlock(1)
if err != nil {
t.Log(err)
}
@@ -188,3 +167,28 @@ func Test_Concurrency(t *testing.T) {
wg.Wait()
}
func randLightBlock(height int64) *types.LightBlock {
vals, _ := types.RandValidatorSet(2, 1)
return &types.LightBlock{
SignedHeader: &types.SignedHeader{
Header: &types.Header{
ChainID: tmrand.Str(12),
Height: height,
Time: time.Now(),
LastBlockID: types.BlockID{},
LastCommitHash: crypto.CRandBytes(tmhash.Size),
DataHash: crypto.CRandBytes(tmhash.Size),
ValidatorsHash: crypto.CRandBytes(tmhash.Size),
NextValidatorsHash: crypto.CRandBytes(tmhash.Size),
ConsensusHash: crypto.CRandBytes(tmhash.Size),
AppHash: crypto.CRandBytes(tmhash.Size),
LastResultsHash: crypto.CRandBytes(tmhash.Size),
EvidenceHash: crypto.CRandBytes(tmhash.Size),
ProposerAddress: crypto.CRandBytes(crypto.AddressSize),
},
Commit: &types.Commit{},
},
ValidatorSet: vals,
}
}

View File

@@ -3,11 +3,7 @@ package store
import "errors"
var (
// ErrSignedHeaderNotFound is returned when a store does not have the
// ErrLightBlockNotFound is returned when a store does not have the
// requested header.
ErrSignedHeaderNotFound = errors.New("signed header not found")
// ErrValidatorSetNotFound is returned when a store does not have the
// requested validator set.
ErrValidatorSetNotFound = errors.New("validator set not found")
ErrLightBlockNotFound = errors.New("light block not found")
)

View File

@@ -8,43 +8,36 @@ type Store interface {
// ValidatorSet (h: sh.Height).
//
// height must be > 0.
SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error
SaveLightBlock(lb *types.LightBlock) error
// DeleteSignedHeaderAndValidatorSet deletes SignedHeader (h: height) and
// ValidatorSet (h: height).
//
// height must be > 0.
DeleteSignedHeaderAndValidatorSet(height int64) error
DeleteLightBlock(height int64) error
// SignedHeader returns the SignedHeader that corresponds to the given
// LightBlock returns the LightBlock that corresponds to the given
// height.
//
// height must be > 0.
//
// If SignedHeader is not found, ErrSignedHeaderNotFound is returned.
SignedHeader(height int64) (*types.SignedHeader, error)
// If LightBlock is not found, ErrLightBlockNotFound is returned.
LightBlock(height int64) (*types.LightBlock, error)
// ValidatorSet returns the ValidatorSet that corresponds to height.
//
// height must be > 0.
//
// If ValidatorSet is not found, ErrValidatorSetNotFound is returned.
ValidatorSet(height int64) (*types.ValidatorSet, error)
// LastSignedHeaderHeight returns the last (newest) SignedHeader height.
// LastLightBlockHeight returns the last (newest) LightBlock height.
//
// If the store is empty, -1 and nil error are returned.
LastSignedHeaderHeight() (int64, error)
LastLightBlockHeight() (int64, error)
// FirstSignedHeaderHeight returns the first (oldest) SignedHeader height.
// FirstLightBlockHeight returns the first (oldest) LightBlock height.
//
// If the store is empty, -1 and nil error are returned.
FirstSignedHeaderHeight() (int64, error)
FirstLightBlockHeight() (int64, error)
// SignedHeaderBefore returns the SignedHeader before a certain height.
// LightBlockBefore returns the LightBlock before a certain height.
//
// height must be > 0 && <= LastSignedHeaderHeight.
SignedHeaderBefore(height int64) (*types.SignedHeader, error)
// height must be > 0 && <= LastLightBlockHeight.
LightBlockBefore(height int64) (*types.LightBlock, error)
// Prune removes headers & the associated validator sets when Store reaches a
// defined size (number of header & validator set pairs).