mirror of
https://github.com/tendermint/tendermint.git
synced 2026-06-07 23:02:36 +00:00
WIP
This commit is contained in:
@@ -14,7 +14,11 @@ var _ Verifier = (*BaseVerifier)(nil)
|
||||
// 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.
|
||||
//
|
||||
// NOTE: Verifier as a supported interface is deprecated, it may be reasonable
|
||||
// to rename this to simply "verifier" and to remove that intereface
|
||||
// declaration. See also VerifyingProvider, which is not a Verifier, but is a
|
||||
// Provider.
|
||||
type BaseVerifier struct {
|
||||
chainID string
|
||||
height int64
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestBaseCert(t *testing.T) {
|
||||
func TestBaseVerifier(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
keys := genPrivKeys(4)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -41,6 +41,12 @@ func (e errEmptyTree) Error() string {
|
||||
return "Tree is empty"
|
||||
}
|
||||
|
||||
type errCommitExpired struct{}
|
||||
|
||||
func (e errCommitExpired) Error() string {
|
||||
return "Commit is too old to be trusted"
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
// Methods for above error types
|
||||
|
||||
@@ -109,3 +115,18 @@ func IsErrEmptyTree(err error) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//-----------------
|
||||
// ErrCommitExpired
|
||||
|
||||
func ErrCommitExpired() error {
|
||||
return cmn.ErrorWrap(errCommitExpired{}, "")
|
||||
}
|
||||
|
||||
func IsErrCommitExpired(err error) bool {
|
||||
if err_, ok := err.(cmn.Error); ok {
|
||||
_, ok := err_.Data().(errCommitExpired)
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
105
lite/provider.go
105
lite/provider.go
@@ -1,6 +1,9 @@
|
||||
package lite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
@@ -30,3 +33,105 @@ type PersistentProvider interface {
|
||||
// SaveFullCommit saves a FullCommit (without verification).
|
||||
SaveFullCommit(fc FullCommit) error
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
func NewConcurrentUpdatingProvider(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.
|
||||
// The callback must be called, otherwise there will be memory leaks.
|
||||
// Other subsequent calls should just return uniq.err.
|
||||
// NOTE: This is a separate function, primarily to make mtx unlocking more obviously safe via defer.
|
||||
func (cp concurrentProvider) joinConcurrency(chainID string, height int64) (uniq *pendingResult, isFirstCall bool, callback func(error)) {
|
||||
cp.mtx.Lock()
|
||||
defer cp.mtx.Unlock()
|
||||
|
||||
var pkey = pendingKey{chainID, height}
|
||||
cp.mtx.Lock()
|
||||
if uniq = cp.pendingVerifications[pkey]; uniq != nil {
|
||||
cp.mtx.Unlock()
|
||||
<-uniq.wait // uniq.wait is of type `chan struct{}`
|
||||
return uniq, false, nil
|
||||
} else {
|
||||
uniq = &pendingResult{wait: make(chan struct{}), err: nil}
|
||||
cp.pendingVerifications[pkey] = uniq
|
||||
cp.mtx.Unlock()
|
||||
// The caller must call this, otherwise there will be memory leaks.
|
||||
return uniq, true, func(err error) {
|
||||
// NOTE: other result parameters can be added here.
|
||||
uniq.err = err
|
||||
// *After* setting the results, *then* call close(uniq.wait).
|
||||
close(uniq.wait)
|
||||
cp.mtx.Lock() // temporarily acquire lock to remove this iteem
|
||||
delete(cp.pendingVerifications, pkey)
|
||||
cp.mtx.Unlock() // and release lock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cp concurrentProvider) UpdateToHeight(chainID string, height int64) error {
|
||||
|
||||
// Performs synchronization for multi-threads verification at the same height.
|
||||
var presult *pendingResult
|
||||
var isFirstCall bool
|
||||
var callback func(error)
|
||||
presult, 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
|
||||
} else {
|
||||
// Is not the first call, so return the error from previous concurrent calls.
|
||||
return presult.err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
dbm "github.com/tendermint/tendermint/libs/db"
|
||||
log "github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/lite"
|
||||
lclient "github.com/tendermint/tendermint/lite/client"
|
||||
)
|
||||
|
||||
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.LevelDBBackend, 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, cmn.ErrorWrap(err, "fetching source full commit @ height 1")
|
||||
}
|
||||
err = trust.SaveFullCommit(fc)
|
||||
if err != nil {
|
||||
return nil, cmn.ErrorWrap(err, "saving full commit to trusted")
|
||||
}
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
@@ -4,7 +4,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"
|
||||
)
|
||||
@@ -15,7 +15,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
|
||||
}
|
||||
|
||||
@@ -23,7 +23,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....
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// NOTE: The Verifier interface is deprecated. BaseVerifier can continue to
|
||||
// exist, but this interface isn't very useful on its own to declare here.
|
||||
//
|
||||
// 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.
|
||||
|
||||
394
lite/verifying/provider.go
Normal file
394
lite/verifying/provider.go
Normal file
@@ -0,0 +1,394 @@
|
||||
package verifying
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
dbm "github.com/tendermint/tendermint/libs/db"
|
||||
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"
|
||||
)
|
||||
|
||||
type TrustOptions struct {
|
||||
// Required: only trust commits up to this old.
|
||||
TrustPeriod time.Duration
|
||||
|
||||
// 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
|
||||
TrustHash []byte
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NOTE If you retain the resulting verifier in memory for a long time,
|
||||
// usage of the verifier may eventually error, but immediate usage should
|
||||
// not error like that, so that e.g. cli usage never errors unexpectedly.
|
||||
// TODO Move some of this initialization to a separate function.
|
||||
func NewProvider(chainID, rootDir string, client lclient.SignStatusClient, logger log.Logger, cacheSize int, options TrustOptions) (*provider, error) {
|
||||
|
||||
logger = logger.With("module", "lite/proxy")
|
||||
logger.Info("lite/proxy/NewProvider()...", "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.LevelDBBackend, rootDir))
|
||||
trust := lite.NewMultiProvider(
|
||||
memProvider,
|
||||
lvlProvider,
|
||||
)
|
||||
source := lclient.NewProvider(chainID, client)
|
||||
vp := makeProvider(chainID, options.TrustPeriod, trust, source)
|
||||
vp.SetLogger(logger)
|
||||
trustPeriod := options.TrustPeriod
|
||||
|
||||
// Get the latest trusted FC.
|
||||
tlfc, err := trust.LatestFullCommit(chainID, 1, 1<<63-1)
|
||||
if err != nil {
|
||||
// Get the latest source commit, or the one provided in options.
|
||||
targetCommit, err := getTargetCommit(client, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = vp.fillValidateAndSaveToTrust(targetCommit, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vp, nil
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if time.Now().Sub(tlfc.SignedHeader.Time) <= 0 {
|
||||
panic(fmt.Sprintf("impossible time %v vs %v", time.Now(), tlfc.SignedHeader.Time))
|
||||
}
|
||||
|
||||
if time.Now().Sub(tlfc.SignedHeader.Time) > trustPeriod {
|
||||
// Get the latest source commit, or the one provided in options.
|
||||
targetCommit, err := getTargetCommit(client, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = vp.fillValidateAndSaveToTrust(targetCommit, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vp, nil
|
||||
}
|
||||
|
||||
// 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 of a validator so it's not a big
|
||||
// deal.
|
||||
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
|
||||
}
|
||||
|
||||
// Returns the desired trusted sync point to fetch from the client.
|
||||
func getTargetCommit(client lclient.SignStatusClient, options TrustOptions) (types.SignedHeader, error) {
|
||||
if options.TrustHeight != 0 {
|
||||
resCommit, err := client.Commit(&options.TrustHeight)
|
||||
if err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
targetCommit := resCommit.SignedHeader
|
||||
if !bytes.Equal(targetCommit.Hash(), options.TrustHash) {
|
||||
return types.SignedHeader{}, fmt.Errorf("WARNING!!! Expected height/hash %v/%X but got %X",
|
||||
options.TrustHeight, options.TrustHash, targetCommit.Hash())
|
||||
}
|
||||
return targetCommit, nil
|
||||
} else {
|
||||
resCommit, err := client.Commit(nil)
|
||||
if err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
targetCommit := resCommit.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.
|
||||
fmt.Printf("trusting source at height %v and hash %X...\n",
|
||||
targetCommit.Height, targetCommit.Hash())
|
||||
if options.Callback != nil {
|
||||
err := options.Callback(targetCommit.Height, targetCommit.Hash())
|
||||
if err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
}
|
||||
return targetCommit, nil
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
|
||||
type nowFn func() time.Time
|
||||
|
||||
const sizeOfPendingMap = 1024
|
||||
|
||||
var _ lite.UpdatingProvider = (*provider)(nil)
|
||||
|
||||
// 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.
|
||||
// NOTE: This provider can only work with one chainID, provided upon
|
||||
// instantiation.
|
||||
type provider struct {
|
||||
chainID string
|
||||
logger log.Logger
|
||||
trustPeriod time.Duration // e.g. the unbonding period, or something smaller.
|
||||
now nowFn
|
||||
|
||||
// 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{}
|
||||
}
|
||||
|
||||
// makeProvider returns a new verifying provider. 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.
|
||||
// NOTE: The external facing constructor is called NewVerifyingProivider.
|
||||
func makeProvider(chainID string, trustPeriod time.Duration, trusted lite.PersistentProvider, source lite.Provider) *provider {
|
||||
if trustPeriod == 0 {
|
||||
panic("VerifyingProvider must have non-zero trust period")
|
||||
}
|
||||
return &provider{
|
||||
logger: log.NewNopLogger(),
|
||||
chainID: chainID,
|
||||
trustPeriod: trustPeriod,
|
||||
trusted: trusted,
|
||||
source: source,
|
||||
pendingVerifications: make(map[int64]chan struct{}, sizeOfPendingMap),
|
||||
}
|
||||
}
|
||||
|
||||
func (vp *provider) SetLogger(logger log.Logger) {
|
||||
logger = logger.With("module", "lite")
|
||||
vp.logger = logger
|
||||
vp.trusted.SetLogger(logger)
|
||||
vp.source.SetLogger(logger)
|
||||
}
|
||||
|
||||
func (vp *provider) ChainID() string {
|
||||
return vp.chainID
|
||||
}
|
||||
|
||||
// Implements UpdatingProvider
|
||||
//
|
||||
// On success, it will store the full commit (SignedHeader + Validators) in
|
||||
// vp.trusted.
|
||||
// NOTE: For concurreent usage, use concurrentProvider.
|
||||
func (vp *provider) UpdateToHeight(chainID string, height int64) error {
|
||||
|
||||
// If we alreeady have the commit, just return nil.
|
||||
_, err := vp.trusted.LatestFullCommit(vp.chainID, height, height)
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if !lerr.IsErrCommitNotFound(err) {
|
||||
// Return error if it is not CommitNotFound error
|
||||
vp.logger.Info(fmt.Sprintf("Encountered unknown error in loading full commit at height %d.", height))
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch trusted FC at exactly height, while updating trust when possible.
|
||||
_, err = vp.fetchAndVerifyToHeight(height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Good!
|
||||
return nil
|
||||
}
|
||||
|
||||
// If valset or nextValset are nil, fetches them.
|
||||
// Then, validatees the full commit, then savees it.
|
||||
func (vp *provider) fillValidateAndSaveToTrust(signedHeader types.SignedHeader, valset, nextValset *types.ValidatorSet) (err error) {
|
||||
|
||||
// Get the valset.
|
||||
if valset != nil {
|
||||
valset, err = vp.source.ValidatorSet(vp.chainID, signedHeader.Height)
|
||||
if err != nil {
|
||||
return cmn.ErrorWrap(err, "fetching the valset")
|
||||
}
|
||||
}
|
||||
|
||||
// Get the next validator set.
|
||||
if nextValset != nil {
|
||||
for {
|
||||
nextValset, err = vp.source.ValidatorSet(vp.chainID, signedHeader.Height+1)
|
||||
if lerr.IsErrUnknownValidators(err) {
|
||||
// try again until we get it.
|
||||
fmt.Printf("fetching validatorset for height %v...\n",
|
||||
signedHeader.Height+1)
|
||||
continue
|
||||
} else if err != nil {
|
||||
return cmn.ErrorWrap(err, "fetching the next valset")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 cmn.ErrorWrap(err, "verifying validators from source")
|
||||
}
|
||||
// Trust it.
|
||||
err = vp.trusted.SaveFullCommit(fc)
|
||||
if err != nil {
|
||||
return cmn.ErrorWrap(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 {
|
||||
if trustedFC.Height() >= newFC.Height() {
|
||||
panic("should not happen")
|
||||
}
|
||||
if vp.now().Sub(trustedFC.SignedHeader.Time) > vp.trustPeriod {
|
||||
return lerr.ErrCommitExpired()
|
||||
}
|
||||
if trustedFC.Height() == newFC.Height()-1 {
|
||||
err := trustedFC.NextValidators.VerifyCommit(
|
||||
vp.chainID, newFC.SignedHeader.Commit.BlockID,
|
||||
newFC.SignedHeader.Height, newFC.SignedHeader.Commit,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := trustedFC.NextValidators.VerifyFutureCommit(
|
||||
newFC.Validators,
|
||||
vp.chainID, newFC.SignedHeader.Commit.BlockID,
|
||||
newFC.SignedHeader.Height, newFC.SignedHeader.Commit,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if vp.now().Before(newFC.SignedHeader.Time) {
|
||||
// TODO print warning
|
||||
// TODO if too egregious, return error.
|
||||
// return FullCommit{}, errors.New("now should not be before source time")
|
||||
}
|
||||
|
||||
return vp.trusted.SaveFullCommit(newFC)
|
||||
}
|
||||
|
||||
// fetchAndVerifyToHeight 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) fetchAndVerifyToHeight(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_LOOP:
|
||||
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
|
||||
_, err = vp.fetchAndVerifyToHeight(mid)
|
||||
if err != nil {
|
||||
return lite.FullCommit{}, err
|
||||
}
|
||||
// If we made it to mid, we retry.
|
||||
continue FOR_LOOP
|
||||
} 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"
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestInquirerValidPath(t *testing.T) {
|
||||
func TestProviderValidPath(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
|
||||
@@ -45,35 +45,41 @@ 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.SignedHeeader)
|
||||
|
||||
// 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.SignedHeeader)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -110,18 +116,15 @@ 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 {
|
||||
@@ -135,10 +138,10 @@ func makeFullCommit(height int64, keys privKeys, vals, nextVals *types.Validator
|
||||
appHash, consHash, resHash, 0, len(keys))
|
||||
}
|
||||
|
||||
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
|
||||
@@ -167,9 +170,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,51 +180,49 @@ 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((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
|
||||
@@ -250,13 +251,14 @@ 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 := NewConcurrentProvider(vp)
|
||||
|
||||
err = source.SaveFullCommit(fcz[7])
|
||||
err = source.SaveFullCommit(fcz[8])
|
||||
require.Nil(err, "%+v", err)
|
||||
require.NoError(err, "%+v", err)
|
||||
sh := fcz[8].SignedHeader
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -265,12 +267,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)
|
||||
}
|
||||
}
|
||||
@@ -403,6 +403,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.Compare(h1Hash, h2Hash) == 0
|
||||
}
|
||||
|
||||
// Hash returns the hash of the header.
|
||||
// It computes a Merkle tree from the header fields
|
||||
// ordered as they appear in the Header.
|
||||
@@ -600,6 +612,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.Compare(c1Hash, c2Hash) == 0
|
||||
}
|
||||
|
||||
// Hash returns the hash of the commit
|
||||
func (commit *Commit) Hash() cmn.HexBytes {
|
||||
if commit == nil {
|
||||
@@ -644,6 +668,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.
|
||||
//
|
||||
|
||||
@@ -416,15 +416,16 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i
|
||||
return errTooMuchChange{talliedVotingPower, vals.TotalVotingPower()*2/3 + 1}
|
||||
}
|
||||
|
||||
// VerifyFutureCommit will check to see if the set would be valid with a different
|
||||
// validator set.
|
||||
// VerifyFutureCommit checks to see if a given future validator set has
|
||||
// committed a block, and whether those who signed of this future validator set
|
||||
// has sufficient overlap with this validator set.
|
||||
//
|
||||
// vals is the old validator set that we know. Over 2/3 of the power in old
|
||||
// signed this block.
|
||||
// vals is the current validator set that we know. Over 2/3 of the power in
|
||||
// this valset is expected to have 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.
|
||||
// Justification for the 2/3: 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
|
||||
@@ -433,18 +434,17 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i
|
||||
// > 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.
|
||||
// NOTE: This doesn't check whether the commit is actually 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.
|
||||
//
|
||||
// NOTE: This function is strictly more restrictive than merely checking
|
||||
// whether newSet.VerifyCommit(...), in fact it calls exactly that.
|
||||
func (vals *ValidatorSet) VerifyFutureCommit(newSet *ValidatorSet, chainID string,
|
||||
blockID BlockID, height int64, commit *Commit) error {
|
||||
oldVals := vals
|
||||
|
||||
Reference in New Issue
Block a user