mirror of
https://github.com/tendermint/tendermint.git
synced 2026-08-16 12:16:11 +00:00
* rename adjusted to adjacent Refs https://github.com/tendermint/tendermint/pull/3989#discussion_r352140829 * rename ErrTooMuchChange to ErrNotEnoughVotingPowerSigned Refs https://github.com/tendermint/tendermint/pull/3989#discussion_r352142785 * verify commit is properly signed * remove no longer trusted headers * restore trustedHeader and trustedNextVals * check trustedHeader using options Refs https://github.com/tendermint/tendermint/pull/4209#issuecomment-562462165 * use correct var when checking if headers are adjacent in bisection func + replace TODO with a comment https://github.com/tendermint/tendermint/pull/3989#discussion_r352125455 * return header in VerifyHeaderAtHeight because that way we avoid DB call + add godoc comments + check if there are no headers yet in AutoClient https://github.com/tendermint/tendermint/pull/3989#pullrequestreview-315454506 * TestVerifyAdjacentHeaders: add 2 more test-cases + add TestVerifyReturnsErrorIfTrustLevelIsInvalid * lite: avoid overflow when parsing key in db store! * lite: rename AutoClient#Err to Errs * lite: add a test for AutoClient * lite: fix keyPattern and call itr.Next in db store * lite: add two tests for db store * lite: add TestClientRemovesNoLongerTrustedHeaders * lite: test Client#Cleanup * lite: test restoring trustedHeader https://github.com/tendermint/tendermint/pull/4209#issuecomment-562462165 * lite: comment out unused code in test_helpers * fix TestVerifyReturnsErrorIfTrustLevelIsInvalid after merge * change defaultRemoveNoLongerTrustedHeadersPeriod and add docs * write more doc * lite: uncomment testable examples * use stdlog.Fatal to stop AutoClient tests * make lll linter happy * separate errors for 2 cases - the validator set of a skipped header cannot be trusted, i.e. <1/3rd of h1 validator set has signed (new error, something like ErrNewValSetCantBeTrusted) - the validator set is trusted but < 2/3rds has signed (ErrNewHeaderCantBeTrusted) https://github.com/tendermint/tendermint/pull/4209#discussion_r360331253 * remove all headers (even the last one) that are outside of the trusting period. By doing this, we avoid checking the trustedHeader's hash in checkTrustedHeaderUsingOptions (case #1). https://github.com/tendermint/tendermint/pull/4209#discussion_r360332460 * explain restoreTrustedHeaderAndNextVals better https://github.com/tendermint/tendermint/pull/4209#discussion_r360602328 * add ConfirmationFunction option for optionally prompting for user input Y/n before removing headers Refs https://github.com/tendermint/tendermint/pull/4209#discussion_r360602945 * make cleaning optional https://github.com/tendermint/tendermint/pull/4209#discussion_r364838189 * return error when user refused to remove headers * check for double votes in VerifyCommitTrusting * leave only ErrNewValSetCantBeTrusted error to differenciate between h2Vals.VerifyCommit and h1NextVals.VerifyCommitTrusting * fix example tests * remove unnecessary if condition https://github.com/tendermint/tendermint/pull/4209#discussion_r365171847 It will be handled by the above switch. * verifyCommitBasic does not depend on vals Co-authored-by: Marko <marbar3778@yahoo.com>
This commit is contained in:
@@ -185,7 +185,7 @@ func (dv *DynamicVerifier) Verify(shdr types.SignedHeader) error {
|
||||
|
||||
// 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.
|
||||
// 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() {
|
||||
@@ -247,8 +247,8 @@ FOR_LOOP:
|
||||
return sourceFC, nil
|
||||
}
|
||||
|
||||
// Handle special case when err is ErrTooMuchChange.
|
||||
if types.IsErrTooMuchChange(err) {
|
||||
// Handle special case when err is ErrNotEnoughVotingPowerSigned.
|
||||
if types.IsErrNotEnoughVotingPowerSigned(err) {
|
||||
// Divide and conquer.
|
||||
start, end := trustedFC.Height(), sourceFC.Height()
|
||||
if !(start < end) {
|
||||
|
||||
+15
-15
@@ -13,7 +13,7 @@ type AutoClient struct {
|
||||
quit chan struct{}
|
||||
|
||||
trustedHeaders chan *types.SignedHeader
|
||||
err chan error
|
||||
errs chan error
|
||||
}
|
||||
|
||||
// NewAutoClient creates a new client and starts a polling goroutine.
|
||||
@@ -23,7 +23,7 @@ func NewAutoClient(base *Client, updatePeriod time.Duration) *AutoClient {
|
||||
updatePeriod: updatePeriod,
|
||||
quit: make(chan struct{}),
|
||||
trustedHeaders: make(chan *types.SignedHeader),
|
||||
err: make(chan error),
|
||||
errs: make(chan error),
|
||||
}
|
||||
go c.autoUpdate()
|
||||
return c
|
||||
@@ -35,8 +35,8 @@ func (c *AutoClient) TrustedHeaders() <-chan *types.SignedHeader {
|
||||
}
|
||||
|
||||
// Err returns a channel onto which errors are posted.
|
||||
func (c *AutoClient) Err() <-chan error {
|
||||
return c.err
|
||||
func (c *AutoClient) Errs() <-chan error {
|
||||
return c.errs
|
||||
}
|
||||
|
||||
// Stop stops the client.
|
||||
@@ -45,30 +45,30 @@ func (c *AutoClient) Stop() {
|
||||
}
|
||||
|
||||
func (c *AutoClient) autoUpdate() {
|
||||
lastTrustedHeight, err := c.base.LastTrustedHeight()
|
||||
if err != nil {
|
||||
c.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(c.updatePeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
err := c.base.VerifyHeaderAtHeight(lastTrustedHeight+1, time.Now())
|
||||
lastTrustedHeight, err := c.base.LastTrustedHeight()
|
||||
if err != nil {
|
||||
c.err <- err
|
||||
c.errs <- err
|
||||
continue
|
||||
}
|
||||
h, err := c.base.TrustedHeader(lastTrustedHeight+1, time.Now())
|
||||
|
||||
if lastTrustedHeight == -1 {
|
||||
// no headers yet => wait
|
||||
continue
|
||||
}
|
||||
|
||||
h, err := c.base.VerifyHeaderAtHeight(lastTrustedHeight+1, time.Now())
|
||||
if err != nil {
|
||||
c.err <- err
|
||||
// no header yet or verification error => try again after updatePeriod
|
||||
c.errs <- err
|
||||
continue
|
||||
}
|
||||
c.trustedHeaders <- h
|
||||
lastTrustedHeight = h.Height
|
||||
case <-c.quit:
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package lite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
mockp "github.com/tendermint/tendermint/lite2/provider/mock"
|
||||
dbs "github.com/tendermint/tendermint/lite2/store/db"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestAutoClient(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestAutoClient"
|
||||
)
|
||||
|
||||
var (
|
||||
keys = genPrivKeys(4)
|
||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do!
|
||||
vals = keys.ToValidators(20, 10)
|
||||
bTime = time.Now().Add(-1 * time.Hour)
|
||||
header = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
)
|
||||
|
||||
base, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 1,
|
||||
Hash: header.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
// trusted header
|
||||
1: header,
|
||||
// interim header (3/3 signed)
|
||||
2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
// last header (3/3 signed)
|
||||
3: keys.GenSignedHeader(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
3: vals,
|
||||
4: vals,
|
||||
},
|
||||
),
|
||||
dbs.New(dbm.NewMemDB(), chainID),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
base.SetLogger(log.TestingLogger())
|
||||
|
||||
c := NewAutoClient(base, 1*time.Second)
|
||||
defer c.Stop()
|
||||
|
||||
for i := 2; i <= 3; i++ {
|
||||
select {
|
||||
case h := <-c.TrustedHeaders():
|
||||
assert.EqualValues(t, i, h.Height)
|
||||
case err := <-c.Errs():
|
||||
require.NoError(t, err)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("no headers/errors received in 2 sec")
|
||||
}
|
||||
}
|
||||
}
|
||||
+307
-48
@@ -47,14 +47,16 @@ type mode byte
|
||||
const (
|
||||
sequential mode = iota + 1
|
||||
skipping
|
||||
|
||||
defaultRemoveNoLongerTrustedHeadersPeriod = 24 * time.Hour
|
||||
)
|
||||
|
||||
// Option sets a parameter for the light client.
|
||||
type Option func(*Client)
|
||||
|
||||
// SequentialVerification option configures the light client to sequentially
|
||||
// check the headers. Note this is much slower than SkippingVerification,
|
||||
// albeit more secure.
|
||||
// check the headers (every header, in ascending height order). Note this is
|
||||
// much slower than SkippingVerification, albeit more secure.
|
||||
func SequentialVerification() Option {
|
||||
return func(c *Client) {
|
||||
c.verificationMode = sequential
|
||||
@@ -68,7 +70,7 @@ func SequentialVerification() Option {
|
||||
//
|
||||
// trustLevel - fraction of the old validator set (in terms of voting power),
|
||||
// which must sign the new header in order for us to trust it. NOTE this only
|
||||
// applies to non-adjusted headers. For adjusted headers, sequential
|
||||
// applies to non-adjacent headers. For adjacent headers, sequential
|
||||
// verification is used.
|
||||
func SkippingVerification(trustLevel tmmath.Fraction) Option {
|
||||
if err := ValidateTrustLevel(trustLevel); err != nil {
|
||||
@@ -88,6 +90,25 @@ func AlternativeSources(providers []provider.Provider) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveNoLongerTrustedHeadersPeriod option can be used to define how often
|
||||
// the routine, which cleans up no longer trusted headers (outside of trusting
|
||||
// period), is run. Default: once a day. When set to zero, the routine won't be
|
||||
// started.
|
||||
func RemoveNoLongerTrustedHeadersPeriod(d time.Duration) Option {
|
||||
return func(c *Client) {
|
||||
c.removeNoLongerTrustedHeadersPeriod = d
|
||||
}
|
||||
}
|
||||
|
||||
// ConfirmationFunction option can be used to prompt to confirm an action. For
|
||||
// example, remove newer headers if the light client is being reset with an
|
||||
// older header. No confirmation is required by default!
|
||||
func ConfirmationFunction(fn func(action string) bool) Option {
|
||||
return func(c *Client) {
|
||||
c.confirmationFn = fn
|
||||
}
|
||||
}
|
||||
|
||||
// Client represents a light client, connected to a single chain, which gets
|
||||
// headers from a primary provider, verifies them either sequentially or by
|
||||
// skipping some and stores them in a trusted store (usually, a local FS).
|
||||
@@ -113,6 +134,12 @@ type Client struct {
|
||||
// Highest next validator set from the store (height=H+1).
|
||||
trustedNextVals *types.ValidatorSet
|
||||
|
||||
removeNoLongerTrustedHeadersPeriod time.Duration
|
||||
|
||||
confirmationFn func(action string) bool
|
||||
|
||||
quit chan struct{}
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
@@ -129,26 +156,136 @@ func NewClient(
|
||||
options ...Option) (*Client, error) {
|
||||
|
||||
c := &Client{
|
||||
chainID: chainID,
|
||||
trustingPeriod: trustOptions.Period,
|
||||
verificationMode: skipping,
|
||||
trustLevel: DefaultTrustLevel,
|
||||
primary: primary,
|
||||
trustedStore: trustedStore,
|
||||
logger: log.NewNopLogger(),
|
||||
chainID: chainID,
|
||||
trustingPeriod: trustOptions.Period,
|
||||
verificationMode: skipping,
|
||||
trustLevel: DefaultTrustLevel,
|
||||
primary: primary,
|
||||
trustedStore: trustedStore,
|
||||
removeNoLongerTrustedHeadersPeriod: defaultRemoveNoLongerTrustedHeadersPeriod,
|
||||
confirmationFn: func(action string) bool { return true },
|
||||
quit: make(chan struct{}),
|
||||
logger: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o(c)
|
||||
}
|
||||
|
||||
if err := c.initializeWithTrustOptions(trustOptions); err != nil {
|
||||
if err := c.restoreTrustedHeaderAndNextVals(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.trustedHeader != nil {
|
||||
if err := c.checkTrustedHeaderUsingOptions(trustOptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if c.trustedHeader == nil || c.trustedHeader.Height != trustOptions.Height {
|
||||
if err := c.initializeWithTrustOptions(trustOptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if c.removeNoLongerTrustedHeadersPeriod > 0 {
|
||||
go c.removeNoLongerTrustedHeadersRoutine()
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Load trustedHeader and trustedNextVals from trustedStore.
|
||||
func (c *Client) restoreTrustedHeaderAndNextVals() error {
|
||||
lastHeight, err := c.trustedStore.LastSignedHeaderHeight()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't get last trusted header height")
|
||||
}
|
||||
|
||||
if lastHeight > 0 {
|
||||
trustedHeader, err := c.trustedStore.SignedHeader(lastHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't get last trusted header")
|
||||
}
|
||||
|
||||
trustedNextVals, err := c.trustedStore.ValidatorSet(lastHeight + 1)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't get last trusted next validators")
|
||||
}
|
||||
|
||||
c.trustedHeader = trustedHeader
|
||||
c.trustedNextVals = trustedNextVals
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// if options.Height:
|
||||
//
|
||||
// 1) ahead of trustedHeader.Height => fetch header (same height as
|
||||
// trustedHeader) from primary provider and check it's hash matches the
|
||||
// trustedHeader's hash (if not, remove trustedHeader and all the headers
|
||||
// before)
|
||||
//
|
||||
// 2) equals trustedHeader.Height => check options.Hash matches the
|
||||
// trustedHeader's hash (if not, remove trustedHeader and all the headers
|
||||
// before)
|
||||
//
|
||||
// 3) behind trustedHeader.Height => remove all the headers between
|
||||
// options.Height and trustedHeader.Height, update trustedHeader, then
|
||||
// check options.Hash matches the trustedHeader's hash (if not, remove
|
||||
// trustedHeader and all the headers before)
|
||||
//
|
||||
// The intuition here is the user is always right. I.e. if she decides to reset
|
||||
// the light client with an older header, there must be a reason for it.
|
||||
func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error {
|
||||
var primaryHash []byte
|
||||
switch {
|
||||
case options.Height > c.trustedHeader.Height:
|
||||
h, err := c.primary.SignedHeader(c.trustedHeader.Height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
primaryHash = h.Hash()
|
||||
case options.Height == c.trustedHeader.Height:
|
||||
primaryHash = options.Hash
|
||||
case options.Height < c.trustedHeader.Height:
|
||||
action := fmt.Sprintf(
|
||||
"Rollback to %d (%X)? Note this will remove newer headers up to %d (%X)",
|
||||
options.Height, options.Hash,
|
||||
c.trustedHeader.Height, c.trustedHeader.Hash())
|
||||
if c.confirmationFn(action) {
|
||||
// remove all the headers ( options.Height, trustedHeader.Height ]
|
||||
c.cleanup(options.Height + 1)
|
||||
// set c.trustedHeader to one at options.Height
|
||||
c.restoreTrustedHeaderAndNextVals()
|
||||
} else {
|
||||
return errors.New("rollback aborted")
|
||||
}
|
||||
|
||||
primaryHash = options.Hash
|
||||
}
|
||||
|
||||
if !bytes.Equal(primaryHash, c.trustedHeader.Hash()) {
|
||||
c.logger.Info("Prev. trusted header's hash %X doesn't match hash %X from primary provider",
|
||||
c.trustedHeader.Hash(), primaryHash)
|
||||
|
||||
action := fmt.Sprintf(
|
||||
"Prev. trusted header's hash %X doesn't match hash %X from primary provider. Remove all the stored headers?",
|
||||
c.trustedHeader.Hash(), primaryHash)
|
||||
if c.confirmationFn(action) {
|
||||
err := c.Cleanup()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to cleanup")
|
||||
}
|
||||
} else {
|
||||
return errors.New("refused to remove the stored headers despite hashes mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch trustedHeader and trustedNextVals from primary provider.
|
||||
func (c *Client) initializeWithTrustOptions(options TrustOptions) error {
|
||||
// 1) Fetch and verify the header.
|
||||
h, err := c.primary.SignedHeader(options.Height)
|
||||
@@ -158,21 +295,44 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error {
|
||||
|
||||
// NOTE: Verify func will check if it's expired or not.
|
||||
if err := h.ValidateBasic(c.chainID); err != nil {
|
||||
return errors.Wrap(err, "ValidateBasic failed")
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(h.Hash(), options.Hash) {
|
||||
return errors.Errorf("expected header's hash %X, but got %X", options.Hash, h.Hash())
|
||||
}
|
||||
|
||||
// 2) Fetch and verify the next vals.
|
||||
vals, err := c.primary.ValidatorSet(options.Height + 1)
|
||||
// 2) Fetch and verify the vals.
|
||||
vals, err := c.primary.ValidatorSet(options.Height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(h.ValidatorsHash, vals.Hash()) {
|
||||
return errors.Errorf("expected header's validators (%X) to match those that were supplied (%X)",
|
||||
h.ValidatorsHash,
|
||||
vals.Hash(),
|
||||
)
|
||||
}
|
||||
// Ensure that +2/3 of validators signed correctly.
|
||||
err = vals.VerifyCommit(c.chainID, h.Commit.BlockID, h.Height, h.Commit)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid commit")
|
||||
}
|
||||
|
||||
// 3) Fetch and verify the next vals (verification happens in
|
||||
// updateTrustedHeaderAndVals).
|
||||
nextVals, err := c.primary.ValidatorSet(options.Height + 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3) Persist both of them and continue.
|
||||
return c.updateTrustedHeaderAndVals(h, vals)
|
||||
// 4) Persist both of them and continue.
|
||||
return c.updateTrustedHeaderAndVals(h, nextVals)
|
||||
}
|
||||
|
||||
// Stop stops the light client.
|
||||
func (c *Client) Stop() {
|
||||
close(c.quit)
|
||||
}
|
||||
|
||||
// SetLogger sets a logger.
|
||||
@@ -182,15 +342,17 @@ func (c *Client) SetLogger(l log.Logger) {
|
||||
|
||||
// TrustedHeader returns a trusted header at the given height (0 - the latest)
|
||||
// or nil if no such header exist.
|
||||
// TODO: mention how many headers will be kept by the light client.
|
||||
//
|
||||
// Headers, which can't be trusted anymore, are removed once a day (can be
|
||||
// changed with RemoveNoLongerTrustedHeadersPeriod option).
|
||||
// .
|
||||
// height must be >= 0.
|
||||
//
|
||||
// It returns an error if:
|
||||
// - the header expired (ErrOldHeaderExpired). In that case, update your
|
||||
// client to more recent height;
|
||||
// - there are some issues with the trusted store, although that should not
|
||||
// happen normally.
|
||||
// - header expired, therefore can't be trusted (ErrOldHeaderExpired);
|
||||
// - there are some issues with the trusted store, although that should not
|
||||
// happen normally;
|
||||
// - negative height is passed.
|
||||
func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) {
|
||||
if height < 0 {
|
||||
return nil, errors.New("negative height")
|
||||
@@ -208,22 +370,25 @@ func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Ensure header can still be trusted.
|
||||
expirationTime := h.Time.Add(c.trustingPeriod)
|
||||
if !expirationTime.After(now) {
|
||||
return nil, ErrOldHeaderExpired{expirationTime, now}
|
||||
if HeaderExpired(h, c.trustingPeriod, now) {
|
||||
return nil, ErrOldHeaderExpired{h.Time.Add(c.trustingPeriod), now}
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// LastTrustedHeight returns a last trusted height.
|
||||
// LastTrustedHeight returns a last trusted height. -1 and nil are returned if
|
||||
// there are no trusted headers.
|
||||
func (c *Client) LastTrustedHeight() (int64, error) {
|
||||
return c.trustedStore.LastSignedHeaderHeight()
|
||||
}
|
||||
|
||||
// ChainID returns the chain ID.
|
||||
// ChainID returns the chain ID the light client was configured with.
|
||||
func (c *Client) ChainID() string {
|
||||
return c.chainID
|
||||
}
|
||||
@@ -232,18 +397,18 @@ func (c *Client) ChainID() string {
|
||||
// and calls VerifyHeader.
|
||||
//
|
||||
// If the trusted header is more recent than one here, an error is returned.
|
||||
func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) error {
|
||||
func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) {
|
||||
if c.trustedHeader.Height >= height {
|
||||
return errors.Errorf("height #%d is already trusted (last: #%d)", height, c.trustedHeader.Height)
|
||||
return nil, errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height)
|
||||
}
|
||||
|
||||
// Request the header and the vals.
|
||||
newHeader, newVals, err := c.fetchHeaderAndValsAtHeight(height)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.VerifyHeader(newHeader, newVals, now)
|
||||
return newHeader, c.VerifyHeader(newHeader, newVals, now)
|
||||
}
|
||||
|
||||
// VerifyHeader verifies new header against the trusted state.
|
||||
@@ -255,13 +420,13 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) error {
|
||||
// SkippingVerification(trustLevel): verifies that {trustLevel} of the trusted
|
||||
// validator set has signed the new header. If it's not the case and the
|
||||
// headers are not adjacent, bisection is performed and necessary (not all)
|
||||
// intermediate headers will be requested. See the specification for the
|
||||
// algorithm.
|
||||
// intermediate headers will be requested. See the specification for details.
|
||||
// https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md
|
||||
//
|
||||
// If the trusted header is more recent than one here, an error is returned.
|
||||
func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error {
|
||||
if c.trustedHeader.Height >= newHeader.Height {
|
||||
return errors.Errorf("height #%d is already trusted (last: #%d)", newHeader.Height, c.trustedHeader.Height)
|
||||
return errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height)
|
||||
}
|
||||
|
||||
if len(c.alternatives) > 0 {
|
||||
@@ -291,6 +456,44 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali
|
||||
return c.updateTrustedHeaderAndVals(newHeader, nextVals)
|
||||
}
|
||||
|
||||
// Cleanup removes all the data (headers and validator sets) stored.
|
||||
func (c *Client) Cleanup() error {
|
||||
return c.cleanup(0)
|
||||
}
|
||||
|
||||
// stopHeight=0 -> remove all data
|
||||
func (c *Client) cleanup(stopHeight int64) error {
|
||||
// 1) Get the oldest height.
|
||||
oldestHeight, err := c.trustedStore.FirstSignedHeaderHeight()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't get first trusted height")
|
||||
}
|
||||
|
||||
// 2) Get the latest height.
|
||||
latestHeight, err := c.trustedStore.LastSignedHeaderHeight()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't get last trusted height")
|
||||
}
|
||||
|
||||
// 3) Remove all headers and validator sets.
|
||||
if stopHeight == 0 {
|
||||
stopHeight = oldestHeight
|
||||
}
|
||||
for height := stopHeight; height <= latestHeight; height++ {
|
||||
err = c.trustedStore.DeleteSignedHeaderAndNextValidatorSet(height)
|
||||
if err != nil {
|
||||
c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
c.trustedHeader = nil
|
||||
c.trustedNextVals = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// see VerifyHeader
|
||||
func (c *Client) sequence(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error {
|
||||
// 1) Verify any intermediate headers.
|
||||
var (
|
||||
@@ -329,6 +532,7 @@ func (c *Client) sequence(newHeader *types.SignedHeader, newVals *types.Validato
|
||||
return Verify(c.chainID, c.trustedHeader, c.trustedNextVals, newHeader, newVals, c.trustingPeriod, now, c.trustLevel)
|
||||
}
|
||||
|
||||
// see VerifyHeader
|
||||
func (c *Client) bisection(
|
||||
lastHeader *types.SignedHeader,
|
||||
lastVals *types.ValidatorSet,
|
||||
@@ -340,15 +544,10 @@ func (c *Client) bisection(
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case types.ErrTooMuchChange:
|
||||
case ErrNewValSetCantBeTrusted:
|
||||
// continue bisection
|
||||
default:
|
||||
return errors.Wrapf(err, "failed to verify the header #%d ", newHeader.Height)
|
||||
}
|
||||
|
||||
if newHeader.Height == c.trustedHeader.Height+1 {
|
||||
// TODO: submit evidence here
|
||||
return errors.Errorf("adjacent headers (#%d and #%d) that are not matching", lastHeader.Height, newHeader.Height)
|
||||
return errors.Wrapf(err, "failed to verify the header #%d", newHeader.Height)
|
||||
}
|
||||
|
||||
pivot := (c.trustedHeader.Height + newHeader.Header.Height) / 2
|
||||
@@ -392,22 +591,23 @@ func (c *Client) bisection(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.ValidatorSet) error {
|
||||
if !bytes.Equal(h.NextValidatorsHash, vals.Hash()) {
|
||||
return errors.Errorf("expected next validator's hash %X, but got %X", h.NextValidatorsHash, vals.Hash())
|
||||
// persist header and next validators to trustedStore.
|
||||
func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, nextVals *types.ValidatorSet) error {
|
||||
if !bytes.Equal(h.NextValidatorsHash, nextVals.Hash()) {
|
||||
return errors.Errorf("expected next validator's hash %X, but got %X", h.NextValidatorsHash, nextVals.Hash())
|
||||
}
|
||||
|
||||
if err := c.trustedStore.SaveSignedHeader(h); err != nil {
|
||||
if err := c.trustedStore.SaveSignedHeaderAndNextValidatorSet(h, nextVals); err != nil {
|
||||
return errors.Wrap(err, "failed to save trusted header")
|
||||
}
|
||||
if err := c.trustedStore.SaveValidatorSet(vals, h.Height+1); err != nil {
|
||||
return errors.Wrap(err, "failed to save trusted vals")
|
||||
}
|
||||
|
||||
c.trustedHeader = h
|
||||
c.trustedNextVals = vals
|
||||
c.trustedNextVals = nextVals
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetch header and validators for the given height from primary provider.
|
||||
func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, *types.ValidatorSet, error) {
|
||||
h, err := c.primary.SignedHeader(height)
|
||||
if err != nil {
|
||||
@@ -420,6 +620,7 @@ func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader,
|
||||
return h, vals, nil
|
||||
}
|
||||
|
||||
// compare header with one from a random alternative provider.
|
||||
func (c *Client) compareNewHeaderWithRandomAlternative(h *types.SignedHeader) error {
|
||||
// 1. Pick an alternative provider.
|
||||
p := c.alternatives[tmrand.Intn(len(c.alternatives))]
|
||||
@@ -442,3 +643,61 @@ func (c *Client) compareNewHeaderWithRandomAlternative(h *types.SignedHeader) er
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) removeNoLongerTrustedHeadersRoutine() {
|
||||
ticker := time.NewTicker(c.removeNoLongerTrustedHeadersPeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.RemoveNoLongerTrustedHeaders(time.Now())
|
||||
case <-c.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveNoLongerTrustedHeaders removes no longer trusted headers (due to
|
||||
// expiration).
|
||||
//
|
||||
// Exposed for testing.
|
||||
func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) {
|
||||
// 1) Get the oldest height.
|
||||
oldestHeight, err := c.trustedStore.FirstSignedHeaderHeight()
|
||||
if err != nil {
|
||||
c.logger.Error("can't get first trusted height", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2) Get the latest height.
|
||||
latestHeight, err := c.LastTrustedHeight()
|
||||
if err != nil {
|
||||
c.logger.Error("can't get last trusted height", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3) Remove all headers that are outside of the trusting period.
|
||||
for height := oldestHeight; height <= latestHeight; height++ {
|
||||
h, err := c.trustedStore.SignedHeader(height)
|
||||
if err != nil {
|
||||
c.logger.Error("can't get a trusted header", "err", err, "height", height)
|
||||
continue
|
||||
}
|
||||
if h == nil {
|
||||
c.logger.Debug("attempted to remove non-existing header", "height", height)
|
||||
continue
|
||||
}
|
||||
|
||||
// Stop if the header is within the trusting period.
|
||||
if !HeaderExpired(h, c.trustingPeriod, now) {
|
||||
break
|
||||
}
|
||||
|
||||
err = c.trustedStore.DeleteSignedHeaderAndNextValidatorSet(height)
|
||||
if err != nil {
|
||||
c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+420
-2
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
mockp "github.com/tendermint/tendermint/lite2/provider/mock"
|
||||
dbs "github.com/tendermint/tendermint/lite2/store/db"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
@@ -135,8 +136,9 @@ func TestClient_SequentialVerification(t *testing.T) {
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
defer c.Stop()
|
||||
|
||||
err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour))
|
||||
_, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour))
|
||||
if tc.verifyErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -232,8 +234,9 @@ func TestClient_SkippingVerification(t *testing.T) {
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
defer c.Stop()
|
||||
|
||||
err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour))
|
||||
_, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour))
|
||||
if tc.verifyErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -241,3 +244,418 @@ func TestClient_SkippingVerification(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestClientRemovesNoLongerTrustedHeaders"
|
||||
)
|
||||
|
||||
var (
|
||||
keys = genPrivKeys(4)
|
||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do!
|
||||
vals = keys.ToValidators(20, 10)
|
||||
bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
|
||||
header = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
)
|
||||
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 1,
|
||||
Hash: header.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
// trusted header
|
||||
1: header,
|
||||
// interim header (3/3 signed)
|
||||
2: keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
// last header (3/3 signed)
|
||||
3: keys.GenSignedHeader(chainID, 3, bTime.Add(4*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
3: vals,
|
||||
4: vals,
|
||||
},
|
||||
),
|
||||
dbs.New(dbm.NewMemDB(), chainID),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer c.Stop()
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Verify new headers.
|
||||
_, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
require.NoError(t, err)
|
||||
now := bTime.Add(4 * time.Hour).Add(1 * time.Second)
|
||||
_, err = c.VerifyHeaderAtHeight(3, now)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Remove expired headers.
|
||||
c.RemoveNoLongerTrustedHeaders(now)
|
||||
|
||||
// Check expired headers are no longer available.
|
||||
h, err := c.TrustedHeader(1, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, h)
|
||||
|
||||
// Check not expired headers are available.
|
||||
h, err = c.TrustedHeader(2, now)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, h)
|
||||
}
|
||||
|
||||
func TestClient_Cleanup(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestClient_Cleanup"
|
||||
)
|
||||
|
||||
var (
|
||||
keys = genPrivKeys(4)
|
||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do!
|
||||
vals = keys.ToValidators(20, 10)
|
||||
bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
|
||||
header = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
)
|
||||
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 1,
|
||||
Hash: header.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
// trusted header
|
||||
1: header,
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
},
|
||||
),
|
||||
dbs.New(dbm.NewMemDB(), chainID),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
c.Cleanup()
|
||||
|
||||
// Check no headers exist after Cleanup.
|
||||
h, err := c.TrustedHeader(1, bTime.Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, h)
|
||||
}
|
||||
|
||||
// trustedHeader.Height == options.Height
|
||||
func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestClientRestoreTrustedHeaderAfterStartup1"
|
||||
)
|
||||
|
||||
var (
|
||||
keys = genPrivKeys(4)
|
||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do!
|
||||
vals = keys.ToValidators(20, 10)
|
||||
bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
|
||||
header = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
)
|
||||
|
||||
// 1. options.Hash == trustedHeader.Hash
|
||||
{
|
||||
trustedStore := dbs.New(dbm.NewMemDB(), chainID)
|
||||
err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 1,
|
||||
Hash: header.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
// trusted header
|
||||
1: header,
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
h, err := c.TrustedHeader(1, bTime.Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, h)
|
||||
assert.Equal(t, h.Hash(), header.Hash())
|
||||
}
|
||||
|
||||
// 2. options.Hash != trustedHeader.Hash
|
||||
{
|
||||
trustedStore := dbs.New(dbm.NewMemDB(), chainID)
|
||||
err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals)
|
||||
require.NoError(t, err)
|
||||
|
||||
// header1 != header
|
||||
header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 1,
|
||||
Hash: header1.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
// trusted header
|
||||
1: header1,
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
h, err := c.TrustedHeader(1, bTime.Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, h)
|
||||
assert.Equal(t, h.Hash(), header1.Hash())
|
||||
}
|
||||
}
|
||||
|
||||
// trustedHeader.Height < options.Height
|
||||
func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestClientRestoreTrustedHeaderAfterStartup2"
|
||||
)
|
||||
|
||||
var (
|
||||
keys = genPrivKeys(4)
|
||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do!
|
||||
vals = keys.ToValidators(20, 10)
|
||||
bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
|
||||
header = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
)
|
||||
|
||||
// 1. options.Hash == trustedHeader.Hash
|
||||
{
|
||||
trustedStore := dbs.New(dbm.NewMemDB(), chainID)
|
||||
err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals)
|
||||
require.NoError(t, err)
|
||||
|
||||
header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 2,
|
||||
Hash: header2.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
1: header,
|
||||
2: header2,
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
3: vals,
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Check we still have the 1st header (+header+).
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, h)
|
||||
assert.Equal(t, h.Hash(), header.Hash())
|
||||
}
|
||||
|
||||
// 2. options.Hash != trustedHeader.Hash
|
||||
// This could happen if previous provider was lying to us.
|
||||
{
|
||||
trustedStore := dbs.New(dbm.NewMemDB(), chainID)
|
||||
err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals)
|
||||
require.NoError(t, err)
|
||||
|
||||
// header1 != header
|
||||
header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
|
||||
header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 2,
|
||||
Hash: header2.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
1: header1,
|
||||
2: header2,
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
3: vals,
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Check we no longer have the invalid 1st header (+header+).
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, h)
|
||||
}
|
||||
}
|
||||
|
||||
// trustedHeader.Height > options.Height
|
||||
func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestClientRestoreTrustedHeaderAfterStartup3"
|
||||
)
|
||||
|
||||
var (
|
||||
keys = genPrivKeys(4)
|
||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do!
|
||||
vals = keys.ToValidators(20, 10)
|
||||
bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
|
||||
header = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
)
|
||||
|
||||
// 1. options.Hash == trustedHeader.Hash
|
||||
{
|
||||
trustedStore := dbs.New(dbm.NewMemDB(), chainID)
|
||||
err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals)
|
||||
require.NoError(t, err)
|
||||
|
||||
header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
err = trustedStore.SaveSignedHeaderAndNextValidatorSet(header2, vals)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 1,
|
||||
Hash: header.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
1: header,
|
||||
2: header2,
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
3: vals,
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Check we still have the 1st header (+header+).
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, h)
|
||||
assert.Equal(t, h.Hash(), header.Hash())
|
||||
|
||||
// Check we no longer have 2nd header (+header2+).
|
||||
h, err = c.TrustedHeader(2, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, h)
|
||||
}
|
||||
|
||||
// 2. options.Hash != trustedHeader.Hash
|
||||
// This could happen if previous provider was lying to us.
|
||||
{
|
||||
trustedStore := dbs.New(dbm.NewMemDB(), chainID)
|
||||
err := trustedStore.SaveSignedHeaderAndNextValidatorSet(header, vals)
|
||||
require.NoError(t, err)
|
||||
|
||||
// header1 != header
|
||||
header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
|
||||
header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
err = trustedStore.SaveSignedHeaderAndNextValidatorSet(header2, vals)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 4 * time.Hour,
|
||||
Height: 1,
|
||||
Hash: header1.Hash(),
|
||||
},
|
||||
mockp.New(
|
||||
chainID,
|
||||
map[int64]*types.SignedHeader{
|
||||
1: header1,
|
||||
},
|
||||
map[int64]*types.ValidatorSet{
|
||||
1: vals,
|
||||
2: vals,
|
||||
},
|
||||
),
|
||||
trustedStore,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// Check we have swapped invalid 1st header (+header+) with correct one (+header1+).
|
||||
h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, h)
|
||||
assert.Equal(t, h.Hash(), header1.Hash())
|
||||
|
||||
// Check we no longer have invalid 2nd header (+header2+).
|
||||
h, err = c.TrustedHeader(2, bTime.Add(2*time.Hour).Add(1*time.Second))
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,81 @@ Subjectivity](https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak
|
||||
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.
|
||||
|
||||
# Common structures
|
||||
|
||||
* SignedHeader
|
||||
|
||||
SignedHeader is a block header along with a commit -- enough validator
|
||||
precommit-vote signatures to prove its validity (> 2/3 of the voting power)
|
||||
given the validator set responsible for signing that header.
|
||||
|
||||
The hash of the next validator set is included and signed in the SignedHeader.
|
||||
This lets the lite client keep track of arbitrary changes to the validator set,
|
||||
as every change to the validator set must be approved by inclusion in the
|
||||
header and signed in the commit.
|
||||
|
||||
In the worst case, with every block changing the validators around completely,
|
||||
a lite client can sync up with every block header to verify each validator set
|
||||
change on the chain. In practice, most applications will not have frequent
|
||||
drastic updates to the validator set, so the logic defined in this package for
|
||||
lite client syncing is optimized to use intelligent bisection.
|
||||
|
||||
# What this package provides
|
||||
|
||||
This package provides three major things:
|
||||
|
||||
1. Client implementation (see client.go)
|
||||
2. Pure functions to verify a new header (see verifier.go)
|
||||
3. Secure RPC proxy
|
||||
|
||||
## 1. Client implementation (see client.go)
|
||||
|
||||
Example usage:
|
||||
|
||||
db, err := dbm.NewGoLevelDB("lite-client-db", dbDir)
|
||||
if err != nil {
|
||||
// return err
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 504 * time.Hour, // 21 days
|
||||
Height: 100,
|
||||
Hash: header.Hash(),
|
||||
},
|
||||
httpp.New(chainID, "tcp://localhost:26657"),
|
||||
dbs.New(db, chainID),
|
||||
)
|
||||
|
||||
err = c.VerifyHeaderAtHeight(101, time.Now())
|
||||
if err != nil {
|
||||
fmt.Println("retry?")
|
||||
}
|
||||
|
||||
h, err := c.TrustedHeader(101)
|
||||
if err != nil {
|
||||
fmt.Println("retry?")
|
||||
}
|
||||
fmt.Println("got header", h)
|
||||
|
||||
## 2. Pure functions to verify a new header (see verifier.go)
|
||||
|
||||
Verify function verifies a new header against some trusted header. See
|
||||
https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md
|
||||
for details.
|
||||
|
||||
## 3. Secure RPC proxy
|
||||
|
||||
Tendermint RPC exposes a lot of info, but a malicious node could return any
|
||||
data it wants to queries, or even to block headers, even making up fake
|
||||
signatures from non-existent validators to justify it. Secure RPC proxy serves
|
||||
as a wrapper, which verifies all the headers, using a light client connected to
|
||||
some other node.
|
||||
|
||||
See
|
||||
https://github.com/tendermint/tendermint/blob/master/cmd/tendermint/commands/lite.go
|
||||
for usage example.
|
||||
*/
|
||||
package lite
|
||||
|
||||
@@ -3,6 +3,8 @@ package lite
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// ErrOldHeaderExpired means the old (trusted) header has expired according to
|
||||
@@ -16,3 +18,13 @@ type ErrOldHeaderExpired struct {
|
||||
func (e ErrOldHeaderExpired) Error() string {
|
||||
return fmt.Sprintf("old header has expired at %v (now: %v)", e.At, e.Now)
|
||||
}
|
||||
|
||||
// ErrNewValSetCantBeTrusted means the new validator set cannot be trusted
|
||||
// because < 1/3rd (+trustLevel+) of the old validator set has signed.
|
||||
type ErrNewValSetCantBeTrusted struct {
|
||||
Reason types.ErrNotEnoughVotingPowerSigned
|
||||
}
|
||||
|
||||
func (e ErrNewValSetCantBeTrusted) Error() string {
|
||||
return fmt.Sprintf("cant trust new val set: %v", e.Reason)
|
||||
}
|
||||
|
||||
+141
-81
@@ -1,95 +1,155 @@
|
||||
package lite
|
||||
|
||||
//func TestExample_Client(t *testing.T) {
|
||||
// const (
|
||||
// chainID = "my-awesome-chain"
|
||||
// )
|
||||
// dbDir, err := ioutil.TempDir("", "lite-client-example")
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// defer os.RemoveAll(dbDir)
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
// // TODO: fetch the "trusted" header from a node
|
||||
// header := (*types.SignedHeader)(nil)
|
||||
"github.com/pkg/errors"
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
// db, err := dbm.NewGoLevelDB("lite-client-db", dbDir)
|
||||
// if err != nil {
|
||||
// // return err
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// c, err := NewClient(
|
||||
// chainID,
|
||||
// TrustOptions{
|
||||
// Period: 504 * time.Hour, // 21 days
|
||||
// Height: 100,
|
||||
// Hash: header.Hash(),
|
||||
// },
|
||||
// httpp.New(chainID, "tcp://localhost:26657"),
|
||||
// dbs.New(db, chainID),
|
||||
// )
|
||||
"github.com/tendermint/tendermint/abci/example/kvstore"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
httpp "github.com/tendermint/tendermint/lite2/provider/http"
|
||||
dbs "github.com/tendermint/tendermint/lite2/store/db"
|
||||
rpctest "github.com/tendermint/tendermint/rpc/test"
|
||||
)
|
||||
|
||||
// err = c.VerifyHeaderAtHeight(101, time.Now())
|
||||
// if err != nil {
|
||||
// fmt.Println("retry?")
|
||||
// }
|
||||
func TestExample_Client(t *testing.T) {
|
||||
// give Tendermint time to generate some blocks
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// h, err := c.TrustedHeader(101)
|
||||
// if err != nil {
|
||||
// fmt.Println("retry?")
|
||||
// }
|
||||
// fmt.Println("got header", h)
|
||||
// // verify some data
|
||||
//}
|
||||
dbDir, err := ioutil.TempDir("", "lite-client-example")
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dbDir)
|
||||
|
||||
//func TestExample_AutoClient(t *testing.T) {
|
||||
// const (
|
||||
// chainID = "my-awesome-chain"
|
||||
// )
|
||||
// dbDir, err := ioutil.TempDir("", "lite-client-example")
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// defer os.RemoveAll(dbDir)
|
||||
var (
|
||||
config = rpctest.GetConfig()
|
||||
chainID = config.ChainID()
|
||||
)
|
||||
|
||||
// // TODO: fetch the "trusted" header from a node
|
||||
// header := (*types.SignedHeader)(nil)
|
||||
provider, err := httpp.New(chainID, config.RPC.ListenAddress)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
header, err := provider.SignedHeader(2)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
// db, err := dbm.NewGoLevelDB("lite-client-db", dbDir)
|
||||
// if err != nil {
|
||||
// // return err
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
db, err := dbm.NewGoLevelDB("lite-client-db", dbDir)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
// base, err := NewClient(
|
||||
// chainID,
|
||||
// TrustOptions{
|
||||
// Period: 504 * time.Hour, // 21 days
|
||||
// Height: 100,
|
||||
// Hash: header.Hash(),
|
||||
// },
|
||||
// httpp.New(chainID, "tcp://localhost:26657"),
|
||||
// dbs.New(db, chainID),
|
||||
// )
|
||||
c, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 504 * time.Hour, // 21 days
|
||||
Height: 2,
|
||||
Hash: header.Hash(),
|
||||
},
|
||||
provider,
|
||||
dbs.New(db, chainID),
|
||||
)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
c.SetLogger(log.TestingLogger())
|
||||
|
||||
// c := NewAutoClient(base, 1*time.Second)
|
||||
// defer c.Stop()
|
||||
_, err = c.VerifyHeaderAtHeight(3, time.Now())
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
// select {
|
||||
// case h := <-c.TrustedHeaders():
|
||||
// fmt.Println("got header", h)
|
||||
// // verify some data
|
||||
// case err := <-c.Err():
|
||||
// switch errors.Cause(err).(type) {
|
||||
// case ErrOldHeaderExpired:
|
||||
// // reobtain trust height and hash
|
||||
// default:
|
||||
// // try with another full node
|
||||
// fmt.Println("got error", err)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
h, err := c.TrustedHeader(3, time.Now())
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("got header", h.Height)
|
||||
// Output: got header 3
|
||||
}
|
||||
|
||||
func TestExample_AutoClient(t *testing.T) {
|
||||
// give Tendermint time to generate some blocks
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
dbDir, err := ioutil.TempDir("", "lite-client-example")
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dbDir)
|
||||
|
||||
var (
|
||||
config = rpctest.GetConfig()
|
||||
chainID = config.ChainID()
|
||||
)
|
||||
|
||||
provider, err := httpp.New(chainID, config.RPC.ListenAddress)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
header, err := provider.SignedHeader(2)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
db, err := dbm.NewGoLevelDB("lite-client-db", dbDir)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
base, err := NewClient(
|
||||
chainID,
|
||||
TrustOptions{
|
||||
Period: 504 * time.Hour, // 21 days
|
||||
Height: 2,
|
||||
Hash: header.Hash(),
|
||||
},
|
||||
provider,
|
||||
dbs.New(db, chainID),
|
||||
)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
base.SetLogger(log.TestingLogger())
|
||||
|
||||
c := NewAutoClient(base, 1*time.Second)
|
||||
defer c.Stop()
|
||||
|
||||
select {
|
||||
case h := <-c.TrustedHeaders():
|
||||
fmt.Println("got header", h.Height)
|
||||
// Output: got header 3
|
||||
case err := <-c.Errs():
|
||||
switch errors.Cause(err).(type) {
|
||||
case ErrOldHeaderExpired:
|
||||
// reobtain trust height and hash
|
||||
stdlog.Fatal(err)
|
||||
default:
|
||||
// try with another full node
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// start a tendermint node (and kvstore) in the background to test against
|
||||
app := kvstore.NewApplication()
|
||||
node := rpctest.StartTendermint(app)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// and shut down proper at the end
|
||||
rpctest.StopTendermint(node)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -39,10 +39,13 @@ func NewWithClient(chainID string, client SignStatusClient) provider.Provider {
|
||||
}
|
||||
}
|
||||
|
||||
// ChainID returns a chainID this provider was configured with.
|
||||
func (p *http) ChainID() string {
|
||||
return p.chainID
|
||||
}
|
||||
|
||||
// SignedHeader fetches a SignedHeader at the given height and checks the
|
||||
// chainID matches.
|
||||
func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) {
|
||||
h, err := validateHeight(height)
|
||||
if err != nil {
|
||||
@@ -62,6 +65,8 @@ func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) {
|
||||
return &commit.SignedHeader, nil
|
||||
}
|
||||
|
||||
// ValidatorSet fetches a ValidatorSet at the given height. Multiple HTTP
|
||||
// requests might be required if the validator set size is over 100.
|
||||
func (p *http) ValidatorSet(height int64) (*types.ValidatorSet, error) {
|
||||
h, err := validateHeight(height)
|
||||
if err != nil {
|
||||
|
||||
+20
-29
@@ -90,14 +90,10 @@ func (c *Client) ABCIQueryWithOptions(path string, data tmbytes.HexBytes,
|
||||
}
|
||||
|
||||
// Update the light client if we're behind.
|
||||
if err := c.updateLiteClientIfNeededTo(resp.Height + 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// AppHash for height H is in header H+1.
|
||||
h, err := c.lc.TrustedHeader(resp.Height+1, time.Now())
|
||||
// NOTE: AppHash for height H is in header H+1.
|
||||
h, err := c.updateLiteClientIfNeededTo(resp.Height + 1)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "TrustedHeader(%d)", resp.Height+1)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate the value proof against the trusted header.
|
||||
@@ -188,7 +184,7 @@ func (c *Client) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlock
|
||||
// Update the light client if we're behind.
|
||||
if len(res.BlockMetas) > 0 {
|
||||
lastHeight := res.BlockMetas[len(res.BlockMetas)-1].Header.Height
|
||||
if err := c.updateLiteClientIfNeededTo(lastHeight); err != nil {
|
||||
if _, err := c.updateLiteClientIfNeededTo(lastHeight); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -232,15 +228,12 @@ func (c *Client) Block(height *int64) (*ctypes.ResultBlock, error) {
|
||||
}
|
||||
|
||||
// Update the light client if we're behind.
|
||||
if err := c.updateLiteClientIfNeededTo(res.Block.Height); err != nil {
|
||||
h, err := c.updateLiteClientIfNeededTo(res.Block.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify block.
|
||||
h, err := c.lc.TrustedHeader(res.Block.Height, time.Now())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "TrustedHeader(%d)", res.Block.Height)
|
||||
}
|
||||
if bH, tH := res.Block.Hash(), h.Hash(); !bytes.Equal(bH, tH) {
|
||||
return nil, errors.Errorf("Block#Header %X does not match with trusted header %X",
|
||||
bH, tH)
|
||||
@@ -265,15 +258,12 @@ func (c *Client) Commit(height *int64) (*ctypes.ResultCommit, error) {
|
||||
}
|
||||
|
||||
// Update the light client if we're behind.
|
||||
if err := c.updateLiteClientIfNeededTo(res.Height); err != nil {
|
||||
h, err := c.updateLiteClientIfNeededTo(res.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify commit.
|
||||
h, err := c.lc.TrustedHeader(res.Height, time.Now())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "TrustedHeader(%d)", res.Height)
|
||||
}
|
||||
if rH, tH := res.Hash(), h.Hash(); !bytes.Equal(rH, tH) {
|
||||
return nil, errors.Errorf("header %X does not match with trusted header %X",
|
||||
rH, tH)
|
||||
@@ -296,15 +286,12 @@ func (c *Client) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) {
|
||||
}
|
||||
|
||||
// Update the light client if we're behind.
|
||||
if err := c.updateLiteClientIfNeededTo(res.Height); err != nil {
|
||||
h, err := c.updateLiteClientIfNeededTo(res.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate the proof.
|
||||
h, err := c.lc.TrustedHeader(res.Height, time.Now())
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "TrustedHeader(%d)", res.Height)
|
||||
}
|
||||
return res, res.Proof.Validate(h.DataHash)
|
||||
}
|
||||
|
||||
@@ -333,17 +320,21 @@ func (c *Client) UnsubscribeAll(ctx context.Context, subscriber string) error {
|
||||
return c.next.UnsubscribeAll(ctx, subscriber)
|
||||
}
|
||||
|
||||
func (c *Client) updateLiteClientIfNeededTo(height int64) error {
|
||||
func (c *Client) updateLiteClientIfNeededTo(height int64) (*types.SignedHeader, error) {
|
||||
lastTrustedHeight, err := c.lc.LastTrustedHeight()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "LastTrustedHeight")
|
||||
return nil, errors.Wrap(err, "LastTrustedHeight")
|
||||
}
|
||||
|
||||
if lastTrustedHeight < height {
|
||||
if err := c.lc.VerifyHeaderAtHeight(height, time.Now()); err != nil {
|
||||
return errors.Wrapf(err, "VerifyHeaderAtHeight(%d)", height)
|
||||
}
|
||||
return c.lc.VerifyHeaderAtHeight(height, time.Now())
|
||||
}
|
||||
return nil
|
||||
|
||||
h, err := c.lc.TrustedHeader(height, time.Now())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "TrustedHeader(#%d)", height)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (c *Client) RegisterOpDecoder(typ string, dec merkle.OpDecoder) {
|
||||
|
||||
+57
-15
@@ -1,7 +1,6 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -23,39 +22,57 @@ type dbs struct {
|
||||
|
||||
// New returns a Store that wraps any DB (with an optional prefix in case you
|
||||
// want to use one DB with many light clients).
|
||||
//
|
||||
// Objects are marshalled using amino (github.com/tendermint/go-amino)
|
||||
func New(db dbm.DB, prefix string) store.Store {
|
||||
cdc := amino.NewCodec()
|
||||
cryptoAmino.RegisterAmino(cdc)
|
||||
return &dbs{db: db, prefix: prefix, cdc: cdc}
|
||||
}
|
||||
|
||||
func (s *dbs) SaveSignedHeader(sh *types.SignedHeader) error {
|
||||
// SaveSignedHeaderAndNextValidatorSet persists SignedHeader and ValidatorSet
|
||||
// to the db.
|
||||
func (s *dbs) SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error {
|
||||
if sh.Height <= 0 {
|
||||
panic("negative or zero height")
|
||||
}
|
||||
|
||||
// TODO: batch
|
||||
bz, err := s.cdc.MarshalBinaryLengthPrefixed(sh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.db.Set(s.shKey(sh.Height), bz)
|
||||
|
||||
bz, err = s.cdc.MarshalBinaryLengthPrefixed(valSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.db.Set(s.vsKey(sh.Height+1), bz)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbs) SaveValidatorSet(valSet *types.ValidatorSet, height int64) error {
|
||||
// DeleteSignedHeaderAndNextValidatorSet deletes SignedHeader and ValidatorSet
|
||||
// from the db.
|
||||
func (s *dbs) DeleteSignedHeaderAndNextValidatorSet(height int64) error {
|
||||
if height <= 0 {
|
||||
panic("negative or zero height")
|
||||
}
|
||||
|
||||
bz, err := s.cdc.MarshalBinaryLengthPrefixed(valSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.db.Set(s.vsKey(height), bz)
|
||||
// TODO: batch
|
||||
s.db.Delete(s.shKey(height))
|
||||
s.db.Delete(s.vsKey(height + 1))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignedHeader loads SignedHeader at the given height.
|
||||
func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) {
|
||||
if height <= 0 {
|
||||
panic("negative or zero height")
|
||||
}
|
||||
|
||||
bz := s.db.Get(s.shKey(height))
|
||||
if bz == nil {
|
||||
return nil, nil
|
||||
@@ -66,7 +83,12 @@ func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) {
|
||||
return signedHeader, err
|
||||
}
|
||||
|
||||
// ValidatorSet loads ValidatorSet at the given height.
|
||||
func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) {
|
||||
if height <= 0 {
|
||||
panic("negative or zero height")
|
||||
}
|
||||
|
||||
bz := s.db.Get(s.vsKey(height))
|
||||
if bz == nil {
|
||||
return nil, nil
|
||||
@@ -77,6 +99,7 @@ func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) {
|
||||
return valSet, err
|
||||
}
|
||||
|
||||
// LastSignedHeaderHeight returns the last SignedHeader height stored.
|
||||
func (s *dbs) LastSignedHeaderHeight() (int64, error) {
|
||||
itr := s.db.ReverseIterator(
|
||||
s.shKey(1),
|
||||
@@ -90,20 +113,41 @@ func (s *dbs) LastSignedHeaderHeight() (int64, error) {
|
||||
if ok {
|
||||
return height, nil
|
||||
}
|
||||
itr.Next()
|
||||
}
|
||||
|
||||
return -1, errors.New("no headers found")
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
// FirstSignedHeaderHeight returns the first SignedHeader height stored.
|
||||
func (s *dbs) FirstSignedHeaderHeight() (int64, error) {
|
||||
itr := s.db.Iterator(
|
||||
s.shKey(1),
|
||||
append(s.shKey(1<<63-1), byte(0x00)),
|
||||
)
|
||||
defer itr.Close()
|
||||
|
||||
for itr.Valid() {
|
||||
key := itr.Key()
|
||||
_, height, ok := parseShKey(key)
|
||||
if ok {
|
||||
return height, nil
|
||||
}
|
||||
itr.Next()
|
||||
}
|
||||
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (s *dbs) shKey(height int64) []byte {
|
||||
return []byte(fmt.Sprintf("sh/%s/%010d", s.prefix, height))
|
||||
return []byte(fmt.Sprintf("sh/%s/%020d", s.prefix, height))
|
||||
}
|
||||
|
||||
func (s *dbs) vsKey(height int64) []byte {
|
||||
return []byte(fmt.Sprintf("vs/%s/%010d", s.prefix, height))
|
||||
return []byte(fmt.Sprintf("vs/%s/%020d", s.prefix, height))
|
||||
}
|
||||
|
||||
var keyPattern = regexp.MustCompile(`^(sh|vs)/([^/]*)/([0-9]+)/$`)
|
||||
var keyPattern = regexp.MustCompile(`^(sh|vs)/([^/]*)/([0-9]+)$`)
|
||||
|
||||
func parseKey(key []byte) (part string, prefix string, height int64, ok bool) {
|
||||
submatch := keyPattern.FindSubmatch(key)
|
||||
@@ -112,12 +156,10 @@ func parseKey(key []byte) (part string, prefix string, height int64, ok bool) {
|
||||
}
|
||||
part = string(submatch[1])
|
||||
prefix = string(submatch[2])
|
||||
heightStr := string(submatch[3])
|
||||
heightInt, err := strconv.Atoi(heightStr)
|
||||
height, err := strconv.ParseInt(string(submatch[3]), 10, 64)
|
||||
if err != nil {
|
||||
return "", "", 0, false
|
||||
}
|
||||
height = int64(heightInt)
|
||||
ok = true // good!
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestLast_FirstSignedHeaderHeight(t *testing.T) {
|
||||
dbStore := New(dbm.NewMemDB(), "TestLast_FirstSignedHeaderHeight")
|
||||
|
||||
// Empty store
|
||||
height, err := dbStore.LastSignedHeaderHeight()
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, -1, height)
|
||||
|
||||
height, err = dbStore.FirstSignedHeaderHeight()
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, -1, height)
|
||||
|
||||
// 1 key
|
||||
err = dbStore.SaveSignedHeaderAndNextValidatorSet(
|
||||
&types.SignedHeader{Header: &types.Header{Height: 1}}, &types.ValidatorSet{})
|
||||
require.NoError(t, err)
|
||||
|
||||
height, err = dbStore.LastSignedHeaderHeight()
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, height)
|
||||
|
||||
height, err = dbStore.FirstSignedHeaderHeight()
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, height)
|
||||
}
|
||||
|
||||
func Test_SaveSignedHeaderAndNextValidatorSet(t *testing.T) {
|
||||
dbStore := New(dbm.NewMemDB(), "Test_SaveSignedHeaderAndNextValidatorSet")
|
||||
|
||||
// Empty store
|
||||
h, err := dbStore.SignedHeader(1)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, h)
|
||||
|
||||
valSet, err := dbStore.ValidatorSet(2)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, valSet)
|
||||
|
||||
// 1 key
|
||||
err = dbStore.SaveSignedHeaderAndNextValidatorSet(
|
||||
&types.SignedHeader{Header: &types.Header{Height: 1}}, &types.ValidatorSet{})
|
||||
require.NoError(t, err)
|
||||
|
||||
h, err = dbStore.SignedHeader(1)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, h)
|
||||
|
||||
valSet, err = dbStore.ValidatorSet(2)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, valSet)
|
||||
|
||||
// Empty store
|
||||
err = dbStore.DeleteSignedHeaderAndNextValidatorSet(1)
|
||||
require.NoError(t, err)
|
||||
|
||||
h, err = dbStore.SignedHeader(1)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, h)
|
||||
|
||||
valSet, err = dbStore.ValidatorSet(2)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, valSet)
|
||||
}
|
||||
+13
-6
@@ -4,15 +4,17 @@ import "github.com/tendermint/tendermint/types"
|
||||
|
||||
// Store is anything that can persistenly store headers.
|
||||
type Store interface {
|
||||
// SaveSignedHeader saves a SignedHeader.
|
||||
// SaveSignedHeaderAndNextValidatorSet saves a SignedHeader (h: sh.Height)
|
||||
// and a ValidatorSet (h: sh.Height+1).
|
||||
//
|
||||
// height must be > 0.
|
||||
SaveSignedHeader(sh *types.SignedHeader) error
|
||||
SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error
|
||||
|
||||
// SaveValidatorSet saves a ValidatorSet.
|
||||
// DeleteSignedHeaderAndNextValidatorSet deletes SignedHeader (h: height) and
|
||||
// ValidatorSet (h: height+1).
|
||||
//
|
||||
// height must be > 0.
|
||||
SaveValidatorSet(valSet *types.ValidatorSet, height int64) error
|
||||
DeleteSignedHeaderAndNextValidatorSet(height int64) error
|
||||
|
||||
// SignedHeader returns the SignedHeader that corresponds to the given
|
||||
// height.
|
||||
@@ -31,8 +33,13 @@ type Store interface {
|
||||
// is returned.
|
||||
ValidatorSet(height int64) (*types.ValidatorSet, error)
|
||||
|
||||
// LastSignedHeaderHeight returns the last SignedHeader height.
|
||||
// LastSignedHeaderHeight returns the last (newest) SignedHeader height.
|
||||
//
|
||||
// If the store is empty, an error is returned.
|
||||
// If the store is empty, -1 and nil error are returned.
|
||||
LastSignedHeaderHeight() (int64, error)
|
||||
|
||||
// FirstSignedHeaderHeight returns the first (oldest) SignedHeader height.
|
||||
//
|
||||
// If the store is empty, -1 and nil error are returned.
|
||||
FirstSignedHeaderHeight() (int64, error)
|
||||
}
|
||||
|
||||
+25
-26
@@ -5,7 +5,6 @@ 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"
|
||||
@@ -29,34 +28,34 @@ func genPrivKeys(n int) privKeys {
|
||||
return res
|
||||
}
|
||||
|
||||
// Change replaces the key at index i.
|
||||
func (pkz privKeys) Change(i int) privKeys {
|
||||
res := make(privKeys, len(pkz))
|
||||
copy(res, pkz)
|
||||
res[i] = ed25519.GenPrivKey()
|
||||
return res
|
||||
}
|
||||
// // Change replaces the key at index i.
|
||||
// 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)
|
||||
return append(pkz, extra...)
|
||||
}
|
||||
// // Extend adds n more keys (to remove, just take a slice).
|
||||
// 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)
|
||||
for i := range res {
|
||||
res[i] = secp256k1.GenPrivKey()
|
||||
}
|
||||
return res
|
||||
}
|
||||
// // GenSecpPrivKeys produces an array of secp256k1 private keys to generate commits.
|
||||
// func GenSecpPrivKeys(n int) privKeys {
|
||||
// res := make(privKeys, n)
|
||||
// for i := range res {
|
||||
// res[i] = secp256k1.GenPrivKey()
|
||||
// }
|
||||
// return res
|
||||
// }
|
||||
|
||||
// ExtendSecp adds n more secp256k1 keys (to remove, just take a slice).
|
||||
func (pkz privKeys) ExtendSecp(n int) privKeys {
|
||||
extra := GenSecpPrivKeys(n)
|
||||
return append(pkz, extra...)
|
||||
}
|
||||
// // ExtendSecp adds n more secp256k1 keys (to remove, just take a slice).
|
||||
// func (pkz privKeys) ExtendSecp(n int) privKeys {
|
||||
// extra := GenSecpPrivKeys(n)
|
||||
// return append(pkz, extra...)
|
||||
// }
|
||||
|
||||
// ToValidators produces a valset from the set of keys.
|
||||
// The first key has weight `init` and it increases by `inc` every step
|
||||
|
||||
+26
-6
@@ -16,6 +16,15 @@ var (
|
||||
DefaultTrustLevel = tmmath.Fraction{Numerator: 1, Denominator: 3}
|
||||
)
|
||||
|
||||
// Verify verifies the new header (h2) against the old header (h1). It ensures that:
|
||||
//
|
||||
// a) h1 can still be trusted (if not, ErrOldHeaderExpired is returned);
|
||||
// b) h2 is valid (if not, ErrInvalidNewHeader is returned);
|
||||
// c) either h2.ValidatorsHash equals h1NextVals.Hash()
|
||||
// OR trustLevel ([1/3, 1]) of last trusted validators (h1NextVals) signed
|
||||
// correctly (if not, ErrNewValSetCantBeTrusted is returned);
|
||||
// c) more than 2/3 of new validators (h2Vals) have signed h2 (if not,
|
||||
// ErrNotEnoughVotingPowerSigned is returned).
|
||||
func Verify(
|
||||
chainID string,
|
||||
h1 *types.SignedHeader,
|
||||
@@ -31,9 +40,8 @@ func Verify(
|
||||
}
|
||||
|
||||
// Ensure last header can still be trusted.
|
||||
expirationTime := h1.Time.Add(trustingPeriod)
|
||||
if !expirationTime.After(now) {
|
||||
return ErrOldHeaderExpired{expirationTime, now}
|
||||
if HeaderExpired(h1, trustingPeriod, now) {
|
||||
return ErrOldHeaderExpired{h1.Time.Add(trustingPeriod), now}
|
||||
}
|
||||
|
||||
if err := verifyNewHeaderAndVals(chainID, h2, h2Vals, h1, now); err != nil {
|
||||
@@ -42,16 +50,22 @@ func Verify(
|
||||
|
||||
if h2.Height == h1.Height+1 {
|
||||
if !bytes.Equal(h2.ValidatorsHash, h1NextVals.Hash()) {
|
||||
return errors.Errorf("expected old header validators (%X) to match those from new header (%X)",
|
||||
err := errors.Errorf("expected old header next validators (%X) to match those from new header (%X)",
|
||||
h1NextVals.Hash(),
|
||||
h2.ValidatorsHash,
|
||||
)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly.
|
||||
err := h1NextVals.VerifyCommitTrusting(chainID, h2.Commit.BlockID, h2.Height, h2.Commit, trustLevel)
|
||||
if err != nil {
|
||||
return err
|
||||
switch e := err.(type) {
|
||||
case types.ErrNotEnoughVotingPowerSigned:
|
||||
return ErrNewValSetCantBeTrusted{e}
|
||||
default:
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +109,8 @@ func verifyNewHeaderAndVals(
|
||||
|
||||
if !bytes.Equal(h2.ValidatorsHash, h2Vals.Hash()) {
|
||||
return errors.Errorf("expected new header validators (%X) to match those that were supplied (%X)",
|
||||
h2.ValidatorsHash,
|
||||
h2Vals.Hash(),
|
||||
h2.NextValidatorsHash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -114,3 +128,9 @@ func ValidateTrustLevel(lvl tmmath.Fraction) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HeaderExpired return true if the given header expired.
|
||||
func HeaderExpired(h *types.SignedHeader, trustingPeriod time.Duration, now time.Time) bool {
|
||||
expirationTime := h.Time.Add(trustingPeriod)
|
||||
return !expirationTime.After(now)
|
||||
}
|
||||
|
||||
+55
-14
@@ -11,9 +11,9 @@ import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestVerifyAdjustedHeaders(t *testing.T) {
|
||||
func TestVerifyAdjacentHeaders(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestVerifyAdjustedHeaders"
|
||||
chainID = "TestVerifyAdjacentHeaders"
|
||||
lastHeight = 1
|
||||
nextHeight = 2
|
||||
)
|
||||
@@ -52,10 +52,30 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
|
||||
3 * time.Hour,
|
||||
bTime.Add(2 * time.Hour),
|
||||
nil,
|
||||
"h2.ValidateBasic failed: signedHeader belongs to another chain 'different-chainID' not 'TestVerifyAdjustedHeaders'",
|
||||
"h2.ValidateBasic failed: signedHeader belongs to another chain 'different-chainID' not 'TestVerifyAdjacentHeaders'",
|
||||
},
|
||||
// new header's time is before old header's time -> error
|
||||
2: {
|
||||
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(-1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
vals,
|
||||
3 * time.Hour,
|
||||
bTime.Add(2 * time.Hour),
|
||||
nil,
|
||||
"to be after old header time",
|
||||
},
|
||||
// new header's time is from the future -> error
|
||||
3: {
|
||||
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(3*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
vals,
|
||||
3 * time.Hour,
|
||||
bTime.Add(2 * time.Hour),
|
||||
nil,
|
||||
"new header has a time from the future",
|
||||
},
|
||||
// 3/3 signed -> no error
|
||||
2: {
|
||||
4: {
|
||||
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
vals,
|
||||
@@ -65,7 +85,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
|
||||
"",
|
||||
},
|
||||
// 2/3 signed -> no error
|
||||
3: {
|
||||
5: {
|
||||
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 1, len(keys)),
|
||||
vals,
|
||||
@@ -75,17 +95,17 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
|
||||
"",
|
||||
},
|
||||
// 1/3 signed -> error
|
||||
4: {
|
||||
6: {
|
||||
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), len(keys)-1, len(keys)),
|
||||
vals,
|
||||
3 * time.Hour,
|
||||
bTime.Add(2 * time.Hour),
|
||||
types.ErrTooMuchChange{Got: 50, Needed: 93},
|
||||
types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93},
|
||||
"",
|
||||
},
|
||||
// vals does not match with what we have -> error
|
||||
5: {
|
||||
7: {
|
||||
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, keys.ToValidators(10, 1), vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
keys.ToValidators(10, 1),
|
||||
@@ -95,7 +115,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
|
||||
"to match those from new header",
|
||||
},
|
||||
// vals are inconsistent with newHeader -> error
|
||||
6: {
|
||||
8: {
|
||||
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
keys.ToValidators(10, 1),
|
||||
@@ -105,7 +125,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
|
||||
"to match those that were supplied",
|
||||
},
|
||||
// old header has expired -> error
|
||||
7: {
|
||||
9: {
|
||||
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
|
||||
keys.ToValidators(10, 1),
|
||||
@@ -131,11 +151,12 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestVerifyNonAdjustedHeaders(t *testing.T) {
|
||||
func TestVerifyNonAdjacentHeaders(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestVerifyNonAdjustedHeaders"
|
||||
chainID = "TestVerifyNonAdjacentHeaders"
|
||||
lastHeight = 1
|
||||
)
|
||||
|
||||
@@ -195,7 +216,7 @@ func TestVerifyNonAdjustedHeaders(t *testing.T) {
|
||||
vals,
|
||||
3 * time.Hour,
|
||||
bTime.Add(2 * time.Hour),
|
||||
types.ErrTooMuchChange{Got: 50, Needed: 93},
|
||||
types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93},
|
||||
"",
|
||||
},
|
||||
// 3/3 new vals signed, 2/3 old vals present -> no error
|
||||
@@ -225,7 +246,7 @@ func TestVerifyNonAdjustedHeaders(t *testing.T) {
|
||||
lessThanOneThirdVals,
|
||||
3 * time.Hour,
|
||||
bTime.Add(2 * time.Hour),
|
||||
types.ErrTooMuchChange{Got: 20, Needed: 46},
|
||||
ErrNewValSetCantBeTrusted{types.ErrNotEnoughVotingPowerSigned{Got: 20, Needed: 46}},
|
||||
"",
|
||||
},
|
||||
}
|
||||
@@ -247,6 +268,26 @@ func TestVerifyNonAdjustedHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyReturnsErrorIfTrustLevelIsInvalid(t *testing.T) {
|
||||
const (
|
||||
chainID = "TestVerifyReturnsErrorIfTrustLevelIsInvalid"
|
||||
lastHeight = 1
|
||||
)
|
||||
|
||||
var (
|
||||
keys = genPrivKeys(4)
|
||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do!
|
||||
vals = keys.ToValidators(20, 10)
|
||||
bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
|
||||
header = keys.GenSignedHeader(chainID, lastHeight, bTime, nil, vals, vals,
|
||||
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys))
|
||||
)
|
||||
|
||||
err := Verify(chainID, header, vals, header, vals, 2*time.Hour, time.Now(),
|
||||
tmmath.Fraction{Numerator: 2, Denominator: 1})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestValidateTrustLevel(t *testing.T) {
|
||||
testCases := []struct {
|
||||
lvl tmmath.Fraction
|
||||
|
||||
+29
-17
@@ -631,7 +631,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID,
|
||||
if vals.Size() != len(commit.Signatures) {
|
||||
return NewErrInvalidCommitSignatures(vals.Size(), len(commit.Signatures))
|
||||
}
|
||||
if err := vals.verifyCommitBasic(commit, height, blockID); err != nil {
|
||||
if err := verifyCommitBasic(commit, height, blockID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -661,7 +661,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID,
|
||||
}
|
||||
|
||||
if got, needed := talliedVotingPower, vals.TotalVotingPower()*2/3; got <= needed {
|
||||
return ErrTooMuchChange{Got: got, Needed: needed}
|
||||
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -738,7 +738,7 @@ func (vals *ValidatorSet) VerifyFutureCommit(newSet *ValidatorSet, chainID strin
|
||||
}
|
||||
|
||||
if got, needed := oldVotingPower, oldVals.TotalVotingPower()*2/3; got <= needed {
|
||||
return ErrTooMuchChange{Got: got, Needed: needed}
|
||||
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -755,11 +755,15 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID,
|
||||
panic(fmt.Sprintf("trustLevel must be within [1/3, 1], given %v", trustLevel))
|
||||
}
|
||||
|
||||
if err := vals.verifyCommitBasic(commit, height, blockID); err != nil {
|
||||
if err := verifyCommitBasic(commit, height, blockID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
talliedVotingPower := int64(0)
|
||||
var (
|
||||
talliedVotingPower int64
|
||||
seenVals = make(map[int]int, len(commit.Signatures)) // validator index -> commit index
|
||||
)
|
||||
|
||||
for idx, commitSig := range commit.Signatures {
|
||||
if commitSig.Absent() {
|
||||
continue // OK, some signatures can be absent.
|
||||
@@ -767,8 +771,16 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID,
|
||||
|
||||
// We don't know the validators that committed this block, so we have to
|
||||
// check for each vote if its validator is already known.
|
||||
_, val := vals.GetByAddress(commitSig.ValidatorAddress)
|
||||
valIdx, val := vals.GetByAddress(commitSig.ValidatorAddress)
|
||||
|
||||
if firstIndex, ok := seenVals[valIdx]; ok { // double vote
|
||||
secondIndex := idx
|
||||
return errors.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex)
|
||||
}
|
||||
|
||||
if val != nil {
|
||||
seenVals[valIdx] = idx
|
||||
|
||||
// Validate signature.
|
||||
voteSignBytes := commit.VoteSignBytes(chainID, idx)
|
||||
if !val.PubKey.VerifyBytes(voteSignBytes, commitSig.Signature) {
|
||||
@@ -789,13 +801,13 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID,
|
||||
got := talliedVotingPower
|
||||
needed := (vals.TotalVotingPower() * trustLevel.Numerator) / trustLevel.Denominator
|
||||
if got <= needed {
|
||||
return ErrTooMuchChange{Got: got, Needed: needed}
|
||||
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vals *ValidatorSet) verifyCommitBasic(commit *Commit, height int64, blockID BlockID) error {
|
||||
func verifyCommitBasic(commit *Commit, height int64, blockID BlockID) error {
|
||||
if err := commit.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -810,23 +822,23 @@ func (vals *ValidatorSet) verifyCommitBasic(commit *Commit, height int64, blockI
|
||||
}
|
||||
|
||||
//-----------------
|
||||
// ErrTooMuchChange
|
||||
|
||||
// IsErrTooMuchChange returns true if err is related to changes in validator
|
||||
// set exceeding max limit.
|
||||
func IsErrTooMuchChange(err error) bool {
|
||||
_, ok := errors.Cause(err).(ErrTooMuchChange)
|
||||
// IsErrNotEnoughVotingPowerSigned returns true if err is
|
||||
// ErrNotEnoughVotingPowerSigned.
|
||||
func IsErrNotEnoughVotingPowerSigned(err error) bool {
|
||||
_, ok := errors.Cause(err).(ErrNotEnoughVotingPowerSigned)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrTooMuchChange indicates that changes in the validator set exceeded max limit.
|
||||
type ErrTooMuchChange struct {
|
||||
// ErrNotEnoughVotingPowerSigned is returned when not enough validators signed
|
||||
// a commit.
|
||||
type ErrNotEnoughVotingPowerSigned struct {
|
||||
Got int64
|
||||
Needed int64
|
||||
}
|
||||
|
||||
func (e ErrTooMuchChange) Error() string {
|
||||
return fmt.Sprintf("invalid commit -- insufficient old voting power: got %d, needed more than %d", e.Got, e.Needed)
|
||||
func (e ErrNotEnoughVotingPowerSigned) Error() string {
|
||||
return fmt.Sprintf("invalid commit -- insufficient voting power: got %d, needed more than %d", e.Got, e.Needed)
|
||||
}
|
||||
|
||||
//----------------
|
||||
|
||||
Reference in New Issue
Block a user