status-go/protocol/transaction_validator.go

329 lines
9.6 KiB
Go

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"
"github.com/status-im/status-go/protocol/common"
)
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 *common.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 *common.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 *common.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 *common.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 := &common.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 *common.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)
}