code.vegaprotocol.io/vega@v0.79.0/core/faucet/service.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 faucet
    17  
    18  import (
    19  	"context"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net/http"
    25  
    26  	vfmt "code.vegaprotocol.io/vega/libs/fmt"
    27  	vghttp "code.vegaprotocol.io/vega/libs/http"
    28  	"code.vegaprotocol.io/vega/libs/num"
    29  	"code.vegaprotocol.io/vega/libs/proto"
    30  	vgrand "code.vegaprotocol.io/vega/libs/rand"
    31  	"code.vegaprotocol.io/vega/logging"
    32  	"code.vegaprotocol.io/vega/paths"
    33  	types "code.vegaprotocol.io/vega/protos/vega"
    34  	api "code.vegaprotocol.io/vega/protos/vega/api/v1"
    35  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    36  
    37  	"github.com/cenkalti/backoff"
    38  	"github.com/julienschmidt/httprouter"
    39  	"github.com/rs/cors"
    40  	"google.golang.org/grpc"
    41  	"google.golang.org/grpc/credentials/insecure"
    42  )
    43  
    44  var (
    45  	// ErrNotABuiltinAsset is raised when a party try to top up for a non builtin asset.
    46  	ErrNotABuiltinAsset = errors.New("asset is not a builtin asset")
    47  
    48  	// ErrAssetNotFound is raised when an asset id is not found.
    49  	ErrAssetNotFound = errors.New("asset was not found")
    50  
    51  	// ErrInvalidMintAmount is raised when the mint amount is too high.
    52  	ErrInvalidMintAmount = errors.New("mint amount is invalid")
    53  
    54  	HealthCheckResponse = struct {
    55  		Success bool `json:"success"`
    56  	}{true}
    57  )
    58  
    59  type Faucet struct {
    60  	*httprouter.Router
    61  
    62  	log    *logging.Logger
    63  	cfg    Config
    64  	wallet *faucetWallet
    65  	s      *http.Server
    66  	rl     *vghttp.RateLimit
    67  	cfunc  context.CancelFunc
    68  	stopCh chan struct{}
    69  
    70  	// node connections stuff
    71  	clt     api.CoreServiceClient
    72  	coreclt api.CoreStateServiceClient
    73  	conn    *grpc.ClientConn
    74  }
    75  
    76  type MintRequest struct {
    77  	Party  string `json:"party"`
    78  	Amount string `json:"amount"`
    79  	Asset  string `json:"asset"`
    80  }
    81  
    82  type MintResponse struct {
    83  	Success bool `json:"success"`
    84  }
    85  
    86  func NewService(log *logging.Logger, vegaPaths paths.Paths, cfg Config, passphrase string) (*Faucet, error) {
    87  	log = log.Named(namedLogger)
    88  	log.SetLevel(cfg.Level.Level)
    89  
    90  	wallet, err := loadWallet(vegaPaths, cfg.WalletName, passphrase)
    91  	if err != nil {
    92  		return nil, fmt.Errorf("could not load the faucet wallet %s: %w", cfg.WalletName, err)
    93  	}
    94  
    95  	nodeAddr := fmt.Sprintf("%v:%v", cfg.Node.IP, cfg.Node.Port)
    96  	conn, err := grpc.Dial(nodeAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    97  	if err != nil {
    98  		return nil, fmt.Errorf("could not initialise the gPRC client: %w", err)
    99  	}
   100  
   101  	client := api.NewCoreServiceClient(conn)
   102  	coreClient := api.NewCoreStateServiceClient(conn)
   103  	ctx, cfunc := context.WithCancel(context.Background())
   104  
   105  	rl, err := vghttp.NewRateLimit(ctx, cfg.RateLimit)
   106  	if err != nil {
   107  		cfunc()
   108  		return nil, fmt.Errorf("could not initialise the rate limiter: %v", err)
   109  	}
   110  
   111  	f := &Faucet{
   112  		Router:  httprouter.New(),
   113  		log:     log,
   114  		cfg:     cfg,
   115  		wallet:  wallet,
   116  		clt:     client,
   117  		coreclt: coreClient,
   118  		conn:    conn,
   119  		cfunc:   cfunc,
   120  		rl:      rl,
   121  		stopCh:  make(chan struct{}),
   122  	}
   123  
   124  	f.POST("/api/v1/mint", f.Mint)
   125  	f.GET("/api/v1/health", f.Health)
   126  
   127  	return f, nil
   128  }
   129  
   130  func (f *Faucet) Health(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
   131  	writeSuccess(w, HealthCheckResponse, http.StatusOK)
   132  }
   133  
   134  func (f *Faucet) Mint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
   135  	req := MintRequest{}
   136  	if err := unmarshalBody(r, &req); err != nil {
   137  		writeError(w, newError(err.Error()), http.StatusBadRequest)
   138  		return
   139  	}
   140  
   141  	if len(req.Party) <= 0 {
   142  		writeError(w, newError("missing party field"), http.StatusBadRequest)
   143  		return
   144  	}
   145  	if len(req.Amount) <= 0 {
   146  		writeError(w, newError("amount need to be a > 0 unsigned integer"), http.StatusBadRequest)
   147  		return
   148  	}
   149  	amount, overflowed := num.UintFromString(req.Amount, 10)
   150  	if overflowed {
   151  		writeError(w, newError("amount overflowed or was not base 10"), http.StatusBadRequest)
   152  	}
   153  
   154  	if len(req.Asset) <= 0 {
   155  		writeError(w, newError("missing asset field"), http.StatusBadRequest)
   156  		return
   157  	}
   158  
   159  	if err := f.getAllowedAmount(r.Context(), amount, req.Asset); err != nil {
   160  		if errors.Is(err, ErrAssetNotFound) || errors.Is(err, ErrInvalidMintAmount) {
   161  			writeError(w, newError(err.Error()), http.StatusBadRequest)
   162  			return
   163  		}
   164  		writeError(w, newError(err.Error()), http.StatusInternalServerError)
   165  		return
   166  	}
   167  
   168  	// rate limit minting by source IP address, party, asset
   169  	ip, err := vghttp.RemoteAddr(r)
   170  	if err != nil {
   171  		writeError(w, newError(fmt.Sprintf("failed to get request remote address: %v", err)), http.StatusBadRequest)
   172  		return
   173  	}
   174  	rlkey := fmt.Sprintf("minting for party %s and asset %s", req.Party, req.Asset)
   175  	if err := f.rl.NewRequest(rlkey, ip); err != nil {
   176  		f.log.Debug("Mint denied - rate limit",
   177  			logging.String("ip", vfmt.Escape(ip)),
   178  			logging.String("rlkey", vfmt.Escape(rlkey)),
   179  		)
   180  		writeError(w, newError(err.Error()), http.StatusForbidden)
   181  		return
   182  	}
   183  
   184  	ce := &commandspb.ChainEvent{
   185  		Nonce: vgrand.NewNonce(),
   186  		Event: &commandspb.ChainEvent_Builtin{
   187  			Builtin: &types.BuiltinAssetEvent{
   188  				Action: &types.BuiltinAssetEvent_Deposit{
   189  					Deposit: &types.BuiltinAssetDeposit{
   190  						VegaAssetId: req.Asset,
   191  						PartyId:     req.Party,
   192  						Amount:      req.Amount,
   193  					},
   194  				},
   195  			},
   196  		},
   197  	}
   198  
   199  	msg, err := proto.Marshal(ce)
   200  	if err != nil {
   201  		writeError(w, newError("unable to marshal"), http.StatusInternalServerError)
   202  		return
   203  	}
   204  
   205  	sig, pubKey, err := f.wallet.Sign(msg)
   206  	if err != nil {
   207  		f.log.Error("unable to sign", logging.Error(err))
   208  		writeError(w, newError("unable to sign crypto"), http.StatusInternalServerError)
   209  		return
   210  	}
   211  
   212  	preq := &api.PropagateChainEventRequest{
   213  		Event:     msg,
   214  		PubKey:    pubKey,
   215  		Signature: sig,
   216  	}
   217  
   218  	var ok bool
   219  	err = backoff.Retry(
   220  		func() error {
   221  			resp, err := f.clt.PropagateChainEvent(context.Background(), preq)
   222  			if err != nil {
   223  				return err
   224  			}
   225  			ok = resp.Success
   226  			return nil
   227  		},
   228  		backoff.WithMaxRetries(backoff.NewExponentialBackOff(), f.cfg.Node.Retries),
   229  	)
   230  	if err != nil {
   231  		writeError(w, newError(err.Error()), http.StatusInternalServerError)
   232  		return
   233  	}
   234  
   235  	resp := MintResponse{ok}
   236  	writeSuccess(w, resp, http.StatusOK)
   237  }
   238  
   239  func (f *Faucet) getAllowedAmount(ctx context.Context, amount *num.Uint, asset string) error {
   240  	req := &api.ListAssetsRequest{
   241  		Asset: asset,
   242  	}
   243  	resp, err := f.coreclt.ListAssets(ctx, req)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	if len(resp.Assets) <= 0 {
   248  		return ErrAssetNotFound
   249  	}
   250  	source := resp.Assets[0].Details.GetBuiltinAsset()
   251  	if source == nil {
   252  		return ErrNotABuiltinAsset
   253  	}
   254  	maxAmount, overflowed := num.UintFromString(source.MaxFaucetAmountMint, 10)
   255  	if overflowed {
   256  		return ErrInvalidMintAmount
   257  	}
   258  	if maxAmount.LT(amount) {
   259  		return fmt.Errorf("amount request exceed maximal amount of %v: %w", maxAmount, ErrInvalidMintAmount)
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func (f *Faucet) Start() error {
   266  	f.s = &http.Server{
   267  		Addr:    fmt.Sprintf("%s:%v", f.cfg.IP, f.cfg.Port),
   268  		Handler: cors.AllowAll().Handler(f), // middleware with cors
   269  	}
   270  
   271  	f.log.Info("starting faucet server", logging.String("address", f.s.Addr))
   272  
   273  	errCh := make(chan error)
   274  	go func() {
   275  		errCh <- f.s.ListenAndServe()
   276  	}()
   277  
   278  	defer func() {
   279  		f.cfunc()
   280  		f.conn.Close()
   281  	}()
   282  
   283  	// close the rate limit
   284  	select {
   285  	case err := <-errCh:
   286  		return err
   287  	case <-f.stopCh:
   288  		f.s.Shutdown(context.Background())
   289  		return nil
   290  	}
   291  }
   292  
   293  func (f *Faucet) Stop() error {
   294  	f.stopCh <- struct{}{}
   295  	return nil
   296  }
   297  
   298  func unmarshalBody(r *http.Request, into interface{}) error {
   299  	defer r.Body.Close()
   300  	body, err := ioutil.ReadAll(r.Body)
   301  	if err != nil {
   302  		return ErrInvalidRequest
   303  	}
   304  	return json.Unmarshal(body, into)
   305  }
   306  
   307  func writeError(w http.ResponseWriter, e error, status int) {
   308  	w.Header().Set("Content-Type", "application/json")
   309  	w.WriteHeader(status)
   310  	buf, _ := json.Marshal(e)
   311  	w.Write(buf)
   312  }
   313  
   314  func writeSuccess(w http.ResponseWriter, data interface{}, status int) {
   315  	w.Header().Set("Content-Type", "application/json")
   316  	w.WriteHeader(status)
   317  	buf, _ := json.Marshal(data)
   318  	w.Write(buf)
   319  }
   320  
   321  var ErrInvalidRequest = newError("invalid request")
   322  
   323  type HTTPError struct {
   324  	ErrorStr string `json:"error"`
   325  }
   326  
   327  func (e HTTPError) Error() string {
   328  	return e.ErrorStr
   329  }
   330  
   331  func newError(e string) HTTPError {
   332  	return HTTPError{
   333  		ErrorStr: e,
   334  	}
   335  }