- package statesync
-
- import (
- "bytes"
- "context"
- "errors"
- "fmt"
- "strings"
- "sync"
- "time"
-
- dbm "github.com/tendermint/tm-db"
-
- "github.com/tendermint/tendermint/internal/p2p"
- sm "github.com/tendermint/tendermint/internal/state"
- "github.com/tendermint/tendermint/libs/log"
- "github.com/tendermint/tendermint/light"
- lightprovider "github.com/tendermint/tendermint/light/provider"
- lighthttp "github.com/tendermint/tendermint/light/provider/http"
- lightrpc "github.com/tendermint/tendermint/light/rpc"
- lightdb "github.com/tendermint/tendermint/light/store/db"
- ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync"
- rpchttp "github.com/tendermint/tendermint/rpc/client/http"
- "github.com/tendermint/tendermint/types"
- "github.com/tendermint/tendermint/version"
- )
-
- //go:generate ../../scripts/mockery_generate.sh StateProvider
-
- // 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.
- 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)
- }
-
- type stateProviderRPC struct {
- sync.Mutex // light.Client is not concurrency-safe
- lc *light.Client
- initialHeight int64
- providers map[lightprovider.Provider]string
- logger log.Logger
- }
-
- // NewRPCStateProvider creates a new StateProvider using a light client and RPC clients.
- func NewRPCStateProvider(
- ctx context.Context,
- chainID string,
- initialHeight int64,
- servers []string,
- trustOptions light.TrustOptions,
- logger log.Logger,
- ) (StateProvider, error) {
- if len(servers) < 2 {
- return nil, fmt.Errorf("at least 2 RPC servers are required, got %d", len(servers))
- }
-
- providers := make([]lightprovider.Provider, 0, len(servers))
- providerRemotes := make(map[lightprovider.Provider]string)
- for _, server := range servers {
- client, err := rpcClient(server)
- if err != nil {
- return nil, fmt.Errorf("failed to set up RPC client: %w", err)
- }
- provider := lighthttp.NewWithClient(chainID, client)
- providers = append(providers, provider)
- // We store the RPC addresses keyed by provider, so we can find the address of the primary
- // provider used by the light client and use it to fetch consensus parameters.
- providerRemotes[provider] = server
- }
-
- lc, err := light.NewClient(ctx, chainID, trustOptions, providers[0], providers[1:],
- lightdb.New(dbm.NewMemDB()), light.Logger(logger))
- if err != nil {
- return nil, err
- }
- return &stateProviderRPC{
- logger: logger,
- lc: lc,
- initialHeight: initialHeight,
- providers: providerRemotes,
- }, nil
- }
-
- func (s *stateProviderRPC) verifyLightBlockAtHeight(ctx context.Context, height uint64, ts time.Time) (*types.LightBlock, error) {
- ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
- defer cancel()
- return s.lc.VerifyLightBlockAtHeight(ctx, int64(height), ts)
- }
-
- // AppHash implements part of StateProvider. It calls the application to verify the
- // light blocks at heights h+1 and h+2 and, if verification succeeds, reports the app
- // hash for the block at height h+1 which correlates to the state at height h.
- func (s *stateProviderRPC) AppHash(ctx context.Context, height uint64) ([]byte, error) {
- s.Lock()
- defer s.Unlock()
-
- // We have to fetch the next height, which contains the app hash for the previous height.
- header, err := s.verifyLightBlockAtHeight(ctx, height+1, time.Now())
- if err != nil {
- return nil, err
- }
-
- // We also try to fetch the blocks at H+2, since we need these
- // when building the state while restoring the snapshot. This avoids the race
- // condition where we try to restore a snapshot before H+2 exists.
- _, err = s.verifyLightBlockAtHeight(ctx, height+2, time.Now())
- if err != nil {
- return nil, err
- }
- return header.AppHash, nil
- }
-
- // Commit implements StateProvider.
- func (s *stateProviderRPC) Commit(ctx context.Context, height uint64) (*types.Commit, error) {
- s.Lock()
- defer s.Unlock()
- header, err := s.verifyLightBlockAtHeight(ctx, height, time.Now())
- if err != nil {
- return nil, err
- }
- return header.Commit, nil
- }
-
- // State implements StateProvider.
- func (s *stateProviderRPC) State(ctx context.Context, height uint64) (sm.State, error) {
- s.Lock()
- defer s.Unlock()
-
- state := sm.State{
- ChainID: s.lc.ChainID(),
- InitialHeight: s.initialHeight,
- }
- if state.InitialHeight == 0 {
- state.InitialHeight = 1
- }
-
- // The snapshot height maps onto the state heights as follows:
- //
- // height: last block, i.e. the snapshotted height
- // height+1: current block, i.e. the first block we'll process after the snapshot
- // height+2: next block, i.e. the second block after the snapshot
- //
- // We need to fetch the NextValidators from height+2 because if the application changed
- // the validator set at the snapshot height then this only takes effect at height+2.
- lastLightBlock, err := s.verifyLightBlockAtHeight(ctx, height, time.Now())
- if err != nil {
- return sm.State{}, err
- }
- currentLightBlock, err := s.verifyLightBlockAtHeight(ctx, height+1, time.Now())
- if err != nil {
- return sm.State{}, err
- }
- nextLightBlock, err := s.verifyLightBlockAtHeight(ctx, height+2, time.Now())
- if err != nil {
- return sm.State{}, err
- }
-
- state.Version = sm.Version{
- Consensus: currentLightBlock.Version,
- Software: version.TMVersion,
- }
- state.LastBlockHeight = lastLightBlock.Height
- state.LastBlockTime = lastLightBlock.Time
- state.LastBlockID = lastLightBlock.Commit.BlockID
- state.AppHash = currentLightBlock.AppHash
- state.LastResultsHash = currentLightBlock.LastResultsHash
- state.LastValidators = lastLightBlock.ValidatorSet
- state.Validators = currentLightBlock.ValidatorSet
- state.NextValidators = nextLightBlock.ValidatorSet
- state.LastHeightValidatorsChanged = nextLightBlock.Height
-
- // We'll also need to fetch consensus params via RPC, using light client verification.
- primaryURL, ok := s.providers[s.lc.Primary()]
- if !ok || primaryURL == "" {
- return sm.State{}, fmt.Errorf("could not find address for primary light client provider")
- }
- primaryRPC, err := rpcClient(primaryURL)
- if err != nil {
- return sm.State{}, fmt.Errorf("unable to create RPC client: %w", err)
- }
- rpcclient := lightrpc.NewClient(s.logger, primaryRPC, s.lc)
- result, err := rpcclient.ConsensusParams(ctx, ¤tLightBlock.Height)
- if err != nil {
- return sm.State{}, fmt.Errorf("unable to fetch consensus parameters for height %v: %w",
- nextLightBlock.Height, err)
- }
- state.ConsensusParams = result.ConsensusParams
- state.LastHeightConsensusParamsChanged = currentLightBlock.Height
-
- return state, nil
- }
-
- // rpcClient sets up a new RPC client
- func rpcClient(server string) (*rpchttp.HTTP, error) {
- if !strings.Contains(server, "://") {
- server = "http://" + server
- }
- return rpchttp.New(server)
- }
-
- type stateProviderP2P struct {
- sync.Mutex // light.Client is not concurrency-safe
- lc *light.Client
- initialHeight int64
- paramsSendCh *p2p.Channel
- paramsRecvCh chan types.ConsensusParams
- }
-
- // NewP2PStateProvider creates a light client state
- // provider but uses a dispatcher connected to the P2P layer
- func NewP2PStateProvider(
- ctx context.Context,
- chainID string,
- initialHeight int64,
- providers []lightprovider.Provider,
- trustOptions light.TrustOptions,
- paramsSendCh *p2p.Channel,
- logger log.Logger,
- ) (StateProvider, error) {
- if len(providers) < 2 {
- return nil, fmt.Errorf("at least 2 peers are required, got %d", len(providers))
- }
-
- lc, err := light.NewClient(ctx, chainID, trustOptions, providers[0], providers[1:],
- lightdb.New(dbm.NewMemDB()), light.Logger(logger))
- if err != nil {
- return nil, err
- }
-
- return &stateProviderP2P{
- lc: lc,
- initialHeight: initialHeight,
- paramsSendCh: paramsSendCh,
- paramsRecvCh: make(chan types.ConsensusParams),
- }, nil
- }
-
- func (s *stateProviderP2P) verifyLightBlockAtHeight(ctx context.Context, height uint64, ts time.Time) (*types.LightBlock, error) {
- ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
- defer cancel()
- return s.lc.VerifyLightBlockAtHeight(ctx, int64(height), ts)
- }
-
- // AppHash implements StateProvider.
- func (s *stateProviderP2P) AppHash(ctx context.Context, height uint64) ([]byte, error) {
- s.Lock()
- defer s.Unlock()
-
- // We have to fetch the next height, which contains the app hash for the previous height.
- header, err := s.verifyLightBlockAtHeight(ctx, height+1, time.Now())
- if err != nil {
- return nil, err
- }
-
- // We also try to fetch the blocks at H+2, since we need these
- // when building the state while restoring the snapshot. This avoids the race
- // condition where we try to restore a snapshot before H+2 exists.
- _, err = s.verifyLightBlockAtHeight(ctx, height+2, time.Now())
- if err != nil {
- return nil, err
- }
- return header.AppHash, nil
- }
-
- // Commit implements StateProvider.
- func (s *stateProviderP2P) Commit(ctx context.Context, height uint64) (*types.Commit, error) {
- s.Lock()
- defer s.Unlock()
- header, err := s.verifyLightBlockAtHeight(ctx, height, time.Now())
- if err != nil {
- return nil, err
- }
- return header.Commit, nil
- }
-
- // State implements StateProvider.
- func (s *stateProviderP2P) State(ctx context.Context, height uint64) (sm.State, error) {
- s.Lock()
- defer s.Unlock()
-
- state := sm.State{
- ChainID: s.lc.ChainID(),
- InitialHeight: s.initialHeight,
- }
- if state.InitialHeight == 0 {
- state.InitialHeight = 1
- }
-
- // The snapshot height maps onto the state heights as follows:
- //
- // height: last block, i.e. the snapshotted height
- // height+1: current block, i.e. the first block we'll process after the snapshot
- // height+2: next block, i.e. the second block after the snapshot
- //
- // We need to fetch the NextValidators from height+2 because if the application changed
- // the validator set at the snapshot height then this only takes effect at height+2.
- lastLightBlock, err := s.verifyLightBlockAtHeight(ctx, height, time.Now())
- if err != nil {
- return sm.State{}, err
- }
- currentLightBlock, err := s.verifyLightBlockAtHeight(ctx, height+1, time.Now())
- if err != nil {
- return sm.State{}, err
- }
- nextLightBlock, err := s.verifyLightBlockAtHeight(ctx, height+2, time.Now())
- if err != nil {
- return sm.State{}, err
- }
-
- state.Version = sm.Version{
- Consensus: currentLightBlock.Version,
- Software: version.TMVersion,
- }
- state.LastBlockHeight = lastLightBlock.Height
- state.LastBlockTime = lastLightBlock.Time
- state.LastBlockID = lastLightBlock.Commit.BlockID
- state.AppHash = currentLightBlock.AppHash
- state.LastResultsHash = currentLightBlock.LastResultsHash
- state.LastValidators = lastLightBlock.ValidatorSet
- state.Validators = currentLightBlock.ValidatorSet
- state.NextValidators = nextLightBlock.ValidatorSet
- state.LastHeightValidatorsChanged = nextLightBlock.Height
-
- // We'll also need to fetch consensus params via P2P.
- state.ConsensusParams, err = s.consensusParams(ctx, currentLightBlock.Height)
- if err != nil {
- return sm.State{}, err
- }
- // validate the consensus params
- if !bytes.Equal(nextLightBlock.ConsensusHash, state.ConsensusParams.HashConsensusParams()) {
- return sm.State{}, fmt.Errorf("consensus params hash mismatch at height %d. Expected %v, got %v",
- currentLightBlock.Height, nextLightBlock.ConsensusHash, state.ConsensusParams.HashConsensusParams())
- }
- // set the last height changed to the current height
- state.LastHeightConsensusParamsChanged = currentLightBlock.Height
-
- return state, nil
- }
-
- // 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 (s *stateProviderP2P) addProvider(p lightprovider.Provider) {
- if len(s.lc.Witnesses()) < 6 {
- s.lc.AddProvider(p)
- }
- }
-
- // consensusParams sends out a request for consensus params blocking
- // until one is returned.
- //
- // If it fails to get a valid set of consensus params from any of the
- // providers it returns an error; however, it will retry indefinitely
- // (with backoff) until the context is canceled.
- func (s *stateProviderP2P) consensusParams(ctx context.Context, height int64) (types.ConsensusParams, error) {
- iterCount := 0
- for {
- params, err := s.tryGetConsensusParamsFromWitnesses(ctx, height)
- if err != nil {
- return types.ConsensusParams{}, err
- }
- if params != nil {
- return *params, nil
- }
- iterCount++
-
- select {
- case <-ctx.Done():
- return types.ConsensusParams{}, ctx.Err()
- case <-time.After(time.Duration(iterCount) * consensusParamsResponseTimeout):
- }
- }
- }
-
- // tryGetConsensusParamsFromWitnesses attempts to get consensus
- // parameters from the light clients available witnesses. If both
- // return parameters are nil, then it can be retried.
- func (s *stateProviderP2P) tryGetConsensusParamsFromWitnesses(
- ctx context.Context,
- height int64,
- ) (*types.ConsensusParams, error) {
- for _, provider := range s.lc.Witnesses() {
- p, ok := provider.(*BlockProvider)
- if !ok {
- panic("expected p2p state provider to use p2p block providers")
- }
-
- // extract the nodeID of the provider
- peer, err := types.NewNodeID(p.String())
- if err != nil {
- return nil, fmt.Errorf("invalid provider (%s) node id: %w", p.String(), err)
- }
-
- if err := s.paramsSendCh.Send(ctx, p2p.Envelope{
- To: peer,
- Message: &ssproto.ParamsRequest{
- Height: uint64(height),
- },
- }); err != nil {
- return nil, err
- }
-
- select {
- // if we get no response from this provider we move on to the next one
- case <-time.After(consensusParamsResponseTimeout):
- continue
- case <-ctx.Done():
- return nil, ctx.Err()
- case params, ok := <-s.paramsRecvCh:
- if !ok {
- return nil, errors.New("params channel closed")
- }
- return ¶ms, nil
- }
- }
-
- // signal to caller to retry.
- return nil, nil
- }
|