Browse Source

lite: follow up from #3989 (#4209)

* 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>
pull/4301/head
Anton Kaliaev 5 years ago
committed by GitHub
parent
commit
86adc2c89f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1371 additions and 277 deletions
  1. +3
    -3
      lite/dynamic_verifier.go
  2. +15
    -15
      lite2/auto_client.go
  3. +76
    -0
      lite2/auto_client_test.go
  4. +307
    -48
      lite2/client.go
  5. +420
    -2
      lite2/client_test.go
  6. +76
    -0
      lite2/doc.go
  7. +12
    -0
      lite2/errors.go
  8. +153
    -93
      lite2/example_test.go
  9. +5
    -0
      lite2/provider/http/http.go
  10. +20
    -29
      lite2/rpc/client.go
  11. +57
    -15
      lite2/store/db/db.go
  12. +76
    -0
      lite2/store/db/db_test.go
  13. +13
    -6
      lite2/store/store.go
  14. +28
    -29
      lite2/test_helpers.go
  15. +26
    -6
      lite2/verifier.go
  16. +55
    -14
      lite2/verifier_test.go
  17. +29
    -17
      types/validator_set.go

+ 3
- 3
lite/dynamic_verifier.go View File

@ -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 // 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. // 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(). // Panics if trustedFC.Height() >= sourceFC.Height().
func (dv *DynamicVerifier) verifyAndSave(trustedFC, sourceFC FullCommit) error { func (dv *DynamicVerifier) verifyAndSave(trustedFC, sourceFC FullCommit) error {
if trustedFC.Height() >= sourceFC.Height() { if trustedFC.Height() >= sourceFC.Height() {
@ -247,8 +247,8 @@ FOR_LOOP:
return sourceFC, nil 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. // Divide and conquer.
start, end := trustedFC.Height(), sourceFC.Height() start, end := trustedFC.Height(), sourceFC.Height()
if !(start < end) { if !(start < end) {


+ 15
- 15
lite2/auto_client.go View File

@ -13,7 +13,7 @@ type AutoClient struct {
quit chan struct{} quit chan struct{}
trustedHeaders chan *types.SignedHeader trustedHeaders chan *types.SignedHeader
err chan error
errs chan error
} }
// NewAutoClient creates a new client and starts a polling goroutine. // NewAutoClient creates a new client and starts a polling goroutine.
@ -23,7 +23,7 @@ func NewAutoClient(base *Client, updatePeriod time.Duration) *AutoClient {
updatePeriod: updatePeriod, updatePeriod: updatePeriod,
quit: make(chan struct{}), quit: make(chan struct{}),
trustedHeaders: make(chan *types.SignedHeader), trustedHeaders: make(chan *types.SignedHeader),
err: make(chan error),
errs: make(chan error),
} }
go c.autoUpdate() go c.autoUpdate()
return c return c
@ -35,8 +35,8 @@ func (c *AutoClient) TrustedHeaders() <-chan *types.SignedHeader {
} }
// Err returns a channel onto which errors are posted. // 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. // Stop stops the client.
@ -45,30 +45,30 @@ func (c *AutoClient) Stop() {
} }
func (c *AutoClient) autoUpdate() { func (c *AutoClient) autoUpdate() {
lastTrustedHeight, err := c.base.LastTrustedHeight()
if err != nil {
c.err <- err
return
}
ticker := time.NewTicker(c.updatePeriod) ticker := time.NewTicker(c.updatePeriod)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
err := c.base.VerifyHeaderAtHeight(lastTrustedHeight+1, time.Now())
lastTrustedHeight, err := c.base.LastTrustedHeight()
if err != nil { if err != nil {
c.err <- err
c.errs <- err
continue
}
if lastTrustedHeight == -1 {
// no headers yet => wait
continue continue
} }
h, err := c.base.TrustedHeader(lastTrustedHeight+1, time.Now())
h, err := c.base.VerifyHeaderAtHeight(lastTrustedHeight+1, time.Now())
if err != nil { if err != nil {
c.err <- err
// no header yet or verification error => try again after updatePeriod
c.errs <- err
continue continue
} }
c.trustedHeaders <- h c.trustedHeaders <- h
lastTrustedHeight = h.Height
case <-c.quit: case <-c.quit:
return return
} }


+ 76
- 0
lite2/auto_client_test.go View File

@ -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
lite2/client.go View File

@ -47,14 +47,16 @@ type mode byte
const ( const (
sequential mode = iota + 1 sequential mode = iota + 1
skipping skipping
defaultRemoveNoLongerTrustedHeadersPeriod = 24 * time.Hour
) )
// Option sets a parameter for the light client. // Option sets a parameter for the light client.
type Option func(*Client) type Option func(*Client)
// SequentialVerification option configures the light client to sequentially // 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 { func SequentialVerification() Option {
return func(c *Client) { return func(c *Client) {
c.verificationMode = sequential c.verificationMode = sequential
@ -68,7 +70,7 @@ func SequentialVerification() Option {
// //
// trustLevel - fraction of the old validator set (in terms of voting power), // 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 // 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. // verification is used.
func SkippingVerification(trustLevel tmmath.Fraction) Option { func SkippingVerification(trustLevel tmmath.Fraction) Option {
if err := ValidateTrustLevel(trustLevel); err != nil { 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 // Client represents a light client, connected to a single chain, which gets
// headers from a primary provider, verifies them either sequentially or by // headers from a primary provider, verifies them either sequentially or by
// skipping some and stores them in a trusted store (usually, a local FS). // 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). // Highest next validator set from the store (height=H+1).
trustedNextVals *types.ValidatorSet trustedNextVals *types.ValidatorSet
removeNoLongerTrustedHeadersPeriod time.Duration
confirmationFn func(action string) bool
quit chan struct{}
logger log.Logger logger log.Logger
} }
@ -129,26 +156,136 @@ func NewClient(
options ...Option) (*Client, error) { options ...Option) (*Client, error) {
c := &Client{ 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 { for _, o := range options {
o(c) o(c)
} }
if err := c.initializeWithTrustOptions(trustOptions); err != nil {
if err := c.restoreTrustedHeaderAndNextVals(); err != nil {
return nil, err 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 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 { func (c *Client) initializeWithTrustOptions(options TrustOptions) error {
// 1) Fetch and verify the header. // 1) Fetch and verify the header.
h, err := c.primary.SignedHeader(options.Height) 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. // NOTE: Verify func will check if it's expired or not.
if err := h.ValidateBasic(c.chainID); err != nil { if err := h.ValidateBasic(c.chainID); err != nil {
return errors.Wrap(err, "ValidateBasic failed")
return err
} }
if !bytes.Equal(h.Hash(), options.Hash) { if !bytes.Equal(h.Hash(), options.Hash) {
return errors.Errorf("expected header's hash %X, but got %X", options.Hash, h.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 { if err != nil {
return err 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. // 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) // TrustedHeader returns a trusted header at the given height (0 - the latest)
// or nil if no such header exist. // 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. // height must be >= 0.
// //
// It returns an error if: // 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) { func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) {
if height < 0 { if height < 0 {
return nil, errors.New("negative height") return nil, errors.New("negative height")
@ -208,22 +370,25 @@ func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader
if err != nil { if err != nil {
return nil, err return nil, err
} }
if h == nil {
return nil, nil
}
// Ensure header can still be trusted. // 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 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) { func (c *Client) LastTrustedHeight() (int64, error) {
return c.trustedStore.LastSignedHeaderHeight() 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 { func (c *Client) ChainID() string {
return c.chainID return c.chainID
} }
@ -232,18 +397,18 @@ func (c *Client) ChainID() string {
// and calls VerifyHeader. // and calls VerifyHeader.
// //
// If the trusted header is more recent than one here, an error is returned. // 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 { 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. // Request the header and the vals.
newHeader, newVals, err := c.fetchHeaderAndValsAtHeight(height) newHeader, newVals, err := c.fetchHeaderAndValsAtHeight(height)
if err != nil { 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. // 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 // SkippingVerification(trustLevel): verifies that {trustLevel} of the trusted
// validator set has signed the new header. If it's not the case and the // 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) // 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. // 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 { func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error {
if c.trustedHeader.Height >= newHeader.Height { 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 { if len(c.alternatives) > 0 {
@ -291,6 +456,44 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali
return c.updateTrustedHeaderAndVals(newHeader, nextVals) 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 { func (c *Client) sequence(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error {
// 1) Verify any intermediate headers. // 1) Verify any intermediate headers.
var ( 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) return Verify(c.chainID, c.trustedHeader, c.trustedNextVals, newHeader, newVals, c.trustingPeriod, now, c.trustLevel)
} }
// see VerifyHeader
func (c *Client) bisection( func (c *Client) bisection(
lastHeader *types.SignedHeader, lastHeader *types.SignedHeader,
lastVals *types.ValidatorSet, lastVals *types.ValidatorSet,
@ -340,15 +544,10 @@ func (c *Client) bisection(
switch err.(type) { switch err.(type) {
case nil: case nil:
return nil return nil
case types.ErrTooMuchChange:
case ErrNewValSetCantBeTrusted:
// continue bisection // continue bisection
default: 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 pivot := (c.trustedHeader.Height + newHeader.Header.Height) / 2
@ -392,22 +591,23 @@ func (c *Client) bisection(
return nil 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") 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.trustedHeader = h
c.trustedNextVals = vals
c.trustedNextVals = nextVals
return nil return nil
} }
// fetch header and validators for the given height from primary provider.
func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, *types.ValidatorSet, error) { func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, *types.ValidatorSet, error) {
h, err := c.primary.SignedHeader(height) h, err := c.primary.SignedHeader(height)
if err != nil { if err != nil {
@ -420,6 +620,7 @@ func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader,
return h, vals, nil return h, vals, nil
} }
// compare header with one from a random alternative provider.
func (c *Client) compareNewHeaderWithRandomAlternative(h *types.SignedHeader) error { func (c *Client) compareNewHeaderWithRandomAlternative(h *types.SignedHeader) error {
// 1. Pick an alternative provider. // 1. Pick an alternative provider.
p := c.alternatives[tmrand.Intn(len(c.alternatives))] p := c.alternatives[tmrand.Intn(len(c.alternatives))]
@ -442,3 +643,61 @@ func (c *Client) compareNewHeaderWithRandomAlternative(h *types.SignedHeader) er
return nil 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
lite2/client_test.go View File

@ -9,6 +9,7 @@ import (
dbm "github.com/tendermint/tm-db" dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/libs/log"
mockp "github.com/tendermint/tendermint/lite2/provider/mock" mockp "github.com/tendermint/tendermint/lite2/provider/mock"
dbs "github.com/tendermint/tendermint/lite2/store/db" dbs "github.com/tendermint/tendermint/lite2/store/db"
"github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/types"
@ -135,8 +136,9 @@ func TestClient_SequentialVerification(t *testing.T) {
} else { } else {
require.NoError(t, err) 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 { if tc.verifyErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
@ -232,8 +234,9 @@ func TestClient_SkippingVerification(t *testing.T) {
} else { } else {
require.NoError(t, err) 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 { if tc.verifyErr {
assert.Error(t, err) assert.Error(t, err)
} else { } 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)
}
}

+ 76
- 0
lite2/doc.go View File

@ -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 NOTE: Tendermint provides a somewhat different (stronger) light client model
than Bitcoin under eclipse, since the eclipsing node(s) can only fool the light 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. 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 package lite

+ 12
- 0
lite2/errors.go View File

@ -3,6 +3,8 @@ package lite
import ( import (
"fmt" "fmt"
"time" "time"
"github.com/tendermint/tendermint/types"
) )
// ErrOldHeaderExpired means the old (trusted) header has expired according to // ErrOldHeaderExpired means the old (trusted) header has expired according to
@ -16,3 +18,13 @@ type ErrOldHeaderExpired struct {
func (e ErrOldHeaderExpired) Error() string { func (e ErrOldHeaderExpired) Error() string {
return fmt.Sprintf("old header has expired at %v (now: %v)", e.At, e.Now) 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)
}

+ 153
- 93
lite2/example_test.go View File

@ -1,95 +1,155 @@
package lite 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)
// // TODO: fetch the "trusted" header from a node
// header := (*types.SignedHeader)(nil)
// /////////////////////////////////////////////////////////////////////////////
// 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)
// // verify some data
//}
//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)
// // TODO: fetch the "trusted" header from a node
// header := (*types.SignedHeader)(nil)
// /////////////////////////////////////////////////////////////////////////////
// db, err := dbm.NewGoLevelDB("lite-client-db", dbDir)
// if err != nil {
// // return err
// t.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 := NewAutoClient(base, 1*time.Second)
// defer c.Stop()
// 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)
// }
// }
//}
import (
"fmt"
"io/ioutil"
stdlog "log"
"os"
"testing"
"time"
"github.com/pkg/errors"
dbm "github.com/tendermint/tm-db"
"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"
)
func TestExample_Client(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)
}
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())
_, err = c.VerifyHeaderAtHeight(3, time.Now())
if err != nil {
stdlog.Fatal(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)
}

+ 5
- 0
lite2/provider/http/http.go View File

@ -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 { func (p *http) ChainID() string {
return p.chainID 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) { func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) {
h, err := validateHeight(height) h, err := validateHeight(height)
if err != nil { if err != nil {
@ -62,6 +65,8 @@ func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) {
return &commit.SignedHeader, nil 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) { func (p *http) ValidatorSet(height int64) (*types.ValidatorSet, error) {
h, err := validateHeight(height) h, err := validateHeight(height)
if err != nil { if err != nil {


+ 20
- 29
lite2/rpc/client.go View File

@ -90,14 +90,10 @@ func (c *Client) ABCIQueryWithOptions(path string, data tmbytes.HexBytes,
} }
// Update the light client if we're behind. // 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 { if err != nil {
return nil, errors.Wrapf(err, "TrustedHeader(%d)", resp.Height+1)
return nil, err
} }
// Validate the value proof against the trusted header. // 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. // Update the light client if we're behind.
if len(res.BlockMetas) > 0 { if len(res.BlockMetas) > 0 {
lastHeight := res.BlockMetas[len(res.BlockMetas)-1].Header.Height 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 return nil, err
} }
} }
@ -232,15 +228,12 @@ func (c *Client) Block(height *int64) (*ctypes.ResultBlock, error) {
} }
// Update the light client if we're behind. // 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 return nil, err
} }
// Verify block. // 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) { 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", return nil, errors.Errorf("Block#Header %X does not match with trusted header %X",
bH, tH) bH, tH)
@ -265,15 +258,12 @@ func (c *Client) Commit(height *int64) (*ctypes.ResultCommit, error) {
} }
// Update the light client if we're behind. // 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 return nil, err
} }
// Verify commit. // 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) { if rH, tH := res.Hash(), h.Hash(); !bytes.Equal(rH, tH) {
return nil, errors.Errorf("header %X does not match with trusted header %X", return nil, errors.Errorf("header %X does not match with trusted header %X",
rH, tH) 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. // 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 return nil, err
} }
// Validate the proof. // 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) 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) 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() lastTrustedHeight, err := c.lc.LastTrustedHeight()
if err != nil { if err != nil {
return errors.Wrap(err, "LastTrustedHeight")
return nil, errors.Wrap(err, "LastTrustedHeight")
} }
if lastTrustedHeight < height { 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) { func (c *Client) RegisterOpDecoder(typ string, dec merkle.OpDecoder) {


+ 57
- 15
lite2/store/db/db.go View File

@ -1,7 +1,6 @@
package db package db
import ( import (
"errors"
"fmt" "fmt"
"regexp" "regexp"
"strconv" "strconv"
@ -23,39 +22,57 @@ type dbs struct {
// New returns a Store that wraps any DB (with an optional prefix in case you // New returns a Store that wraps any DB (with an optional prefix in case you
// want to use one DB with many light clients). // 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 { func New(db dbm.DB, prefix string) store.Store {
cdc := amino.NewCodec() cdc := amino.NewCodec()
cryptoAmino.RegisterAmino(cdc) cryptoAmino.RegisterAmino(cdc)
return &dbs{db: db, prefix: prefix, cdc: 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 { if sh.Height <= 0 {
panic("negative or zero height") panic("negative or zero height")
} }
// TODO: batch
bz, err := s.cdc.MarshalBinaryLengthPrefixed(sh) bz, err := s.cdc.MarshalBinaryLengthPrefixed(sh)
if err != nil { if err != nil {
return err return err
} }
s.db.Set(s.shKey(sh.Height), bz) 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 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 { if height <= 0 {
panic("negative or zero height") 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 return nil
} }
// SignedHeader loads SignedHeader at the given height.
func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) { func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) {
if height <= 0 {
panic("negative or zero height")
}
bz := s.db.Get(s.shKey(height)) bz := s.db.Get(s.shKey(height))
if bz == nil { if bz == nil {
return nil, nil return nil, nil
@ -66,7 +83,12 @@ func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) {
return signedHeader, err return signedHeader, err
} }
// ValidatorSet loads ValidatorSet at the given height.
func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) { func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) {
if height <= 0 {
panic("negative or zero height")
}
bz := s.db.Get(s.vsKey(height)) bz := s.db.Get(s.vsKey(height))
if bz == nil { if bz == nil {
return nil, nil return nil, nil
@ -77,6 +99,7 @@ func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) {
return valSet, err return valSet, err
} }
// LastSignedHeaderHeight returns the last SignedHeader height stored.
func (s *dbs) LastSignedHeaderHeight() (int64, error) { func (s *dbs) LastSignedHeaderHeight() (int64, error) {
itr := s.db.ReverseIterator( itr := s.db.ReverseIterator(
s.shKey(1), s.shKey(1),
@ -90,20 +113,41 @@ func (s *dbs) LastSignedHeaderHeight() (int64, error) {
if ok { if ok {
return height, nil return height, nil
} }
itr.Next()
}
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, errors.New("no headers found")
return -1, nil
} }
func (s *dbs) shKey(height int64) []byte { 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 { 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) { func parseKey(key []byte) (part string, prefix string, height int64, ok bool) {
submatch := keyPattern.FindSubmatch(key) submatch := keyPattern.FindSubmatch(key)
@ -112,12 +156,10 @@ func parseKey(key []byte) (part string, prefix string, height int64, ok bool) {
} }
part = string(submatch[1]) part = string(submatch[1])
prefix = string(submatch[2]) 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 { if err != nil {
return "", "", 0, false return "", "", 0, false
} }
height = int64(heightInt)
ok = true // good! ok = true // good!
return return
} }


+ 76
- 0
lite2/store/db/db_test.go View File

@ -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
lite2/store/store.go View File

@ -4,15 +4,17 @@ import "github.com/tendermint/tendermint/types"
// Store is anything that can persistenly store headers. // Store is anything that can persistenly store headers.
type Store interface { 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. // 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. // height must be > 0.
SaveValidatorSet(valSet *types.ValidatorSet, height int64) error
DeleteSignedHeaderAndNextValidatorSet(height int64) error
// SignedHeader returns the SignedHeader that corresponds to the given // SignedHeader returns the SignedHeader that corresponds to the given
// height. // height.
@ -31,8 +33,13 @@ type Store interface {
// is returned. // is returned.
ValidatorSet(height int64) (*types.ValidatorSet, error) 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) LastSignedHeaderHeight() (int64, error)
// FirstSignedHeaderHeight returns the first (oldest) SignedHeader height.
//
// If the store is empty, -1 and nil error are returned.
FirstSignedHeaderHeight() (int64, error)
} }

+ 28
- 29
lite2/test_helpers.go View File

@ -5,7 +5,6 @@ import (
"github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/types"
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
@ -29,34 +28,34 @@ func genPrivKeys(n int) privKeys {
return res 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...)
}
// 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...)
}
// // 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...)
// }
// // 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...)
// }
// ToValidators produces a valset from the set of keys. // ToValidators produces a valset from the set of keys.
// The first key has weight `init` and it increases by `inc` every step // The first key has weight `init` and it increases by `inc` every step


+ 26
- 6
lite2/verifier.go View File

@ -16,6 +16,15 @@ var (
DefaultTrustLevel = tmmath.Fraction{Numerator: 1, Denominator: 3} 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( func Verify(
chainID string, chainID string,
h1 *types.SignedHeader, h1 *types.SignedHeader,
@ -31,9 +40,8 @@ func Verify(
} }
// Ensure last header can still be trusted. // 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 { if err := verifyNewHeaderAndVals(chainID, h2, h2Vals, h1, now); err != nil {
@ -42,16 +50,22 @@ func Verify(
if h2.Height == h1.Height+1 { if h2.Height == h1.Height+1 {
if !bytes.Equal(h2.ValidatorsHash, h1NextVals.Hash()) { 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(), h1NextVals.Hash(),
h2.ValidatorsHash, h2.ValidatorsHash,
) )
return err
} }
} else { } else {
// Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly. // 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) err := h1NextVals.VerifyCommitTrusting(chainID, h2.Commit.BlockID, h2.Height, h2.Commit, trustLevel)
if err != nil { 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()) { if !bytes.Equal(h2.ValidatorsHash, h2Vals.Hash()) {
return errors.Errorf("expected new header validators (%X) to match those that were supplied (%X)", return errors.Errorf("expected new header validators (%X) to match those that were supplied (%X)",
h2.ValidatorsHash,
h2Vals.Hash(), h2Vals.Hash(),
h2.NextValidatorsHash,
) )
} }
@ -114,3 +128,9 @@ func ValidateTrustLevel(lvl tmmath.Fraction) error {
} }
return nil 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
lite2/verifier_test.go View File

@ -11,9 +11,9 @@ import (
"github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/types"
) )
func TestVerifyAdjustedHeaders(t *testing.T) {
func TestVerifyAdjacentHeaders(t *testing.T) {
const ( const (
chainID = "TestVerifyAdjustedHeaders"
chainID = "TestVerifyAdjacentHeaders"
lastHeight = 1 lastHeight = 1
nextHeight = 2 nextHeight = 2
) )
@ -52,10 +52,30 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
3 * time.Hour, 3 * time.Hour,
bTime.Add(2 * time.Hour), bTime.Add(2 * time.Hour),
nil, 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'",
}, },
// 3/3 signed -> no error
// new header's time is before old header's time -> error
2: { 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
4: {
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
vals, vals,
@ -65,7 +85,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
"", "",
}, },
// 2/3 signed -> no error // 2/3 signed -> no error
3: {
5: {
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 1, len(keys)), []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 1, len(keys)),
vals, vals,
@ -75,17 +95,17 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
"", "",
}, },
// 1/3 signed -> error // 1/3 signed -> error
4: {
6: {
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, 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)), []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), len(keys)-1, len(keys)),
vals, vals,
3 * time.Hour, 3 * time.Hour,
bTime.Add(2 * 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 // 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, 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)), []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
keys.ToValidators(10, 1), keys.ToValidators(10, 1),
@ -95,7 +115,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
"to match those from new header", "to match those from new header",
}, },
// vals are inconsistent with newHeader -> error // vals are inconsistent with newHeader -> error
6: {
8: {
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
keys.ToValidators(10, 1), keys.ToValidators(10, 1),
@ -105,7 +125,7 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
"to match those that were supplied", "to match those that were supplied",
}, },
// old header has expired -> error // old header has expired -> error
7: {
9: {
keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals,
[]byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)),
keys.ToValidators(10, 1), keys.ToValidators(10, 1),
@ -131,11 +151,12 @@ func TestVerifyAdjustedHeaders(t *testing.T) {
} }
}) })
} }
} }
func TestVerifyNonAdjustedHeaders(t *testing.T) {
func TestVerifyNonAdjacentHeaders(t *testing.T) {
const ( const (
chainID = "TestVerifyNonAdjustedHeaders"
chainID = "TestVerifyNonAdjacentHeaders"
lastHeight = 1 lastHeight = 1
) )
@ -195,7 +216,7 @@ func TestVerifyNonAdjustedHeaders(t *testing.T) {
vals, vals,
3 * time.Hour, 3 * time.Hour,
bTime.Add(2 * 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 // 3/3 new vals signed, 2/3 old vals present -> no error
@ -225,7 +246,7 @@ func TestVerifyNonAdjustedHeaders(t *testing.T) {
lessThanOneThirdVals, lessThanOneThirdVals,
3 * time.Hour, 3 * time.Hour,
bTime.Add(2 * 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) { func TestValidateTrustLevel(t *testing.T) {
testCases := []struct { testCases := []struct {
lvl tmmath.Fraction lvl tmmath.Fraction


+ 29
- 17
types/validator_set.go View File

@ -631,7 +631,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID,
if vals.Size() != len(commit.Signatures) { if vals.Size() != len(commit.Signatures) {
return NewErrInvalidCommitSignatures(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 return err
} }
@ -661,7 +661,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID,
} }
if got, needed := talliedVotingPower, vals.TotalVotingPower()*2/3; got <= needed { if got, needed := talliedVotingPower, vals.TotalVotingPower()*2/3; got <= needed {
return ErrTooMuchChange{Got: got, Needed: needed}
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
} }
return nil return nil
@ -738,7 +738,7 @@ func (vals *ValidatorSet) VerifyFutureCommit(newSet *ValidatorSet, chainID strin
} }
if got, needed := oldVotingPower, oldVals.TotalVotingPower()*2/3; got <= needed { if got, needed := oldVotingPower, oldVals.TotalVotingPower()*2/3; got <= needed {
return ErrTooMuchChange{Got: got, Needed: needed}
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
} }
return nil 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)) 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 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 { for idx, commitSig := range commit.Signatures {
if commitSig.Absent() { if commitSig.Absent() {
continue // OK, some signatures can be 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 // We don't know the validators that committed this block, so we have to
// check for each vote if its validator is already known. // 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 { if val != nil {
seenVals[valIdx] = idx
// Validate signature. // Validate signature.
voteSignBytes := commit.VoteSignBytes(chainID, idx) voteSignBytes := commit.VoteSignBytes(chainID, idx)
if !val.PubKey.VerifyBytes(voteSignBytes, commitSig.Signature) { if !val.PubKey.VerifyBytes(voteSignBytes, commitSig.Signature) {
@ -789,13 +801,13 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID,
got := talliedVotingPower got := talliedVotingPower
needed := (vals.TotalVotingPower() * trustLevel.Numerator) / trustLevel.Denominator needed := (vals.TotalVotingPower() * trustLevel.Numerator) / trustLevel.Denominator
if got <= needed { if got <= needed {
return ErrTooMuchChange{Got: got, Needed: needed}
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
} }
return nil 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 { if err := commit.ValidateBasic(); err != nil {
return err 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 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 Got int64
Needed 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)
} }
//---------------- //----------------


Loading…
Cancel
Save