code.vegaprotocol.io/vega@v0.79.0/wallet/service/starter.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package service 17 18 import ( 19 "context" 20 "errors" 21 "fmt" 22 "net/http" 23 "sync/atomic" 24 "time" 25 26 vgclose "code.vegaprotocol.io/vega/libs/close" 27 vgjob "code.vegaprotocol.io/vega/libs/job" 28 vgzap "code.vegaprotocol.io/vega/libs/zap" 29 coreversion "code.vegaprotocol.io/vega/version" 30 "code.vegaprotocol.io/vega/wallet/api" 31 nodeapi "code.vegaprotocol.io/vega/wallet/api/node" 32 "code.vegaprotocol.io/vega/wallet/api/spam" 33 "code.vegaprotocol.io/vega/wallet/network" 34 "code.vegaprotocol.io/vega/wallet/node" 35 servicev1 "code.vegaprotocol.io/vega/wallet/service/v1" 36 servicev2 "code.vegaprotocol.io/vega/wallet/service/v2" 37 "code.vegaprotocol.io/vega/wallet/service/v2/connections" 38 walletversion "code.vegaprotocol.io/vega/wallet/version" 39 "code.vegaprotocol.io/vega/wallet/wallets" 40 41 "go.uber.org/zap" 42 ) 43 44 //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/wallet/service NetworkStore 45 46 const serviceStoppingTimeout = 3 * time.Minute 47 48 var ErrCannotStartMultipleServiceAtTheSameTime = errors.New("cannot start multiple service at the same time") 49 50 // LoggerBuilderFunc is used to build a logger. It returns the built logger and a 51 // zap.AtomicLevel to allow the caller to dynamically change the log level. 52 type LoggerBuilderFunc func(level string) (*zap.Logger, zap.AtomicLevel, error) 53 54 type ProcessStoppedNotifier func() 55 56 type NetworkStore interface { 57 NetworkExists(string) (bool, error) 58 GetNetwork(string) (*network.Network, error) 59 } 60 61 type Starter struct { 62 walletStore api.WalletStore 63 netStore NetworkStore 64 svcStore Store 65 policy servicev1.Policy 66 connectionsManager *connections.Manager 67 interactor api.Interactor 68 69 loggerBuilderFunc LoggerBuilderFunc 70 71 isStarted atomic.Bool 72 } 73 74 type ResourceContext struct { 75 ServiceURL string 76 ErrCh chan error 77 } 78 79 // Start builds the components the service relies on and start it. 80 // 81 // # Why build certain components only at start up, and not during the build phase? 82 // 83 // This is because some components are relying on editable configuration. So, the 84 // service must be able to be restarted with an updated configuration. Building 85 // these components up front would prevent that. This is particularly true for 86 // desktop applications that can edit the configuration and start the service 87 // in the same process. 88 func (s *Starter) Start(jobRunner *vgjob.Runner, network string, noVersionCheck bool) (_ *ResourceContext, err error) { 89 rc := &ResourceContext{} 90 if s.isStarted.Load() { 91 return nil, ErrCannotStartMultipleServiceAtTheSameTime 92 } 93 s.isStarted.Store(true) 94 defer func() { 95 if err != nil { 96 // If we exit with an error, we reset the state. 97 s.isStarted.Store(false) 98 } 99 }() 100 101 logger, logLevel, errDetails := s.buildServiceLogger(network) 102 if errDetails != nil { 103 return nil, errDetails 104 } 105 defer vgzap.Sync(logger) 106 107 serviceCfg, err := s.svcStore.GetConfig() 108 if err != nil { 109 return nil, fmt.Errorf("could not retrieve the service configuration: %w", err) 110 } 111 112 if err := serviceCfg.Validate(); err != nil { 113 return nil, err 114 } 115 116 rc.ServiceURL = serviceCfg.Server.String() 117 118 // Since we successfully retrieve the service configuration, we can update 119 // the log level to the specified one. 120 if err := updateLogLevel(logLevel, serviceCfg); err != nil { 121 return nil, err 122 } 123 124 networkCfg, err := s.networkConfig(logger, network) 125 if err != nil { 126 return nil, err 127 } 128 129 if !noVersionCheck { 130 if err := s.ensureSoftwareIsCompatibleWithNetwork(logger, networkCfg); err != nil { 131 return nil, err 132 } 133 } else { 134 logger.Warn("The compatibility check between the software and the network has been skipped") 135 } 136 137 if err := s.ensureServiceIsInitialised(logger); err != nil { 138 return nil, err 139 } 140 141 // Check if the port we want to bind is free. It is not fool-proof, but it 142 // should catch most of the port-binding problems. 143 if err := ensurePortCanBeBound(jobRunner.Ctx(), logger, serviceCfg.Server.String()); err != nil { 144 return nil, err 145 } 146 147 apiLogger := logger.Named("api") 148 149 // We have several components that hold resources that needs to be released 150 // when stopping the service. 151 closer := vgclose.NewCloser() 152 153 proofOfWork := spam.NewHandler() 154 155 // API v1 156 apiV1, err := s.buildAPIV1(apiLogger, closer, networkCfg, serviceCfg, proofOfWork) 157 if err != nil { 158 logger.Error("Could not build the HTTP API v1", zap.Error(err)) 159 return nil, err 160 } 161 162 // API v2 163 apiV2, err := s.buildAPIV2(apiLogger, networkCfg, proofOfWork, closer, serviceCfg.APIV2) 164 if err != nil { 165 logger.Error("Could not build the HTTP API v2", zap.Error(err)) 166 return nil, err 167 } 168 169 svc := NewService(logger.Named("http-server"), serviceCfg, apiV1, apiV2) 170 171 // This job stops the service when the job context is set as done. 172 // This is required because we can't bind the service to a context. 173 jobRunner.Go(func(jobCtx context.Context) { 174 defer s.isStarted.Store(false) 175 defer vgzap.Sync(logger) 176 177 // We wait for the job context to be cancelled to stop the service. 178 <-jobCtx.Done() 179 180 // Stopping the service with a maximum wait of 3 minutes. 181 ctxWithTimeout, cancelFunc := context.WithTimeout(context.Background(), serviceStoppingTimeout) 182 defer cancelFunc() 183 if err := svc.Stop(ctxWithTimeout); err != nil { 184 logger.Warn("Could not properly stop the HTTP server", 185 zap.Duration("timeout", serviceStoppingTimeout), 186 zap.Error(err), 187 ) 188 } else { 189 logger.Warn("the HTTP server gracefully stopped") 190 } 191 }) 192 193 internalErrorReporter := make(chan error, 1) 194 rc.ErrCh = internalErrorReporter 195 196 jobRunner.Go(func(_ context.Context) { 197 defer close(internalErrorReporter) 198 defer vgzap.Sync(logger) 199 200 logger.Info("Starting the HTTP server") 201 if err := svc.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) { 202 logger.Error("Error while running HTTP server", zap.Error(err)) 203 // We warn the caller about the error, so it know something went wrong 204 // with the service and can cancel the service. 205 internalErrorReporter <- err 206 } 207 208 // Freeing associated components. 209 closer.CloseAll() 210 211 logger.Info("The service exited") 212 }) 213 214 return rc, nil 215 } 216 217 // buildAPIV1 218 // This API is deprecated. 219 func (s *Starter) buildAPIV1(logger *zap.Logger, closer *vgclose.Closer, networkCfg *network.Network, serviceCfg *Config, spam *spam.Handler) (*servicev1.API, error) { 220 apiV1Logger := logger.Named("v1") 221 222 forwarder, err := node.NewForwarder(apiV1Logger.Named("forwarder"), networkCfg.API.GRPC) 223 if err != nil { 224 logger.Error("Could not initialise the node forwarder", zap.Error(err)) 225 return nil, fmt.Errorf("could not initialise the node forwarder: %w", err) 226 } 227 // Don't forget to stop all connections to the nodes. 228 closer.Add(forwarder.Stop) 229 230 auth, err := servicev1.NewAuth(apiV1Logger.Named("auth"), s.svcStore, serviceCfg.APIV1.MaximumTokenDuration.Get()) 231 if err != nil { 232 logger.Error("Could not initialise the authentication layer", zap.Error(err)) 233 return nil, fmt.Errorf("could not initialise the authentication layer: %w", err) 234 } 235 // Don't forget to close the sessions. 236 closer.Add(auth.RevokeAllToken) 237 238 handler := wallets.NewHandler(s.walletStore) 239 240 return servicev1.NewAPI(apiV1Logger, handler, auth, forwarder, s.policy, networkCfg, spam), nil 241 } 242 243 func (s *Starter) buildAPIV2(logger *zap.Logger, cfg *network.Network, pow api.SpamHandler, closer *vgclose.Closer, apiV2Cfg APIV2Config) (*servicev2.API, error) { 244 apiV2logger := logger.Named("v2") 245 clientAPILogger := apiV2logger.Named("client-api") 246 247 nodeSelector, err := nodeapi.BuildRoundRobinSelectorWithRetryingNodes( 248 clientAPILogger, 249 cfg.API.GRPC.Hosts, 250 apiV2Cfg.Nodes.MaximumRetryPerRequest, 251 apiV2Cfg.Nodes.MaximumRequestDuration.Duration, 252 ) 253 if err != nil { 254 logger.Error("Could not build the node selector", zap.Error(err)) 255 return nil, err 256 } 257 closer.Add(nodeSelector.Stop) 258 259 clientAPI, err := api.BuildClientAPI(s.walletStore, s.interactor, nodeSelector, pow) 260 if err != nil { 261 logger.Error("Could not instantiate the client part of the JSON-RPC API", zap.Error(err)) 262 return nil, fmt.Errorf("could not instantiate the client part of the JSON-RPC API: %w", err) 263 } 264 265 return servicev2.NewAPI(apiV2logger, clientAPI, s.connectionsManager), nil 266 } 267 268 func (s *Starter) buildServiceLogger(network string) (*zap.Logger, zap.AtomicLevel, error) { 269 // We set the logger with the "INFO" level by default. It will be changed once 270 // we get to retrieve the log level from the network configuration. 271 logger, level, err := s.loggerBuilderFunc("info") 272 if err != nil { 273 return nil, zap.AtomicLevel{}, err 274 } 275 276 logger = logger. 277 Named("service"). 278 With(zap.String("network", network)) 279 280 return logger, level, nil 281 } 282 283 func (s *Starter) ensureSoftwareIsCompatibleWithNetwork(logger *zap.Logger, networkCfg *network.Network) error { 284 networkVersion, err := walletversion.GetNetworkVersionThroughGRPC(networkCfg.API.GRPC.Hosts) 285 if err != nil { 286 logger.Error("Could not verify the compatibility between the network and the software", zap.Error(err)) 287 return fmt.Errorf("could not verify the compatibility between the network and the software: %w", err) 288 } 289 290 coreVersion := coreversion.Get() 291 292 if networkVersion != coreVersion { 293 logger.Error("This software is not compatible with the network", 294 zap.String("network-version", networkVersion), 295 zap.String("core-version", coreVersion), 296 ) 297 return fmt.Errorf("this software is not compatible with this network as the network is running version %s but this software expects the version %s", networkVersion, coreversion.Get()) 298 } 299 300 logger.Info("This software is compatible with the network") 301 302 return nil 303 } 304 305 func (s *Starter) networkConfig(logger *zap.Logger, network string) (*network.Network, error) { 306 exists, err := s.netStore.NetworkExists(network) 307 if err != nil { 308 logger.Error("Could not verify the network existence", zap.Error(err)) 309 return nil, fmt.Errorf("could not verify the network existence: %w", err) 310 } 311 if !exists { 312 logger.Error("The requested network does not exists", zap.String("network", network)) 313 return nil, api.ErrNetworkDoesNotExist 314 } 315 316 networkCfg, err := s.netStore.GetNetwork(network) 317 if err != nil { 318 logger.Error("Could not retrieve the network configuration", zap.Error(err)) 319 return nil, fmt.Errorf("could not retrieve the network configuration: %w", err) 320 } 321 322 if err := networkCfg.EnsureCanConnectGRPCNode(); err != nil { 323 logger.Error("The requested network can't connect to the nodes gRPC API", zap.Error(err), zap.String("network", network)) 324 return nil, err 325 } 326 327 logger.Info("The network configuration has been loaded", zap.String("network", network)) 328 329 return networkCfg, nil 330 } 331 332 func (s *Starter) ensureServiceIsInitialised(logger *zap.Logger) error { 333 if isInit, err := IsInitialised(s.svcStore); err != nil { 334 logger.Error("Could not verify if the service is properly running", zap.Error(err)) 335 return fmt.Errorf("could not verify if the service is properly initialised: %w", err) 336 } else if !isInit { 337 logger.Info("The service is not initialise") 338 if err = InitialiseService(s.svcStore, false); err != nil { 339 logger.Error("Could not initialise the service", zap.Error(err)) 340 return fmt.Errorf("could not initialise the service: %w", err) 341 } 342 logger.Info("The service has been initialised") 343 } else { 344 logger.Info("The service has already been initialised") 345 } 346 return nil 347 } 348 349 func updateLogLevel(logLevel zap.AtomicLevel, serviceCfg *Config) error { 350 parsedLevel, err := zap.ParseAtomicLevel(serviceCfg.LogLevel.String()) 351 if err != nil { 352 return fmt.Errorf("invalid log level specified in the service configuration: %w", err) 353 } 354 logLevel.SetLevel(parsedLevel.Level()) 355 return nil 356 } 357 358 func NewStarter(walletStore api.WalletStore, netStore api.NetworkStore, svcStore Store, connectionsManager *connections.Manager, policy servicev1.Policy, interactor api.Interactor, loggerBuilderFunc LoggerBuilderFunc) *Starter { 359 return &Starter{ 360 walletStore: walletStore, 361 netStore: netStore, 362 svcStore: svcStore, 363 connectionsManager: connectionsManager, 364 policy: policy, 365 interactor: interactor, 366 loggerBuilderFunc: loggerBuilderFunc, 367 } 368 } 369 370 func ensurePortCanBeBound(ctx context.Context, logger *zap.Logger, url string) error { 371 req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) 372 if err != nil { 373 logger.Error("Could not build the request verifying the state of the port to bind", zap.Error(err)) 374 return fmt.Errorf("could not build the request verifying the state of the port to bind: %w", err) 375 } 376 377 response, err := http.DefaultClient.Do(req) 378 if err == nil { 379 // If there is no error, it means the server managed to establish a 380 // connection of some kind, whereas we would have liked it to be unable 381 // to connect to anything, which would have implied this host is free to 382 // use. 383 logger.Error("Could not start the service as an application is already served on that URL", zap.String("url", url)) 384 return fmt.Errorf("could not start the service as an application is already served on %q", url) 385 } 386 defer func() { 387 if response != nil && response.Body != nil { 388 _ = response.Body.Close() 389 } 390 }() 391 392 logger.Info("The URL seems available for use") 393 return nil 394 }