package protocol import ( "context" "crypto/ecdsa" "encoding/hex" "fmt" "time" "math/big" "strings" "github.com/pkg/errors" "go.uber.org/zap" coretypes "github.com/status-im/status-go/eth-node/core/types" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" ) const ( transferFunction = "a9059cbb" tokenTransferDataLength = 68 transactionHashLength = 66 ) type TransactionValidator struct { persistence *sqlitePersistence addresses map[string]bool client EthClient logger *zap.Logger } var invalidResponse = &VerifyTransactionResponse{Valid: false} type TransactionToValidate struct { TransactionHash string CommandID string MessageID string RetryCount int // First seen indicates the whisper timestamp of the first time we seen this FirstSeen uint64 // Validate indicates whether we should be validating this transaction Validate bool Signature []byte From *ecdsa.PublicKey } func NewTransactionValidator(addresses []types.Address, persistence *sqlitePersistence, client EthClient, logger *zap.Logger) *TransactionValidator { addressesMap := make(map[string]bool) for _, a := range addresses { addressesMap[strings.ToLower(a.Hex())] = true } logger.Debug("Checking addresses", zap.Any("addrse", addressesMap)) return &TransactionValidator{ persistence: persistence, addresses: addressesMap, logger: logger, client: client, } } type EthClient interface { TransactionByHash(context.Context, types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) } func (t *TransactionValidator) verifyTransactionSignature(ctx context.Context, from *ecdsa.PublicKey, address types.Address, transactionHash string, signature []byte) error { publicKeyBytes := crypto.FromECDSAPub(from) if len(transactionHash) != transactionHashLength { return errors.New("wrong transaction hash length") } hashBytes, err := hex.DecodeString(transactionHash[2:]) if err != nil { return err } signatureMaterial := append(publicKeyBytes, hashBytes...) // We take a copy as EcRecover modifies the byte slice signatureCopy := make([]byte, len(signature)) copy(signatureCopy, signature) extractedAddress, err := crypto.EcRecover(ctx, signatureMaterial, signatureCopy) if err != nil { return err } if extractedAddress != address { return errors.New("failed to verify signature") } return nil } func (t *TransactionValidator) validateTokenTransfer(parameters *CommandParameters, transaction coretypes.Message) (*VerifyTransactionResponse, error) { data := transaction.Data() if len(data) != tokenTransferDataLength { return nil, errors.New(fmt.Sprintf("wrong data length: %d", len(data))) } functionCalled := hex.EncodeToString(data[:4]) if functionCalled != transferFunction { return invalidResponse, nil } actualContractAddress := strings.ToLower(transaction.To().Hex()) if parameters.Contract != "" && actualContractAddress != parameters.Contract { return invalidResponse, nil } to := types.EncodeHex(data[16:36]) if !t.validateToAddress(parameters.Address, to) { return invalidResponse, nil } value := data[36:] amount := new(big.Int).SetBytes(value) if parameters.Value != "" { advertisedAmount, ok := new(big.Int).SetString(parameters.Value, 10) if !ok { return nil, errors.New("can't parse amount") } return &VerifyTransactionResponse{ Value: parameters.Value, Contract: actualContractAddress, Address: to, AccordingToSpec: amount.Cmp(advertisedAmount) == 0, Valid: true, }, nil } return &VerifyTransactionResponse{ Value: amount.String(), Address: to, Contract: actualContractAddress, AccordingToSpec: false, Valid: true, }, nil } func (t *TransactionValidator) validateToAddress(specifiedTo, actualTo string) bool { if len(specifiedTo) != 0 && (!strings.EqualFold(specifiedTo, actualTo) || !t.addresses[strings.ToLower(actualTo)]) { return false } return t.addresses[actualTo] } func (t *TransactionValidator) validateEthereumTransfer(parameters *CommandParameters, transaction coretypes.Message) (*VerifyTransactionResponse, error) { toAddress := strings.ToLower(transaction.To().Hex()) if !t.validateToAddress(parameters.Address, toAddress) { return invalidResponse, nil } amount := transaction.Value() if parameters.Value != "" { advertisedAmount, ok := new(big.Int).SetString(parameters.Value, 10) if !ok { return nil, errors.New("can't parse amount") } return &VerifyTransactionResponse{ AccordingToSpec: amount.Cmp(advertisedAmount) == 0, Valid: true, Value: amount.String(), Address: toAddress, }, nil } return &VerifyTransactionResponse{ AccordingToSpec: false, Valid: true, Value: amount.String(), Address: toAddress, }, nil } type VerifyTransactionResponse struct { Pending bool // AccordingToSpec means that the transaction is valid, // the user should be notified, but is not the same as // what was requested, for example because the value is different AccordingToSpec bool // Valid means that the transaction is valid Valid bool // The actual value received Value string // The contract used in case of tokens Contract string // The address the transaction was actually sent Address string Message *Message Transaction *TransactionToValidate } // validateTransaction validates a transaction and returns a response. // If a negative response is returned, i.e `Valid` is false, it should // not be retried. // If an error is returned, validation can be retried. func (t *TransactionValidator) validateTransaction(ctx context.Context, message coretypes.Message, parameters *CommandParameters, from *ecdsa.PublicKey) (*VerifyTransactionResponse, error) { fromAddress := types.BytesToAddress(message.From().Bytes()) err := t.verifyTransactionSignature(ctx, from, fromAddress, parameters.TransactionHash, parameters.Signature) if err != nil { t.logger.Error("failed validating signature", zap.Error(err)) return invalidResponse, nil } if len(message.Data()) != 0 { t.logger.Debug("Validating token") return t.validateTokenTransfer(parameters, message) } t.logger.Debug("Validating eth") return t.validateEthereumTransfer(parameters, message) } func (t *TransactionValidator) ValidateTransactions(ctx context.Context) ([]*VerifyTransactionResponse, error) { if t.client == nil { return nil, nil } var response []*VerifyTransactionResponse t.logger.Debug("Started validating transactions") transactions, err := t.persistence.TransactionsToValidate() if err != nil { return nil, err } t.logger.Debug("Transactions to validated", zap.Any("transactions", transactions)) for _, transaction := range transactions { var validationResult *VerifyTransactionResponse t.logger.Debug("Validating transaction", zap.Any("transaction", transaction)) if transaction.CommandID != "" { chatID := contactIDFromPublicKey(transaction.From) message, err := t.persistence.MessageByCommandID(chatID, transaction.CommandID) if err != nil { t.logger.Error("error pulling message", zap.Error(err)) return nil, err } if message == nil { t.logger.Info("No message found, ignoring transaction") // This is not a valid case, ignore transaction transaction.Validate = false transaction.RetryCount++ err = t.persistence.UpdateTransactionToValidate(transaction) if err != nil { return nil, err } continue } commandParameters := message.CommandParameters commandParameters.TransactionHash = transaction.TransactionHash commandParameters.Signature = transaction.Signature validationResult, err = t.ValidateTransaction(ctx, message.CommandParameters, transaction.From) if err != nil { t.logger.Error("Error validating transaction", zap.Error(err)) continue } validationResult.Message = message } else { commandParameters := &CommandParameters{} commandParameters.TransactionHash = transaction.TransactionHash commandParameters.Signature = transaction.Signature validationResult, err = t.ValidateTransaction(ctx, commandParameters, transaction.From) if err != nil { t.logger.Error("Error validating transaction", zap.Error(err)) continue } } if validationResult.Pending { t.logger.Debug("Pending transaction skipping") // Check if we should stop updating continue } // Mark transaction as valid transaction.Validate = false transaction.RetryCount++ err = t.persistence.UpdateTransactionToValidate(transaction) if err != nil { return nil, err } if !validationResult.Valid { t.logger.Debug("Transaction not valid") continue } t.logger.Debug("Transaction valid") validationResult.Transaction = transaction response = append(response, validationResult) } return response, nil } func (t *TransactionValidator) ValidateTransaction(ctx context.Context, parameters *CommandParameters, from *ecdsa.PublicKey) (*VerifyTransactionResponse, error) { t.logger.Debug("validating transaction", zap.Any("transaction", parameters), zap.Any("from", from)) hash := parameters.TransactionHash c, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() message, status, err := t.client.TransactionByHash(c, types.HexToHash(hash)) if err != nil { return nil, err } switch status { case coretypes.TransactionStatusPending: t.logger.Debug("Transaction pending") return &VerifyTransactionResponse{Pending: true}, nil case coretypes.TransactionStatusFailed: return invalidResponse, nil } return t.validateTransaction(ctx, message, parameters, from) }