light

package
v0.0.0-...-1abf0d1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 19, 2023 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

package light provides a light client implementation.

The concept of light clients was introduced in the Bitcoin white paper. It describes a watcher of distributed consensus process that only validates the consensus algorithm and not the state machine transactions within.

Tendermint light clients allow bandwidth & compute-constrained devices, such as smartphones, low-power embedded chips, or other blockchains to efficiently verify the consensus of a Tendermint blockchain. This forms the basis of safe and efficient state synchronization for new network nodes and inter-blockchain communication (where a light client of one Tendermint instance runs in another chain's state machine).

In a network that is expected to reliably punish validators for misbehavior by slashing bonded stake and where the validator set changes infrequently, clients can take advantage of this assumption to safely synchronize a light client without downloading the intervening headers.

Light clients (and full nodes) operating in the Proof Of Stake context need a trusted block height from a trusted source that is no older than 1 unbonding window plus a configurable evidence submission synchrony bound. This is called weak subjectivity.

Weak subjectivity is required in Proof of Stake blockchains because it is costless for an attacker to buy up voting keys that are no longer bonded and fork the network at some point in its prior history. See Vitalik's post at [Proof of Stake: How I Learned to Love Weak Subjectivity](https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity/).

NOTE: Tendermint provides a somewhat different (stronger) light client model than Bitcoin under eclipse, since the eclipsing node(s) can only fool the light client if they have two-thirds of the private keys from the last root-of-trust.

Common structures

* SignedHeader

SignedHeader is a block header along with a commit -- enough validator precommit-vote signatures to prove its validity (> 2/3 of the voting power) given the validator set responsible for signing that header.

The hash of the next validator set is included and signed in the SignedHeader. This lets the light 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 light 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 light 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("light-client-db", dbDir)
if err != nil {
	// handle error
}

c, err := NewHTTPClient(
	chainID,
	TrustOptions{
		Period: 504 * time.Hour, // 21 days
		Height: 100,
		Hash:   header.Hash(),
	},
	"http://localhost:26657",
	[]string{"http://witness1:26657"},
	dbs.New(db, ""),
)
if err != nil {
	// handle error
}

h, err := c.TrustedHeader(100)
if err != nil {
	// handle error
}
fmt.Println("header", h)

Check out other examples in example_test.go

## 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/ari-anchor/sei-tendermint/blob/master/spec/light-client/verification/README.md for details.

There are two methods of verification: sequential and bisection

Sequential uses the headers hashes and the validator sets to verify each adjacent header until it reaches the target header.

Bisection finds the middle header between a trusted and new header, reiterating the action until it verifies a header. A cache of headers requested by the primary is kept such that when a verification is made, and the light client tries again to verify the new header in the middle, the light client does not need to ask for all the same headers again.

refer to docs/imgs/light_client_bisection_alg.png

## 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/ari-anchor/sei-tendermint/tree/master/spec/light-client for the light client specification.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNoConnectedPeers    = errors.New("no available peers to dispatch request to")
	ErrUnsolicitedResponse = errors.New("unsolicited light block response")
	ErrPeerAlreadyBusy     = errors.New("peer is already processing a request")
	ErrDisconnected        = errors.New("dispatcher disconnected")
)
View Source
var (
	// DefaultTrustLevel - new header can be trusted if at least one correct
	// validator signed it.
	DefaultTrustLevel = tmmath.Fraction{Numerator: 1, Denominator: 3}
)
View Source
var ErrFailedHeaderCrossReferencing = errors.New(
	`all witnesses have either not responded, don't have the blocks or sent invalid blocks. 
	You should look to change your witnesses or review the light client's logs for more information`,
)

ErrFailedHeaderCrossReferencing is returned when the detector was not able to cross reference the header with any of the connected witnesses.

View Source
var ErrLightClientAttack = errors.New(`attempted attack detected.
	Light client received valid conflicting header from witness.
	Unable to verify header. Evidence has been sent to both providers.
	Check logs for full evidence and trace`,
)

ErrLightClientAttack is returned when the light client has detected an attempt to verify a false header and has sent the evidence to either a witness or primary.

View Source
var ErrNoWitnesses = errors.New("no witnesses connected. please reset light client")

ErrNoWitnesses means that there are not enough witnesses connected to continue running the light client.

Functions

func HeaderExpired

func HeaderExpired(h *types.SignedHeader, trustingPeriod time.Duration, now time.Time) bool

HeaderExpired return true if the given header expired.

func ValidateTrustLevel

func ValidateTrustLevel(lvl tmmath.Fraction) error

ValidateTrustLevel checks that trustLevel is within the allowed range [1/3, 1]. If not, it returns an error. 1/3 is the minimum amount of trust needed which does not break the security model. Must be strictly less than 1.

func Verify

func Verify(
	trustedHeader *types.SignedHeader,
	trustedVals *types.ValidatorSet,
	untrustedHeader *types.SignedHeader,
	untrustedVals *types.ValidatorSet,
	trustingPeriod time.Duration,
	now time.Time,
	maxClockDrift time.Duration,
	trustLevel tmmath.Fraction) error

Verify combines both VerifyAdjacent and VerifyNonAdjacent functions.

func VerifyAdjacent

func VerifyAdjacent(
	trustedHeader *types.SignedHeader,
	untrustedHeader *types.SignedHeader,
	untrustedVals *types.ValidatorSet,
	trustingPeriod time.Duration,
	now time.Time,
	maxClockDrift time.Duration,
) error

VerifyAdjacent verifies directly adjacent untrustedHeader against trustedHeader. It ensures that:

a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned)
b) untrustedHeader is valid (if not, ErrInvalidHeader is returned)
c) untrustedHeader.ValidatorsHash equals trustedHeader.NextValidatorsHash
d) more than 2/3 of new validators (untrustedVals) have signed h2
  (otherwise, ErrInvalidHeader is returned)
e) headers are adjacent.

maxClockDrift defines how much untrustedHeader.Time can drift into the future. trustedHeader must have a ChainID, Height, Time and NextValidatorsHash

func VerifyBackwards

func VerifyBackwards(untrustedHeader, trustedHeader *types.Header) error

VerifyBackwards verifies an untrusted header with a height one less than that of an adjacent trusted header. It ensures that:

	a) untrusted header is valid
 b) untrusted header has a time before the trusted header
 c) that the LastBlockID hash of the trusted header is the same as the hash
 of the trusted header

For any of these cases ErrInvalidHeader is returned. NOTE: This does not check whether the trusted or untrusted header has expired or not. These checks are not necessary because the detector never runs during backwards verification and thus evidence that needs to be within a certain time bound is never sent.

func VerifyNonAdjacent

func VerifyNonAdjacent(
	trustedHeader *types.SignedHeader,
	trustedVals *types.ValidatorSet,
	untrustedHeader *types.SignedHeader,
	untrustedVals *types.ValidatorSet,
	trustingPeriod time.Duration,
	now time.Time,
	maxClockDrift time.Duration,
	trustLevel tmmath.Fraction,
) error

VerifyNonAdjacent verifies non-adjacent untrustedHeader against trustedHeader. It ensures that:

	a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned)
	b) untrustedHeader is valid (if not, ErrInvalidHeader is returned)
	c) trustLevel ([1/3, 1]) of trustedHeaderVals (or trustedHeaderNextVals)
 signed correctly (if not, ErrNewValSetCantBeTrusted is returned)
	d) more than 2/3 of untrustedVals have signed h2
   (otherwise, ErrInvalidHeader is returned)
 e) headers are non-adjacent.

maxClockDrift defines how much untrustedHeader.Time can drift into the future. trustedHeader must have a ChainID, Height and Time

Types

type BlockProvider

type BlockProvider struct {
	// contains filtered or unexported fields
}

BlockProvider is a p2p based light provider which uses a dispatcher connected to the state sync reactor to serve light blocks to the light client

TODO: This should probably be moved over to the light package but as we're not yet officially supporting p2p light clients we'll leave this here for now.

NOTE: BlockProvider will return an error with concurrent calls. However, we don't need a mutex because a light client (and the backfill process) will never call a method more than once at the same time

func NewBlockProvider

func NewBlockProvider(peer types.NodeID, chainID string, dispatcher *Dispatcher) *BlockProvider

Creates a block provider which implements the light client Provider interface.

func (*BlockProvider) ID

func (p *BlockProvider) ID() string

Returns the ID address of the provider (NodeID of peer)

func (*BlockProvider) LightBlock

func (p *BlockProvider) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error)

LightBlock fetches a light block from the peer at a specified height returning either a light block or an appropriate error.

func (*BlockProvider) ReportEvidence

func (p *BlockProvider) ReportEvidence(ctx context.Context, ev types.Evidence) error

ReportEvidence should allow for the light client to report any light client attacks. This is a no op as there currently isn't a way to wire this up to the evidence reactor (we should endeavor to do this in the future but for now it's not critical for backwards verification)

func (*BlockProvider) String

func (p *BlockProvider) String() string

String implements stringer interface

type Client

type Client struct {
	// contains filtered or unexported fields
}

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)

func NewClient

func NewClient(
	ctx context.Context,
	chainID string,
	trustOptions TrustOptions,
	primary provider.Provider,
	witnesses []provider.Provider,
	trustedStore store.Store,
	options ...Option,
) (*Client, error)

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 NewClientFromTrustedStore

func NewClientFromTrustedStore(
	chainID string,
	trustingPeriod time.Duration,
	primary provider.Provider,
	witnesses []provider.Provider,
	trustedStore store.Store,
	options ...Option) (*Client, error)

NewClientFromTrustedStore initializes an existing client from the trusted store. It does not check that the providers have the same trusted block.

func NewHTTPClient

func NewHTTPClient(
	ctx context.Context,
	chainID string,
	trustOptions TrustOptions,
	primaryAddress string,
	witnessesAddresses []string,
	trustedStore store.Store,
	options ...Option) (*Client, error)

NewHTTPClient initiates an instance of a light client using HTTP addresses for both the primary provider and witnesses of the light client. A trusted header and hash must be passed to initialize the client.

See all Option(s) for the additional configuration. See NewClient.

func (*Client) AddProvider

func (c *Client) AddProvider(p provider.Provider)

AddProvider adds a providers to the light clients set

NOTE: The light client does not check for uniqueness

func (*Client) ChainID

func (c *Client) ChainID() string

ChainID returns the chain ID the light client was configured with.

Safe for concurrent use by multiple goroutines.

func (*Client) Cleanup

func (c *Client) Cleanup() error

Cleanup removes all the data (headers and validator sets) stored. Note: the client must be stopped at this point.

func (*Client) FirstTrustedHeight

func (c *Client) FirstTrustedHeight() (int64, error)

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 (*Client) LastTrustedHeight

func (c *Client) LastTrustedHeight() (int64, error)

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 (*Client) Primary

func (c *Client) Primary() provider.Provider

Primary returns the primary provider.

NOTE: provider may be not safe for concurrent access.

func (*Client) Status

func (c *Client) Status(ctx context.Context) *types.LightClientInfo

func (*Client) TrustedLightBlock

func (c *Client) TrustedLightBlock(height int64) (*types.LightBlock, error)

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 (*Client) Update

func (c *Client) Update(ctx context.Context, now time.Time) (*types.LightBlock, error)

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 (*Client) VerifyHeader

func (c *Client) VerifyHeader(ctx context.Context, newHeader *types.Header, now time.Time) error

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/ari-anchor/sei-tendermint/blob/master/spec/light-client/README.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 (*Client) VerifyLightBlockAtHeight

func (c *Client) VerifyLightBlockAtHeight(ctx context.Context, height int64, now time.Time) (*types.LightBlock, error)

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 (*Client) Witnesses

func (c *Client) Witnesses() []provider.Provider

Witnesses returns the witness providers.

NOTE: providers may be not safe for concurrent access.

type Dispatcher

type Dispatcher struct {
	// contains filtered or unexported fields
}

A Dispatcher multiplexes concurrent requests by multiple peers for light blocks. Only one request per peer can be sent at a time. Subsequent concurrent requests will report an error from the LightBlock method. NOTE: It is not the responsibility of the dispatcher to verify the light blocks.

func NewDispatcher

func NewDispatcher(requestChannel *p2p.Channel, lightBlockMsgCreator func(uint64) proto.Message) *Dispatcher

func (*Dispatcher) Close

func (d *Dispatcher) Close()

Close shuts down the dispatcher and cancels any pending calls awaiting responses. Peers awaiting responses that have not arrived are delivered a nil block.

func (*Dispatcher) LightBlock

func (d *Dispatcher) LightBlock(ctx context.Context, height int64, peer types.NodeID) (*types.LightBlock, error)

LightBlock uses the request channel to fetch a light block from a given peer tracking, the call and waiting for the reactor to pass back the response. A nil LightBlock response is used to signal that the peer doesn't have the requested LightBlock.

func (*Dispatcher) Respond

func (d *Dispatcher) Respond(ctx context.Context, lb *tmproto.LightBlock, peer types.NodeID) error

Respond allows the underlying process which receives requests on the requestCh to respond with the respective light block. A nil response is used to represent that the receiver of the request does not have a light block at that height.

type ErrInvalidHeader

type ErrInvalidHeader struct {
	Reason error
}

ErrInvalidHeader means the header either failed the basic validation or commit is not signed by 2/3+.

func (ErrInvalidHeader) Error

func (e ErrInvalidHeader) Error() string

type ErrNewValSetCantBeTrusted

type ErrNewValSetCantBeTrusted struct {
	Reason types.ErrNotEnoughVotingPowerSigned
}

ErrNewValSetCantBeTrusted means the new validator set cannot be trusted because < 1/3rd (+trustLevel+) of the old validator set has signed.

func (ErrNewValSetCantBeTrusted) Error

type ErrOldHeaderExpired

type ErrOldHeaderExpired struct {
	At  time.Time
	Now time.Time
}

ErrOldHeaderExpired means the old (trusted) header has expired according to the given trustingPeriod and current time. If so, the light client must be reset subjectively.

func (ErrOldHeaderExpired) Error

func (e ErrOldHeaderExpired) Error() string

type ErrVerificationFailed

type ErrVerificationFailed struct {
	From   int64
	To     int64
	Reason error
}

ErrVerificationFailed means either sequential or skipping verification has failed to verify from header #1 to header #2 due to some reason.

func (ErrVerificationFailed) Error

func (e ErrVerificationFailed) Error() string

func (ErrVerificationFailed) Unwrap

func (e ErrVerificationFailed) Unwrap() error

Unwrap returns underlying reason.

type Option

type Option func(*Client)

Option sets a parameter for the light client.

func Logger

func Logger(l log.Logger) Option

Logger option can be used to set a logger for the client.

func MaxBlockLag

func MaxBlockLag(d time.Duration) Option

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 MaxClockDrift

func MaxClockDrift(d time.Duration) Option

MaxClockDrift defines how much new header's time can drift into the future relative to the light clients local time. Default: 10s.

func PruningSize

func PruningSize(h uint16) Option

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 SequentialVerification

func SequentialVerification() Option

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 SkippingVerification

func SkippingVerification(trustLevel tmmath.Fraction) Option

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.

type PeerList

type PeerList struct {
	// contains filtered or unexported fields
}

peerList is a rolling list of peers. This is used to distribute the load of retrieving blocks over all the peers the reactor is connected to

func NewPeerList

func NewPeerList() *PeerList

func (*PeerList) All

func (l *PeerList) All() []types.NodeID

func (*PeerList) Append

func (l *PeerList) Append(peer types.NodeID)

func (*PeerList) Contains

func (l *PeerList) Contains(id types.NodeID) bool

func (*PeerList) Len

func (l *PeerList) Len() int

func (*PeerList) Pop

func (l *PeerList) Pop(ctx context.Context) types.NodeID

func (*PeerList) Remove

func (l *PeerList) Remove(peer types.NodeID)

type StateProvider

type StateProvider interface {
	// AppHash returns the app hash after the given height has been committed.
	AppHash(ctx context.Context, height uint64) ([]byte, error)
	// Commit returns the commit at the given height.
	Commit(ctx context.Context, height uint64) (*types.Commit, error)
	// State returns a state object at the given height.
	State(ctx context.Context, height uint64) (sm.State, error)
}

StateProvider is a provider of trusted state data for bootstrapping a node. This refers to the state.State object, not the state machine. There are two implementations. One uses the P2P layer and the other uses the RPC layer. Both use light client verification.

func NewP2PStateProvider

func NewP2PStateProvider(
	ctx context.Context,
	chainID string,
	initialHeight int64,
	providers []lightprovider.Provider,
	trustOptions TrustOptions,
	paramsSendCh *p2p.Channel,
	logger log.Logger,
	paramsReqCreator func(uint64) proto.Message,
) (StateProvider, error)

NewP2PStateProvider creates a light client state provider but uses a dispatcher connected to the P2P layer

func NewRPCStateProvider

func NewRPCStateProvider(
	ctx context.Context,
	chainID string,
	initialHeight int64,
	servers []string,
	trustOptions TrustOptions,
	logger log.Logger,
) (StateProvider, error)

NewRPCStateProvider creates a new StateProvider using a light client and RPC clients.

type StateProviderP2P

type StateProviderP2P struct {
	sync.Mutex // light.Client is not concurrency-safe
	// contains filtered or unexported fields
}

func (*StateProviderP2P) AddProvider

func (s *StateProviderP2P) AddProvider(p lightprovider.Provider)

addProvider dynamically adds a peer as a new witness. A limit of 6 providers is kept as a heuristic. Too many overburdens the network and too little compromises the second layer of security.

func (*StateProviderP2P) AppHash

func (s *StateProviderP2P) AppHash(ctx context.Context, height uint64) ([]byte, error)

AppHash implements StateProvider.

func (*StateProviderP2P) Commit

func (s *StateProviderP2P) Commit(ctx context.Context, height uint64) (*types.Commit, error)

Commit implements StateProvider.

func (*StateProviderP2P) ParamsRecvCh

func (s *StateProviderP2P) ParamsRecvCh() chan types.ConsensusParams

func (*StateProviderP2P) State

func (s *StateProviderP2P) State(ctx context.Context, height uint64) (sm.State, error)

State implements StateProvider.

type TrustOptions

type TrustOptions struct {
	// tp: trusting period.
	//
	// Should be significantly less than the unbonding period (e.g. unbonding
	// period = 3 weeks, trusting period = 2 weeks).
	//
	// More specifically, trusting period + time needed to check headers + time
	// needed to report and punish misbehavior should be less than the unbonding
	// period.
	Period time.Duration

	// Header's Height and Hash must both be provided to force the trusting of a
	// particular header.
	Height int64
	Hash   []byte
}

TrustOptions are the trust parameters needed when a new light client connects to the network or when an existing light client that has been offline for longer than the trusting period connects to the network.

The expectation is the user will get this information from a trusted source like a validator, a friend, or a secure website. A more user friendly solution with trust tradeoffs is that we establish an https based protocol with a default end point that populates this information. Also an on-chain registry of roots-of-trust (e.g. on the Cosmos Hub) seems likely in the future.

func (TrustOptions) ValidateBasic

func (opts TrustOptions) ValidateBasic() error

ValidateBasic performs basic validation.

Directories

Path Synopsis
Package mbt provides a test runner for model-based tests
Package mbt provides a test runner for model-based tests
rpc
db

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL