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  }