code.vegaprotocol.io/vega@v0.79.0/wallet/api/node/retrying_node.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 node
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"time"
    22  
    23  	apipb "code.vegaprotocol.io/vega/protos/vega/api/v1"
    24  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    25  	"code.vegaprotocol.io/vega/wallet/api/node/adapters"
    26  	nodetypes "code.vegaprotocol.io/vega/wallet/api/node/types"
    27  
    28  	"github.com/cenkalti/backoff/v4"
    29  	"go.uber.org/zap"
    30  	"google.golang.org/grpc/codes"
    31  )
    32  
    33  //go:generate go run github.com/golang/mock/mockgen -destination mocks/node_mock.go -package mocks code.vegaprotocol.io/vega/wallet/api/node GRPCAdapter
    34  
    35  type GRPCAdapter interface {
    36  	Host() string
    37  	Statistics(ctx context.Context) (nodetypes.Statistics, error)
    38  	SpamStatistics(ctx context.Context, pubKey string) (nodetypes.SpamStatistics, error)
    39  	SubmitTransaction(ctx context.Context, in *apipb.SubmitTransactionRequest) (*apipb.SubmitTransactionResponse, error)
    40  	CheckTransaction(ctx context.Context, in *apipb.CheckTransactionRequest) (*apipb.CheckTransactionResponse, error)
    41  	LastBlock(ctx context.Context) (nodetypes.LastBlock, error)
    42  	Stop() error
    43  }
    44  
    45  type RetryingNode struct {
    46  	log *zap.Logger
    47  
    48  	grpcAdapter GRPCAdapter
    49  
    50  	retries uint64
    51  
    52  	requestTTL time.Duration
    53  }
    54  
    55  func (n *RetryingNode) Host() string {
    56  	return n.grpcAdapter.Host()
    57  }
    58  
    59  func (n *RetryingNode) Statistics(ctx context.Context) (nodetypes.Statistics, error) {
    60  	n.log.Debug("querying the node statistics through the graphQL API", zap.String("host", n.grpcAdapter.Host()))
    61  	requestTime := time.Now()
    62  	ctx, cancel := context.WithTimeout(ctx, n.requestTTL)
    63  	defer cancel()
    64  	resp, err := n.grpcAdapter.Statistics(ctx)
    65  	if err != nil {
    66  		n.log.Error("could not get the statistics",
    67  			zap.String("host", n.grpcAdapter.Host()),
    68  			zap.Error(err),
    69  		)
    70  		return nodetypes.Statistics{}, err
    71  	}
    72  	n.log.Debug("response from Statistics",
    73  		zap.String("host", n.grpcAdapter.Host()),
    74  		zap.Uint64("block-height", resp.BlockHeight),
    75  		zap.String("block-hash", resp.BlockHash),
    76  		zap.String("chain-id", resp.ChainID),
    77  		zap.String("vega-time", resp.VegaTime),
    78  		zap.Time("request-time", requestTime),
    79  	)
    80  	return resp, nil
    81  }
    82  
    83  func (n *RetryingNode) SpamStatistics(ctx context.Context, pubKey string) (nodetypes.SpamStatistics, error) {
    84  	n.log.Debug("querying the node statistics through the graphQL API", zap.String("host", n.grpcAdapter.Host()))
    85  	requestTime := time.Now()
    86  	ctx, cancel := context.WithTimeout(ctx, n.requestTTL)
    87  	defer cancel()
    88  	resp, err := n.grpcAdapter.SpamStatistics(ctx, pubKey)
    89  	if err != nil {
    90  		n.log.Error("could not get the statistics",
    91  			zap.String("host", n.grpcAdapter.Host()),
    92  			zap.Error(err),
    93  		)
    94  		return nodetypes.SpamStatistics{}, err
    95  	}
    96  
    97  	n.log.Debug("response from SpamStatistics",
    98  		zap.String("host", n.grpcAdapter.Host()),
    99  		zap.String("chain-id", resp.ChainID),
   100  		zap.Uint64("epoch", resp.EpochSeq),
   101  		zap.Uint64("block-height", resp.LastBlockHeight),
   102  		zap.Uint64("prosposals-count-for-epoch", resp.Proposals.CountForEpoch),
   103  		zap.Uint64("transfers-count-for-epoch", resp.Transfers.CountForEpoch),
   104  		zap.Uint64("delegations-count-for-epoch", resp.Delegations.CountForEpoch),
   105  		zap.Uint64("issue-signatures-count-for-epoch", resp.IssuesSignatures.CountForEpoch),
   106  		zap.Uint64("node-announcements-count-for-epoch", resp.NodeAnnouncements.CountForEpoch),
   107  		zap.Time("request-time", requestTime),
   108  	)
   109  	return resp, nil
   110  }
   111  
   112  // LastBlock returns information about the last block acknowledged by the node.
   113  func (n *RetryingNode) LastBlock(ctx context.Context) (nodetypes.LastBlock, error) {
   114  	n.log.Debug("getting the last block from the gRPC API", zap.String("host", n.grpcAdapter.Host()))
   115  	var resp nodetypes.LastBlock
   116  	if err := n.retry(func() error {
   117  		requestTime := time.Now()
   118  		ctx, cancel := context.WithTimeout(ctx, n.requestTTL)
   119  		defer cancel()
   120  		r, err := n.grpcAdapter.LastBlock(ctx)
   121  		if err != nil {
   122  			return err
   123  		}
   124  		resp = r
   125  		n.log.Debug("response from LastBlockHeight",
   126  			zap.String("host", n.grpcAdapter.Host()),
   127  			zap.Uint64("block-height", r.BlockHeight),
   128  			zap.String("block-hash", r.BlockHash),
   129  			zap.Uint32("pow-difficulty", r.ProofOfWorkDifficulty),
   130  			zap.String("pow-hash-function", r.ProofOfWorkHashFunction),
   131  			zap.Time("request-time", requestTime),
   132  		)
   133  		return nil
   134  	}); err != nil {
   135  		n.log.Error("could not the get last block",
   136  			zap.String("host", n.grpcAdapter.Host()),
   137  			zap.Error(err),
   138  		)
   139  		return nodetypes.LastBlock{}, err
   140  	}
   141  
   142  	return resp, nil
   143  }
   144  
   145  func (n *RetryingNode) CheckTransaction(ctx context.Context, tx *commandspb.Transaction) error {
   146  	n.log.Debug("checking the transaction through the gRPC API", zap.String("host", n.grpcAdapter.Host()))
   147  	var resp *apipb.CheckTransactionResponse
   148  	if err := n.retry(func() error {
   149  		req := apipb.CheckTransactionRequest{
   150  			Tx: tx,
   151  		}
   152  		requestTime := time.Now()
   153  		ctx, cancel := context.WithTimeout(ctx, n.requestTTL)
   154  		defer cancel()
   155  		r, err := n.grpcAdapter.CheckTransaction(ctx, &req)
   156  		if err != nil {
   157  			return n.handleSubmissionError(err)
   158  		}
   159  		n.log.Debug("response from CheckTransaction",
   160  			zap.String("host", n.grpcAdapter.Host()),
   161  			zap.Bool("success", r.Success),
   162  			zap.Time("request-time", requestTime),
   163  		)
   164  		resp = r
   165  		return nil
   166  	}); err != nil {
   167  		return err
   168  	}
   169  
   170  	if !resp.Success {
   171  		return nodetypes.TransactionError{
   172  			ABCICode: resp.Code,
   173  			Message:  resp.Data,
   174  		}
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  func (n *RetryingNode) SendTransaction(ctx context.Context, tx *commandspb.Transaction, ty apipb.SubmitTransactionRequest_Type) (string, error) {
   181  	n.log.Debug("sending the transaction through the gRPC API", zap.String("host", n.grpcAdapter.Host()))
   182  	var resp *apipb.SubmitTransactionResponse
   183  	if err := n.retry(func() error {
   184  		req := apipb.SubmitTransactionRequest{
   185  			Tx:   tx,
   186  			Type: ty,
   187  		}
   188  		requestTime := time.Now()
   189  		ctx, cancel := context.WithTimeout(ctx, n.requestTTL)
   190  		defer cancel()
   191  		r, err := n.grpcAdapter.SubmitTransaction(ctx, &req)
   192  		if err != nil {
   193  			return n.handleSubmissionError(err)
   194  		}
   195  		n.log.Debug("response from SubmitTransaction",
   196  			zap.String("host", n.grpcAdapter.Host()),
   197  			zap.Bool("success", r.Success),
   198  			zap.String("hash", r.TxHash),
   199  			zap.Time("request-time", requestTime),
   200  		)
   201  		resp = r
   202  		return nil
   203  	}); err != nil {
   204  		return "", err
   205  	}
   206  
   207  	if !resp.Success {
   208  		return "", nodetypes.TransactionError{
   209  			ABCICode: resp.Code,
   210  			Message:  resp.Data,
   211  		}
   212  	}
   213  
   214  	return resp.TxHash, nil
   215  }
   216  
   217  func (n *RetryingNode) Stop() error {
   218  	n.log.Debug("closing the gRPC API client", zap.String("host", n.grpcAdapter.Host()))
   219  	if err := n.grpcAdapter.Stop(); err != nil {
   220  		n.log.Warn("could not stop the gRPC API client-",
   221  			zap.String("host", n.grpcAdapter.Host()),
   222  			zap.Error(err),
   223  		)
   224  		return fmt.Errorf("could not close properly stop the gRPC API client: %w", err)
   225  	}
   226  	n.log.Info("the gRPC API client successfully closed", zap.String("host", n.grpcAdapter.Host()))
   227  	return nil
   228  }
   229  
   230  func (n *RetryingNode) handleSubmissionError(err error) error {
   231  	statusErr := intoStatusError(err)
   232  
   233  	if statusErr == nil {
   234  		n.log.Error("could not submit the transaction",
   235  			zap.String("host", n.grpcAdapter.Host()),
   236  			zap.Error(err),
   237  		)
   238  		return err
   239  	}
   240  
   241  	if statusErr.Code == codes.InvalidArgument {
   242  		n.log.Error(
   243  			"the transaction has been rejected because of an invalid argument or state, skipping retry...",
   244  			zap.String("host", n.grpcAdapter.Host()),
   245  			zap.Error(statusErr),
   246  		)
   247  		// Returning a permanent error kills the retry loop.
   248  		return backoff.Permanent(statusErr)
   249  	}
   250  
   251  	n.log.Error("could not submit the transaction",
   252  		zap.String("host", n.grpcAdapter.Host()),
   253  		zap.Error(statusErr),
   254  	)
   255  	return statusErr
   256  }
   257  
   258  func (n *RetryingNode) retry(o backoff.Operation) error {
   259  	return backoff.Retry(o, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), n.retries))
   260  }
   261  
   262  func NewRetryingNode(log *zap.Logger, host string, retries uint64, ttl time.Duration) (*RetryingNode, error) {
   263  	grpcAdapter, err := adapters.NewGRPCAdapter(host)
   264  	if err != nil {
   265  		log.Error("could not initialise an insecure gRPC adapter",
   266  			zap.String("host", host),
   267  			zap.Error(err),
   268  		)
   269  		return nil, err
   270  	}
   271  
   272  	return BuildRetryingNode(log, grpcAdapter, retries, ttl), nil
   273  }
   274  
   275  func BuildRetryingNode(log *zap.Logger, grpcAdapter GRPCAdapter, retries uint64, requestTTL time.Duration) *RetryingNode {
   276  	return &RetryingNode{
   277  		log:         log,
   278  		grpcAdapter: grpcAdapter,
   279  		retries:     retries,
   280  		requestTTL:  requestTTL,
   281  	}
   282  }