send tx with signature (#1374)

* add SendTransactionWithSignature to Transactor

* add comment to SendTransactionWithSignature

* add SendTransactionWithSignature to StatusBackend

*  add SendTransactionWithSignature to lib

* add SendTransactionWithSignature to mobile pkg

* increment nonce if needed

* add TestSendTransactionWithSignature_IncrementingNonce

* add NoError assertion

* validate tx args

* debug CI adding `env`

* CI debug

* remove debug

* return error if tx nonce is not the expected one

* fix lint warning
This commit is contained in:
Andrea Franz 2019-02-15 12:31:20 +01:00 committed by GitHub
parent e7c8b33c2f
commit 72906ac655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 241 additions and 0 deletions

View File

@ -263,6 +263,17 @@ func (b *StatusBackend) SendTransaction(sendArgs transactions.SendTxArgs, passwo
return
}
func (b *StatusBackend) SendTransactionWithSignature(sendArgs transactions.SendTxArgs, sig []byte) (hash gethcommon.Hash, err error) {
hash, err = b.transactor.SendTransactionWithSignature(sendArgs, sig)
if err != nil {
return
}
go b.rpcFilters.TriggerTransactionSentToUpstreamEvent(hash)
return
}
// 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) {

View File

@ -3,6 +3,7 @@ package main
// #include <stdlib.h>
import "C"
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
@ -402,6 +403,28 @@ func SendTransaction(txArgsJSON, password *C.char) *C.char {
return C.CString(prepareJSONResponseWithCode(hash.String(), err, code))
}
// SendTransactionWithSignature converts RPC args and calls backend.SendTransactionWithSignature
//export SendTransactionWithSignature
func SendTransactionWithSignature(txArgsJSON, sigString *C.char) *C.char {
var params transactions.SendTxArgs
err := json.Unmarshal([]byte(C.GoString(txArgsJSON)), &params)
if err != nil {
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
}
sig, err := hex.DecodeString(C.GoString(sigString))
if err != nil {
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
}
hash, err := statusBackend.SendTransactionWithSignature(params, sig)
code := codeUnknown
if c, ok := errToCodeMap[err]; ok {
code = c
}
return C.CString(prepareJSONResponseWithCode(hash.String(), err, code))
}
// SignTypedData unmarshall data into TypedData, validate it and signs with selected account,
// if password matches selected account.
//export SignTypedData

View File

@ -1,6 +1,7 @@
package statusgo
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
@ -375,6 +376,27 @@ func SendTransaction(txArgsJSON, password string) string {
return prepareJSONResponseWithCode(hash.String(), err, code)
}
// SendTransactionWithSignature converts RPC args and calls backend.SendTransactionWithSignature
func SendTransactionWithSignature(txArgsJSON, sigString string) string {
var params transactions.SendTxArgs
err := json.Unmarshal([]byte(txArgsJSON), &params)
if err != nil {
return prepareJSONResponseWithCode(nil, err, codeFailedParseParams)
}
sig, err := hex.DecodeString(sigString)
if err != nil {
return prepareJSONResponseWithCode(nil, err, codeFailedParseParams)
}
hash, err := statusBackend.SendTransactionWithSignature(params, sig)
code := codeUnknown
if c, ok := errToCodeMap[err]; ok {
code = c
}
return prepareJSONResponseWithCode(hash.String(), err, code)
}
// StartCPUProfile runs pprof for CPU.
func StartCPUProfile(dataDir string) string {
err := profiling.StartCPUProfile(dataDir)

View File

@ -3,6 +3,7 @@ package transactions
import (
"bytes"
"context"
"fmt"
"math/big"
"sync"
"time"
@ -24,6 +25,16 @@ const (
defaultGas = 90000
)
type ErrBadNonce struct {
nonce uint64
localNonce uint64
remoteNonce uint64
}
func (e *ErrBadNonce) Error() string {
return fmt.Sprintf("bad nonce %d. local nonce: %d, remote nonce: %d", e.nonce, e.localNonce, e.remoteNonce)
}
// Transactor validates, signs transactions.
// It uses upstream to propagate transactions to the Ethereum network.
type Transactor struct {
@ -69,6 +80,91 @@ func (t *Transactor) SendTransaction(sendArgs SendTxArgs, verifiedAccount *accou
return
}
// SendTransactionWithSignature receive a transaction and a signature, serialize them together and propage it to the network.
// It's different from eth_sendRawTransaction because it receives a signature and not a serialized transaction with signature.
// Since the transactions is already signed, we assume it was validated and used the right nonce.
func (t *Transactor) SendTransactionWithSignature(args SendTxArgs, sig []byte) (hash gethcommon.Hash, err error) {
if !args.Valid() {
return hash, ErrInvalidSendTxArgs
}
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
)
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.addrLock.UnlockAddr(args.From)
}()
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
remoteNonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, args.From)
if err != nil {
return hash, err
}
if tx.Nonce() != localNonce || tx.Nonce() != remoteNonce {
return hash, &ErrBadNonce{tx.Nonce(), localNonce, remoteNonce}
}
signedTx, err := tx.WithSignature(signer, sig)
if err != nil {
return hash, err
}
ctx, cancel = context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
if err := t.sender.SendTransaction(ctx, signedTx); err != nil {
return hash, err
}
return signedTx.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 {

View File

@ -3,6 +3,7 @@ package transactions
import (
"context"
"errors"
"fmt"
"math"
"math/big"
"reflect"
@ -283,3 +284,91 @@ func (s *TransactorSuite) TestContractCreation() {
s.NoError(err)
s.Equal(crypto.CreateAddress(testaddr, 0), receipt.ContractAddress)
}
func (s *TransactorSuite) TestSendTransactionWithSignature() {
privKey, err := crypto.GenerateKey()
s.Require().NoError(err)
address := crypto.PubkeyToAddress(privKey.PublicKey)
scenarios := []struct {
localNonce hexutil.Uint64
txNonce hexutil.Uint64
expectError bool
}{
{
localNonce: hexutil.Uint64(0),
txNonce: hexutil.Uint64(0),
expectError: false,
},
{
localNonce: hexutil.Uint64(1),
txNonce: hexutil.Uint64(0),
expectError: true,
},
{
localNonce: hexutil.Uint64(0),
txNonce: hexutil.Uint64(1),
expectError: true,
},
}
for _, scenario := range scenarios {
desc := fmt.Sprintf("local nonce: %d, tx nonce: %d, expect error: %v", scenario.localNonce, scenario.txNonce, scenario.expectError)
s.T().Run(desc, func(t *testing.T) {
s.manager.localNonce.Store(address, uint64(scenario.localNonce))
nonce := scenario.txNonce
from := address
to := address
value := (*hexutil.Big)(big.NewInt(10))
gas := hexutil.Uint64(21000)
gasPrice := (*hexutil.Big)(big.NewInt(2000000000))
data := []byte{}
chainID := big.NewInt(int64(s.nodeConfig.NetworkID))
args := SendTxArgs{
From: from,
To: &to,
Gas: &gas,
GasPrice: gasPrice,
Value: value,
Nonce: &nonce,
Data: nil,
}
// simulate transaction signed externally
signer := types.NewEIP155Signer(chainID)
tx := types.NewTransaction(uint64(nonce), to, (*big.Int)(value), uint64(gas), (*big.Int)(gasPrice), data)
hash := signer.Hash(tx)
sig, err := crypto.Sign(hash[:], privKey)
s.Require().NoError(err)
txWithSig, err := tx.WithSignature(signer, sig)
s.Require().NoError(err)
expectedEncodedTx, err := rlp.EncodeToBytes(txWithSig)
s.Require().NoError(err)
s.txServiceMock.EXPECT().
GetTransactionCount(gomock.Any(), address, gethrpc.PendingBlockNumber).
Return(&scenario.localNonce, nil)
if !scenario.expectError {
s.txServiceMock.EXPECT().
SendRawTransaction(gomock.Any(), hexutil.Bytes(expectedEncodedTx)).
Return(gethcommon.Hash{}, nil)
}
_, err = s.manager.SendTransactionWithSignature(args, sig)
if scenario.expectError {
s.Error(err)
// local nonce should not be incremented
resultNonce, _ := s.manager.localNonce.Load(args.From)
s.Equal(uint64(scenario.localNonce), resultNonce.(uint64))
} else {
s.NoError(err)
// local nonce should be incremented
resultNonce, _ := s.manager.localNonce.Load(args.From)
s.Equal(uint64(nonce)+1, resultNonce.(uint64))
}
})
}
}