You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

187 lines
5.9 KiB

package statesync
import (
"fmt"
"strings"
"time"
dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/libs/log"
tmsync "github.com/tendermint/tendermint/libs/sync"
"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"
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
sm "github.com/tendermint/tendermint/state"
"github.com/tendermint/tendermint/types"
)
//go:generate mockery --case underscore --name StateProvider
// StateProvider is a provider of trusted state data for bootstrapping a node. This refers
// to the state.State object, not the state machine.
type StateProvider interface {
// AppHash returns the app hash after the given height has been committed.
AppHash(height uint64) ([]byte, error)
// Commit returns the commit at the given height.
Commit(height uint64) (*types.Commit, error)
// State returns a state object at the given height.
State(height uint64) (sm.State, error)
}
// lightClientStateProvider is a state provider using the light client.
type lightClientStateProvider struct {
tmsync.Mutex // light.Client is not concurrency-safe
lc *light.Client
version tmstate.Version
initialHeight int64
providers map[lightprovider.Provider]string
}
// NewLightClientStateProvider creates a new StateProvider using a light client and RPC clients.
func NewLightClientStateProvider(
chainID string,
version tmstate.Version,
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 %v", 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(chainID, trustOptions, providers[0], providers[1:],
lightdb.New(dbm.NewMemDB(), ""), light.Logger(logger), light.MaxRetryAttempts(5))
if err != nil {
return nil, err
}
return &lightClientStateProvider{
lc: lc,
version: version,
initialHeight: initialHeight,
providers: providerRemotes,
}, nil
}
// AppHash implements StateProvider.
func (s *lightClientStateProvider) AppHash(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.lc.VerifyHeaderAtHeight(int64(height+1), time.Now())
if err != nil {
return nil, err
}
return header.AppHash, nil
}
// Commit implements StateProvider.
func (s *lightClientStateProvider) Commit(height uint64) (*types.Commit, error) {
s.Lock()
defer s.Unlock()
header, err := s.lc.VerifyHeaderAtHeight(int64(height), time.Now())
if err != nil {
return nil, err
}
return header.Commit, nil
}
// State implements StateProvider.
func (s *lightClientStateProvider) State(height uint64) (sm.State, error) {
s.Lock()
defer s.Unlock()
state := sm.State{
ChainID: s.lc.ChainID(),
Version: s.version,
InitialHeight: s.initialHeight,
}
if state.InitialHeight == 0 {
state.InitialHeight = 1
}
// We need to verify up until h+2, to get the validator set. This also prefetches the headers
// for h and h+1 in the typical case where the trusted header is after the snapshot height.
_, err := s.lc.VerifyHeaderAtHeight(int64(height+2), time.Now())
if err != nil {
return sm.State{}, err
}
header, err := s.lc.VerifyHeaderAtHeight(int64(height), time.Now())
if err != nil {
return sm.State{}, err
}
nextHeader, err := s.lc.VerifyHeaderAtHeight(int64(height+1), time.Now())
if err != nil {
return sm.State{}, err
}
state.LastBlockHeight = header.Height
state.LastBlockTime = header.Time
state.LastBlockID = header.Commit.BlockID
state.AppHash = nextHeader.AppHash
state.LastResultsHash = nextHeader.LastResultsHash
state.LastValidators, _, err = s.lc.TrustedValidatorSet(int64(height))
if err != nil {
return sm.State{}, err
}
state.Validators, _, err = s.lc.TrustedValidatorSet(int64(height + 1))
if err != nil {
return sm.State{}, err
}
state.NextValidators, _, err = s.lc.TrustedValidatorSet(int64(height + 2))
if err != nil {
return sm.State{}, err
}
state.LastHeightValidatorsChanged = int64(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(primaryRPC, s.lc)
result, err := rpcclient.ConsensusParams(&nextHeader.Height)
if err != nil {
return sm.State{}, fmt.Errorf("unable to fetch consensus parameters for height %v: %w",
nextHeader.Height, err)
}
state.ConsensusParams = result.ConsensusParams
return state, nil
}
// rpcClient sets up a new RPC client
func rpcClient(server string) (*rpchttp.HTTP, error) {
if !strings.Contains(server, "://") {
server = "http://" + server
}
c, err := rpchttp.New(server, "/websocket")
if err != nil {
return nil, err
}
return c, nil
}