github.com/Finschia/ostracon@v1.1.5/statesync/stateprovider.go (about)

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