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 }