mirror of
https://github.com/fluencelabs/tendermint
synced 2025-07-30 19:51:58 +00:00
Compare commits
44 Commits
zm_fork-ac
...
jae/verify
Author | SHA1 | Date | |
---|---|---|---|
|
806e8d2077 | ||
|
a31a4c8c2f | ||
|
1b9e0261b6 | ||
|
97131a1544 | ||
|
2f96903494 | ||
|
6b38564409 | ||
|
355834a691 | ||
|
f9061f11e6 | ||
|
46c3d0baec | ||
|
91e69bbdac | ||
|
415d9579e1 | ||
|
88b7c53dcf | ||
|
f95b11e07a | ||
|
1f03ceb0ba | ||
|
45a447aa2f | ||
|
8d43cdd846 | ||
|
7849675827 | ||
|
ceff0af81d | ||
|
06720dcd6b | ||
|
6810bab382 | ||
|
4b2e323728 | ||
|
ac330194b9 | ||
|
b441a71221 | ||
|
39e589e3c7 | ||
|
24cd2eba8e | ||
|
2872df6634 | ||
|
6746befb44 | ||
|
fb3663cfb1 | ||
|
b97a8b69ed | ||
|
0be87cdcba | ||
|
49bea520cf | ||
|
3acc6e60e8 | ||
|
0a145b83be | ||
|
d15f0f8df0 | ||
|
02c467237e | ||
|
165a3d15fb | ||
|
ce9cde0888 | ||
|
7635b1ed7e | ||
|
312a36a0ab | ||
|
59d3d40dbf | ||
|
0a87ddef96 | ||
|
236cdf87aa | ||
|
88b69a956f | ||
|
1a86c869e8 |
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"github.com/tendermint/tendermint/lite/proxy"
|
||||
"github.com/tendermint/tendermint/lite/verifying"
|
||||
rpcclient "github.com/tendermint/tendermint/rpc/client"
|
||||
)
|
||||
|
||||
@@ -78,10 +79,10 @@ func runProxy(cmd *cobra.Command, args []string) error {
|
||||
logger.Info("Connecting to source HTTP client...")
|
||||
node := rpcclient.NewHTTP(nodeAddr, "/websocket")
|
||||
|
||||
logger.Info("Constructing Verifier...")
|
||||
cert, err := proxy.NewVerifier(chainID, home, node, logger, cacheSize)
|
||||
logger.Info("Constructing verifying provider...")
|
||||
cert, err := verifying.NewProvider(chainID, home, node, logger, cacheSize, verifying.TrustOptions{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "constructing Verifier")
|
||||
return errors.Wrap(err, "constructing verifying provider")
|
||||
}
|
||||
cert.SetLogger(logger)
|
||||
sc := proxy.SecureClient(node, cert)
|
||||
|
@@ -1,7 +1,9 @@
|
||||
/*
|
||||
Package client defines a provider that uses a rpcclient
|
||||
to get information, which is used to get new headers
|
||||
and validators directly from a Tendermint client.
|
||||
Package client defines a provider that uses an RPC client (or SignStatusClient
|
||||
more generally) to get information like new headers and validators directly
|
||||
from a Tendermint node.
|
||||
|
||||
Use either NewProvider or NewHTTPProvider to construct one.
|
||||
*/
|
||||
package client
|
||||
|
||||
@@ -28,7 +30,8 @@ type provider struct {
|
||||
client SignStatusClient
|
||||
}
|
||||
|
||||
// NewProvider implements Provider (but not PersistentProvider).
|
||||
// NewProvider creates a lite.Provider using the given chain ID and
|
||||
// SignStatusClient.
|
||||
func NewProvider(chainID string, client SignStatusClient) lite.Provider {
|
||||
return &provider{
|
||||
logger: log.NewNopLogger(),
|
||||
@@ -37,87 +40,59 @@ func NewProvider(chainID string, client SignStatusClient) lite.Provider {
|
||||
}
|
||||
}
|
||||
|
||||
// NewHTTPProvider can connect to a tendermint json-rpc endpoint
|
||||
// at the given url, and uses that as a read-only provider.
|
||||
// NewHTTPProvider creates a lite.Provider, which is using the rpcclient.HTTP
|
||||
// client under the hood.
|
||||
func NewHTTPProvider(chainID, remote string) lite.Provider {
|
||||
return NewProvider(chainID, rpcclient.NewHTTP(remote, "/websocket"))
|
||||
}
|
||||
|
||||
// Implements Provider.
|
||||
// SetLogger implements lite.Provider.
|
||||
func (p *provider) SetLogger(logger log.Logger) {
|
||||
logger = logger.With("module", "lite/client")
|
||||
p.logger = logger
|
||||
}
|
||||
|
||||
// StatusClient returns the internal client as a StatusClient
|
||||
func (p *provider) StatusClient() rpcclient.StatusClient {
|
||||
return p.client
|
||||
}
|
||||
|
||||
// LatestFullCommit implements Provider.
|
||||
// LatestFullCommit implements lite.Provider.
|
||||
func (p *provider) LatestFullCommit(chainID string, minHeight, maxHeight int64) (fc lite.FullCommit, err error) {
|
||||
if chainID != p.chainID {
|
||||
err = fmt.Errorf("expected chainID %s, got %s", p.chainID, chainID)
|
||||
return
|
||||
return fc, fmt.Errorf("expected chainID %s, got %s", p.chainID, chainID)
|
||||
}
|
||||
|
||||
if maxHeight != 0 && maxHeight < minHeight {
|
||||
err = fmt.Errorf("need maxHeight == 0 or minHeight <= maxHeight, got min %v and max %v",
|
||||
return fc, fmt.Errorf("need maxHeight == 0 or minHeight <= maxHeight, got min %v and max %v",
|
||||
minHeight, maxHeight)
|
||||
return
|
||||
}
|
||||
|
||||
commit, err := p.fetchLatestCommit(minHeight, maxHeight)
|
||||
if err != nil {
|
||||
return
|
||||
return fc, err
|
||||
}
|
||||
fc, err = p.fillFullCommit(commit.SignedHeader)
|
||||
return
|
||||
|
||||
return p.fillFullCommit(commit.SignedHeader)
|
||||
}
|
||||
|
||||
// fetchLatestCommit fetches the latest commit from the client.
|
||||
func (p *provider) fetchLatestCommit(minHeight int64, maxHeight int64) (*ctypes.ResultCommit, error) {
|
||||
status, err := p.client.Status()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status.SyncInfo.LatestBlockHeight < minHeight {
|
||||
err = fmt.Errorf("provider is at %v but require minHeight=%v",
|
||||
return nil, fmt.Errorf("provider is at %d but require minHeight=%d",
|
||||
status.SyncInfo.LatestBlockHeight, minHeight)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if maxHeight == 0 {
|
||||
maxHeight = status.SyncInfo.LatestBlockHeight
|
||||
} else if status.SyncInfo.LatestBlockHeight < maxHeight {
|
||||
maxHeight = status.SyncInfo.LatestBlockHeight
|
||||
}
|
||||
|
||||
return p.client.Commit(&maxHeight)
|
||||
}
|
||||
|
||||
// Implements Provider.
|
||||
func (p *provider) ValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) {
|
||||
return p.getValidatorSet(chainID, height)
|
||||
}
|
||||
|
||||
func (p *provider) getValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) {
|
||||
if chainID != p.chainID {
|
||||
err = fmt.Errorf("expected chainID %s, got %s", p.chainID, chainID)
|
||||
return
|
||||
}
|
||||
if height < 1 {
|
||||
err = fmt.Errorf("expected height >= 1, got height %v", height)
|
||||
return
|
||||
}
|
||||
res, err := p.client.Validators(&height)
|
||||
if err != nil {
|
||||
// TODO pass through other types of errors.
|
||||
return nil, lerr.ErrUnknownValidators(chainID, height)
|
||||
}
|
||||
valset = types.NewValidatorSet(res.Validators)
|
||||
return
|
||||
}
|
||||
|
||||
// This does no validation.
|
||||
func (p *provider) fillFullCommit(signedHeader types.SignedHeader) (fc lite.FullCommit, err error) {
|
||||
|
||||
// Get the validators.
|
||||
valset, err := p.getValidatorSet(signedHeader.ChainID, signedHeader.Height)
|
||||
if err != nil {
|
||||
@@ -132,3 +107,26 @@ func (p *provider) fillFullCommit(signedHeader types.SignedHeader) (fc lite.Full
|
||||
|
||||
return lite.NewFullCommit(signedHeader, valset, nextValset), nil
|
||||
}
|
||||
|
||||
// ValidatorSet implements lite.Provider.
|
||||
func (p *provider) ValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) {
|
||||
return p.getValidatorSet(chainID, height)
|
||||
}
|
||||
|
||||
func (p *provider) getValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) {
|
||||
if chainID != p.chainID {
|
||||
return nil, fmt.Errorf("expected chainID %s, got %s", p.chainID, chainID)
|
||||
}
|
||||
|
||||
if height < 1 {
|
||||
return nil, fmt.Errorf("expected height >= 1, got height %d", height)
|
||||
}
|
||||
|
||||
res, err := p.client.Validators(&height)
|
||||
if err != nil {
|
||||
// TODO pass through other types of errors.
|
||||
return nil, lerr.ErrUnknownValidators(chainID, height)
|
||||
}
|
||||
|
||||
return types.NewValidatorSet(res.Validators), nil
|
||||
}
|
||||
|
@@ -8,9 +8,10 @@ import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// FullCommit contains a SignedHeader (the block header and a commit that signs it),
|
||||
// the validator set which signed the commit, and the next validator set. The
|
||||
// next validator set (which is proven from the block header) allows us to
|
||||
// FullCommit contains a SignedHeader (the block's header and a commit that
|
||||
// signs it), the validator set which signed the commit, and the next validator
|
||||
// set.
|
||||
// The next validator set (which is proven from the block header) allows us to
|
||||
// revert to block-by-block updating of lite Verifier's latest validator set,
|
||||
// even in the face of arbitrarily large power changes.
|
||||
type FullCommit struct {
|
||||
@@ -28,46 +29,41 @@ func NewFullCommit(signedHeader types.SignedHeader, valset, nextValset *types.Va
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the components and check for consistency.
|
||||
// This also checks to make sure that Validators actually
|
||||
// signed the SignedHeader.Commit.
|
||||
// If > 2/3 did not sign the Commit from fc.Validators, it
|
||||
// is not a valid commit!
|
||||
// ValidateFull validates the components and ensures consistency.
|
||||
// It also checks that Validators actually signed the SignedHeader.Commit.
|
||||
// If > 2/3 did not sign the Commit from fc.Validators, it is not a valid
|
||||
// commit!
|
||||
func (fc FullCommit) ValidateFull(chainID string) error {
|
||||
// Ensure that Validators exists and matches the header.
|
||||
if fc.Validators.Size() == 0 {
|
||||
return errors.New("need FullCommit.Validators")
|
||||
return errors.New("empty Validators")
|
||||
}
|
||||
if !bytes.Equal(
|
||||
fc.SignedHeader.ValidatorsHash,
|
||||
fc.Validators.Hash()) {
|
||||
return fmt.Errorf("header has vhash %X but valset hash is %X",
|
||||
|
||||
if !bytes.Equal(fc.SignedHeader.ValidatorsHash, fc.Validators.Hash()) {
|
||||
return fmt.Errorf("header has ValidatorsHash %X, but valset hash is %X",
|
||||
fc.SignedHeader.ValidatorsHash,
|
||||
fc.Validators.Hash(),
|
||||
)
|
||||
}
|
||||
// Ensure that NextValidators exists and matches the header.
|
||||
|
||||
if fc.NextValidators.Size() == 0 {
|
||||
return errors.New("need FullCommit.NextValidators")
|
||||
return errors.New("empty NextValidators")
|
||||
}
|
||||
if !bytes.Equal(
|
||||
fc.SignedHeader.NextValidatorsHash,
|
||||
fc.NextValidators.Hash()) {
|
||||
return fmt.Errorf("header has next vhash %X but next valset hash is %X",
|
||||
|
||||
if !bytes.Equal(fc.SignedHeader.NextValidatorsHash, fc.NextValidators.Hash()) {
|
||||
return fmt.Errorf("header has next ValidatorsHash %X, but next valset hash is %X",
|
||||
fc.SignedHeader.NextValidatorsHash,
|
||||
fc.NextValidators.Hash(),
|
||||
)
|
||||
}
|
||||
// Validate the header.
|
||||
|
||||
err := fc.SignedHeader.ValidateBasic(chainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate the signatures on the commit.
|
||||
hdr, cmt := fc.SignedHeader.Header, fc.SignedHeader.Commit
|
||||
return fc.Validators.VerifyCommit(
|
||||
hdr.ChainID, cmt.BlockID,
|
||||
hdr.Height, cmt)
|
||||
return fc.Validators.VerifyCommit(hdr.ChainID, cmt.BlockID, hdr.Height, cmt)
|
||||
}
|
||||
|
||||
// Height returns the height of the header.
|
||||
@@ -75,6 +71,7 @@ func (fc FullCommit) Height() int64 {
|
||||
if fc.SignedHeader.Header == nil {
|
||||
panic("should not happen")
|
||||
}
|
||||
|
||||
return fc.SignedHeader.Height
|
||||
}
|
||||
|
||||
@@ -83,5 +80,6 @@ func (fc FullCommit) ChainID() string {
|
||||
if fc.SignedHeader.Header == nil {
|
||||
panic("should not happen")
|
||||
}
|
||||
|
||||
return fc.SignedHeader.ChainID
|
||||
}
|
||||
|
98
lite/concurrent_provider.go
Normal file
98
lite/concurrent_provider.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package lite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ConcurrentProvider is a provider which is safe to use by multiple threads.
|
||||
type ConcurrentProvider struct {
|
||||
UpdatingProvider
|
||||
|
||||
// pending map to synchronize concurrent verification requests
|
||||
mtx sync.Mutex
|
||||
pendingVerifications map[pendingKey]*pendingResult
|
||||
}
|
||||
|
||||
// convenience to create the key for the lookup map
|
||||
type pendingKey struct {
|
||||
chainID string
|
||||
height int64
|
||||
}
|
||||
|
||||
// used to cache the result from underlying UpdatingProvider.
|
||||
type pendingResult struct {
|
||||
wait chan struct{}
|
||||
err error // cached result.
|
||||
}
|
||||
|
||||
// NewConcurrentProvider creates a ConcurrentProvider using the given
|
||||
// UpdatingProvider.
|
||||
func NewConcurrentProvider(up UpdatingProvider) *ConcurrentProvider {
|
||||
return &ConcurrentProvider{
|
||||
UpdatingProvider: up,
|
||||
pendingVerifications: make(map[pendingKey]*pendingResult),
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the unique pending request for all identical calls to
|
||||
// joinConcurrency(chainID,height), and returns true for isFirstCall only for
|
||||
// the first call, which should call the returned callback w/ results if any.
|
||||
//
|
||||
// NOTE: The callback must be called, otherwise there will be memory leaks.
|
||||
//
|
||||
// Other subsequent calls should just return pr.err.
|
||||
// This is a separate function, primarily to make mtx unlocking more
|
||||
// obviously safe via defer.
|
||||
func (cp *ConcurrentProvider) joinConcurrency(chainID string, height int64) (pr *pendingResult, isFirstCall bool, callback func(error)) {
|
||||
cp.mtx.Lock()
|
||||
defer cp.mtx.Unlock()
|
||||
|
||||
pk := pendingKey{chainID, height}
|
||||
|
||||
if pr = cp.pendingVerifications[pk]; pr != nil {
|
||||
<-pr.wait
|
||||
return pr, false, nil
|
||||
}
|
||||
|
||||
pr = &pendingResult{wait: make(chan struct{}), err: nil}
|
||||
cp.pendingVerifications[pk] = pr
|
||||
|
||||
// The caller must call this, otherwise there will be memory leaks.
|
||||
return pr, true, func(err error) {
|
||||
// NOTE: other result parameters can be added here.
|
||||
pr.err = err
|
||||
|
||||
// *After* setting the results, *then* call close(pr.wait).
|
||||
close(pr.wait)
|
||||
|
||||
cp.mtx.Lock()
|
||||
delete(cp.pendingVerifications, pk)
|
||||
cp.mtx.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateToHeight implements UpdatingProvider.
|
||||
func (cp *ConcurrentProvider) UpdateToHeight(chainID string, height int64) error {
|
||||
// Performs synchronization for multi-threads verifications at the same height.
|
||||
pr, isFirstCall, callback := cp.joinConcurrency(chainID, height)
|
||||
|
||||
if isFirstCall {
|
||||
var err error
|
||||
// Use a defer in case UpdateToHeight itself fails.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %v", r)
|
||||
}
|
||||
callback(err)
|
||||
}()
|
||||
err = cp.UpdatingProvider.UpdateToHeight(chainID, height)
|
||||
return err
|
||||
}
|
||||
|
||||
// Is not the first call, so return the error from previous concurrent calls.
|
||||
if callback != nil {
|
||||
panic("expected callback to be nil")
|
||||
}
|
||||
return pr.err
|
||||
}
|
@@ -13,19 +13,23 @@ import (
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
var _ PersistentProvider = (*DBProvider)(nil)
|
||||
|
||||
// DBProvider stores commits and validator sets in a DB.
|
||||
//
|
||||
// The number of heights for which DBProvider stores commits and validator sets
|
||||
// can be optionally limited by calling SetLimit with the desired limit.
|
||||
type DBProvider struct {
|
||||
label string
|
||||
db dbm.DB
|
||||
cdc *amino.Codec
|
||||
limit int
|
||||
|
||||
logger log.Logger
|
||||
label string
|
||||
db dbm.DB
|
||||
cdc *amino.Codec
|
||||
limit int
|
||||
}
|
||||
|
||||
func NewDBProvider(label string, db dbm.DB) *DBProvider {
|
||||
var _ PersistentProvider = (*DBProvider)(nil)
|
||||
|
||||
// NewDBProvider returns a DBProvider.
|
||||
func NewDBProvider(label string, db dbm.DB) *DBProvider {
|
||||
// NOTE: when debugging, this type of construction might be useful.
|
||||
//db = dbm.NewDebugDB("db provider "+cmn.RandStr(4), db)
|
||||
|
||||
@@ -40,19 +44,21 @@ func NewDBProvider(label string, db dbm.DB) *DBProvider {
|
||||
return dbp
|
||||
}
|
||||
|
||||
func (dbp *DBProvider) SetLogger(logger log.Logger) {
|
||||
dbp.logger = logger.With("label", dbp.label)
|
||||
}
|
||||
|
||||
// SetLimit limits the number of heights for which DBProvider stores commits
|
||||
// and validator sets. E.g. 3 will result in storing only commits and validator
|
||||
// sets for the 3 latest heights.
|
||||
func (dbp *DBProvider) SetLimit(limit int) *DBProvider {
|
||||
dbp.limit = limit
|
||||
return dbp
|
||||
}
|
||||
|
||||
// Implements PersistentProvider.
|
||||
func (dbp *DBProvider) SaveFullCommit(fc FullCommit) error {
|
||||
func (dbp *DBProvider) SetLogger(logger log.Logger) {
|
||||
dbp.logger = logger.With("label", dbp.label)
|
||||
}
|
||||
|
||||
func (dbp *DBProvider) SaveFullCommit(fc FullCommit) error {
|
||||
dbp.logger.Info("DBProvider.SaveFullCommit()...", "fc", fc)
|
||||
|
||||
batch := dbp.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
@@ -94,17 +100,21 @@ func (dbp *DBProvider) SaveFullCommit(fc FullCommit) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implements Provider.
|
||||
func (dbp *DBProvider) LatestFullCommit(chainID string, minHeight, maxHeight int64) (
|
||||
FullCommit, error) {
|
||||
|
||||
dbp.logger.Info("DBProvider.LatestFullCommit()...",
|
||||
"chainID", chainID, "minHeight", minHeight, "maxHeight", maxHeight)
|
||||
|
||||
if maxHeight != 0 && maxHeight < minHeight {
|
||||
return FullCommit{}, fmt.Errorf("need maxHeight == 0 or minHeight <= maxHeight, got min %v and max %v",
|
||||
minHeight, maxHeight)
|
||||
}
|
||||
|
||||
if minHeight <= 0 {
|
||||
minHeight = 1
|
||||
}
|
||||
if maxHeight == 0 {
|
||||
if maxHeight <= 0 {
|
||||
maxHeight = 1<<63 - 1
|
||||
}
|
||||
|
||||
@@ -131,16 +141,16 @@ func (dbp *DBProvider) LatestFullCommit(chainID string, minHeight, maxHeight int
|
||||
} else {
|
||||
lfc, err := dbp.fillFullCommit(sh)
|
||||
if err == nil {
|
||||
dbp.logger.Info("DBProvider.LatestFullCommit() found latest.", "height", lfc.Height())
|
||||
dbp.logger.Info("DBProvider.LatestFullCommit() found latest", "height", lfc.Height())
|
||||
return lfc, nil
|
||||
} else {
|
||||
dbp.logger.Error("DBProvider.LatestFullCommit() got error", "lfc", lfc)
|
||||
dbp.logger.Error(fmt.Sprintf("%+v", err))
|
||||
dbp.logger.Error("DBProvider.LatestFullCommit() got error", "lfc", lfc, "err", fmt.Sprintf("%+v", err))
|
||||
return lfc, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FullCommit{}, lerr.ErrCommitNotFound()
|
||||
}
|
||||
|
||||
@@ -168,19 +178,23 @@ func (dbp *DBProvider) getValidatorSet(chainID string, height int64) (valset *ty
|
||||
}
|
||||
|
||||
func (dbp *DBProvider) fillFullCommit(sh types.SignedHeader) (FullCommit, error) {
|
||||
var chainID = sh.ChainID
|
||||
var height = sh.Height
|
||||
var valset, nextValset *types.ValidatorSet
|
||||
var (
|
||||
chainID = sh.ChainID
|
||||
height = sh.Height
|
||||
)
|
||||
|
||||
// Load the validator set.
|
||||
valset, err := dbp.getValidatorSet(chainID, height)
|
||||
if err != nil {
|
||||
return FullCommit{}, err
|
||||
}
|
||||
|
||||
// Load the next validator set.
|
||||
nextValset, err = dbp.getValidatorSet(chainID, height+1)
|
||||
nextValset, err := dbp.getValidatorSet(chainID, height+1)
|
||||
if err != nil {
|
||||
return FullCommit{}, err
|
||||
}
|
||||
|
||||
// Return filled FullCommit.
|
||||
return FullCommit{
|
||||
SignedHeader: sh,
|
||||
@@ -189,9 +203,16 @@ func (dbp *DBProvider) fillFullCommit(sh types.SignedHeader) (FullCommit, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deleteAfterN deletes all items except skipping first {after} items.
|
||||
// example - deleteAfterN("test", 1):
|
||||
// - signedHeader#188
|
||||
// - signedHeader#187
|
||||
// - validatorSet#187
|
||||
// - signedHeader#186
|
||||
// ==>
|
||||
// - signedHeader#188
|
||||
func (dbp *DBProvider) deleteAfterN(chainID string, after int) error {
|
||||
|
||||
dbp.logger.Info("DBProvider.deleteAfterN()...", "chainID", chainID, "after", after)
|
||||
dbp.logger.Debug("DBProvider.deleteAfterN()...", "chainID", chainID, "after", after)
|
||||
|
||||
itr := dbp.db.ReverseIterator(
|
||||
signedHeaderKey(chainID, 1),
|
||||
@@ -199,29 +220,30 @@ func (dbp *DBProvider) deleteAfterN(chainID string, after int) error {
|
||||
)
|
||||
defer itr.Close()
|
||||
|
||||
var lastHeight int64 = 1<<63 - 1
|
||||
var numSeen = 0
|
||||
var numDeleted = 0
|
||||
var (
|
||||
minHeight int64 = 1<<63 - 1
|
||||
numSeen = 0
|
||||
numDeleted = 0
|
||||
)
|
||||
|
||||
for itr.Valid() {
|
||||
key := itr.Key()
|
||||
_, height, ok := parseChainKeyPrefix(key)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected key %v", key)
|
||||
} else {
|
||||
if height < lastHeight {
|
||||
lastHeight = height
|
||||
numSeen += 1
|
||||
}
|
||||
if numSeen > after {
|
||||
dbp.db.Delete(key)
|
||||
numDeleted += 1
|
||||
}
|
||||
}
|
||||
if height < minHeight {
|
||||
minHeight = height
|
||||
numSeen++
|
||||
}
|
||||
if numSeen > after {
|
||||
dbp.db.Delete(key)
|
||||
numDeleted++
|
||||
}
|
||||
itr.Next()
|
||||
}
|
||||
|
||||
dbp.logger.Info(fmt.Sprintf("DBProvider.deleteAfterN() deleted %v items", numDeleted))
|
||||
dbp.logger.Debug(fmt.Sprintf("DBProvider.deleteAfterN() deleted %d items (seen %d)", numDeleted, numSeen))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
47
lite/doc.go
47
lite/doc.go
@@ -1,5 +1,38 @@
|
||||
/*
|
||||
Package lite allows you to securely validate headers without a full node.
|
||||
Package lite provides a light client implementation.
|
||||
|
||||
The concept of light clients was introduced in the Bitcoin white paper. It
|
||||
describes a watcher of distributed consensus process that only validates the
|
||||
consensus algorithm and not the state machine transactions within.
|
||||
|
||||
Tendermint light clients allow bandwidth & compute-constrained devices, such as
|
||||
smartphones, low-power embedded chips, or other blockchains to efficiently
|
||||
verify the consensus of a Tendermint blockchain. This forms the basis of safe
|
||||
and efficient state synchronization for new network nodes and inter-blockchain
|
||||
communication (where a light client of one Tendermint instance runs in another
|
||||
chain's state machine).
|
||||
|
||||
In a network that is expected to reliably punish validators for misbehavior by
|
||||
slashing bonded stake and where the validator set changes infrequently, clients
|
||||
can take advantage of this assumption to safely synchronize a lite client
|
||||
without downloading the intervening headers.
|
||||
|
||||
Light clients (and full nodes) operating in the Proof Of Stake context need a
|
||||
trusted block height from a trusted source that is no older than 1 unbonding
|
||||
window plus a configurable evidence submission synchrony bound. This is called
|
||||
weak subjectivity.
|
||||
|
||||
Weak subjectivity is required in Proof of Stake blockchains because it is
|
||||
costless for an attacker to buy up voting keys that are no longer bonded and
|
||||
fork the network at some point in its prior history. See Vitalik's post at
|
||||
[Proof of Stake: How I Learned to Love Weak
|
||||
Subjectivity](https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity/).
|
||||
|
||||
NOTE: Tendermint provides a somewhat different (stronger) light client model
|
||||
than Bitcoin under eclipse, since the eclipsing node(s) can only fool the light
|
||||
client if they have two-thirds of the private keys from the last root-of-trust.
|
||||
|
||||
===
|
||||
|
||||
This library pulls together all the crypto and algorithms, so given a
|
||||
relatively recent (< unbonding period) known validator set, one can get
|
||||
@@ -42,13 +75,13 @@ ValidatorSets.
|
||||
|
||||
Verifier
|
||||
|
||||
A Verifier validates a new SignedHeader given the currently known state. There
|
||||
are two different types of Verifiers provided.
|
||||
Verifier validates a new SignedHeader given the currently known state. There
|
||||
re two different types of Verifiers provided.
|
||||
|
||||
BaseVerifier - given a validator set and a height, this Verifier verifies
|
||||
that > 2/3 of the voting power of the given validator set had signed the
|
||||
SignedHeader, and that the SignedHeader was to be signed by the exact given
|
||||
validator set, and that the height of the commit is at least height (or
|
||||
erifier - given a validator set and a height, this Verifier verifies
|
||||
hat > 2/3 of the voting power of the given validator set had signed the
|
||||
ignedHeader, and that the SignedHeader was to be signed by the exact given
|
||||
alidator set, and that the height of the commit is at least height (or
|
||||
greater).
|
||||
|
||||
DynamicVerifier - this Verifier implements an auto-update and persistence
|
||||
|
@@ -1,275 +0,0 @@
|
||||
package lite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/tendermint/tendermint/libs/log"
|
||||
lerr "github.com/tendermint/tendermint/lite/errors"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
const sizeOfPendingMap = 1024
|
||||
|
||||
var _ Verifier = (*DynamicVerifier)(nil)
|
||||
|
||||
// DynamicVerifier implements an auto-updating Verifier. It uses a
|
||||
// "source" provider to obtain the needed FullCommits to securely sync with
|
||||
// validator set changes. It stores properly validated data on the
|
||||
// "trusted" local system.
|
||||
// TODO: make this single threaded and create a new
|
||||
// ConcurrentDynamicVerifier that wraps it with concurrency.
|
||||
// see https://github.com/tendermint/tendermint/issues/3170
|
||||
type DynamicVerifier struct {
|
||||
chainID string
|
||||
logger log.Logger
|
||||
|
||||
// Already validated, stored locally
|
||||
trusted PersistentProvider
|
||||
|
||||
// New info, like a node rpc, or other import method.
|
||||
source Provider
|
||||
|
||||
// pending map to synchronize concurrent verification requests
|
||||
mtx sync.Mutex
|
||||
pendingVerifications map[int64]chan struct{}
|
||||
}
|
||||
|
||||
// NewDynamicVerifier returns a new DynamicVerifier. It uses the
|
||||
// trusted provider to store validated data and the source provider to
|
||||
// obtain missing data (e.g. FullCommits).
|
||||
//
|
||||
// The trusted provider should be a DBProvider.
|
||||
// The source provider should be a client.HTTPProvider.
|
||||
func NewDynamicVerifier(chainID string, trusted PersistentProvider, source Provider) *DynamicVerifier {
|
||||
return &DynamicVerifier{
|
||||
logger: log.NewNopLogger(),
|
||||
chainID: chainID,
|
||||
trusted: trusted,
|
||||
source: source,
|
||||
pendingVerifications: make(map[int64]chan struct{}, sizeOfPendingMap),
|
||||
}
|
||||
}
|
||||
|
||||
func (dv *DynamicVerifier) SetLogger(logger log.Logger) {
|
||||
logger = logger.With("module", "lite")
|
||||
dv.logger = logger
|
||||
dv.trusted.SetLogger(logger)
|
||||
dv.source.SetLogger(logger)
|
||||
}
|
||||
|
||||
// Implements Verifier.
|
||||
func (dv *DynamicVerifier) ChainID() string {
|
||||
return dv.chainID
|
||||
}
|
||||
|
||||
// Implements Verifier.
|
||||
//
|
||||
// If the validators have changed since the last known time, it looks to
|
||||
// dv.trusted and dv.source to prove the new validators. On success, it will
|
||||
// try to store the SignedHeader in dv.trusted if the next
|
||||
// validator can be sourced.
|
||||
func (dv *DynamicVerifier) Verify(shdr types.SignedHeader) error {
|
||||
|
||||
// Performs synchronization for multi-threads verification at the same height.
|
||||
dv.mtx.Lock()
|
||||
if pending := dv.pendingVerifications[shdr.Height]; pending != nil {
|
||||
dv.mtx.Unlock()
|
||||
<-pending // pending is chan struct{}
|
||||
} else {
|
||||
pending := make(chan struct{})
|
||||
dv.pendingVerifications[shdr.Height] = pending
|
||||
defer func() {
|
||||
close(pending)
|
||||
dv.mtx.Lock()
|
||||
delete(dv.pendingVerifications, shdr.Height)
|
||||
dv.mtx.Unlock()
|
||||
}()
|
||||
dv.mtx.Unlock()
|
||||
}
|
||||
|
||||
//Get the exact trusted commit for h, and if it is
|
||||
// equal to shdr, then it's already trusted, so
|
||||
// just return nil.
|
||||
trustedFCSameHeight, err := dv.trusted.LatestFullCommit(dv.chainID, shdr.Height, shdr.Height)
|
||||
if err == nil {
|
||||
// If loading trust commit successfully, and trust commit equal to shdr, then don't verify it,
|
||||
// just return nil.
|
||||
if bytes.Equal(trustedFCSameHeight.SignedHeader.Hash(), shdr.Hash()) {
|
||||
dv.logger.Info(fmt.Sprintf("Load full commit at height %d from cache, there is not need to verify.", shdr.Height))
|
||||
return nil
|
||||
}
|
||||
} else if !lerr.IsErrCommitNotFound(err) {
|
||||
// Return error if it is not CommitNotFound error
|
||||
dv.logger.Info(fmt.Sprintf("Encountered unknown error in loading full commit at height %d.", shdr.Height))
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the latest known full commit <= h-1 from our trusted providers.
|
||||
// The full commit at h-1 contains the valset to sign for h.
|
||||
prevHeight := shdr.Height - 1
|
||||
trustedFC, err := dv.trusted.LatestFullCommit(dv.chainID, 1, prevHeight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sync up to the prevHeight and assert our latest NextValidatorSet
|
||||
// is the ValidatorSet for the SignedHeader
|
||||
if trustedFC.Height() == prevHeight {
|
||||
// Return error if valset doesn't match.
|
||||
if !bytes.Equal(
|
||||
trustedFC.NextValidators.Hash(),
|
||||
shdr.Header.ValidatorsHash) {
|
||||
return lerr.ErrUnexpectedValidators(
|
||||
trustedFC.NextValidators.Hash(),
|
||||
shdr.Header.ValidatorsHash)
|
||||
}
|
||||
} else {
|
||||
// If valset doesn't match, try to update
|
||||
if !bytes.Equal(
|
||||
trustedFC.NextValidators.Hash(),
|
||||
shdr.Header.ValidatorsHash) {
|
||||
// ... update.
|
||||
trustedFC, err = dv.updateToHeight(prevHeight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Return error if valset _still_ doesn't match.
|
||||
if !bytes.Equal(trustedFC.NextValidators.Hash(),
|
||||
shdr.Header.ValidatorsHash) {
|
||||
return lerr.ErrUnexpectedValidators(
|
||||
trustedFC.NextValidators.Hash(),
|
||||
shdr.Header.ValidatorsHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the signed header using the matching valset.
|
||||
cert := NewBaseVerifier(dv.chainID, trustedFC.Height()+1, trustedFC.NextValidators)
|
||||
err = cert.Verify(shdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// By now, the SignedHeader is fully validated and we're synced up to
|
||||
// SignedHeader.Height - 1. To sync to SignedHeader.Height, we need
|
||||
// the validator set at SignedHeader.Height + 1 so we can verify the
|
||||
// SignedHeader.NextValidatorSet.
|
||||
// TODO: is the ValidateFull below mostly redundant with the BaseVerifier.Verify above?
|
||||
// See https://github.com/tendermint/tendermint/issues/3174.
|
||||
|
||||
// Get the next validator set.
|
||||
nextValset, err := dv.source.ValidatorSet(dv.chainID, shdr.Height+1)
|
||||
if lerr.IsErrUnknownValidators(err) {
|
||||
// Ignore this error.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create filled FullCommit.
|
||||
nfc := FullCommit{
|
||||
SignedHeader: shdr,
|
||||
Validators: trustedFC.NextValidators,
|
||||
NextValidators: nextValset,
|
||||
}
|
||||
// Validate the full commit. This checks the cryptographic
|
||||
// signatures of Commit against Validators.
|
||||
if err := nfc.ValidateFull(dv.chainID); err != nil {
|
||||
return err
|
||||
}
|
||||
// Trust it.
|
||||
return dv.trusted.SaveFullCommit(nfc)
|
||||
}
|
||||
|
||||
// verifyAndSave will verify if this is a valid source full commit given the
|
||||
// best match trusted full commit, and if good, persist to dv.trusted.
|
||||
// Returns ErrTooMuchChange when >2/3 of trustedFC did not sign sourceFC.
|
||||
// Panics if trustedFC.Height() >= sourceFC.Height().
|
||||
func (dv *DynamicVerifier) verifyAndSave(trustedFC, sourceFC FullCommit) error {
|
||||
if trustedFC.Height() >= sourceFC.Height() {
|
||||
panic("should not happen")
|
||||
}
|
||||
err := trustedFC.NextValidators.VerifyFutureCommit(
|
||||
sourceFC.Validators,
|
||||
dv.chainID, sourceFC.SignedHeader.Commit.BlockID,
|
||||
sourceFC.SignedHeader.Height, sourceFC.SignedHeader.Commit,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dv.trusted.SaveFullCommit(sourceFC)
|
||||
}
|
||||
|
||||
// updateToHeight will use divide-and-conquer to find a path to h.
|
||||
// Returns nil error iff we successfully verify and persist a full commit
|
||||
// for height h, using repeated applications of bisection if necessary.
|
||||
//
|
||||
// Returns ErrCommitNotFound if source provider doesn't have the commit for h.
|
||||
func (dv *DynamicVerifier) updateToHeight(h int64) (FullCommit, error) {
|
||||
|
||||
// Fetch latest full commit from source.
|
||||
sourceFC, err := dv.source.LatestFullCommit(dv.chainID, h, h)
|
||||
if err != nil {
|
||||
return FullCommit{}, err
|
||||
}
|
||||
|
||||
// If sourceFC.Height() != h, we can't do it.
|
||||
if sourceFC.Height() != h {
|
||||
return FullCommit{}, lerr.ErrCommitNotFound()
|
||||
}
|
||||
|
||||
// Validate the full commit. This checks the cryptographic
|
||||
// signatures of Commit against Validators.
|
||||
if err := sourceFC.ValidateFull(dv.chainID); err != nil {
|
||||
return FullCommit{}, err
|
||||
}
|
||||
|
||||
// Verify latest FullCommit against trusted FullCommits
|
||||
FOR_LOOP:
|
||||
for {
|
||||
// Fetch latest full commit from trusted.
|
||||
trustedFC, err := dv.trusted.LatestFullCommit(dv.chainID, 1, h)
|
||||
if err != nil {
|
||||
return FullCommit{}, err
|
||||
}
|
||||
// We have nothing to do.
|
||||
if trustedFC.Height() == h {
|
||||
return trustedFC, nil
|
||||
}
|
||||
|
||||
// Try to update to full commit with checks.
|
||||
err = dv.verifyAndSave(trustedFC, sourceFC)
|
||||
if err == nil {
|
||||
// All good!
|
||||
return sourceFC, nil
|
||||
}
|
||||
|
||||
// Handle special case when err is ErrTooMuchChange.
|
||||
if types.IsErrTooMuchChange(err) {
|
||||
// Divide and conquer.
|
||||
start, end := trustedFC.Height(), sourceFC.Height()
|
||||
if !(start < end) {
|
||||
panic("should not happen")
|
||||
}
|
||||
mid := (start + end) / 2
|
||||
_, err = dv.updateToHeight(mid)
|
||||
if err != nil {
|
||||
return FullCommit{}, err
|
||||
}
|
||||
// If we made it to mid, we retry.
|
||||
continue FOR_LOOP
|
||||
}
|
||||
return FullCommit{}, err
|
||||
}
|
||||
}
|
||||
|
||||
func (dv *DynamicVerifier) LastTrustedHeight() int64 {
|
||||
fc, err := dv.trusted.LatestFullCommit(dv.chainID, 1, 1<<63-1)
|
||||
if err != nil {
|
||||
panic("should not happen")
|
||||
}
|
||||
return fc.Height()
|
||||
}
|
@@ -6,9 +6,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//----------------------------------------
|
||||
// Error types
|
||||
|
||||
type errCommitNotFound struct{}
|
||||
|
||||
func (e errCommitNotFound) Error() string {
|
||||
@@ -41,11 +38,11 @@ func (e errEmptyTree) Error() string {
|
||||
return "Tree is empty"
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
// Methods for above error types
|
||||
type errCommitExpired struct{}
|
||||
|
||||
//-----------------
|
||||
// ErrCommitNotFound
|
||||
func (e errCommitExpired) Error() string {
|
||||
return "commit is too old to be trusted"
|
||||
}
|
||||
|
||||
// ErrCommitNotFound indicates that a the requested commit was not found.
|
||||
func ErrCommitNotFound() error {
|
||||
@@ -57,9 +54,6 @@ func IsErrCommitNotFound(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
//-----------------
|
||||
// ErrUnexpectedValidators
|
||||
|
||||
// ErrUnexpectedValidators indicates a validator set mismatch.
|
||||
func ErrUnexpectedValidators(got, want []byte) error {
|
||||
return errors.Wrap(errUnexpectedValidators{
|
||||
@@ -73,9 +67,6 @@ func IsErrUnexpectedValidators(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
//-----------------
|
||||
// ErrUnknownValidators
|
||||
|
||||
// ErrUnknownValidators indicates that some validator set was missing or unknown.
|
||||
func ErrUnknownValidators(chainID string, height int64) error {
|
||||
return errors.Wrap(errUnknownValidators{chainID, height}, "")
|
||||
@@ -86,9 +77,6 @@ func IsErrUnknownValidators(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
//-----------------
|
||||
// ErrEmptyTree
|
||||
|
||||
func ErrEmptyTree() error {
|
||||
return errors.Wrap(errEmptyTree{}, "")
|
||||
}
|
||||
@@ -97,3 +85,29 @@ func IsErrEmptyTree(err error) bool {
|
||||
_, ok := errors.Cause(err).(errEmptyTree)
|
||||
return ok
|
||||
}
|
||||
|
||||
func ErrCommitExpired() error {
|
||||
return errors.Wrap(errCommitExpired{}, "")
|
||||
}
|
||||
|
||||
func IsErrCommitExpired(err error) bool {
|
||||
_, ok := errors.Cause(err).(errCommitExpired)
|
||||
return ok
|
||||
}
|
||||
|
||||
type errValidatorChange struct {
|
||||
change float64
|
||||
}
|
||||
|
||||
func (e errValidatorChange) Error() string {
|
||||
return fmt.Sprintf("%f is more than 1/3rd validator change", e.change)
|
||||
}
|
||||
|
||||
func ErrValidatorChange(change float64) error {
|
||||
return errors.Wrap(errValidatorChange{change: change}, "")
|
||||
}
|
||||
|
||||
func IsErrValidatorChange(err error) bool {
|
||||
_, ok := errors.Cause(err).(errValidatorChange)
|
||||
return ok
|
||||
}
|
||||
|
@@ -1,26 +1,25 @@
|
||||
package lite
|
||||
package privkeys
|
||||
|
||||
import (
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
"github.com/tendermint/tendermint/crypto/secp256k1"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
tmtime "github.com/tendermint/tendermint/types/time"
|
||||
)
|
||||
|
||||
// privKeys is a helper type for testing.
|
||||
// PrivKeys is a helper type for testing.
|
||||
//
|
||||
// It lets us simulate signing with many keys. The main use case is to create
|
||||
// a set, and call GenSignedHeader to get properly signed header for testing.
|
||||
//
|
||||
// You can set different weights of validators each time you call ToValidators,
|
||||
// and can optionally extend the validator set later with Extend.
|
||||
type privKeys []crypto.PrivKey
|
||||
type PrivKeys []crypto.PrivKey
|
||||
|
||||
// genPrivKeys produces an array of private keys to generate commits.
|
||||
func genPrivKeys(n int) privKeys {
|
||||
res := make(privKeys, n)
|
||||
// GenPrivKeys produces an array of private keys to generate commits.
|
||||
func GenPrivKeys(n int) PrivKeys {
|
||||
res := make(PrivKeys, n)
|
||||
for i := range res {
|
||||
res[i] = ed25519.GenPrivKey()
|
||||
}
|
||||
@@ -28,22 +27,22 @@ func genPrivKeys(n int) privKeys {
|
||||
}
|
||||
|
||||
// Change replaces the key at index i.
|
||||
func (pkz privKeys) Change(i int) privKeys {
|
||||
res := make(privKeys, len(pkz))
|
||||
func (pkz PrivKeys) Change(i int) PrivKeys {
|
||||
res := make(PrivKeys, len(pkz))
|
||||
copy(res, pkz)
|
||||
res[i] = ed25519.GenPrivKey()
|
||||
return res
|
||||
}
|
||||
|
||||
// Extend adds n more keys (to remove, just take a slice).
|
||||
func (pkz privKeys) Extend(n int) privKeys {
|
||||
extra := genPrivKeys(n)
|
||||
func (pkz PrivKeys) Extend(n int) PrivKeys {
|
||||
extra := GenPrivKeys(n)
|
||||
return append(pkz, extra...)
|
||||
}
|
||||
|
||||
// GenSecpPrivKeys produces an array of secp256k1 private keys to generate commits.
|
||||
func GenSecpPrivKeys(n int) privKeys {
|
||||
res := make(privKeys, n)
|
||||
func GenSecpPrivKeys(n int) PrivKeys {
|
||||
res := make(PrivKeys, n)
|
||||
for i := range res {
|
||||
res[i] = secp256k1.GenPrivKey()
|
||||
}
|
||||
@@ -51,7 +50,7 @@ func GenSecpPrivKeys(n int) privKeys {
|
||||
}
|
||||
|
||||
// ExtendSecp adds n more secp256k1 keys (to remove, just take a slice).
|
||||
func (pkz privKeys) ExtendSecp(n int) privKeys {
|
||||
func (pkz PrivKeys) ExtendSecp(n int) PrivKeys {
|
||||
extra := GenSecpPrivKeys(n)
|
||||
return append(pkz, extra...)
|
||||
}
|
||||
@@ -60,7 +59,7 @@ func (pkz privKeys) ExtendSecp(n int) privKeys {
|
||||
// The first key has weight `init` and it increases by `inc` every step
|
||||
// so we can have all the same weight, or a simple linear distribution
|
||||
// (should be enough for testing).
|
||||
func (pkz privKeys) ToValidators(init, inc int64) *types.ValidatorSet {
|
||||
func (pkz PrivKeys) ToValidators(init, inc int64) *types.ValidatorSet {
|
||||
res := make([]*types.Validator, len(pkz))
|
||||
for i, k := range pkz {
|
||||
res[i] = types.NewValidator(k.PubKey(), init+int64(i)*inc)
|
||||
@@ -69,7 +68,7 @@ func (pkz privKeys) ToValidators(init, inc int64) *types.ValidatorSet {
|
||||
}
|
||||
|
||||
// signHeader properly signs the header with all keys from first to last exclusive.
|
||||
func (pkz privKeys) signHeader(header *types.Header, first, last int) *types.Commit {
|
||||
func (pkz PrivKeys) signHeader(header *types.Header, first, last int) *types.Commit {
|
||||
commitSigs := make([]*types.CommitSig, len(pkz))
|
||||
|
||||
// We need this list to keep the ordering.
|
||||
@@ -84,6 +83,18 @@ func (pkz privKeys) signHeader(header *types.Header, first, last int) *types.Com
|
||||
return types.NewCommit(blockID, commitSigs)
|
||||
}
|
||||
|
||||
// GenSignedHeader calls genHeader and signHeader and combines them into a SignedHeader.
|
||||
func (pkz PrivKeys) GenSignedHeader(chainID string, height int64, txs types.Txs,
|
||||
valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) types.SignedHeader {
|
||||
|
||||
header := genHeader(chainID, height, txs, valset, nextValset, appHash, consHash, resHash)
|
||||
check := types.SignedHeader{
|
||||
Header: header,
|
||||
Commit: pkz.signHeader(header, first, last),
|
||||
}
|
||||
return check
|
||||
}
|
||||
|
||||
func makeVote(header *types.Header, valset *types.ValidatorSet, key crypto.PrivKey) *types.Vote {
|
||||
addr := key.PubKey().Address()
|
||||
idx, _ := valset.GetByAddress(addr)
|
||||
@@ -127,27 +138,3 @@ func genHeader(chainID string, height int64, txs types.Txs,
|
||||
LastResultsHash: resHash,
|
||||
}
|
||||
}
|
||||
|
||||
// GenSignedHeader calls genHeader and signHeader and combines them into a SignedHeader.
|
||||
func (pkz privKeys) GenSignedHeader(chainID string, height int64, txs types.Txs,
|
||||
valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) types.SignedHeader {
|
||||
|
||||
header := genHeader(chainID, height, txs, valset, nextValset, appHash, consHash, resHash)
|
||||
check := types.SignedHeader{
|
||||
Header: header,
|
||||
Commit: pkz.signHeader(header, first, last),
|
||||
}
|
||||
return check
|
||||
}
|
||||
|
||||
// GenFullCommit calls genHeader and signHeader and combines them into a FullCommit.
|
||||
func (pkz privKeys) GenFullCommit(chainID string, height int64, txs types.Txs,
|
||||
valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) FullCommit {
|
||||
|
||||
header := genHeader(chainID, height, txs, valset, nextValset, appHash, consHash, resHash)
|
||||
commit := types.SignedHeader{
|
||||
Header: header,
|
||||
Commit: pkz.signHeader(header, first, last),
|
||||
}
|
||||
return NewFullCommit(commit, valset, nextValset)
|
||||
}
|
@@ -6,17 +6,16 @@ import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
var _ PersistentProvider = (*multiProvider)(nil)
|
||||
|
||||
// multiProvider allows you to place one or more caches in front of a source
|
||||
// Provider. It runs through them in order until a match is found.
|
||||
// Provider. It runs through them in order until a match is found.
|
||||
type multiProvider struct {
|
||||
logger log.Logger
|
||||
providers []PersistentProvider
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// NewMultiProvider returns a new provider which wraps multiple other providers.
|
||||
func NewMultiProvider(providers ...PersistentProvider) *multiProvider {
|
||||
func NewMultiProvider(providers ...PersistentProvider) PersistentProvider {
|
||||
return &multiProvider{
|
||||
logger: log.NewNopLogger(),
|
||||
providers: providers,
|
||||
@@ -47,8 +46,8 @@ func (mc *multiProvider) SaveFullCommit(fc FullCommit) (err error) {
|
||||
// Returns the first error encountered.
|
||||
func (mc *multiProvider) LatestFullCommit(chainID string, minHeight, maxHeight int64) (fc FullCommit, err error) {
|
||||
for _, p := range mc.providers {
|
||||
var fc_ FullCommit
|
||||
fc_, err = p.LatestFullCommit(chainID, minHeight, maxHeight)
|
||||
var pfc FullCommit
|
||||
pfc, err = p.LatestFullCommit(chainID, minHeight, maxHeight)
|
||||
if lerr.IsErrCommitNotFound(err) {
|
||||
err = nil
|
||||
continue
|
||||
@@ -56,18 +55,20 @@ func (mc *multiProvider) LatestFullCommit(chainID string, minHeight, maxHeight i
|
||||
return
|
||||
}
|
||||
if fc == (FullCommit{}) {
|
||||
fc = fc_
|
||||
} else if fc_.Height() > fc.Height() {
|
||||
fc = fc_
|
||||
fc = pfc
|
||||
} else if pfc.Height() > fc.Height() {
|
||||
fc = pfc
|
||||
}
|
||||
if fc.Height() == maxHeight {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if fc == (FullCommit{}) {
|
||||
err = lerr.ErrCommitNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -76,10 +77,11 @@ func (mc *multiProvider) LatestFullCommit(chainID string, minHeight, maxHeight i
|
||||
func (mc *multiProvider) ValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) {
|
||||
for _, p := range mc.providers {
|
||||
valset, err = p.ValidatorSet(chainID, height)
|
||||
if err == nil {
|
||||
// TODO Log unexpected types of errors.
|
||||
return valset, nil
|
||||
if lerr.IsErrUnknownValidators(err) {
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
return nil, lerr.ErrUnknownValidators(chainID, height)
|
||||
}
|
||||
|
@@ -6,27 +6,40 @@ import (
|
||||
)
|
||||
|
||||
// Provider provides information for the lite client to sync validators.
|
||||
// Examples: MemProvider, files.Provider, client.Provider, CacheProvider.
|
||||
type Provider interface {
|
||||
|
||||
// LatestFullCommit returns the latest commit with minHeight <= height <=
|
||||
// maxHeight.
|
||||
// If maxHeight is zero, returns the latest where minHeight <= height.
|
||||
// If maxHeight is greater than the latest height, the latter one should be returned.
|
||||
LatestFullCommit(chainID string, minHeight, maxHeight int64) (FullCommit, error)
|
||||
|
||||
// Get the valset that corresponds to chainID and height and return.
|
||||
// Height must be >= 1.
|
||||
// ValidatorSet returns the valset that corresponds to chainID and height.
|
||||
// height must be >= 1.
|
||||
ValidatorSet(chainID string, height int64) (*types.ValidatorSet, error)
|
||||
|
||||
// Set a logger.
|
||||
// SetLogger sets a logger.
|
||||
SetLogger(logger log.Logger)
|
||||
}
|
||||
|
||||
// A provider that can also persist new information.
|
||||
// Examples: MemProvider, files.Provider, CacheProvider.
|
||||
// PersistentProvider is a provider that can also persist new information.
|
||||
type PersistentProvider interface {
|
||||
Provider
|
||||
|
||||
// SaveFullCommit saves a FullCommit (without verification).
|
||||
SaveFullCommit(fc FullCommit) error
|
||||
}
|
||||
|
||||
// UpdatingProvider is a provider that can update itself w/ more recent commit
|
||||
// data.
|
||||
type UpdatingProvider interface {
|
||||
Provider
|
||||
|
||||
// Update internal information by fetching information somehow.
|
||||
// UpdateToHeight will block until the request is complete, or returns an
|
||||
// error if the request cannot complete. Generally, one must call
|
||||
// UpdateToHeight(h) before LatestFullCommit(_,h,h) will return this height.
|
||||
//
|
||||
// NOTE: Behavior with concurrent requests is undefined. To make concurrent
|
||||
// calls safe, look at the struct `ConcurrentUpdatingProvider`.
|
||||
UpdateToHeight(chainID string, height int64) error
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
log "github.com/tendermint/tendermint/libs/log"
|
||||
lerr "github.com/tendermint/tendermint/lite/errors"
|
||||
pks "github.com/tendermint/tendermint/lite/internal/privkeys"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
@@ -48,7 +49,7 @@ func TestMultiProvider(t *testing.T) {
|
||||
func checkProvider(t *testing.T, p PersistentProvider, chainID, app string) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
appHash := []byte(app)
|
||||
keys := genPrivKeys(5)
|
||||
keys := pks.GenPrivKeys(5)
|
||||
count := 10
|
||||
|
||||
// Make a bunch of full commits.
|
||||
@@ -56,7 +57,9 @@ func checkProvider(t *testing.T, p PersistentProvider, chainID, app string) {
|
||||
for i := 0; i < count; i++ {
|
||||
vals := keys.ToValidators(10, int64(count/2))
|
||||
h := int64(20 + 10*i)
|
||||
fcz[i] = keys.GenFullCommit(chainID, h, nil, vals, vals, appHash, []byte("params"), []byte("results"), 0, 5)
|
||||
signedHeader := keys.GenSignedHeader(chainID, h, nil, vals, vals, appHash,
|
||||
[]byte("params"), []byte("results"), 0, 5)
|
||||
fcz[i] = NewFullCommit(signedHeader, vals, vals)
|
||||
}
|
||||
|
||||
// Check that provider is initially empty.
|
||||
@@ -113,14 +116,15 @@ func TestMultiLatestFullCommit(t *testing.T) {
|
||||
|
||||
chainID := "cache-best-height"
|
||||
appHash := []byte("01234567")
|
||||
keys := genPrivKeys(5)
|
||||
keys := pks.GenPrivKeys(5)
|
||||
count := 10
|
||||
|
||||
// Set a bunch of full commits.
|
||||
for i := 0; i < count; i++ {
|
||||
vals := keys.ToValidators(10, int64(count/2))
|
||||
h := int64(10 * (i + 1))
|
||||
fc := keys.GenFullCommit(chainID, h, nil, vals, vals, appHash, []byte("params"), []byte("results"), 0, 5)
|
||||
signedHeader := keys.GenSignedHeader(chainID, h, nil, vals, vals, appHash, []byte("params"), []byte("results"), 0, 5)
|
||||
fc := NewFullCommit(signedHeader, vals, vals)
|
||||
err := p2.SaveFullCommit(fc)
|
||||
require.NoError(err)
|
||||
}
|
||||
|
@@ -54,7 +54,7 @@ func _TestAppProofs(t *testing.T) {
|
||||
source := certclient.NewProvider(chainID, cl)
|
||||
seed, err := source.LatestFullCommit(chainID, 1, 1)
|
||||
require.NoError(err, "%#v", err)
|
||||
cert := lite.NewBaseVerifier(chainID, seed.Height(), seed.Validators)
|
||||
cert := lite.NewVerifier(chainID, seed.Height(), seed.Validators)
|
||||
|
||||
// Wait for tx confirmation.
|
||||
done := make(chan int64)
|
||||
@@ -139,7 +139,7 @@ func TestTxProofs(t *testing.T) {
|
||||
source := certclient.NewProvider(chainID, cl)
|
||||
seed, err := source.LatestFullCommit(chainID, brh-2, brh-2)
|
||||
require.NoError(err, "%#v", err)
|
||||
cert := lite.NewBaseVerifier(chainID, seed.Height(), seed.Validators)
|
||||
cert := lite.NewVerifier(chainID, seed.Height(), seed.Validators)
|
||||
|
||||
// First let's make sure a bogus transaction hash returns a valid non-existence proof.
|
||||
key := types.Tx([]byte("bogus")).Hash()
|
||||
|
@@ -1,42 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
log "github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/lite"
|
||||
lclient "github.com/tendermint/tendermint/lite/client"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
func NewVerifier(chainID, rootDir string, client lclient.SignStatusClient, logger log.Logger, cacheSize int) (*lite.DynamicVerifier, error) {
|
||||
|
||||
logger = logger.With("module", "lite/proxy")
|
||||
logger.Info("lite/proxy/NewVerifier()...", "chainID", chainID, "rootDir", rootDir, "client", client)
|
||||
|
||||
memProvider := lite.NewDBProvider("trusted.mem", dbm.NewMemDB()).SetLimit(cacheSize)
|
||||
lvlProvider := lite.NewDBProvider("trusted.lvl", dbm.NewDB("trust-base", dbm.GoLevelDBBackend, rootDir))
|
||||
trust := lite.NewMultiProvider(
|
||||
memProvider,
|
||||
lvlProvider,
|
||||
)
|
||||
source := lclient.NewProvider(chainID, client)
|
||||
cert := lite.NewDynamicVerifier(chainID, trust, source)
|
||||
cert.SetLogger(logger) // Sets logger recursively.
|
||||
|
||||
// TODO: Make this more secure, e.g. make it interactive in the console?
|
||||
_, err := trust.LatestFullCommit(chainID, 1, 1<<63-1)
|
||||
if err != nil {
|
||||
logger.Info("lite/proxy/NewVerifier found no trusted full commit, initializing from source from height 1...")
|
||||
fc, err := source.LatestFullCommit(chainID, 1, 1)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "fetching source full commit @ height 1")
|
||||
}
|
||||
err = trust.SaveFullCommit(fc)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "saving full commit to trusted")
|
||||
}
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
@@ -7,7 +7,7 @@ import (
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto/merkle"
|
||||
"github.com/tendermint/tendermint/lite"
|
||||
"github.com/tendermint/tendermint/lite/verifying"
|
||||
rpcclient "github.com/tendermint/tendermint/rpc/client"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/lib/types"
|
||||
@@ -19,7 +19,7 @@ var _ rpcclient.Client = Wrapper{}
|
||||
// provable before passing it along. Allows you to make any rpcclient fully secure.
|
||||
type Wrapper struct {
|
||||
rpcclient.Client
|
||||
cert *lite.DynamicVerifier
|
||||
cert *verifying.Provider
|
||||
prt *merkle.ProofRuntime
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ type Wrapper struct {
|
||||
// host and return a cryptographically secure rpc client.
|
||||
//
|
||||
// If it is wrapping an HTTP rpcclient, it will also wrap the websocket interface
|
||||
func SecureClient(c rpcclient.Client, cert *lite.DynamicVerifier) Wrapper {
|
||||
func SecureClient(c rpcclient.Client, cert *verifying.Provider) Wrapper {
|
||||
prt := defaultProofRuntime()
|
||||
wrap := Wrapper{c, cert, prt}
|
||||
// TODO: no longer possible as no more such interface exposed....
|
||||
|
@@ -1,13 +0,0 @@
|
||||
package lite
|
||||
|
||||
import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// Verifier checks the votes to make sure the block really is signed properly.
|
||||
// Verifier must know the current or recent set of validitors by some other
|
||||
// means.
|
||||
type Verifier interface {
|
||||
Verify(sheader types.SignedHeader) error
|
||||
ChainID() string
|
||||
}
|
@@ -10,55 +10,48 @@ import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
var _ Verifier = (*BaseVerifier)(nil)
|
||||
|
||||
// BaseVerifier lets us check the validity of SignedHeaders at height or
|
||||
// later, requiring sufficient votes (> 2/3) from the given valset.
|
||||
// To verify blocks produced by a blockchain with mutable validator sets,
|
||||
// use the DynamicVerifier.
|
||||
// TODO: Handle unbonding time.
|
||||
type BaseVerifier struct {
|
||||
// verifier lets us check the validity of SignedHeaders at the given height or later,
|
||||
// requiring sufficient votes (> 2/3) from the given valset. To verify blocks
|
||||
// produced by a blockchain with mutable validator sets, use the
|
||||
// DynamicVerifier.
|
||||
type verifier struct {
|
||||
chainID string
|
||||
height int64
|
||||
valset *types.ValidatorSet
|
||||
}
|
||||
|
||||
// NewBaseVerifier returns a new Verifier initialized with a validator set at
|
||||
// NewVerifier returns a new Verifier initialized with a validator set at
|
||||
// some height.
|
||||
func NewBaseVerifier(chainID string, height int64, valset *types.ValidatorSet) *BaseVerifier {
|
||||
func NewVerifier(chainID string, height int64, valset *types.ValidatorSet) *verifier {
|
||||
if valset.IsNilOrEmpty() {
|
||||
panic("NewBaseVerifier requires a valid valset")
|
||||
panic("NewVerifier requires a valid valset")
|
||||
}
|
||||
return &BaseVerifier{
|
||||
return &verifier{
|
||||
chainID: chainID,
|
||||
height: height,
|
||||
valset: valset,
|
||||
}
|
||||
}
|
||||
|
||||
// Implements Verifier.
|
||||
func (bv *BaseVerifier) ChainID() string {
|
||||
func (bv *verifier) ChainID() string {
|
||||
return bv.chainID
|
||||
}
|
||||
|
||||
// Implements Verifier.
|
||||
func (bv *BaseVerifier) Verify(signedHeader types.SignedHeader) error {
|
||||
|
||||
func (bv *verifier) Verify(signedHeader types.SignedHeader) error {
|
||||
// We can't verify commits for a different chain.
|
||||
if signedHeader.ChainID != bv.chainID {
|
||||
return cmn.NewError("BaseVerifier chainID is %v, cannot verify chainID %v",
|
||||
return cmn.NewError("verifier chainID is %v, cannot verify chainID %v",
|
||||
bv.chainID, signedHeader.ChainID)
|
||||
}
|
||||
|
||||
// We can't verify commits older than bv.height.
|
||||
if signedHeader.Height < bv.height {
|
||||
return cmn.NewError("BaseVerifier height is %v, cannot verify height %v",
|
||||
return cmn.NewError("verifier height is %v, cannot verify height %v",
|
||||
bv.height, signedHeader.Height)
|
||||
}
|
||||
|
||||
// We can't verify with the wrong validator set.
|
||||
if !bytes.Equal(signedHeader.ValidatorsHash,
|
||||
bv.valset.Hash()) {
|
||||
if !bytes.Equal(signedHeader.ValidatorsHash, bv.valset.Hash()) {
|
||||
return lerr.ErrUnexpectedValidators(signedHeader.ValidatorsHash, bv.valset.Hash())
|
||||
}
|
||||
|
@@ -6,21 +6,22 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
lerr "github.com/tendermint/tendermint/lite/errors"
|
||||
pks "github.com/tendermint/tendermint/lite/internal/privkeys"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestBaseCert(t *testing.T) {
|
||||
func TestVerifier(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
keys := genPrivKeys(4)
|
||||
keys := pks.GenPrivKeys(4)
|
||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do!
|
||||
vals := keys.ToValidators(20, 10)
|
||||
// and a Verifier based on our known set
|
||||
chainID := "test-static"
|
||||
cert := NewBaseVerifier(chainID, 2, vals)
|
||||
cert := NewVerifier(chainID, 2, vals)
|
||||
|
||||
cases := []struct {
|
||||
keys privKeys
|
||||
keys pks.PrivKeys
|
||||
vals *types.ValidatorSet
|
||||
height int64
|
||||
first, last int // who actually signs
|
533
lite/verifying/provider.go
Normal file
533
lite/verifying/provider.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package verifying
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
log "github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/lite"
|
||||
lclient "github.com/tendermint/tendermint/lite/client"
|
||||
lerr "github.com/tendermint/tendermint/lite/errors"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
const (
|
||||
loggerPath = "lite"
|
||||
memDBFile = "trusted.mem"
|
||||
lvlDBFile = "trusted.lvl"
|
||||
dbName = "trust-base"
|
||||
|
||||
sizeOfPendingMap = 1024
|
||||
)
|
||||
|
||||
// TrustOptions are the trust parameters needed for when a new light client
|
||||
// connects to the network or when a light client that has been offline for
|
||||
// longer than the unbonding period connects to the network.
|
||||
//
|
||||
// The expectation is the user will get this information from a trusted source
|
||||
// like a validator, a friend, or a secure website. A more user friendly
|
||||
// solution with trust tradeoffs is that we establish an https based protocol
|
||||
// with a default end point that populates this information. Also an on-chain
|
||||
// registry of roots-of-trust (e.g. on the Cosmos Hub) seems likely in the
|
||||
// future.
|
||||
type TrustOptions struct {
|
||||
// Required: only trust commits up to this old.
|
||||
// Should be equal to the unbonding period minus a configurable evidence
|
||||
// submission synchrony bound.
|
||||
TrustPeriod time.Duration `json:"trust-period"`
|
||||
|
||||
// Option 1: TrustHeight and TrustHash can both be provided
|
||||
// to force the trusting of a particular height and hash.
|
||||
// If the latest trusted height/hash is more recent, then this option is
|
||||
// ignored.
|
||||
TrustHeight int64 `json:"trust-height"`
|
||||
TrustHash []byte `json:"trust-hash"`
|
||||
|
||||
// Option 2: Callback can be set to implement a confirmation
|
||||
// step if the trust store is uninitialized, or expired.
|
||||
Callback func(height int64, hash []byte) error
|
||||
}
|
||||
|
||||
// HeightAndHashPresent returns true if TrustHeight and TrustHash are present.
|
||||
func (opts TrustOptions) HeightAndHashPresent() bool {
|
||||
return opts.TrustHeight > 0 && len(opts.TrustHash) > 0
|
||||
}
|
||||
|
||||
// Provider implements a persistent caching Provider that auto-validates. It
|
||||
// uses a "source" Provider to obtain the needed FullCommits to securely sync
|
||||
// with validator set changes. It stores properly validated data on the
|
||||
// "trusted" local system using a "trusted" Provider.
|
||||
//
|
||||
// NOTE:
|
||||
// - This Provider can only work with one chainID, provided upon
|
||||
// instantiation;
|
||||
// - For concurrent usage, use ConcurrentProvider.
|
||||
type Provider struct {
|
||||
chainID string
|
||||
trustPeriod time.Duration // see TrustOptions above
|
||||
now nowFn
|
||||
height int64
|
||||
logger log.Logger
|
||||
|
||||
// Already validated, stored locally
|
||||
trusted lite.PersistentProvider
|
||||
|
||||
// New info, like a node rpc, or other import method.
|
||||
source lite.Provider
|
||||
|
||||
// pending map to synchronize concurrent verification requests
|
||||
mtx sync.Mutex
|
||||
pendingVerifications map[int64]chan struct{}
|
||||
}
|
||||
|
||||
var _ lite.UpdatingProvider = (*Provider)(nil)
|
||||
|
||||
type nowFn func() time.Time
|
||||
|
||||
// NewProvider creates a Provider.
|
||||
//
|
||||
// NOTE: If you retain the resulting struct in memory for a long time, usage of
|
||||
// it may eventually error, but immediate usage should not error like that, so
|
||||
// that e.g. cli usage never errors unexpectedly.
|
||||
func NewProvider(chainID, rootDir string, client lclient.SignStatusClient,
|
||||
logger log.Logger, cacheSize int, options TrustOptions) (*Provider, error) {
|
||||
|
||||
vp := initProvider(chainID, rootDir, client, logger, cacheSize, options)
|
||||
|
||||
// Get the latest source commit, or the one provided in options.
|
||||
trustCommit, err := getTrustedCommit(vp.logger, client, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = vp.fillValsetAndSaveFC(trustCommit, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sanity check
|
||||
// FIXME: Can't it happen that the local clock is a bit off and the
|
||||
// trustCommit.Time is a few seconds in the future?
|
||||
now := time.Now()
|
||||
if now.Sub(trustCommit.Time) <= 0 {
|
||||
panic(fmt.Sprintf("impossible time %v vs %v", now, trustCommit.Time))
|
||||
}
|
||||
|
||||
// Otherwise we're syncing within the unbonding period.
|
||||
// NOTE: There is a duplication of fetching this latest commit (since
|
||||
// UpdateToHeight() will fetch it again, and latestCommit isn't used), but
|
||||
// it's only once upon initialization so it's not a big deal.
|
||||
if options.HeightAndHashPresent() {
|
||||
// Fetch latest commit (nil means latest height).
|
||||
latestCommit, err := client.Commit(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = vp.UpdateToHeight(chainID, latestCommit.SignedHeader.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return vp, nil
|
||||
}
|
||||
|
||||
func initProvider(chainID, rootDir string, client lclient.SignStatusClient,
|
||||
logger log.Logger, cacheSize int, options TrustOptions) *Provider {
|
||||
|
||||
// Validate TrustOptions.
|
||||
if options.TrustPeriod == 0 {
|
||||
panic("Provider must have non-zero trust period")
|
||||
}
|
||||
|
||||
// Init logger.
|
||||
logger = logger.With("module", loggerPath)
|
||||
logger.Info("lite/verifying/NewProvider", "chainID", chainID, "rootDir", rootDir, "client", client)
|
||||
|
||||
// The trusted Provider should be a DBProvider.
|
||||
trusted := lite.NewMultiProvider(
|
||||
lite.NewDBProvider(memDBFile, dbm.NewMemDB()).SetLimit(cacheSize),
|
||||
lite.NewDBProvider(lvlDBFile, dbm.NewDB(dbName, dbm.GoLevelDBBackend, rootDir)),
|
||||
)
|
||||
trusted.SetLogger(logger)
|
||||
|
||||
// The source Provider should be a client.HTTPProvider.
|
||||
source := lclient.NewProvider(chainID, client)
|
||||
source.SetLogger(logger)
|
||||
|
||||
return &Provider{
|
||||
chainID: chainID,
|
||||
trustPeriod: options.TrustPeriod,
|
||||
trusted: trusted,
|
||||
source: source,
|
||||
logger: logger,
|
||||
pendingVerifications: make(map[int64]chan struct{}, sizeOfPendingMap),
|
||||
}
|
||||
}
|
||||
|
||||
// getTrustedCommit returns a commit trusted with weak subjectivity. It either:
|
||||
// 1. Fetches a commit at height provided in options and ensures the specified
|
||||
// commit is within the trust period of latest block
|
||||
// 2. Trusts the remote node and gets the latest commit
|
||||
// 3. Returns an error if the height provided in trust option is too old to
|
||||
// sync to latest.
|
||||
func getTrustedCommit(logger log.Logger, client lclient.SignStatusClient, options TrustOptions) (types.SignedHeader, error) {
|
||||
// Get the latest commit always.
|
||||
latestCommit, err := client.Commit(nil)
|
||||
if err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
|
||||
// If the user has set a root of trust, confirm it then update to newest.
|
||||
if options.HeightAndHashPresent() {
|
||||
trustCommit, err := client.Commit(&options.TrustHeight)
|
||||
if err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
|
||||
if latestCommit.Time.Sub(trustCommit.Time) > options.TrustPeriod {
|
||||
return types.SignedHeader{},
|
||||
errors.New("your trusted block height is older than the trust period from latest block")
|
||||
}
|
||||
|
||||
signedHeader := trustCommit.SignedHeader
|
||||
if !bytes.Equal(signedHeader.Hash(), options.TrustHash) {
|
||||
return types.SignedHeader{},
|
||||
fmt.Errorf("WARNING: expected hash %X, but got %X", options.TrustHash, signedHeader.Hash())
|
||||
}
|
||||
return signedHeader, nil
|
||||
}
|
||||
|
||||
signedHeader := latestCommit.SignedHeader
|
||||
|
||||
// NOTE: This should really belong in the callback.
|
||||
// WARN THE USER IN ALL CAPS THAT THE LITE CLIENT IS NEW, AND THAT WE WILL
|
||||
// SYNC TO AND VERIFY LATEST COMMIT.
|
||||
logger.Info("WARNING: trusting source at height %d and hash %X...\n", signedHeader.Height, signedHeader.Hash())
|
||||
if options.Callback != nil {
|
||||
err := options.Callback(signedHeader.Height, signedHeader.Hash())
|
||||
if err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
}
|
||||
return signedHeader, nil
|
||||
}
|
||||
|
||||
func (vp *Provider) Verify(signedHeader types.SignedHeader) error {
|
||||
if signedHeader.ChainID != vp.chainID {
|
||||
return fmt.Errorf("expected chainID %s, got %s", vp.chainID, signedHeader.ChainID)
|
||||
}
|
||||
|
||||
valSet, err := vp.ValidatorSet(signedHeader.ChainID, signedHeader.Height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if signedHeader.Height < vp.height {
|
||||
return fmt.Errorf("expected height %d, got %d", vp.height, signedHeader.Height)
|
||||
}
|
||||
|
||||
if !bytes.Equal(signedHeader.ValidatorsHash, valSet.Hash()) {
|
||||
return lerr.ErrUnexpectedValidators(signedHeader.ValidatorsHash, valSet.Hash())
|
||||
}
|
||||
|
||||
err = signedHeader.ValidateBasic(vp.chainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check commit signatures.
|
||||
err = valSet.VerifyCommit(vp.chainID, signedHeader.Commit.BlockID, signedHeader.Height, signedHeader.Commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vp *Provider) SetLogger(logger log.Logger) {
|
||||
vp.logger = logger
|
||||
vp.trusted.SetLogger(logger)
|
||||
vp.source.SetLogger(logger)
|
||||
}
|
||||
|
||||
func (vp *Provider) ChainID() string { return vp.chainID }
|
||||
|
||||
// UpdateToHeight ... stores the full commit (SignedHeader + Validators) in
|
||||
// vp.trusted.
|
||||
func (vp *Provider) UpdateToHeight(chainID string, height int64) error {
|
||||
_, err := vp.trusted.LatestFullCommit(vp.chainID, height, height)
|
||||
// If we alreedy have the commit, just return nil.
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if !lerr.IsErrCommitNotFound(err) {
|
||||
// Return error if it is not CommitNotFound error.
|
||||
vp.logger.Error("Encountered unknown error while loading full commit", "height", height, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch trusted FC at exactly height, while updating trust when possible.
|
||||
_, err = vp.fetchAndVerifyToHeightBisecting(height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vp.height = height
|
||||
|
||||
// Good!
|
||||
return nil
|
||||
}
|
||||
|
||||
// If valset or nextValset are nil, fetches them.
|
||||
// Then validates full commit, then saves it.
|
||||
func (vp *Provider) fillValsetAndSaveFC(signedHeader types.SignedHeader,
|
||||
valset, nextValset *types.ValidatorSet) (err error) {
|
||||
|
||||
// If there is no valset passed, fetch it
|
||||
if valset == nil {
|
||||
valset, err = vp.source.ValidatorSet(vp.chainID, signedHeader.Height)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching the valset")
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no nextvalset passed, fetch it
|
||||
if nextValset == nil {
|
||||
// TODO: Don't loop forever, just do it 10 times
|
||||
for {
|
||||
// fetch block at signedHeader.Height+1
|
||||
nextValset, err = vp.source.ValidatorSet(vp.chainID, signedHeader.Height+1)
|
||||
if lerr.IsErrUnknownValidators(err) {
|
||||
// try again until we get it.
|
||||
vp.logger.Debug("fetching valset for height %d...\n", signedHeader.Height+1)
|
||||
continue
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "fetching the next valset")
|
||||
} else if nextValset != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create filled FullCommit.
|
||||
fc := lite.FullCommit{
|
||||
SignedHeader: signedHeader,
|
||||
Validators: valset,
|
||||
NextValidators: nextValset,
|
||||
}
|
||||
|
||||
// Validate the full commit. This checks the cryptographic
|
||||
// signatures of Commit against Validators.
|
||||
if err := fc.ValidateFull(vp.chainID); err != nil {
|
||||
return errors.Wrap(err, "verifying validators from source")
|
||||
}
|
||||
|
||||
// Trust it.
|
||||
err = vp.trusted.SaveFullCommit(fc)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "saving full commit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyAndSave will verify if this is a valid source full commit given the
|
||||
// best match trusted full commit, and persist to vp.trusted.
|
||||
//
|
||||
// Returns ErrTooMuchChange when >2/3 of trustedFC did not sign newFC.
|
||||
// Returns ErrCommitExpired when trustedFC is too old.
|
||||
// Panics if trustedFC.Height() >= newFC.Height().
|
||||
func (vp *Provider) verifyAndSave(trustedFC, newFC lite.FullCommit) error {
|
||||
// Shouldn't have trusted commits before the new commit height.
|
||||
if trustedFC.Height() >= newFC.Height() {
|
||||
panic("should not happen")
|
||||
}
|
||||
|
||||
// Check that the latest commit isn't beyond the vp.trustPeriod.
|
||||
if vp.now().Sub(trustedFC.SignedHeader.Time) > vp.trustPeriod {
|
||||
return lerr.ErrCommitExpired()
|
||||
}
|
||||
|
||||
// Validate the new commit in terms of validator set of last trusted commit.
|
||||
if err := trustedFC.NextValidators.VerifyCommit(vp.chainID, newFC.SignedHeader.Commit.BlockID, newFC.SignedHeader.Height, newFC.SignedHeader.Commit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Locally validate the full commit before we can trust it.
|
||||
if newFC.Height() >= trustedFC.Height()+1 {
|
||||
err := newFC.ValidateFull(vp.chainID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
change := compareVotingPowers(trustedFC, newFC)
|
||||
if change > float64(1/3) {
|
||||
return lerr.ErrValidatorChange(change)
|
||||
}
|
||||
|
||||
return vp.trusted.SaveFullCommit(newFC)
|
||||
}
|
||||
|
||||
func compareVotingPowers(trustedFC, newFC lite.FullCommit) float64 {
|
||||
var diffAccumulator float64
|
||||
|
||||
for _, val := range newFC.Validators.Validators {
|
||||
newPowerRatio := float64(val.VotingPower) / float64(newFC.Validators.TotalVotingPower())
|
||||
_, tval := trustedFC.NextValidators.GetByAddress(val.Address)
|
||||
oldPowerRatio := float64(tval.VotingPower) / float64(trustedFC.NextValidators.TotalVotingPower())
|
||||
diffAccumulator += math.Abs(newPowerRatio - oldPowerRatio)
|
||||
}
|
||||
|
||||
return diffAccumulator
|
||||
}
|
||||
|
||||
func (vp *Provider) fetchAndVerifyToHeightLinear(h int64) (lite.FullCommit, error) {
|
||||
// Fetch latest full commit from source.
|
||||
sourceFC, err := vp.source.LatestFullCommit(vp.chainID, h, h)
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
|
||||
// If sourceFC.Height() != h, we can't do it.
|
||||
if sourceFC.Height() != h {
|
||||
return lite.FullCommit{}, lerr.ErrCommitNotFound()
|
||||
}
|
||||
|
||||
// Validate the full commit. This checks the cryptographic
|
||||
// signatures of Commit against Validators.
|
||||
if err := sourceFC.ValidateFull(vp.chainID); err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
|
||||
if h == sourceFC.Height()+1 {
|
||||
trustedFC, err := vp.trusted.LatestFullCommit(vp.chainID, 1, h)
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
|
||||
err = vp.verifyAndSave(trustedFC, sourceFC)
|
||||
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
return sourceFC, nil
|
||||
}
|
||||
|
||||
// Verify latest FullCommit against trusted FullCommits
|
||||
// Use a loop rather than recursion to avoid stack overflows.
|
||||
for {
|
||||
// Fetch latest full commit from trusted.
|
||||
trustedFC, err := vp.trusted.LatestFullCommit(vp.chainID, 1, h)
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
|
||||
// We have nothing to do.
|
||||
if trustedFC.Height() == h {
|
||||
return trustedFC, nil
|
||||
}
|
||||
sourceFC, err = vp.source.LatestFullCommit(vp.chainID, trustedFC.Height()+1, trustedFC.Height()+1)
|
||||
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
err = vp.verifyAndSave(trustedFC, sourceFC)
|
||||
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetchAndVerifyToHeightBiscecting will use divide-and-conquer to find a path to h.
|
||||
// Returns nil error iff we successfully verify for height h, using repeated
|
||||
// applications of bisection if necessary.
|
||||
// Along the way, if a recent trust is used to verify a more recent header, the
|
||||
// more recent header becomes trusted.
|
||||
//
|
||||
// Returns ErrCommitNotFound if source Provider doesn't have the commit for h.
|
||||
func (vp *Provider) fetchAndVerifyToHeightBisecting(h int64) (lite.FullCommit, error) {
|
||||
// Fetch latest full commit from source.
|
||||
sourceFC, err := vp.source.LatestFullCommit(vp.chainID, h, h)
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
|
||||
// If sourceFC.Height() != h, we can't do it.
|
||||
if sourceFC.Height() != h {
|
||||
return lite.FullCommit{}, lerr.ErrCommitNotFound()
|
||||
}
|
||||
|
||||
// Validate the full commit. This checks the cryptographic
|
||||
// signatures of Commit against Validators.
|
||||
if err := sourceFC.ValidateFull(vp.chainID); err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
|
||||
// Verify latest FullCommit against trusted FullCommits
|
||||
// Use a loop rather than recursion to avoid stack overflows.
|
||||
for {
|
||||
// Fetch latest full commit from trusted.
|
||||
trustedFC, err := vp.trusted.LatestFullCommit(vp.chainID, 1, h)
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
|
||||
// We have nothing to do.
|
||||
if trustedFC.Height() == h {
|
||||
return trustedFC, nil
|
||||
}
|
||||
|
||||
// Update to full commit with checks.
|
||||
err = vp.verifyAndSave(trustedFC, sourceFC)
|
||||
|
||||
// Handle special case when err is ErrTooMuchChange.
|
||||
if types.IsErrTooMuchChange(err) {
|
||||
// Divide and conquer.
|
||||
start, end := trustedFC.Height(), sourceFC.Height()
|
||||
if !(start < end) {
|
||||
panic("should not happen")
|
||||
}
|
||||
mid := (start + end) / 2
|
||||
|
||||
// Recursive call back into fetchAndVerifyToHeight. Once you get to an inner
|
||||
// call that succeeeds, the outer calls will succeed.
|
||||
_, err = vp.fetchAndVerifyToHeightBisecting(mid)
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
// If we made it to mid, we retry.
|
||||
continue
|
||||
} else if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
|
||||
// All good!
|
||||
return sourceFC, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (vp *Provider) LastTrustedHeight() int64 {
|
||||
fc, err := vp.trusted.LatestFullCommit(vp.chainID, 1, 1<<63-1)
|
||||
if err != nil {
|
||||
panic("should not happen")
|
||||
}
|
||||
return fc.Height()
|
||||
}
|
||||
|
||||
func (vp *Provider) LatestFullCommit(chainID string, minHeight, maxHeight int64) (lite.FullCommit, error) {
|
||||
return vp.trusted.LatestFullCommit(chainID, minHeight, maxHeight)
|
||||
}
|
||||
|
||||
func (vp *Provider) ValidatorSet(chainID string, height int64) (*types.ValidatorSet, error) {
|
||||
// XXX try to sync?
|
||||
return vp.trusted.ValidatorSet(chainID, height)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package lite
|
||||
package verifying
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -9,18 +9,20 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
log "github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/lite"
|
||||
pks "github.com/tendermint/tendermint/lite/internal/privkeys"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
func TestInquirerValidPath(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
trust := NewDBProvider("trust", dbm.NewMemDB())
|
||||
source := NewDBProvider("source", dbm.NewMemDB())
|
||||
func TestProviderValidPath(t *testing.T) {
|
||||
require := require.New(t)
|
||||
trust := lite.NewDBProvider("trust", dbm.NewMemDB())
|
||||
source := lite.NewDBProvider("source", dbm.NewMemDB())
|
||||
|
||||
// Set up the validators to generate test blocks.
|
||||
var vote int64 = 10
|
||||
keys := genPrivKeys(5)
|
||||
keys := pks.GenPrivKeys(5)
|
||||
nkeys := keys.Extend(1)
|
||||
|
||||
// Construct a bunch of commits, each with one more height than the last.
|
||||
@@ -28,16 +30,17 @@ func TestInquirerValidPath(t *testing.T) {
|
||||
consHash := []byte("params")
|
||||
resHash := []byte("results")
|
||||
count := 50
|
||||
fcz := make([]FullCommit, count)
|
||||
fcz := make([]lite.FullCommit, count)
|
||||
for i := 0; i < count; i++ {
|
||||
vals := keys.ToValidators(vote, 0)
|
||||
nextVals := nkeys.ToValidators(vote, 0)
|
||||
h := int64(1 + i)
|
||||
appHash := []byte(fmt.Sprintf("h=%d", h))
|
||||
fcz[i] = keys.GenFullCommit(
|
||||
signedHeader := keys.GenSignedHeader(
|
||||
chainID, h, nil,
|
||||
vals, nextVals,
|
||||
appHash, consHash, resHash, 0, len(keys))
|
||||
fcz[i] = lite.NewFullCommit(signedHeader, vals, nextVals)
|
||||
// Extend the keys by 1 each time.
|
||||
keys = nkeys
|
||||
nkeys = nkeys.Extend(1)
|
||||
@@ -45,49 +48,55 @@ func TestInquirerValidPath(t *testing.T) {
|
||||
|
||||
// Initialize a Verifier with the initial state.
|
||||
err := trust.SaveFullCommit(fcz[0])
|
||||
require.Nil(err)
|
||||
cert := NewDynamicVerifier(chainID, trust, source)
|
||||
cert.SetLogger(log.TestingLogger())
|
||||
require.NoError(err)
|
||||
vp, _ := NewProvider(chainID, trust, source)
|
||||
vp.SetLogger(log.TestingLogger())
|
||||
|
||||
// This should fail validation:
|
||||
sh := fcz[count-1].SignedHeader
|
||||
err = cert.Verify(sh)
|
||||
require.NotNil(err)
|
||||
// The latest commit is the first one.
|
||||
fc, err := vp.LatestFullCommit(chainID, 0, fcz[count-1].SignedHeader.Height)
|
||||
require.NoError(err)
|
||||
require.NoError(fc.ValidateFull(chainID))
|
||||
require.Equal(fcz[0].SignedHeader, fc.SignedHeader)
|
||||
|
||||
// Adding a few commits in the middle should be insufficient.
|
||||
// The latest commit is still the first one.
|
||||
for i := 10; i < 13; i++ {
|
||||
err := source.SaveFullCommit(fcz[i])
|
||||
require.Nil(err)
|
||||
require.NoError(err)
|
||||
}
|
||||
err = cert.Verify(sh)
|
||||
assert.NotNil(err)
|
||||
fc, err = vp.LatestFullCommit(chainID, 0, fcz[count-1].SignedHeader.Height)
|
||||
require.NoError(err)
|
||||
require.NoError(fc.ValidateFull(chainID))
|
||||
require.Equal(fcz[0].SignedHeader, fc.SignedHeader)
|
||||
|
||||
// With more info, we succeed.
|
||||
for i := 0; i < count; i++ {
|
||||
err := source.SaveFullCommit(fcz[i])
|
||||
require.Nil(err)
|
||||
require.NoError(err)
|
||||
}
|
||||
err = cert.Verify(sh)
|
||||
assert.Nil(err, "%+v", err)
|
||||
fc, err = vp.LatestFullCommit(chainID, 0, fcz[count-1].SignedHeader.Height)
|
||||
require.NoError(err)
|
||||
require.NoError(fc.ValidateFull(chainID))
|
||||
require.Equal(fcz[count-1].SignedHeader, fc.SignedHeader)
|
||||
}
|
||||
|
||||
func TestDynamicVerify(t *testing.T) {
|
||||
trust := NewDBProvider("trust", dbm.NewMemDB())
|
||||
source := NewDBProvider("source", dbm.NewMemDB())
|
||||
func TestProviderDynamicVerification(t *testing.T) {
|
||||
trust := lite.NewDBProvider("trust", dbm.NewMemDB())
|
||||
source := lite.NewDBProvider("source", dbm.NewMemDB())
|
||||
|
||||
// 10 commits with one valset, 1 to change,
|
||||
// 10 commits with the next one
|
||||
n1, n2 := 10, 10
|
||||
nCommits := n1 + n2 + 1
|
||||
maxHeight := int64(nCommits)
|
||||
fcz := make([]FullCommit, nCommits)
|
||||
fcz := make([]lite.FullCommit, nCommits)
|
||||
|
||||
// gen the 2 val sets
|
||||
chainID := "dynamic-verifier"
|
||||
power := int64(10)
|
||||
keys1 := genPrivKeys(5)
|
||||
keys1 := pks.GenPrivKeys(5)
|
||||
vals1 := keys1.ToValidators(power, 0)
|
||||
keys2 := genPrivKeys(5)
|
||||
keys2 := pks.GenPrivKeys(5)
|
||||
vals2 := keys2.ToValidators(power, 0)
|
||||
|
||||
// make some commits with the first
|
||||
@@ -110,56 +119,55 @@ func TestDynamicVerify(t *testing.T) {
|
||||
|
||||
// Initialize a Verifier with the initial state.
|
||||
err := trust.SaveFullCommit(fcz[0])
|
||||
require.Nil(t, err)
|
||||
ver := NewDynamicVerifier(chainID, trust, source)
|
||||
ver.SetLogger(log.TestingLogger())
|
||||
require.NoError(t, err)
|
||||
vp, _ := NewProvider(chainID, trust, source)
|
||||
vp.SetLogger(log.TestingLogger())
|
||||
|
||||
// fetch the latest from the source
|
||||
latestFC, err := source.LatestFullCommit(chainID, 1, maxHeight)
|
||||
require.NoError(t, err)
|
||||
|
||||
// try to update to the latest
|
||||
err = ver.Verify(latestFC.SignedHeader)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(latestFC.ValidateFull(chainID))
|
||||
require.Equal(fcz[nCommits-1].SignedHeader, latestFC.SignedHeader)
|
||||
}
|
||||
|
||||
func makeFullCommit(height int64, keys privKeys, vals, nextVals *types.ValidatorSet, chainID string) FullCommit {
|
||||
height += 1
|
||||
func makeFullCommit(height int64, keys lite.PrivKeys, vals, nextVals *types.ValidatorSet, chainID string) lite.FullCommit {
|
||||
height++
|
||||
consHash := []byte("special-params")
|
||||
appHash := []byte(fmt.Sprintf("h=%d", height))
|
||||
resHash := []byte(fmt.Sprintf("res=%d", height))
|
||||
return keys.GenFullCommit(
|
||||
signedHeader := keys.GenSignedHeader(
|
||||
chainID, height, nil,
|
||||
vals, nextVals,
|
||||
appHash, consHash, resHash, 0, len(keys))
|
||||
return lite.NewFullCommit(signedHeader, vals, nextVals)
|
||||
}
|
||||
|
||||
func TestInquirerVerifyHistorical(t *testing.T) {
|
||||
func TestVerifingProviderHistorical(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
trust := NewDBProvider("trust", dbm.NewMemDB())
|
||||
source := NewDBProvider("source", dbm.NewMemDB())
|
||||
trust := lite.NewDBProvider("trust", dbm.NewMemDB())
|
||||
source := lite.NewDBProvider("source", dbm.NewMemDB())
|
||||
|
||||
// Set up the validators to generate test blocks.
|
||||
var vote int64 = 10
|
||||
keys := genPrivKeys(5)
|
||||
keys := pks.GenPrivKeys(5)
|
||||
nkeys := keys.Extend(1)
|
||||
|
||||
// Construct a bunch of commits, each with one more height than the last.
|
||||
chainID := "inquiry-test"
|
||||
count := 10
|
||||
consHash := []byte("special-params")
|
||||
fcz := make([]FullCommit, count)
|
||||
fcz := make([]lite.FullCommit, count)
|
||||
for i := 0; i < count; i++ {
|
||||
vals := keys.ToValidators(vote, 0)
|
||||
nextVals := nkeys.ToValidators(vote, 0)
|
||||
h := int64(1 + i)
|
||||
appHash := []byte(fmt.Sprintf("h=%d", h))
|
||||
resHash := []byte(fmt.Sprintf("res=%d", h))
|
||||
fcz[i] = keys.GenFullCommit(
|
||||
signedHeader := keys.GenSignedHeader(
|
||||
chainID, h, nil,
|
||||
vals, nextVals,
|
||||
appHash, consHash, resHash, 0, len(keys))
|
||||
fcz[i] = lite.NewFullCommit(signedHeader, vals, nextVals)
|
||||
// Extend the keys by 1 each time.
|
||||
keys = nkeys
|
||||
nkeys = nkeys.Extend(1)
|
||||
@@ -167,9 +175,9 @@ func TestInquirerVerifyHistorical(t *testing.T) {
|
||||
|
||||
// Initialize a Verifier with the initial state.
|
||||
err := trust.SaveFullCommit(fcz[0])
|
||||
require.Nil(err)
|
||||
cert := NewDynamicVerifier(chainID, trust, source)
|
||||
cert.SetLogger(log.TestingLogger())
|
||||
require.NoError(err)
|
||||
vp, _ := NewProvider(chainID, trust, source)
|
||||
vp.SetLogger(log.TestingLogger())
|
||||
|
||||
// Store a few full commits as trust.
|
||||
for _, i := range []int{2, 5} {
|
||||
@@ -177,72 +185,71 @@ func TestInquirerVerifyHistorical(t *testing.T) {
|
||||
}
|
||||
|
||||
// See if we can jump forward using trusted full commits.
|
||||
// Souce doesn't have fcz[9] so cert.LastTrustedHeight wont' change.
|
||||
// Souce doesn't have fcz[9] so vp.LastTrustedHeight wont' change.
|
||||
err = source.SaveFullCommit(fcz[7])
|
||||
require.Nil(err, "%+v", err)
|
||||
sh := fcz[8].SignedHeader
|
||||
err = cert.Verify(sh)
|
||||
require.Nil(err, "%+v", err)
|
||||
assert.Equal(fcz[7].Height(), cert.LastTrustedHeight())
|
||||
require.NoError(err, "%+v", err)
|
||||
assert.Equal(fcz[7].Height(), vp.LastTrustedHeight())
|
||||
fc_, err := trust.LatestFullCommit(chainID, fcz[8].Height(), fcz[8].Height())
|
||||
require.NotNil(err, "%+v", err)
|
||||
assert.Equal(fc_, (FullCommit{}))
|
||||
require.Error(err, "%+v", err)
|
||||
assert.Equal((lite.FullCommit{}), fc_)
|
||||
|
||||
// With fcz[9] Verify will update last trusted height.
|
||||
err = source.SaveFullCommit(fcz[9])
|
||||
require.Nil(err, "%+v", err)
|
||||
sh = fcz[8].SignedHeader
|
||||
err = cert.Verify(sh)
|
||||
require.Nil(err, "%+v", err)
|
||||
assert.Equal(fcz[8].Height(), cert.LastTrustedHeight())
|
||||
require.NoError(err, "%+v", err)
|
||||
assert.Equal(fcz[8].Height(), vp.LastTrustedHeight())
|
||||
fc_, err = trust.LatestFullCommit(chainID, fcz[8].Height(), fcz[8].Height())
|
||||
require.Nil(err, "%+v", err)
|
||||
assert.Equal(fc_.Height(), fcz[8].Height())
|
||||
require.NoError(err, "%+v", err)
|
||||
assert.Equal(fcz[8].Height(), fc_.Height())
|
||||
|
||||
// Add access to all full commits via untrusted source.
|
||||
for i := 0; i < count; i++ {
|
||||
err := source.SaveFullCommit(fcz[i])
|
||||
require.Nil(err)
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
// Try to check an unknown seed in the past.
|
||||
sh = fcz[3].SignedHeader
|
||||
err = cert.Verify(sh)
|
||||
require.Nil(err, "%+v", err)
|
||||
assert.Equal(fcz[8].Height(), cert.LastTrustedHeight())
|
||||
// Try to fetch an unknown commit from the past.
|
||||
fc_, err = trust.LatestFullCommit(chainID, fcz[2].Height(), fcz[3].Height())
|
||||
require.NoError(err, "%+v", err)
|
||||
assert.Equal(fcz[2].Height(), fc_.Height())
|
||||
assert.Equal(fcz[8].Height(), vp.LastTrustedHeight())
|
||||
// TODO This should work for as long as the trust period hasn't passed for
|
||||
// fcz[2]. Write a test that tries to retroactively fetchees fcz[3] from
|
||||
// source. Initially it should fail since source doesn't have it, but it
|
||||
// should succeed once source is provided it.
|
||||
|
||||
// Jump all the way forward again.
|
||||
sh = fcz[count-1].SignedHeader
|
||||
err = cert.Verify(sh)
|
||||
require.Nil(err, "%+v", err)
|
||||
assert.Equal(fcz[9].Height(), cert.LastTrustedHeight())
|
||||
// Try to fetch the latest known commit.
|
||||
fc_, err = trust.LatestFullCommit(chainID, 0, fcz[9].Height())
|
||||
require.NoError(err, "%+v", err)
|
||||
assert.Equal(fcz[9].Height(), fc_.Height())
|
||||
assert.Equal(fcz[9].Height(), vp.LastTrustedHeight())
|
||||
}
|
||||
|
||||
func TestConcurrencyInquirerVerify(t *testing.T) {
|
||||
func TestConcurrentProvider(t *testing.T) {
|
||||
_, require := assert.New(t), require.New(t)
|
||||
trust := NewDBProvider("trust", dbm.NewMemDB()).SetLimit(10)
|
||||
source := NewDBProvider("source", dbm.NewMemDB())
|
||||
trust := lite.NewDBProvider("trust", dbm.NewMemDB()).SetLimit(10)
|
||||
source := lite.NewDBProvider("source", dbm.NewMemDB())
|
||||
|
||||
// Set up the validators to generate test blocks.
|
||||
var vote int64 = 10
|
||||
keys := genPrivKeys(5)
|
||||
keys := pks.GenPrivKeys(5)
|
||||
nkeys := keys.Extend(1)
|
||||
|
||||
// Construct a bunch of commits, each with one more height than the last.
|
||||
chainID := "inquiry-test"
|
||||
count := 10
|
||||
consHash := []byte("special-params")
|
||||
fcz := make([]FullCommit, count)
|
||||
fcz := make([]lite.FullCommit, count)
|
||||
for i := 0; i < count; i++ {
|
||||
vals := keys.ToValidators(vote, 0)
|
||||
nextVals := nkeys.ToValidators(vote, 0)
|
||||
h := int64(1 + i)
|
||||
appHash := []byte(fmt.Sprintf("h=%d", h))
|
||||
resHash := []byte(fmt.Sprintf("res=%d", h))
|
||||
fcz[i] = keys.GenFullCommit(
|
||||
signedHeader := keys.GenSignedHeader(
|
||||
chainID, h, nil,
|
||||
vals, nextVals,
|
||||
appHash, consHash, resHash, 0, len(keys))
|
||||
fcz[i] = lite.NewFullCommit(signedHeader, vals, nextVals)
|
||||
// Extend the keys by 1 each time.
|
||||
keys = nkeys
|
||||
nkeys = nkeys.Extend(1)
|
||||
@@ -250,15 +257,16 @@ func TestConcurrencyInquirerVerify(t *testing.T) {
|
||||
|
||||
// Initialize a Verifier with the initial state.
|
||||
err := trust.SaveFullCommit(fcz[0])
|
||||
require.Nil(err)
|
||||
cert := NewDynamicVerifier(chainID, trust, source)
|
||||
cert.SetLogger(log.TestingLogger())
|
||||
require.NoError(err)
|
||||
vp, _ := NewProvider(chainID, trust, source)
|
||||
vp.SetLogger(log.TestingLogger())
|
||||
cp := lite.NewConcurrentProvider(vp)
|
||||
|
||||
err = source.SaveFullCommit(fcz[7])
|
||||
require.Nil(err, "%+v", err)
|
||||
err = source.SaveFullCommit(fcz[8])
|
||||
require.Nil(err, "%+v", err)
|
||||
sh := fcz[8].SignedHeader
|
||||
require.NoError(err, "%+v", err)
|
||||
// sh := fcz[8].SignedHeader unused
|
||||
|
||||
var wg sync.WaitGroup
|
||||
count = 100
|
||||
@@ -266,12 +274,12 @@ func TestConcurrencyInquirerVerify(t *testing.T) {
|
||||
for i := 0; i < count; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
errList[index] = cert.Verify(sh)
|
||||
errList[index] = cp.UpdateToHeight(chainID, fcz[8].SignedHeader.Height)
|
||||
defer wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for _, err := range errList {
|
||||
require.Nil(err)
|
||||
require.NoError(err)
|
||||
}
|
||||
}
|
@@ -384,6 +384,18 @@ func (h *Header) Populate(
|
||||
h.ProposerAddress = proposerAddress
|
||||
}
|
||||
|
||||
// NOTE: While it's possible to make this faster via a custom implementation,
|
||||
// (or naively via a struct copy, though this isn't yet a frozen design goal),
|
||||
// for now use hashes in case of any issues that may arise in implementation.
|
||||
func (h *Header) Equal(h2 *Header) bool {
|
||||
h1Hash := h.Hash()
|
||||
if h1Hash == nil {
|
||||
panic("incomplete heaeders cannot be compared")
|
||||
}
|
||||
h2Hash := h2.Hash()
|
||||
return bytes.Equal(h1Hash, h2Hash)
|
||||
}
|
||||
|
||||
// Hash returns the hash of the header.
|
||||
// It computes a Merkle tree from the header fields
|
||||
// ordered as they appear in the Header.
|
||||
@@ -668,6 +680,18 @@ func (commit *Commit) ValidateBasic() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NOTE: While it's possible to make this faster via a custom implementation,
|
||||
// (naively via a struct copy won't work due to the volatile fields),
|
||||
// for now use hashes in case of any issues that may arise in implementation.
|
||||
func (commit *Commit) Equal(commit2 *Commit) bool {
|
||||
c1Hash := commit.Hash()
|
||||
if c1Hash == nil {
|
||||
panic("incomplete commit cannot be compared")
|
||||
}
|
||||
c2Hash := commit2.Hash()
|
||||
return bytes.Equal(c1Hash, c2Hash)
|
||||
}
|
||||
|
||||
// Hash returns the hash of the commit
|
||||
func (commit *Commit) Hash() cmn.HexBytes {
|
||||
if commit == nil {
|
||||
@@ -712,6 +736,15 @@ type SignedHeader struct {
|
||||
Commit *Commit `json:"commit"`
|
||||
}
|
||||
|
||||
// Returns true iff both the header and commit hold identical information
|
||||
// (disregarding any volatile memoized fields).
|
||||
// Header and Commit must be their final immutable forms, otherwise this
|
||||
// function will panic.
|
||||
func (sh SignedHeader) Equal(sh2 SignedHeader) bool {
|
||||
return sh.Header.Equal(sh2.Header) &&
|
||||
sh.Commit.Equal(sh2.Commit)
|
||||
}
|
||||
|
||||
// ValidateBasic does basic consistency checks and makes sure the header
|
||||
// and commit are consistent.
|
||||
//
|
||||
|
@@ -596,9 +596,13 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i
|
||||
if vals.Size() != len(commit.Precommits) {
|
||||
return NewErrInvalidCommitPrecommits(vals.Size(), len(commit.Precommits))
|
||||
}
|
||||
|
||||
// If the height to check is different than the commit height return an error
|
||||
if height != commit.Height() {
|
||||
return NewErrInvalidCommitHeight(height, commit.Height())
|
||||
}
|
||||
|
||||
// If the blockHash is not equal to the commit block hash return an error
|
||||
if !blockID.Equals(commit.BlockID) {
|
||||
return fmt.Errorf("Invalid commit -- wrong block id: want %v got %v",
|
||||
blockID, commit.BlockID)
|
||||
@@ -607,8 +611,9 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i
|
||||
talliedVotingPower := int64(0)
|
||||
|
||||
for idx, precommit := range commit.Precommits {
|
||||
// Some precommits will likely be missing, skip those
|
||||
if precommit == nil {
|
||||
continue // OK, some precommits can be missing.
|
||||
continue
|
||||
}
|
||||
_, val := vals.GetByIndex(idx)
|
||||
// Validate signature.
|
||||
@@ -616,6 +621,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i
|
||||
if !val.PubKey.VerifyBytes(precommitSignBytes, precommit.Signature) {
|
||||
return fmt.Errorf("Invalid commit -- invalid signature: %v", precommit)
|
||||
}
|
||||
|
||||
// Good precommit!
|
||||
if blockID.Equals(precommit.BlockID) {
|
||||
talliedVotingPower += val.VotingPower
|
||||
@@ -629,97 +635,13 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i
|
||||
if talliedVotingPower > vals.TotalVotingPower()*2/3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errTooMuchChange{talliedVotingPower, vals.TotalVotingPower()*2/3 + 1}
|
||||
}
|
||||
|
||||
// VerifyFutureCommit will check to see if the set would be valid with a different
|
||||
// validator set.
|
||||
//
|
||||
// vals is the old validator set that we know. Over 2/3 of the power in old
|
||||
// signed this block.
|
||||
//
|
||||
// In Tendermint, 1/3 of the voting power can halt or fork the chain, but 1/3
|
||||
// can't make arbitrary state transitions. You still need > 2/3 Byzantine to
|
||||
// make arbitrary state transitions.
|
||||
//
|
||||
// To preserve this property in the light client, we also require > 2/3 of the
|
||||
// old vals to sign the future commit at H, that way we preserve the property
|
||||
// that if they weren't being truthful about the validator set at H (block hash
|
||||
// -> vals hash) or about the app state (block hash -> app hash) we can slash
|
||||
// > 2/3. Otherwise, the lite client isn't providing the same security
|
||||
// guarantees.
|
||||
//
|
||||
// Even if we added a slashing condition that if you sign a block header with
|
||||
// the wrong validator set, then we would only need > 1/3 of signatures from
|
||||
// the old vals on the new commit, it wouldn't be sufficient because the new
|
||||
// vals can be arbitrary and commit some arbitrary app hash.
|
||||
//
|
||||
// newSet is the validator set that signed this block. Only votes from new are
|
||||
// sufficient for 2/3 majority in the new set as well, for it to be a valid
|
||||
// commit.
|
||||
//
|
||||
// NOTE: This doesn't check whether the commit is a future commit, because the
|
||||
// current height isn't part of the ValidatorSet. Caller must check that the
|
||||
// commit height is greater than the height for this validator set.
|
||||
func (vals *ValidatorSet) VerifyFutureCommit(newSet *ValidatorSet, chainID string,
|
||||
blockID BlockID, height int64, commit *Commit) error {
|
||||
oldVals := vals
|
||||
|
||||
// Commit must be a valid commit for newSet.
|
||||
err := newSet.VerifyCommit(chainID, blockID, height, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check old voting power.
|
||||
oldVotingPower := int64(0)
|
||||
seen := map[int]bool{}
|
||||
round := commit.Round()
|
||||
|
||||
for idx, precommit := range commit.Precommits {
|
||||
if precommit == nil {
|
||||
continue
|
||||
}
|
||||
if precommit.Height != height {
|
||||
return errors.Errorf("Blocks don't match - %d vs %d", round, precommit.Round)
|
||||
}
|
||||
if precommit.Round != round {
|
||||
return errors.Errorf("Invalid commit -- wrong round: %v vs %v", round, precommit.Round)
|
||||
}
|
||||
if precommit.Type != PrecommitType {
|
||||
return errors.Errorf("Invalid commit -- not precommit @ index %v", idx)
|
||||
}
|
||||
// See if this validator is in oldVals.
|
||||
oldIdx, val := oldVals.GetByAddress(precommit.ValidatorAddress)
|
||||
if val == nil || seen[oldIdx] {
|
||||
continue // missing or double vote...
|
||||
}
|
||||
seen[oldIdx] = true
|
||||
|
||||
// Validate signature.
|
||||
precommitSignBytes := commit.VoteSignBytes(chainID, idx)
|
||||
if !val.PubKey.VerifyBytes(precommitSignBytes, precommit.Signature) {
|
||||
return errors.Errorf("Invalid commit -- invalid signature: %v", precommit)
|
||||
}
|
||||
// Good precommit!
|
||||
if blockID.Equals(precommit.BlockID) {
|
||||
oldVotingPower += val.VotingPower
|
||||
}
|
||||
// else {
|
||||
// It's OK that the BlockID doesn't match. We include stray
|
||||
// precommits to measure validator availability.
|
||||
// }
|
||||
}
|
||||
|
||||
if oldVotingPower <= oldVals.TotalVotingPower()*2/3 {
|
||||
return errTooMuchChange{oldVotingPower, oldVals.TotalVotingPower()*2/3 + 1}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//-----------------
|
||||
// ErrTooMuchChange
|
||||
|
||||
// IsErrTooMuchChange
|
||||
func IsErrTooMuchChange(err error) bool {
|
||||
_, ok := errors.Cause(err).(errTooMuchChange)
|
||||
return ok
|
||||
|
Reference in New Issue
Block a user