329 lines
9.6 KiB
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)
|
|
}
|