|
package light
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
tmmath "github.com/tendermint/tendermint/libs/math"
|
|
"github.com/tendermint/tendermint/light/provider"
|
|
"github.com/tendermint/tendermint/light/store"
|
|
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
type mode byte
|
|
|
|
const (
|
|
sequential mode = iota + 1
|
|
skipping
|
|
|
|
defaultPruningSize = 1000
|
|
|
|
// For verifySkipping, we need an algorithm to find what height to check
|
|
// next to see if it has sufficient validator set overlap. The most
|
|
// intuitive method is to take the halfway point i.e. if you trusted block
|
|
// 1 and were not able to verify block 128 then your next try would be 64.
|
|
//
|
|
// However, because this implementation caches all the prior results, instead of always taking halfpoints
|
|
// it is more efficient to re-check cached blocks. Take this simple example. Say
|
|
// you failed to verify 64 but were able to verify block 32. Following a strict half-way policy,
|
|
// you would start over again and try verify to block 128. If this failed
|
|
// then the halfway point between 32 and 128 is 80. But you already have
|
|
// block 64. Instead of requesting and waiting for another block it is far
|
|
// better to try again with block 64. This is of course not directly in the
|
|
// middle. In fact, no matter how the algrorithm plays out, the blocks in
|
|
// cache are always going to be a little less than the halfway point (
|
|
// maximum 1/8 less). To account for this we add a heuristic, bumping the
|
|
// next height to 9/16 instead of 1/2
|
|
verifySkippingNumerator = 9
|
|
verifySkippingDenominator = 16
|
|
|
|
// 10s should cover most of the clients.
|
|
// References:
|
|
// - http://vancouver-webpages.com/time/web.html
|
|
// - https://blog.codinghorror.com/keeping-time-on-the-pc/
|
|
defaultMaxClockDrift = 10 * time.Second
|
|
|
|
// 10s is sufficient for most networks.
|
|
defaultMaxBlockLag = 10 * time.Second
|
|
)
|
|
|
|
// Option sets a parameter for the light client.
|
|
type Option func(*Client)
|
|
|
|
// SequentialVerification option configures the light client to sequentially
|
|
// check the blocks (every block, in ascending height order). Note this is
|
|
// much slower than SkippingVerification, albeit more secure.
|
|
func SequentialVerification() Option {
|
|
return func(c *Client) { c.verificationMode = sequential }
|
|
}
|
|
|
|
// SkippingVerification option configures the light client to skip blocks as
|
|
// long as {trustLevel} of the old validator set signed the new header. The
|
|
// verifySkipping algorithm from the specification is used for finding the minimal
|
|
// "trust path".
|
|
//
|
|
// 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-adjacent headers. For adjacent headers, sequential
|
|
// verification is used.
|
|
func SkippingVerification(trustLevel tmmath.Fraction) Option {
|
|
return func(c *Client) {
|
|
c.verificationMode = skipping
|
|
c.trustLevel = trustLevel
|
|
}
|
|
}
|
|
|
|
// PruningSize option sets the maximum amount of light blocks that the light
|
|
// client stores. When Prune() is run, all light blocks that are earlier than
|
|
// the h amount of light blocks will be removed from the store.
|
|
// Default: 1000. A pruning size of 0 will not prune the light client at all.
|
|
func PruningSize(h uint16) Option {
|
|
return func(c *Client) { c.pruningSize = h }
|
|
}
|
|
|
|
// Logger option can be used to set a logger for the client.
|
|
func Logger(l log.Logger) Option {
|
|
return func(c *Client) { c.logger = l }
|
|
}
|
|
|
|
// MaxClockDrift defines how much new header's time can drift into
|
|
// the future relative to the light clients local time. Default: 10s.
|
|
func MaxClockDrift(d time.Duration) Option {
|
|
return func(c *Client) { c.maxClockDrift = d }
|
|
}
|
|
|
|
// MaxBlockLag represents the maximum time difference between the realtime
|
|
// that a block is received and the timestamp of that block.
|
|
// One can approximate it to the maximum block production time
|
|
//
|
|
// As an example, say the light client received block B at a time
|
|
// 12:05 (this is the real time) and the time on the block
|
|
// was 12:00. Then the lag here is 5 minutes.
|
|
// Default: 10s
|
|
func MaxBlockLag(d time.Duration) Option {
|
|
return func(c *Client) { c.maxBlockLag = d }
|
|
}
|
|
|
|
// Client represents a light client, connected to a single chain, which gets
|
|
// light blocks from a primary provider, verifies them either sequentially or by
|
|
// skipping some and stores them in a trusted store (usually, a local FS).
|
|
//
|
|
// Default verification: SkippingVerification(DefaultTrustLevel)
|
|
type Client struct {
|
|
chainID string
|
|
trustingPeriod time.Duration // see TrustOptions.Period
|
|
verificationMode mode
|
|
trustLevel tmmath.Fraction
|
|
maxClockDrift time.Duration
|
|
maxBlockLag time.Duration
|
|
|
|
// Mutex for locking during changes of the light clients providers
|
|
providerMutex sync.Mutex
|
|
// Primary provider of new headers.
|
|
primary provider.Provider
|
|
// Providers used to "witness" new headers.
|
|
witnesses []provider.Provider
|
|
|
|
// Where trusted light blocks are stored.
|
|
trustedStore store.Store
|
|
// Highest trusted light block from the store (height=H).
|
|
latestTrustedBlock *types.LightBlock
|
|
|
|
// See PruningSize option
|
|
pruningSize uint16
|
|
|
|
logger log.Logger
|
|
}
|
|
|
|
func validatePrimaryAndWitnesses(primary provider.Provider, witnesses []provider.Provider) error {
|
|
witnessMap := make(map[string]struct{})
|
|
for _, w := range witnesses {
|
|
if w.ID() == primary.ID() {
|
|
return fmt.Errorf("primary (%s) cannot be also configured as witness", primary.ID())
|
|
}
|
|
if _, duplicate := witnessMap[w.ID()]; duplicate {
|
|
return fmt.Errorf("witness list must not contain duplicates; duplicate found: %s", w.ID())
|
|
}
|
|
witnessMap[w.ID()] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewClient returns a new light client. It returns an error if it fails to
|
|
// obtain the light block from the primary, or they are invalid (e.g. trust
|
|
// hash does not match with the one from the headers).
|
|
//
|
|
// Witnesses are providers, which will be used for cross-checking the primary
|
|
// provider. At least one witness should be given when skipping verification is
|
|
// used (default). A verified header is compared with the headers at same height
|
|
// obtained from the specified witnesses. A witness can become a primary iff the
|
|
// current primary is unavailable.
|
|
//
|
|
// See all Option(s) for the additional configuration.
|
|
func NewClient(
|
|
ctx context.Context,
|
|
chainID string,
|
|
trustOptions TrustOptions,
|
|
primary provider.Provider,
|
|
witnesses []provider.Provider,
|
|
trustedStore store.Store,
|
|
options ...Option,
|
|
) (*Client, error) {
|
|
|
|
// Check whether the trusted store already has a trusted block. If so, then create
|
|
// a new client from the trusted store instead of the trust options.
|
|
lastHeight, err := trustedStore.LastLightBlockHeight()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if lastHeight > 0 {
|
|
return NewClientFromTrustedStore(
|
|
chainID, trustOptions.Period, primary, witnesses, trustedStore, options...,
|
|
)
|
|
}
|
|
|
|
// Check that the witness list does not include duplicates or the primary
|
|
if err := validatePrimaryAndWitnesses(primary, witnesses); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate trust options
|
|
if err := trustOptions.ValidateBasic(); err != nil {
|
|
return nil, fmt.Errorf("invalid TrustOptions: %w", err)
|
|
}
|
|
|
|
c := &Client{
|
|
chainID: chainID,
|
|
trustingPeriod: trustOptions.Period,
|
|
verificationMode: skipping,
|
|
primary: primary,
|
|
witnesses: witnesses,
|
|
trustedStore: trustedStore,
|
|
trustLevel: DefaultTrustLevel,
|
|
maxClockDrift: defaultMaxClockDrift,
|
|
maxBlockLag: defaultMaxBlockLag,
|
|
pruningSize: defaultPruningSize,
|
|
logger: log.NewNopLogger(),
|
|
}
|
|
|
|
for _, o := range options {
|
|
o(c)
|
|
}
|
|
|
|
// Validate trust level.
|
|
if err := ValidateTrustLevel(c.trustLevel); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use the trusted hash and height to fetch the first weakly-trusted block
|
|
// from the primary provider. Assert that all the witnesses have the same block
|
|
if err := c.initializeWithTrustOptions(ctx, trustOptions); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// NewClientFromTrustedStore initializes an existing client from the trusted store.
|
|
// It does not check that the providers have the same trusted block.
|
|
func NewClientFromTrustedStore(
|
|
chainID string,
|
|
trustingPeriod time.Duration,
|
|
primary provider.Provider,
|
|
witnesses []provider.Provider,
|
|
trustedStore store.Store,
|
|
options ...Option) (*Client, error) {
|
|
|
|
// Check that the witness list does not include duplicates or the primary
|
|
if err := validatePrimaryAndWitnesses(primary, witnesses); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &Client{
|
|
chainID: chainID,
|
|
trustingPeriod: trustingPeriod,
|
|
verificationMode: skipping,
|
|
trustLevel: DefaultTrustLevel,
|
|
maxClockDrift: defaultMaxClockDrift,
|
|
maxBlockLag: defaultMaxBlockLag,
|
|
primary: primary,
|
|
witnesses: witnesses,
|
|
trustedStore: trustedStore,
|
|
pruningSize: defaultPruningSize,
|
|
logger: log.NewNopLogger(),
|
|
}
|
|
|
|
for _, o := range options {
|
|
o(c)
|
|
}
|
|
|
|
// Validate trust level.
|
|
if err := ValidateTrustLevel(c.trustLevel); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check that the trusted store has at least one block and
|
|
if err := c.restoreTrustedLightBlock(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// restoreTrustedLightBlock loads the latest trusted light block from the store
|
|
func (c *Client) restoreTrustedLightBlock() error {
|
|
lastHeight, err := c.trustedStore.LastLightBlockHeight()
|
|
if err != nil {
|
|
return fmt.Errorf("can't get last trusted light block height: %w", err)
|
|
}
|
|
if lastHeight <= 0 {
|
|
return errors.New("trusted store is empty")
|
|
}
|
|
|
|
trustedBlock, err := c.trustedStore.LightBlock(lastHeight)
|
|
if err != nil {
|
|
return fmt.Errorf("can't get last trusted light block: %w", err)
|
|
}
|
|
c.latestTrustedBlock = trustedBlock
|
|
c.logger.Info("restored trusted light block", "height", lastHeight)
|
|
|
|
return nil
|
|
}
|
|
|
|
// initializeWithTrustOptions fetches the weakly-trusted light block from
|
|
// primary provider, matches it to the trusted hash, and sets it as the
|
|
// lastTrustedBlock. It then asserts that all witnesses have the same light block.
|
|
func (c *Client) initializeWithTrustOptions(ctx context.Context, options TrustOptions) error {
|
|
// 1) Fetch and verify the light block. Note that we do not verify the time of the first block
|
|
l, err := c.lightBlockFromPrimary(ctx, options.Height)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2) Assert that the hashes match
|
|
if !bytes.Equal(l.Header.Hash(), options.Hash) {
|
|
return fmt.Errorf("expected header's hash %X, but got %X", options.Hash, l.Hash())
|
|
}
|
|
|
|
// 3) Ensure that +2/3 of validators signed correctly. This also sanity checks that the
|
|
// chain ID is the same.
|
|
err = l.ValidatorSet.VerifyCommitLight(c.chainID, l.Commit.BlockID, l.Height, l.Commit)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid commit: %w", err)
|
|
}
|
|
|
|
// 4) Cross-verify with witnesses to ensure everybody has the same state.
|
|
if err := c.compareFirstHeaderWithWitnesses(ctx, l.SignedHeader); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 5) Persist both of them and continue.
|
|
return c.updateTrustedLightBlock(l)
|
|
}
|
|
|
|
// TrustedLightBlock returns a trusted light block at the given height (0 - the latest).
|
|
//
|
|
// It returns an error if:
|
|
// - there are some issues with the trusted store, although that should not
|
|
// happen normally;
|
|
// - negative height is passed;
|
|
// - header has not been verified yet and is therefore not in the store
|
|
//
|
|
// Safe for concurrent use by multiple goroutines.
|
|
func (c *Client) TrustedLightBlock(height int64) (*types.LightBlock, error) {
|
|
height, err := c.compareWithLatestHeight(height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.trustedStore.LightBlock(height)
|
|
}
|
|
|
|
func (c *Client) compareWithLatestHeight(height int64) (int64, error) {
|
|
latestHeight, err := c.LastTrustedHeight()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("can't get last trusted height: %w", err)
|
|
}
|
|
if latestHeight == -1 {
|
|
return 0, errors.New("no headers exist")
|
|
}
|
|
|
|
switch {
|
|
case height > latestHeight:
|
|
return 0, fmt.Errorf("unverified header/valset requested (latest: %d)", latestHeight)
|
|
case height == 0:
|
|
return latestHeight, nil
|
|
case height < 0:
|
|
return 0, errors.New("negative height")
|
|
}
|
|
|
|
return height, nil
|
|
}
|
|
|
|
// Update attempts to advance the state by downloading the latest light
|
|
// block and verifying it. It returns a new light block on a successful
|
|
// update. Otherwise, it returns nil (plus an error, if any).
|
|
func (c *Client) Update(ctx context.Context, now time.Time) (*types.LightBlock, error) {
|
|
lastTrustedHeight, err := c.LastTrustedHeight()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't get last trusted height: %w", err)
|
|
}
|
|
|
|
if lastTrustedHeight == -1 {
|
|
// no light blocks yet => wait
|
|
return nil, nil
|
|
}
|
|
|
|
latestBlock, err := c.lightBlockFromPrimary(ctx, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If there is a new light block then verify it
|
|
if latestBlock.Height > lastTrustedHeight {
|
|
err = c.verifyLightBlock(ctx, latestBlock, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.logger.Info("advanced to new state", "height", latestBlock.Height, "hash", latestBlock.Hash())
|
|
return latestBlock, nil
|
|
}
|
|
|
|
// else return the latestTrustedBlock
|
|
return c.latestTrustedBlock, nil
|
|
}
|
|
|
|
// VerifyLightBlockAtHeight fetches the light block at the given height
|
|
// and verifies it. It returns the block immediately if it exists in
|
|
// the trustedStore (no verification is needed).
|
|
//
|
|
// height must be > 0.
|
|
//
|
|
// It returns provider.ErrlightBlockNotFound if light block is not found by
|
|
// primary.
|
|
//
|
|
// It will replace the primary provider if an error from a request to the provider occurs
|
|
func (c *Client) VerifyLightBlockAtHeight(ctx context.Context, height int64, now time.Time) (*types.LightBlock, error) {
|
|
if height <= 0 {
|
|
return nil, errors.New("negative or zero height")
|
|
}
|
|
|
|
// Check if the light block is already verified.
|
|
h, err := c.TrustedLightBlock(height)
|
|
if err == nil {
|
|
c.logger.Debug("header has already been verified", "height", height, "hash", h.Hash())
|
|
// Return already trusted light block
|
|
return h, nil
|
|
}
|
|
|
|
// Request the light block from primary
|
|
l, err := c.lightBlockFromPrimary(ctx, height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return l, c.verifyLightBlock(ctx, l, now)
|
|
}
|
|
|
|
// VerifyHeader verifies a new header against the trusted state. It returns
|
|
// immediately if newHeader exists in trustedStore (no verification is
|
|
// needed). Else it performs one of the two types of verification:
|
|
//
|
|
// SequentialVerification: verifies that 2/3 of the trusted validator set has
|
|
// signed the new header. If the headers are not adjacent, **all** intermediate
|
|
// headers will be requested. Intermediate headers are not saved to database.
|
|
//
|
|
// 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, verifySkipping is performed and necessary (not all)
|
|
// intermediate headers will be requested. See the specification for details.
|
|
// Intermediate headers are not saved to database.
|
|
// https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md
|
|
//
|
|
// If the header, which is older than the currently trusted header, is
|
|
// requested and the light client does not have it, VerifyHeader will perform:
|
|
// a) verifySkipping verification if nearest trusted header is found & not expired
|
|
// b) backwards verification in all other cases
|
|
//
|
|
// It returns ErrOldHeaderExpired if the latest trusted header expired.
|
|
//
|
|
// If the primary provides an invalid header (ErrInvalidHeader), it is rejected
|
|
// and replaced by another provider until all are exhausted.
|
|
//
|
|
// If, at any moment, a LightBlock is not found by the primary provider as part of
|
|
// verification then the provider will be replaced by another and the process will
|
|
// restart.
|
|
func (c *Client) VerifyHeader(ctx context.Context, newHeader *types.Header, now time.Time) error {
|
|
if newHeader == nil {
|
|
return errors.New("nil header")
|
|
}
|
|
if newHeader.Height <= 0 {
|
|
return errors.New("negative or zero height")
|
|
}
|
|
|
|
// Check if newHeader already verified.
|
|
l, err := c.TrustedLightBlock(newHeader.Height)
|
|
if err == nil {
|
|
// Make sure it's the same header.
|
|
if !bytes.Equal(l.Hash(), newHeader.Hash()) {
|
|
return fmt.Errorf("existing trusted header %X does not match newHeader %X", l.Hash(), newHeader.Hash())
|
|
}
|
|
c.logger.Debug("header has already been verified",
|
|
"height", newHeader.Height, "hash", newHeader.Hash())
|
|
return nil
|
|
}
|
|
|
|
// Request the header and the vals.
|
|
l, err = c.lightBlockFromPrimary(ctx, newHeader.Height)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve light block from primary to verify against: %w", err)
|
|
}
|
|
|
|
if !bytes.Equal(l.Hash(), newHeader.Hash()) {
|
|
return fmt.Errorf("header from primary %X does not match newHeader %X", l.Hash(), newHeader.Hash())
|
|
}
|
|
|
|
return c.verifyLightBlock(ctx, l, now)
|
|
}
|
|
|
|
func (c *Client) verifyLightBlock(ctx context.Context, newLightBlock *types.LightBlock, now time.Time) error {
|
|
c.logger.Info("verify light block", "height", newLightBlock.Height, "hash", newLightBlock.Hash())
|
|
|
|
var (
|
|
verifyFunc func(ctx context.Context, trusted *types.LightBlock, new *types.LightBlock, now time.Time) error
|
|
err error
|
|
)
|
|
|
|
switch c.verificationMode {
|
|
case sequential:
|
|
verifyFunc = c.verifySequential
|
|
case skipping:
|
|
verifyFunc = c.verifySkippingAgainstPrimary
|
|
default:
|
|
panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode))
|
|
}
|
|
|
|
firstBlockHeight, err := c.FirstTrustedHeight()
|
|
if err != nil {
|
|
return fmt.Errorf("can't get first light block height: %w", err)
|
|
}
|
|
|
|
switch {
|
|
// Verifying forwards
|
|
case newLightBlock.Height >= c.latestTrustedBlock.Height:
|
|
err = verifyFunc(ctx, c.latestTrustedBlock, newLightBlock, now)
|
|
|
|
// Verifying backwards
|
|
case newLightBlock.Height < firstBlockHeight:
|
|
var firstBlock *types.LightBlock
|
|
firstBlock, err = c.trustedStore.LightBlock(firstBlockHeight)
|
|
if err != nil {
|
|
return fmt.Errorf("can't get first light block: %w", err)
|
|
}
|
|
err = c.backwards(ctx, firstBlock.Header, newLightBlock.Header)
|
|
|
|
// Verifying between first and last trusted light block. In this situation
|
|
// we find the closest block prior to the target height then perform
|
|
// verification forwards.
|
|
default:
|
|
var closestBlock *types.LightBlock
|
|
closestBlock, err = c.trustedStore.LightBlockBefore(newLightBlock.Height)
|
|
if err != nil {
|
|
return fmt.Errorf("can't get signed header before height %d: %w", newLightBlock.Height, err)
|
|
}
|
|
err = verifyFunc(ctx, closestBlock, newLightBlock, now)
|
|
}
|
|
if err != nil {
|
|
c.logger.Error("failed to verify", "err", err)
|
|
return err
|
|
}
|
|
|
|
// Once verified, save and return
|
|
return c.updateTrustedLightBlock(newLightBlock)
|
|
}
|
|
|
|
// see VerifyHeader
|
|
func (c *Client) verifySequential(
|
|
ctx context.Context,
|
|
trustedBlock *types.LightBlock,
|
|
newLightBlock *types.LightBlock,
|
|
now time.Time) error {
|
|
|
|
var (
|
|
verifiedBlock = trustedBlock
|
|
interimBlock *types.LightBlock
|
|
err error
|
|
trace = []*types.LightBlock{trustedBlock}
|
|
)
|
|
|
|
for height := trustedBlock.Height + 1; height <= newLightBlock.Height; height++ {
|
|
// 1) Fetch interim light block if needed.
|
|
if height == newLightBlock.Height { // last light block
|
|
interimBlock = newLightBlock
|
|
} else { // intermediate light blocks
|
|
interimBlock, err = c.lightBlockFromPrimary(ctx, height)
|
|
if err != nil {
|
|
return ErrVerificationFailed{From: verifiedBlock.Height, To: height, Reason: err}
|
|
}
|
|
}
|
|
|
|
// 2) Verify them
|
|
c.logger.Debug("verify adjacent newLightBlock against verifiedBlock",
|
|
"trustedHeight", verifiedBlock.Height,
|
|
"trustedHash", verifiedBlock.Hash(),
|
|
"newHeight", interimBlock.Height,
|
|
"newHash", interimBlock.Hash())
|
|
|
|
err = VerifyAdjacent(verifiedBlock.SignedHeader, interimBlock.SignedHeader, interimBlock.ValidatorSet,
|
|
c.trustingPeriod, now, c.maxClockDrift)
|
|
if err != nil {
|
|
err := ErrVerificationFailed{From: verifiedBlock.Height, To: interimBlock.Height, Reason: err}
|
|
|
|
switch errors.Unwrap(err).(type) {
|
|
case ErrInvalidHeader:
|
|
// If the target header is invalid, return immediately.
|
|
if err.To == newLightBlock.Height {
|
|
c.logger.Debug("target header is invalid", "err", err)
|
|
return err
|
|
}
|
|
|
|
// If some intermediate header is invalid, remove the primary and try again.
|
|
c.logger.Info("primary sent invalid header -> removing", "err", err, "primary", c.primary)
|
|
|
|
replacementBlock, removeErr := c.findNewPrimary(ctx, newLightBlock.Height, true)
|
|
if removeErr != nil {
|
|
c.logger.Debug("failed to replace primary. Returning original error", "err", removeErr)
|
|
return err
|
|
}
|
|
|
|
if !bytes.Equal(replacementBlock.Hash(), newLightBlock.Hash()) {
|
|
c.logger.Debug("replaced primary but new primary has a different block to the initial one")
|
|
return err
|
|
}
|
|
|
|
// attempt to verify header again
|
|
height--
|
|
|
|
continue
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 3) Update verifiedBlock
|
|
verifiedBlock = interimBlock
|
|
|
|
// 4) Add verifiedBlock to trace
|
|
trace = append(trace, verifiedBlock)
|
|
}
|
|
|
|
// Compare header with the witnesses to ensure it's not a fork.
|
|
// More witnesses we have, more chance to notice one.
|
|
//
|
|
// CORRECTNESS ASSUMPTION: there's at least 1 correct full node
|
|
// (primary or one of the witnesses).
|
|
return c.detectDivergence(ctx, trace, now)
|
|
}
|
|
|
|
// see VerifyHeader
|
|
//
|
|
// verifySkipping finds the middle light block between a trusted and new light block,
|
|
// reiterating the action until it verifies a light block. A cache of light blocks
|
|
// requested from source is kept such that when a verification is made, and the
|
|
// light client tries again to verify the new light block in the middle, the light
|
|
// client does not need to ask for all the same light blocks again.
|
|
//
|
|
// If this function errors, it should always wrap it in a `ErrVerifcationFailed`
|
|
// struct so that the calling function can determine where it failed and handle
|
|
// it accordingly.
|
|
func (c *Client) verifySkipping(
|
|
ctx context.Context,
|
|
source provider.Provider,
|
|
trustedBlock *types.LightBlock,
|
|
newLightBlock *types.LightBlock,
|
|
now time.Time) ([]*types.LightBlock, error) {
|
|
|
|
var (
|
|
// The block cache is ordered in height from highest to lowest. We start
|
|
// with the newLightBlock and for any height requested in between we add
|
|
// it.
|
|
blockCache = []*types.LightBlock{newLightBlock}
|
|
depth = 0
|
|
|
|
verifiedBlock = trustedBlock
|
|
trace = []*types.LightBlock{trustedBlock}
|
|
)
|
|
|
|
for {
|
|
c.logger.Debug("verify non-adjacent newHeader against verifiedBlock",
|
|
"trustedHeight", verifiedBlock.Height,
|
|
"trustedHash", verifiedBlock.Hash(),
|
|
"newHeight", blockCache[depth].Height,
|
|
"newHash", blockCache[depth].Hash())
|
|
|
|
// Verify the untrusted header. This function is equivalent to
|
|
// ValidAndVerified in the spec
|
|
err := Verify(verifiedBlock.SignedHeader, verifiedBlock.ValidatorSet, blockCache[depth].SignedHeader,
|
|
blockCache[depth].ValidatorSet, c.trustingPeriod, now, c.maxClockDrift, c.trustLevel)
|
|
switch err.(type) {
|
|
case nil:
|
|
// If we have verified the last header then depth will be 0 and we
|
|
// can return a success along with the trace of intermediate headers
|
|
if depth == 0 {
|
|
trace = append(trace, newLightBlock)
|
|
return trace, nil
|
|
}
|
|
// If not, update the lower bound to the previous upper bound
|
|
verifiedBlock = blockCache[depth]
|
|
// Remove the light block at the lower bound in the header cache - it will no longer be needed
|
|
blockCache = blockCache[:depth]
|
|
// Reset the cache depth so that we start from the upper bound again
|
|
depth = 0
|
|
// add verifiedBlock to the trace
|
|
trace = append(trace, verifiedBlock)
|
|
|
|
case ErrNewValSetCantBeTrusted:
|
|
// the light block current passed validation, but the validator
|
|
// set is too different to verify it. We keep the block because it
|
|
// may become valuable later on.
|
|
//
|
|
// If we have reached the end of the cache we need to request a
|
|
// completely new block else we recycle a previously requested one.
|
|
// In both cases we are taking a block with a closer height to the
|
|
// previously verified one in the hope that it has a better chance
|
|
// of having a similar validator set
|
|
if depth == len(blockCache)-1 {
|
|
// schedule what the next height we need to fetch is
|
|
pivotHeight := c.schedule(verifiedBlock.Height, blockCache[depth].Height)
|
|
interimBlock, providerErr := c.getLightBlock(ctx, source, pivotHeight)
|
|
if providerErr != nil {
|
|
return nil, ErrVerificationFailed{From: verifiedBlock.Height, To: pivotHeight, Reason: providerErr}
|
|
}
|
|
blockCache = append(blockCache, interimBlock)
|
|
}
|
|
depth++
|
|
|
|
// for any verification error we abort the operation and return the error
|
|
default:
|
|
return nil, ErrVerificationFailed{From: verifiedBlock.Height, To: blockCache[depth].Height, Reason: err}
|
|
}
|
|
}
|
|
}
|
|
|
|
// schedule works out the next height to attempt sequential verification
|
|
func (c *Client) schedule(lastVerifiedHeight, lastFailedHeight int64) int64 {
|
|
return lastVerifiedHeight +
|
|
(lastFailedHeight-lastVerifiedHeight)*verifySkippingNumerator/verifySkippingDenominator
|
|
}
|
|
|
|
// verifySkippingAgainstPrimary does verifySkipping plus it compares new header with
|
|
// witnesses and replaces primary if it sends the light client an invalid header
|
|
func (c *Client) verifySkippingAgainstPrimary(
|
|
ctx context.Context,
|
|
trustedBlock *types.LightBlock,
|
|
newLightBlock *types.LightBlock,
|
|
now time.Time) error {
|
|
|
|
trace, err := c.verifySkipping(ctx, c.primary, trustedBlock, newLightBlock, now)
|
|
if err == nil {
|
|
// Success! Now compare the header with the witnesses to ensure it's not a fork.
|
|
// More witnesses we have, more chance to notice one.
|
|
//
|
|
// CORRECTNESS ASSUMPTION: there's at least 1 correct full node
|
|
// (primary or one of the witnesses).
|
|
if cmpErr := c.detectDivergence(ctx, trace, now); cmpErr != nil {
|
|
return cmpErr
|
|
}
|
|
}
|
|
|
|
var e = &ErrVerificationFailed{}
|
|
// all errors from verify skipping should be `ErrVerificationFailed`
|
|
// if it's not we just return the error directly
|
|
if !errors.As(err, e) {
|
|
return err
|
|
}
|
|
|
|
replace := true
|
|
switch e.Reason.(type) {
|
|
// Verification returned an invalid header
|
|
case ErrInvalidHeader:
|
|
// If it was the target header, return immediately.
|
|
if e.To == newLightBlock.Height {
|
|
c.logger.Debug("target header is invalid", "err", err)
|
|
return err
|
|
}
|
|
|
|
// If some intermediate header is invalid, remove the primary and try
|
|
// again.
|
|
|
|
// An intermediate header expired. We can no longer validate it as there is
|
|
// no longer the ability to punish invalid blocks as evidence of misbehavior
|
|
case ErrOldHeaderExpired:
|
|
return err
|
|
|
|
// This happens if there was a problem in finding the next block or a
|
|
// context was canceled.
|
|
default:
|
|
if errors.Is(e.Reason, context.Canceled) || errors.Is(e.Reason, context.DeadlineExceeded) {
|
|
return e.Reason
|
|
}
|
|
|
|
if !c.providerShouldBeRemoved(e.Reason) {
|
|
replace = false
|
|
}
|
|
}
|
|
|
|
// if we've reached here we're attempting to retry verification with a
|
|
// different provider
|
|
c.logger.Info("primary returned error", "err", e, "primary", c.primary, "replace", replace)
|
|
|
|
replacementBlock, removeErr := c.findNewPrimary(ctx, newLightBlock.Height, replace)
|
|
if removeErr != nil {
|
|
c.logger.Error("failed to replace primary. Returning original error", "err", removeErr)
|
|
return e.Reason
|
|
}
|
|
|
|
if !bytes.Equal(replacementBlock.Hash(), newLightBlock.Hash()) {
|
|
c.logger.Debug("replaced primary but new primary has a different block to the initial one. Returning original error")
|
|
return e.Reason
|
|
}
|
|
|
|
// attempt to verify the header again from the trusted block
|
|
return c.verifySkippingAgainstPrimary(ctx, trustedBlock, replacementBlock, now)
|
|
}
|
|
|
|
// LastTrustedHeight returns a last trusted height. -1 and nil are returned if
|
|
// there are no trusted headers.
|
|
//
|
|
// Safe for concurrent use by multiple goroutines.
|
|
func (c *Client) LastTrustedHeight() (int64, error) {
|
|
return c.trustedStore.LastLightBlockHeight()
|
|
}
|
|
|
|
// FirstTrustedHeight returns a first trusted height. -1 and nil are returned if
|
|
// there are no trusted headers.
|
|
//
|
|
// Safe for concurrent use by multiple goroutines.
|
|
func (c *Client) FirstTrustedHeight() (int64, error) {
|
|
return c.trustedStore.FirstLightBlockHeight()
|
|
}
|
|
|
|
// ChainID returns the chain ID the light client was configured with.
|
|
//
|
|
// Safe for concurrent use by multiple goroutines.
|
|
func (c *Client) ChainID() string {
|
|
return c.chainID
|
|
}
|
|
|
|
// Primary returns the primary provider.
|
|
//
|
|
// NOTE: provider may be not safe for concurrent access.
|
|
func (c *Client) Primary() provider.Provider {
|
|
c.providerMutex.Lock()
|
|
defer c.providerMutex.Unlock()
|
|
return c.primary
|
|
}
|
|
|
|
// Witnesses returns the witness providers.
|
|
//
|
|
// NOTE: providers may be not safe for concurrent access.
|
|
func (c *Client) Witnesses() []provider.Provider {
|
|
c.providerMutex.Lock()
|
|
defer c.providerMutex.Unlock()
|
|
return c.witnesses
|
|
}
|
|
|
|
// AddProvider adds a providers to the light clients set
|
|
//
|
|
// NOTE: The light client does not check for uniqueness
|
|
func (c *Client) AddProvider(p provider.Provider) {
|
|
c.providerMutex.Lock()
|
|
defer c.providerMutex.Unlock()
|
|
c.witnesses = append(c.witnesses, p)
|
|
}
|
|
|
|
// Cleanup removes all the data (headers and validator sets) stored. Note: the
|
|
// client must be stopped at this point.
|
|
func (c *Client) Cleanup() error {
|
|
c.logger.Info("removing all light blocks")
|
|
c.latestTrustedBlock = nil
|
|
return c.trustedStore.Prune(0)
|
|
}
|
|
|
|
func (c *Client) updateTrustedLightBlock(l *types.LightBlock) error {
|
|
c.logger.Debug("updating trusted light block", "light_block", l)
|
|
|
|
if err := c.trustedStore.SaveLightBlock(l); err != nil {
|
|
return fmt.Errorf("failed to save trusted header: %w", err)
|
|
}
|
|
|
|
if c.pruningSize > 0 {
|
|
if err := c.trustedStore.Prune(c.pruningSize); err != nil {
|
|
return fmt.Errorf("prune: %w", err)
|
|
}
|
|
}
|
|
|
|
if c.latestTrustedBlock == nil || l.Height > c.latestTrustedBlock.Height {
|
|
c.latestTrustedBlock = l
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// backwards verification (see VerifyHeaderBackwards func in the spec) verifies
|
|
// headers before a trusted header. If a sent header is invalid the primary is
|
|
// replaced with another provider and the operation is repeated.
|
|
func (c *Client) backwards(
|
|
ctx context.Context,
|
|
trustedHeader *types.Header,
|
|
newHeader *types.Header) error {
|
|
|
|
var (
|
|
verifiedHeader = trustedHeader
|
|
interimHeader *types.Header
|
|
)
|
|
|
|
for verifiedHeader.Height > newHeader.Height {
|
|
interimBlock, err := c.lightBlockFromPrimary(ctx, verifiedHeader.Height-1)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to obtain the header at height #%d: %w", verifiedHeader.Height-1, err)
|
|
}
|
|
interimHeader = interimBlock.Header
|
|
c.logger.Debug("verify newHeader against verifiedHeader",
|
|
"trustedHeight", verifiedHeader.Height,
|
|
"trustedHash", verifiedHeader.Hash(),
|
|
"newHeight", interimHeader.Height,
|
|
"newHash", interimHeader.Hash())
|
|
if err := VerifyBackwards(interimHeader, verifiedHeader); err != nil {
|
|
// verification has failed
|
|
c.logger.Info("backwards verification failed, replacing primary...", "err", err, "primary", c.primary)
|
|
|
|
// the client tries to see if it can get a witness to continue with the request
|
|
newPrimarysBlock, replaceErr := c.findNewPrimary(ctx, newHeader.Height, true)
|
|
if replaceErr != nil {
|
|
c.logger.Debug("failed to replace primary. Returning original error", "err", replaceErr)
|
|
return err
|
|
}
|
|
|
|
// before continuing we must check that they have the same target header to validate
|
|
if !bytes.Equal(newPrimarysBlock.Hash(), newHeader.Hash()) {
|
|
c.logger.Debug("replaced primary but new primary has a different block to the initial one")
|
|
// return the original error
|
|
return err
|
|
}
|
|
|
|
// try again with the new primary
|
|
return c.backwards(ctx, verifiedHeader, newPrimarysBlock.Header)
|
|
}
|
|
verifiedHeader = interimHeader
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// lightBlockFromPrimary retrieves the lightBlock from the primary provider
|
|
// at the specified height. This method also handles provider behavior as follows:
|
|
//
|
|
// 1. If the provider does not respond or does not have the block, it tries again
|
|
// with a different provider
|
|
// 2. If all providers return the same error, the light client forwards the error to
|
|
// where the initial request came from
|
|
// 3. If the provider provides an invalid light block, is deemed unreliable or returns
|
|
// any other error, the primary is permanently dropped and is replaced by a witness.
|
|
func (c *Client) lightBlockFromPrimary(ctx context.Context, height int64) (*types.LightBlock, error) {
|
|
c.providerMutex.Lock()
|
|
l, err := c.getLightBlock(ctx, c.primary, height)
|
|
c.providerMutex.Unlock()
|
|
|
|
switch err {
|
|
case nil:
|
|
// Everything went smoothly. We reset the lightBlockRequests and return the light block
|
|
return l, nil
|
|
|
|
// catch canceled contexts or deadlines
|
|
case context.Canceled, context.DeadlineExceeded:
|
|
return nil, err
|
|
|
|
case provider.ErrNoResponse, provider.ErrLightBlockNotFound, provider.ErrHeightTooHigh:
|
|
// we find a new witness to replace the primary
|
|
c.logger.Info("error from light block request from primary, replacing...",
|
|
"error", err, "height", height, "primary", c.primary)
|
|
return c.findNewPrimary(ctx, height, false)
|
|
|
|
default:
|
|
// The light client has most likely received either provider.ErrUnreliableProvider or provider.ErrBadLightBlock
|
|
// These errors mean that the light client should drop the primary and try with another provider instead
|
|
c.logger.Info("error from light block request from primary, removing...",
|
|
"error", err, "height", height, "primary", c.primary)
|
|
return c.findNewPrimary(ctx, height, true)
|
|
}
|
|
}
|
|
|
|
func (c *Client) getLightBlock(ctx context.Context, p provider.Provider, height int64) (*types.LightBlock, error) {
|
|
l, err := p.LightBlock(ctx, height)
|
|
if ctx.Err() != nil {
|
|
return nil, provider.ErrNoResponse
|
|
}
|
|
return l, err
|
|
}
|
|
|
|
// NOTE: requires a providerMutex lock
|
|
func (c *Client) removeWitnesses(indexes []int) error {
|
|
if len(c.witnesses) <= len(indexes) {
|
|
return ErrNoWitnesses
|
|
}
|
|
|
|
// we need to make sure that we remove witnesses by index in the reverse
|
|
// order so as to not affect the indexes themselves
|
|
sort.Ints(indexes)
|
|
for i := len(indexes) - 1; i >= 0; i-- {
|
|
c.witnesses[indexes[i]] = c.witnesses[len(c.witnesses)-1]
|
|
c.witnesses = c.witnesses[:len(c.witnesses)-1]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type witnessResponse struct {
|
|
lb *types.LightBlock
|
|
witnessIndex int
|
|
err error
|
|
}
|
|
|
|
// findNewPrimary concurrently sends a light block request, promoting the first witness to return
|
|
// a valid light block as the new primary. The remove option indicates whether the primary should be
|
|
// entire removed or just appended to the back of the witnesses list. This method also handles witness
|
|
// errors. If no witness is available, it returns the last error of the witness.
|
|
func (c *Client) findNewPrimary(ctx context.Context, height int64, remove bool) (*types.LightBlock, error) {
|
|
c.providerMutex.Lock()
|
|
defer c.providerMutex.Unlock()
|
|
|
|
if len(c.witnesses) < 1 {
|
|
return nil, ErrNoWitnesses
|
|
}
|
|
|
|
var (
|
|
witnessResponsesC = make(chan witnessResponse, len(c.witnesses))
|
|
witnessesToRemove []int
|
|
lastError error
|
|
wg sync.WaitGroup
|
|
)
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
// send out a light block request to all witnesses
|
|
for index := range c.witnesses {
|
|
wg.Add(1)
|
|
go func(witnessIndex int, witnessResponsesC chan witnessResponse) {
|
|
defer wg.Done()
|
|
|
|
lb, err := c.witnesses[witnessIndex].LightBlock(ctx, height)
|
|
select {
|
|
case witnessResponsesC <- witnessResponse{lb, witnessIndex, err}:
|
|
case <-ctx.Done():
|
|
}
|
|
|
|
}(index, witnessResponsesC)
|
|
}
|
|
|
|
// process all the responses as they come in
|
|
for i := 0; i < cap(witnessResponsesC); i++ {
|
|
response := <-witnessResponsesC
|
|
switch response.err {
|
|
// success! We have found a new primary
|
|
case nil:
|
|
cancel() // cancel all remaining requests to other witnesses
|
|
|
|
wg.Wait() // wait for all goroutines to finish
|
|
|
|
// if we are not intending on removing the primary then append the old primary to the end of the witness slice
|
|
if !remove {
|
|
c.witnesses = append(c.witnesses, c.primary)
|
|
}
|
|
|
|
// promote respondent as the new primary
|
|
c.logger.Debug("found new primary", "primary", c.witnesses[response.witnessIndex])
|
|
c.primary = c.witnesses[response.witnessIndex]
|
|
|
|
// add promoted witness to the list of witnesses to be removed
|
|
witnessesToRemove = append(witnessesToRemove, response.witnessIndex)
|
|
|
|
// remove witnesses marked as bad (the client must do this before we alter the witness slice and change the indexes
|
|
// of witnesses). Removal is done in descending order
|
|
if err := c.removeWitnesses(witnessesToRemove); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// return the light block that new primary responded with
|
|
return response.lb, nil
|
|
|
|
// catch canceled contexts or deadlines
|
|
case context.Canceled, context.DeadlineExceeded:
|
|
return nil, response.err
|
|
|
|
// process benign errors by logging them only
|
|
case provider.ErrNoResponse, provider.ErrLightBlockNotFound, provider.ErrHeightTooHigh:
|
|
lastError = response.err
|
|
c.logger.Info("error on light block request from witness",
|
|
"error", response.err, "primary", c.witnesses[response.witnessIndex])
|
|
continue
|
|
|
|
// process malevolent errors like ErrUnreliableProvider and ErrBadLightBlock by removing the witness
|
|
default:
|
|
lastError = response.err
|
|
c.logger.Error("error on light block request from witness, removing...",
|
|
"error", response.err, "primary", c.witnesses[response.witnessIndex])
|
|
witnessesToRemove = append(witnessesToRemove, response.witnessIndex)
|
|
}
|
|
}
|
|
|
|
return nil, lastError
|
|
}
|
|
|
|
// compareFirstHeaderWithWitnesses concurrently compares h with all witnesses. If any
|
|
// witness reports a different header than h, the function returns an error.
|
|
func (c *Client) compareFirstHeaderWithWitnesses(ctx context.Context, h *types.SignedHeader) error {
|
|
compareCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
c.providerMutex.Lock()
|
|
defer c.providerMutex.Unlock()
|
|
|
|
if len(c.witnesses) < 1 {
|
|
return nil
|
|
}
|
|
|
|
errc := make(chan error, len(c.witnesses))
|
|
for i, witness := range c.witnesses {
|
|
go c.compareNewHeaderWithWitness(compareCtx, errc, h, witness, i)
|
|
}
|
|
|
|
witnessesToRemove := make([]int, 0, len(c.witnesses))
|
|
|
|
// handle errors from the header comparisons as they come in
|
|
for i := 0; i < cap(errc); i++ {
|
|
err := <-errc
|
|
|
|
switch e := err.(type) {
|
|
case nil:
|
|
continue
|
|
case errConflictingHeaders:
|
|
c.logger.Error(`witness has a different header. Please check primary is
|
|
correct and remove witness. Otherwise, use a different primary`,
|
|
"Witness", c.witnesses[e.WitnessIndex], "ExpHeader", h.Hash(), "GotHeader", e.Block.Hash())
|
|
return err
|
|
case errBadWitness:
|
|
// If witness sent us an invalid header, then remove it
|
|
c.logger.Info("witness returned an error, removing...",
|
|
"err", err)
|
|
witnessesToRemove = append(witnessesToRemove, e.WitnessIndex)
|
|
default:
|
|
// check for canceled contexts or deadlines
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
return err
|
|
}
|
|
|
|
// the witness either didn't respond or didn't have the block. We ignore it.
|
|
c.logger.Debug("unable to compare first header with witness, ignoring",
|
|
"err", err)
|
|
}
|
|
|
|
}
|
|
|
|
// remove all witnesses that misbehaved
|
|
return c.removeWitnesses(witnessesToRemove)
|
|
}
|
|
|
|
// providerShouldBeRemoved analyzes the nature of the error and whether the provider
|
|
// should be removed from the light clients set
|
|
func (c *Client) providerShouldBeRemoved(err error) bool {
|
|
return errors.As(err, &provider.ErrUnreliableProvider{}) ||
|
|
errors.As(err, &provider.ErrBadLightBlock{}) ||
|
|
errors.Is(err, provider.ErrConnectionClosed)
|
|
}
|
|
|
|
func (c *Client) Status(ctx context.Context) *types.LightClientInfo {
|
|
chunks := make([]string, len(c.witnesses))
|
|
|
|
// If primary is in witness list we do not want to count it twice in the number of peers
|
|
primaryNotInWitnessList := 1
|
|
for i, val := range c.witnesses {
|
|
chunks[i] = val.ID()
|
|
if chunks[i] == c.primary.ID() {
|
|
primaryNotInWitnessList = 0
|
|
}
|
|
}
|
|
|
|
return &types.LightClientInfo{
|
|
PrimaryID: c.primary.ID(),
|
|
WitnessesID: chunks,
|
|
NumPeers: len(chunks) + primaryNotInWitnessList,
|
|
LastTrustedHeight: c.latestTrustedBlock.Height,
|
|
LastTrustedHash: c.latestTrustedBlock.Hash(),
|
|
LatestBlockTime: c.latestTrustedBlock.Time,
|
|
TrustingPeriod: c.trustingPeriod.String(),
|
|
// The caller of /status can deduce this from the two variables above
|
|
// Having a boolean flag improves readbility
|
|
TrustedBlockExpired: HeaderExpired(c.latestTrustedBlock.SignedHeader, c.trustingPeriod, time.Now()),
|
|
}
|
|
}
|