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, ¤tLightBlock.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 }