github.com/vipernet-xyz/tm@v0.34.24/statesync/stateprovider.go (about)

     1  package statesync
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	dbm "github.com/tendermint/tm-db"
    10  
    11  	"github.com/vipernet-xyz/tm/libs/log"
    12  	tmsync "github.com/vipernet-xyz/tm/libs/sync"
    13  	"github.com/vipernet-xyz/tm/light"
    14  	lightprovider "github.com/vipernet-xyz/tm/light/provider"
    15  	lighthttp "github.com/vipernet-xyz/tm/light/provider/http"
    16  	lightrpc "github.com/vipernet-xyz/tm/light/rpc"
    17  	lightdb "github.com/vipernet-xyz/tm/light/store/db"
    18  	tmstate "github.com/vipernet-xyz/tm/proto/tendermint/state"
    19  	rpchttp "github.com/vipernet-xyz/tm/rpc/client/http"
    20  	sm "github.com/vipernet-xyz/tm/state"
    21  	"github.com/vipernet-xyz/tm/types"
    22  	"github.com/vipernet-xyz/tm/version"
    23  )
    24  
    25  //go:generate ../scripts/mockery_generate.sh StateProvider
    26  
    27  // StateProvider is a provider of trusted state data for bootstrapping a node. This refers
    28  // to the state.State object, not the state machine.
    29  type StateProvider interface {
    30  	// AppHash returns the app hash after the given height has been committed.
    31  	AppHash(ctx context.Context, height uint64) ([]byte, error)
    32  	// Commit returns the commit at the given height.
    33  	Commit(ctx context.Context, height uint64) (*types.Commit, error)
    34  	// State returns a state object at the given height.
    35  	State(ctx context.Context, height uint64) (sm.State, error)
    36  }
    37  
    38  // lightClientStateProvider is a state provider using the light client.
    39  type lightClientStateProvider struct {
    40  	tmsync.Mutex  // light.Client is not concurrency-safe
    41  	lc            *light.Client
    42  	version       tmstate.Version
    43  	initialHeight int64
    44  	providers     map[lightprovider.Provider]string
    45  }
    46  
    47  // NewLightClientStateProvider creates a new StateProvider using a light client and RPC clients.
    48  func NewLightClientStateProvider(
    49  	ctx context.Context,
    50  	chainID string,
    51  	version tmstate.Version,
    52  	initialHeight int64,
    53  	servers []string,
    54  	trustOptions light.TrustOptions,
    55  	logger log.Logger,
    56  ) (StateProvider, error) {
    57  	if len(servers) < 2 {
    58  		return nil, fmt.Errorf("at least 2 RPC servers are required, got %v", len(servers))
    59  	}
    60  
    61  	providers := make([]lightprovider.Provider, 0, len(servers))
    62  	providerRemotes := make(map[lightprovider.Provider]string)
    63  	for _, server := range servers {
    64  		client, err := rpcClient(server)
    65  		if err != nil {
    66  			return nil, fmt.Errorf("failed to set up RPC client: %w", err)
    67  		}
    68  		provider := lighthttp.NewWithClient(chainID, client)
    69  		providers = append(providers, provider)
    70  		// We store the RPC addresses keyed by provider, so we can find the address of the primary
    71  		// provider used by the light client and use it to fetch consensus parameters.
    72  		providerRemotes[provider] = server
    73  	}
    74  
    75  	lc, err := light.NewClient(ctx, chainID, trustOptions, providers[0], providers[1:],
    76  		lightdb.New(dbm.NewMemDB(), ""), light.Logger(logger), light.MaxRetryAttempts(5))
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	return &lightClientStateProvider{
    81  		lc:            lc,
    82  		version:       version,
    83  		initialHeight: initialHeight,
    84  		providers:     providerRemotes,
    85  	}, nil
    86  }
    87  
    88  // AppHash implements StateProvider.
    89  func (s *lightClientStateProvider) AppHash(ctx context.Context, height uint64) ([]byte, error) {
    90  	s.Lock()
    91  	defer s.Unlock()
    92  
    93  	// We have to fetch the next height, which contains the app hash for the previous height.
    94  	header, err := s.lc.VerifyLightBlockAtHeight(ctx, int64(height+1), time.Now())
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	// We also try to fetch the blocks at height H and H+2, since we need these
    99  	// when building the state while restoring the snapshot. This avoids the race
   100  	// condition where we try to restore a snapshot before H+2 exists.
   101  	//
   102  	// FIXME This is a hack, since we can't add new methods to the interface without
   103  	// breaking it. We should instead have a Has(ctx, height) method which checks
   104  	// that the state provider has access to the necessary data for the height.
   105  	// We piggyback on AppHash() since it's called when adding snapshots to the pool.
   106  	_, err = s.lc.VerifyLightBlockAtHeight(ctx, int64(height+2), time.Now())
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	return header.AppHash, nil
   111  }
   112  
   113  // Commit implements StateProvider.
   114  func (s *lightClientStateProvider) Commit(ctx context.Context, height uint64) (*types.Commit, error) {
   115  	s.Lock()
   116  	defer s.Unlock()
   117  	header, err := s.lc.VerifyLightBlockAtHeight(ctx, int64(height), time.Now())
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	return header.Commit, nil
   122  }
   123  
   124  // State implements StateProvider.
   125  func (s *lightClientStateProvider) State(ctx context.Context, height uint64) (sm.State, error) {
   126  	s.Lock()
   127  	defer s.Unlock()
   128  
   129  	state := sm.State{
   130  		ChainID:       s.lc.ChainID(),
   131  		Version:       s.version,
   132  		InitialHeight: s.initialHeight,
   133  	}
   134  	if state.InitialHeight == 0 {
   135  		state.InitialHeight = 1
   136  	}
   137  
   138  	// The snapshot height maps onto the state heights as follows:
   139  	//
   140  	// height: last block, i.e. the snapshotted height
   141  	// height+1: current block, i.e. the first block we'll process after the snapshot
   142  	// height+2: next block, i.e. the second block after the snapshot
   143  	//
   144  	// We need to fetch the NextValidators from height+2 because if the application changed
   145  	// the validator set at the snapshot height then this only takes effect at height+2.
   146  	lastLightBlock, err := s.lc.VerifyLightBlockAtHeight(ctx, int64(height), time.Now())
   147  	if err != nil {
   148  		return sm.State{}, err
   149  	}
   150  	currentLightBlock, err := s.lc.VerifyLightBlockAtHeight(ctx, int64(height+1), time.Now())
   151  	if err != nil {
   152  		return sm.State{}, err
   153  	}
   154  	nextLightBlock, err := s.lc.VerifyLightBlockAtHeight(ctx, int64(height+2), time.Now())
   155  	if err != nil {
   156  		return sm.State{}, err
   157  	}
   158  
   159  	state.Version = tmstate.Version{
   160  		Consensus: currentLightBlock.Version,
   161  		Software:  version.TMCoreSemVer,
   162  	}
   163  	state.LastBlockHeight = lastLightBlock.Height
   164  	state.LastBlockTime = lastLightBlock.Time
   165  	state.LastBlockID = lastLightBlock.Commit.BlockID
   166  	state.AppHash = currentLightBlock.AppHash
   167  	state.LastResultsHash = currentLightBlock.LastResultsHash
   168  	state.LastValidators = lastLightBlock.ValidatorSet
   169  	state.Validators = currentLightBlock.ValidatorSet
   170  	state.NextValidators = nextLightBlock.ValidatorSet
   171  	state.LastHeightValidatorsChanged = nextLightBlock.Height
   172  
   173  	// We'll also need to fetch consensus params via RPC, using light client verification.
   174  	primaryURL, ok := s.providers[s.lc.Primary()]
   175  	if !ok || primaryURL == "" {
   176  		return sm.State{}, fmt.Errorf("could not find address for primary light client provider")
   177  	}
   178  	primaryRPC, err := rpcClient(primaryURL)
   179  	if err != nil {
   180  		return sm.State{}, fmt.Errorf("unable to create RPC client: %w", err)
   181  	}
   182  	rpcclient := lightrpc.NewClient(primaryRPC, s.lc)
   183  	result, err := rpcclient.ConsensusParams(ctx, &currentLightBlock.Height)
   184  	if err != nil {
   185  		return sm.State{}, fmt.Errorf("unable to fetch consensus parameters for height %v: %w",
   186  			nextLightBlock.Height, err)
   187  	}
   188  	state.ConsensusParams = result.ConsensusParams
   189  	state.LastHeightConsensusParamsChanged = currentLightBlock.Height
   190  
   191  	return state, nil
   192  }
   193  
   194  // rpcClient sets up a new RPC client
   195  func rpcClient(server string) (*rpchttp.HTTP, error) {
   196  	if !strings.Contains(server, "://") {
   197  		server = "http://" + server
   198  	}
   199  	c, err := rpchttp.New(server, "/websocket")
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	return c, nil
   204  }