diff --git a/api/backend.go b/api/backend.go index 02a982cb9..36a59a76a 100644 --- a/api/backend.go +++ b/api/backend.go @@ -274,6 +274,11 @@ func (b *StatusBackend) SendTransactionWithSignature(sendArgs transactions.SendT return } +// HashTransaction validate the transaction and returns new sendArgs and the transaction hash. +func (b *StatusBackend) HashTransaction(sendArgs transactions.SendTxArgs) (transactions.SendTxArgs, gethcommon.Hash, error) { + return b.transactor.HashTransaction(sendArgs) +} + // SignMessage checks the pwd vs the selected account and passes on the signParams // to personalAPI for message signature func (b *StatusBackend) SignMessage(rpcParams personal.SignParams) (hexutil.Bytes, error) { diff --git a/lib/library.go b/lib/library.go index 54762e562..bec4baa34 100644 --- a/lib/library.go +++ b/lib/library.go @@ -9,6 +9,7 @@ import ( "os" "unsafe" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/api" "github.com/status-im/status-go/logutils" @@ -425,6 +426,32 @@ func SendTransactionWithSignature(txArgsJSON, sigString *C.char) *C.char { return C.CString(prepareJSONResponseWithCode(hash.String(), err, code)) } +// HashTransaction validate the transaction and returns new txArgs and the transaction hash. +//export HashTransaction +func HashTransaction(txArgsJSON *C.char) *C.char { + var params transactions.SendTxArgs + err := json.Unmarshal([]byte(C.GoString(txArgsJSON)), ¶ms) + if err != nil { + return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams)) + } + + newTxArgs, hash, err := statusBackend.HashTransaction(params) + code := codeUnknown + if c, ok := errToCodeMap[err]; ok { + code = c + } + + result := struct { + Transaction transactions.SendTxArgs `json:"transaction"` + Hash common.Hash `json:"hash"` + }{ + Transaction: newTxArgs, + Hash: hash, + } + + return C.CString(prepareJSONResponseWithCode(result, err, code)) +} + // SignTypedData unmarshall data into TypedData, validate it and signs with selected account, // if password matches selected account. //export SignTypedData diff --git a/mobile/status.go b/mobile/status.go index b6f98f7fa..f348395e9 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -7,6 +7,7 @@ import ( "os" "unsafe" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/api" "github.com/status-im/status-go/logutils" @@ -397,6 +398,31 @@ func SendTransactionWithSignature(txArgsJSON, sigString string) string { return prepareJSONResponseWithCode(hash.String(), err, code) } +// HashTransaction validate the transaction and returns new txArgs and the transaction hash. +func HashTransaction(txArgsJSON string) string { + var params transactions.SendTxArgs + err := json.Unmarshal([]byte(txArgsJSON), ¶ms) + if err != nil { + return prepareJSONResponseWithCode(nil, err, codeFailedParseParams) + } + + newTxArgs, hash, err := statusBackend.HashTransaction(params) + code := codeUnknown + if c, ok := errToCodeMap[err]; ok { + code = c + } + + result := struct { + Transaction transactions.SendTxArgs `json:"transaction"` + Hash common.Hash `json:"hash"` + }{ + Transaction: newTxArgs, + Hash: hash, + } + + return prepareJSONResponseWithCode(result, err, code) +} + // StartCPUProfile runs pprof for CPU. func StartCPUProfile(dataDir string) string { err := profiling.StartCPUProfile(dataDir) diff --git a/transactions/transactor.go b/transactions/transactor.go index 39db497bc..2027f5487 100644 --- a/transactions/transactor.go +++ b/transactions/transactor.go @@ -10,6 +10,7 @@ import ( ethereum "github.com/ethereum/go-ethereum" gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -26,13 +27,12 @@ const ( ) type ErrBadNonce struct { - nonce uint64 - localNonce uint64 - remoteNonce uint64 + nonce uint64 + expectedNonce uint64 } func (e *ErrBadNonce) Error() string { - return fmt.Sprintf("bad nonce %d. local nonce: %d, remote nonce: %d", e.nonce, e.localNonce, e.remoteNonce) + return fmt.Sprintf("bad nonce. expected %d, got %d", e.expectedNonce, e.nonce) } // Transactor validates, signs transactions. @@ -91,63 +91,24 @@ func (t *Transactor) SendTransactionWithSignature(args SendTxArgs, sig []byte) ( chainID := big.NewInt(int64(t.networkID)) signer := types.NewEIP155Signer(chainID) - txNonce := uint64(*args.Nonce) - to := *args.To - value := (*big.Int)(args.Value) - gas := uint64(*args.Gas) - gasPrice := (*big.Int)(args.GasPrice) - data := args.GetInput() - - var tx *types.Transaction - if args.To != nil { - t.log.Info("New transaction", - "From", args.From, - "To", *args.To, - "Gas", gas, - "GasPrice", gasPrice, - "Value", value, - ) - tx = types.NewTransaction(txNonce, to, value, gas, gasPrice, data) - } else { - // contract creation is rare enough to log an expected address - t.log.Info("New contract", - "From", args.From, - "Gas", gas, - "GasPrice", gasPrice, - "Value", value, - "Contract address", crypto.CreateAddress(args.From, txNonce), - ) - tx = types.NewContractCreation(txNonce, value, gas, gasPrice, data) - } - - var ( - localNonce uint64 - remoteNonce uint64 - ) - + tx := t.buildTransaction(args) t.addrLock.LockAddr(args.From) - if val, ok := t.localNonce.Load(args.From); ok { - localNonce = val.(uint64) - } - defer func() { // nonce should be incremented only if tx completed without error // and if no other transactions have been sent while signing the current one. if err == nil { - t.localNonce.Store(args.From, txNonce+1) + t.localNonce.Store(args.From, uint64(*args.Nonce)+1) } t.addrLock.UnlockAddr(args.From) }() - ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout) - defer cancel() - remoteNonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, args.From) + expectedNonce, err := t.getTransactionNonce(args) if err != nil { return hash, err } - if tx.Nonce() != localNonce || tx.Nonce() != remoteNonce { - return hash, &ErrBadNonce{tx.Nonce(), localNonce, remoteNonce} + if tx.Nonce() != expectedNonce { + return hash, &ErrBadNonce{tx.Nonce(), expectedNonce} } signedTx, err := tx.WithSignature(signer, sig) @@ -155,7 +116,7 @@ func (t *Transactor) SendTransactionWithSignature(args SendTxArgs, sig []byte) ( return hash, err } - ctx, cancel = context.WithTimeout(context.Background(), t.rpcCallTimeout) + ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout) defer cancel() if err := t.sender.SendTransaction(ctx, signedTx); err != nil { @@ -165,6 +126,70 @@ func (t *Transactor) SendTransactionWithSignature(args SendTxArgs, sig []byte) ( return signedTx.Hash(), nil } +func (t *Transactor) HashTransaction(args SendTxArgs) (validatedArgs SendTxArgs, hash gethcommon.Hash, err error) { + if !args.Valid() { + return validatedArgs, hash, ErrInvalidSendTxArgs + } + + validatedArgs = args + + t.addrLock.LockAddr(args.From) + defer func() { + t.addrLock.UnlockAddr(args.From) + }() + + nonce, err := t.getTransactionNonce(validatedArgs) + if err != nil { + return validatedArgs, hash, err + } + + gasPrice := (*big.Int)(args.GasPrice) + if args.GasPrice == nil { + ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout) + defer cancel() + gasPrice, err = t.gasCalculator.SuggestGasPrice(ctx) + if err != nil { + return validatedArgs, hash, err + } + } + + chainID := big.NewInt(int64(t.networkID)) + value := (*big.Int)(args.Value) + + var gas uint64 + if args.Gas == nil { + ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout) + defer cancel() + gas, err = t.gasCalculator.EstimateGas(ctx, ethereum.CallMsg{ + From: args.From, + To: args.To, + GasPrice: gasPrice, + Value: value, + Data: args.GetInput(), + }) + if err != nil { + return validatedArgs, hash, err + } + if gas < defaultGas { + t.log.Info("default gas will be used because estimated is lower", "estimated", gas, "default", defaultGas) + gas = defaultGas + } + } else { + gas = uint64(*args.Gas) + } + + newNonce := hexutil.Uint64(nonce) + newGas := hexutil.Uint64(gas) + validatedArgs.Nonce = &newNonce + validatedArgs.GasPrice = (*hexutil.Big)(gasPrice) + validatedArgs.Gas = &newGas + + tx := t.buildTransaction(validatedArgs) + hash = types.NewEIP155Signer(chainID).Hash(tx) + + return validatedArgs, hash, nil +} + // make sure that only account which created the tx can complete it func (t *Transactor) validateAccount(args SendTxArgs, selectedAccount *account.SelectedExtKey) error { if selectedAccount == nil { @@ -249,25 +274,13 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe var tx *types.Transaction if args.To != nil { - t.log.Info("New transaction", - "From", args.From, - "To", *args.To, - "Gas", gas, - "GasPrice", gasPrice, - "Value", value, - ) tx = types.NewTransaction(nonce, *args.To, value, gas, gasPrice, args.GetInput()) + t.logNewTx(args, gas, gasPrice, value) } else { - // contract creation is rare enough to log an expected address - t.log.Info("New contract", - "From", args.From, - "Gas", gas, - "GasPrice", gasPrice, - "Value", value, - "Contract address", crypto.CreateAddress(args.From, nonce), - ) tx = types.NewContractCreation(nonce, value, gas, gasPrice, args.GetInput()) + t.logNewContract(args, gas, gasPrice, value, nonce) } + signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAccount.AccountKey.PrivateKey) if err != nil { return hash, err @@ -280,3 +293,72 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe } return signedTx.Hash(), nil } + +func (t *Transactor) buildTransaction(args SendTxArgs) *types.Transaction { + nonce := uint64(*args.Nonce) + value := (*big.Int)(args.Value) + gas := uint64(*args.Gas) + gasPrice := (*big.Int)(args.GasPrice) + + var tx *types.Transaction + + if args.To != nil { + tx = types.NewTransaction(nonce, *args.To, value, gas, gasPrice, args.GetInput()) + t.logNewTx(args, gas, gasPrice, value) + } else { + tx = types.NewContractCreation(nonce, value, gas, gasPrice, args.GetInput()) + t.logNewContract(args, gas, gasPrice, value, nonce) + } + + return tx +} + +func (t *Transactor) getTransactionNonce(args SendTxArgs) (newNonce uint64, err error) { + var ( + localNonce uint64 + remoteNonce uint64 + ) + + // get the local nonce + if val, ok := t.localNonce.Load(args.From); ok { + localNonce = val.(uint64) + } + + // get the remote nonce + ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout) + defer cancel() + remoteNonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, args.From) + if err != nil { + return newNonce, err + } + + // if upstream node returned nonce higher than ours we will use it, as it probably means + // that another client was used for sending transactions + if remoteNonce > localNonce { + newNonce = remoteNonce + } else { + newNonce = localNonce + } + + return newNonce, nil +} + +func (t *Transactor) logNewTx(args SendTxArgs, gas uint64, gasPrice *big.Int, value *big.Int) { + t.log.Info("New transaction", + "From", args.From, + "To", *args.To, + "Gas", gas, + "GasPrice", gasPrice, + "Value", value, + ) +} + +func (t *Transactor) logNewContract(args SendTxArgs, gas uint64, gasPrice *big.Int, value *big.Int, nonce uint64) { + t.log.Info("New contract", + "From", args.From, + "Gas", gas, + "GasPrice", gasPrice, + "Value", value, + "Contract address", crypto.CreateAddress(args.From, nonce), + ) +} diff --git a/transactions/transactor_test.go b/transactions/transactor_test.go index 4bee5b47f..a9092738c 100644 --- a/transactions/transactor_test.go +++ b/transactions/transactor_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/contracts/ens/contract" @@ -372,3 +373,39 @@ func (s *TransactorSuite) TestSendTransactionWithSignature() { }) } } + +func (s *TransactorSuite) TestHashTransaction() { + privKey, err := crypto.GenerateKey() + s.Require().NoError(err) + address := crypto.PubkeyToAddress(privKey.PublicKey) + + remoteNonce := hexutil.Uint64(1) + txNonce := hexutil.Uint64(0) + from := address + to := address + value := (*hexutil.Big)(big.NewInt(10)) + gas := hexutil.Uint64(21000) + gasPrice := (*hexutil.Big)(big.NewInt(2000000000)) + + args := SendTxArgs{ + From: from, + To: &to, + Gas: &gas, + GasPrice: gasPrice, + Value: value, + Nonce: &txNonce, + Data: nil, + } + + s.txServiceMock.EXPECT(). + GetTransactionCount(gomock.Any(), address, gethrpc.PendingBlockNumber). + Return(&remoteNonce, nil) + + newArgs, hash, err := s.manager.HashTransaction(args) + s.Require().NoError(err) + // args should be updated with the right nonce + s.NotEqual(*args.Nonce, *newArgs.Nonce) + s.Equal(remoteNonce, *newArgs.Nonce) + + s.NotEqual(common.Hash{}, hash) +}