github.com/status-im/status-go@v1.1.0/protocol/transaction_validator.go (about)

     1  package protocol
     2  
     3  import (
     4  	"context"
     5  	"crypto/ecdsa"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"time"
     9  
    10  	"math/big"
    11  	"strings"
    12  
    13  	"github.com/pkg/errors"
    14  	"go.uber.org/zap"
    15  
    16  	coretypes "github.com/status-im/status-go/eth-node/core/types"
    17  	"github.com/status-im/status-go/eth-node/crypto"
    18  	"github.com/status-im/status-go/eth-node/types"
    19  	"github.com/status-im/status-go/protocol/common"
    20  )
    21  
    22  const (
    23  	transferFunction        = "a9059cbb"
    24  	tokenTransferDataLength = 68
    25  	transactionHashLength   = 66
    26  )
    27  
    28  type TransactionValidator struct {
    29  	persistence *sqlitePersistence
    30  	addresses   map[string]bool
    31  	client      EthClient
    32  	logger      *zap.Logger
    33  }
    34  
    35  var invalidResponse = &VerifyTransactionResponse{Valid: false}
    36  
    37  type TransactionToValidate struct {
    38  	TransactionHash string
    39  	CommandID       string
    40  	MessageID       string
    41  	RetryCount      int
    42  	// First seen indicates the whisper timestamp of the first time we seen this
    43  	FirstSeen uint64
    44  	// Validate indicates whether we should be validating this transaction
    45  	Validate  bool
    46  	Signature []byte
    47  	From      *ecdsa.PublicKey
    48  }
    49  
    50  func NewTransactionValidator(addresses []types.Address, persistence *sqlitePersistence, client EthClient, logger *zap.Logger) *TransactionValidator {
    51  	addressesMap := make(map[string]bool)
    52  	for _, a := range addresses {
    53  		addressesMap[strings.ToLower(a.Hex())] = true
    54  	}
    55  	logger.Debug("Checking addresses", zap.Any("addrse", addressesMap))
    56  
    57  	return &TransactionValidator{
    58  		persistence: persistence,
    59  		addresses:   addressesMap,
    60  		logger:      logger,
    61  		client:      client,
    62  	}
    63  }
    64  
    65  type EthClient interface {
    66  	TransactionByHash(context.Context, types.Hash) (coretypes.Message, coretypes.TransactionStatus, error)
    67  }
    68  
    69  func (t *TransactionValidator) verifyTransactionSignature(ctx context.Context, from *ecdsa.PublicKey, address types.Address, transactionHash string, signature []byte) error {
    70  	publicKeyBytes := crypto.FromECDSAPub(from)
    71  
    72  	if len(transactionHash) != transactionHashLength {
    73  		return errors.New("wrong transaction hash length")
    74  	}
    75  
    76  	hashBytes, err := hex.DecodeString(transactionHash[2:])
    77  	if err != nil {
    78  		return err
    79  	}
    80  	signatureMaterial := append(publicKeyBytes, hashBytes...)
    81  
    82  	// We take a copy as EcRecover modifies the byte slice
    83  	signatureCopy := make([]byte, len(signature))
    84  	copy(signatureCopy, signature)
    85  	extractedAddress, err := crypto.EcRecover(ctx, signatureMaterial, signatureCopy)
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	if extractedAddress != address {
    91  		return errors.New("failed to verify signature")
    92  	}
    93  	return nil
    94  }
    95  
    96  func (t *TransactionValidator) validateTokenTransfer(parameters *common.CommandParameters, transaction coretypes.Message) (*VerifyTransactionResponse, error) {
    97  
    98  	data := transaction.Data()
    99  	if len(data) != tokenTransferDataLength {
   100  		return nil, errors.New(fmt.Sprintf("wrong data length: %d", len(data)))
   101  	}
   102  
   103  	functionCalled := hex.EncodeToString(data[:4])
   104  
   105  	if functionCalled != transferFunction {
   106  		return invalidResponse, nil
   107  	}
   108  
   109  	actualContractAddress := strings.ToLower(transaction.To().Hex())
   110  
   111  	if parameters.Contract != "" && actualContractAddress != parameters.Contract {
   112  		return invalidResponse, nil
   113  	}
   114  
   115  	to := types.EncodeHex(data[16:36])
   116  
   117  	if !t.validateToAddress(parameters.Address, to) {
   118  		return invalidResponse, nil
   119  	}
   120  
   121  	value := data[36:]
   122  	amount := new(big.Int).SetBytes(value)
   123  
   124  	if parameters.Value != "" {
   125  		advertisedAmount, ok := new(big.Int).SetString(parameters.Value, 10)
   126  		if !ok {
   127  			return nil, errors.New("can't parse amount")
   128  		}
   129  
   130  		return &VerifyTransactionResponse{
   131  			Value:           parameters.Value,
   132  			Contract:        actualContractAddress,
   133  			Address:         to,
   134  			AccordingToSpec: amount.Cmp(advertisedAmount) == 0,
   135  			Valid:           true,
   136  		}, nil
   137  	}
   138  
   139  	return &VerifyTransactionResponse{
   140  		Value:           amount.String(),
   141  		Address:         to,
   142  		Contract:        actualContractAddress,
   143  		AccordingToSpec: false,
   144  		Valid:           true,
   145  	}, nil
   146  
   147  }
   148  
   149  func (t *TransactionValidator) validateToAddress(specifiedTo, actualTo string) bool {
   150  	if len(specifiedTo) != 0 && (!strings.EqualFold(specifiedTo, actualTo) || !t.addresses[strings.ToLower(actualTo)]) {
   151  		return false
   152  	}
   153  
   154  	return t.addresses[actualTo]
   155  }
   156  
   157  func (t *TransactionValidator) validateEthereumTransfer(parameters *common.CommandParameters, transaction coretypes.Message) (*VerifyTransactionResponse, error) {
   158  	toAddress := strings.ToLower(transaction.To().Hex())
   159  
   160  	if !t.validateToAddress(parameters.Address, toAddress) {
   161  		return invalidResponse, nil
   162  	}
   163  
   164  	amount := transaction.Value()
   165  	if parameters.Value != "" {
   166  		advertisedAmount, ok := new(big.Int).SetString(parameters.Value, 10)
   167  		if !ok {
   168  			return nil, errors.New("can't parse amount")
   169  		}
   170  		return &VerifyTransactionResponse{
   171  			AccordingToSpec: amount.Cmp(advertisedAmount) == 0,
   172  			Valid:           true,
   173  			Value:           amount.String(),
   174  			Address:         toAddress,
   175  		}, nil
   176  	}
   177  
   178  	return &VerifyTransactionResponse{
   179  		AccordingToSpec: false,
   180  		Valid:           true,
   181  		Value:           amount.String(),
   182  		Address:         toAddress,
   183  	}, nil
   184  }
   185  
   186  type VerifyTransactionResponse struct {
   187  	Pending bool
   188  	// AccordingToSpec means that the transaction is valid,
   189  	// the user should be notified, but is not the same as
   190  	// what was requested, for example because the value is different
   191  	AccordingToSpec bool
   192  	// Valid means that the transaction is valid
   193  	Valid bool
   194  	// The actual value received
   195  	Value string
   196  	// The contract used in case of tokens
   197  	Contract string
   198  	// The address the transaction was actually sent
   199  	Address string
   200  
   201  	Message     *common.Message
   202  	Transaction *TransactionToValidate
   203  }
   204  
   205  // validateTransaction validates a transaction and returns a response.
   206  // If a negative response is returned, i.e `Valid` is false, it should
   207  // not be retried.
   208  // If an error is returned, validation can be retried.
   209  func (t *TransactionValidator) validateTransaction(ctx context.Context, message coretypes.Message, parameters *common.CommandParameters, from *ecdsa.PublicKey) (*VerifyTransactionResponse, error) {
   210  	fromAddress := types.BytesToAddress(message.From().Bytes())
   211  
   212  	err := t.verifyTransactionSignature(ctx, from, fromAddress, parameters.TransactionHash, parameters.Signature)
   213  	if err != nil {
   214  		t.logger.Error("failed validating signature", zap.Error(err))
   215  		return invalidResponse, nil
   216  	}
   217  
   218  	if len(message.Data()) != 0 {
   219  		t.logger.Debug("Validating token")
   220  		return t.validateTokenTransfer(parameters, message)
   221  	}
   222  
   223  	t.logger.Debug("Validating eth")
   224  	return t.validateEthereumTransfer(parameters, message)
   225  }
   226  
   227  func (t *TransactionValidator) ValidateTransactions(ctx context.Context) ([]*VerifyTransactionResponse, error) {
   228  	if t.client == nil {
   229  		return nil, nil
   230  	}
   231  	var response []*VerifyTransactionResponse
   232  	t.logger.Debug("Started validating transactions")
   233  	transactions, err := t.persistence.TransactionsToValidate()
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	t.logger.Debug("Transactions to validated", zap.Any("transactions", transactions))
   239  
   240  	for _, transaction := range transactions {
   241  		var validationResult *VerifyTransactionResponse
   242  		t.logger.Debug("Validating transaction", zap.Any("transaction", transaction))
   243  		if transaction.CommandID != "" {
   244  			chatID := contactIDFromPublicKey(transaction.From)
   245  			message, err := t.persistence.MessageByCommandID(chatID, transaction.CommandID)
   246  			if err != nil {
   247  				t.logger.Error("error pulling message", zap.Error(err))
   248  				return nil, err
   249  			}
   250  			if message == nil {
   251  				t.logger.Info("No message found, ignoring transaction")
   252  				// This is not a valid case, ignore transaction
   253  				transaction.Validate = false
   254  				transaction.RetryCount++
   255  				err = t.persistence.UpdateTransactionToValidate(transaction)
   256  				if err != nil {
   257  					return nil, err
   258  				}
   259  				continue
   260  
   261  			}
   262  			commandParameters := message.CommandParameters
   263  			commandParameters.TransactionHash = transaction.TransactionHash
   264  			commandParameters.Signature = transaction.Signature
   265  			validationResult, err = t.ValidateTransaction(ctx, message.CommandParameters, transaction.From)
   266  			if err != nil {
   267  				t.logger.Error("Error validating transaction", zap.Error(err))
   268  				continue
   269  			}
   270  			validationResult.Message = message
   271  		} else {
   272  			commandParameters := &common.CommandParameters{}
   273  			commandParameters.TransactionHash = transaction.TransactionHash
   274  			commandParameters.Signature = transaction.Signature
   275  
   276  			validationResult, err = t.ValidateTransaction(ctx, commandParameters, transaction.From)
   277  			if err != nil {
   278  				t.logger.Error("Error validating transaction", zap.Error(err))
   279  				continue
   280  			}
   281  		}
   282  
   283  		if validationResult.Pending {
   284  			t.logger.Debug("Pending transaction skipping")
   285  			// Check if we should stop updating
   286  			continue
   287  		}
   288  
   289  		// Mark transaction as valid
   290  		transaction.Validate = false
   291  		transaction.RetryCount++
   292  		err = t.persistence.UpdateTransactionToValidate(transaction)
   293  		if err != nil {
   294  			return nil, err
   295  		}
   296  
   297  		if !validationResult.Valid {
   298  			t.logger.Debug("Transaction not valid")
   299  			continue
   300  		}
   301  		t.logger.Debug("Transaction valid")
   302  		validationResult.Transaction = transaction
   303  		response = append(response, validationResult)
   304  	}
   305  	return response, nil
   306  }
   307  
   308  func (t *TransactionValidator) ValidateTransaction(ctx context.Context, parameters *common.CommandParameters, from *ecdsa.PublicKey) (*VerifyTransactionResponse, error) {
   309  	t.logger.Debug("validating transaction", zap.Any("transaction", parameters), zap.Any("from", from))
   310  	hash := parameters.TransactionHash
   311  	c, cancel := context.WithTimeout(ctx, 10*time.Second)
   312  	defer cancel()
   313  
   314  	message, status, err := t.client.TransactionByHash(c, types.HexToHash(hash))
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	switch status {
   319  	case coretypes.TransactionStatusPending:
   320  		t.logger.Debug("Transaction pending")
   321  		return &VerifyTransactionResponse{Pending: true}, nil
   322  	case coretypes.TransactionStatusFailed:
   323  
   324  		return invalidResponse, nil
   325  	}
   326  
   327  	return t.validateTransaction(ctx, message, parameters, from)
   328  }