|
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 ErrNotEnoughVotingPowerSigned 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 ErrNotEnoughVotingPowerSigned.
|
|
if types.IsErrNotEnoughVotingPowerSigned(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()
|
|
}
|