Commit 9a2634d4 authored by Ralph Pichler's avatar Ralph Pichler Committed by GitHub

transaction: add simple nonce tracking (#1266)

parent 3c9f6cfe
......@@ -14,11 +14,15 @@ import (
)
type signerMock struct {
signTx func(transaction *types.Transaction) (*types.Transaction, error)
signTypedData func(*eip712.TypedData) ([]byte, error)
signTx func(transaction *types.Transaction) (*types.Transaction, error)
signTypedData func(*eip712.TypedData) ([]byte, error)
ethereumAddress func() (common.Address, error)
}
func (*signerMock) EthereumAddress() (common.Address, error) {
func (m *signerMock) EthereumAddress() (common.Address, error) {
if m.ethereumAddress != nil {
return m.ethereumAddress()
}
return common.Address{}, nil
}
......@@ -66,3 +70,9 @@ func WithSignTypedDataFunc(f func(*eip712.TypedData) ([]byte, error)) Option {
s.signTypedData = f
})
}
func WithEthereumAddressFunc(f func() (common.Address, error)) Option {
return optionFunc(func(s *signerMock) {
s.ethereumAddress = f
})
}
......@@ -153,7 +153,7 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey,
if err != nil {
return nil, err
}
transactionService, err := transaction.NewService(logger, swapBackend, signer)
transactionService, err := transaction.NewService(logger, swapBackend, signer, stateStore)
if err != nil {
return nil, err
}
......
......@@ -24,6 +24,10 @@ type Backend interface {
BalanceAt(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error)
}
// IsSynced will check if we are synced with the given blockchain backend. This
// is true if the current wall clock is after the block time of last block
// with the given maxDelay as the maximum duration we can be behind the block
// time.
func IsSynced(ctx context.Context, backend Backend, maxDelay time.Duration) (bool, error) {
number, err := backend.BlockNumber(ctx)
if err != nil {
......@@ -40,6 +44,9 @@ func IsSynced(ctx context.Context, backend Backend, maxDelay time.Duration) (boo
return blockTime.After(time.Now().UTC().Add(-maxDelay)), nil
}
// WaitSynced will wait until we are synced with the given blockchain backend,
// with the given maxDelay duration as the maximum time we can be behind the
// last block.
func WaitSynced(ctx context.Context, backend Backend, maxDelay time.Duration) error {
for {
synced, err := IsSynced(ctx, backend, maxDelay)
......
......@@ -6,7 +6,9 @@ package transaction
import (
"errors"
"fmt"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum"
......@@ -14,10 +16,17 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/storage"
"golang.org/x/net/context"
)
const (
noncePrefix = "transaction_nonce_"
)
var (
// ErrTransactionReverted denotes that the sent transaction has been
// reverted.
ErrTransactionReverted = errors.New("transaction reverted")
)
......@@ -30,7 +39,8 @@ type TxRequest struct {
Value *big.Int // amount of wei to send
}
// Service is the service to send transactions. It takes care of gas price, gas limit and nonce management.
// Service is the service to send transactions. It takes care of gas price, gas
// limit and nonce management.
type Service interface {
// Send creates a transaction based on the request and sends it.
Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error)
......@@ -39,14 +49,17 @@ type Service interface {
}
type transactionService struct {
lock sync.Mutex
logger logging.Logger
backend Backend
signer crypto.Signer
sender common.Address
store storage.StateStorer
}
// NewService creates a new transaction service.
func NewService(logger logging.Logger, backend Backend, signer crypto.Signer) (Service, error) {
func NewService(logger logging.Logger, backend Backend, signer crypto.Signer, store storage.StateStorer) (Service, error) {
senderAddress, err := signer.EthereumAddress()
if err != nil {
return nil, err
......@@ -56,12 +69,21 @@ func NewService(logger logging.Logger, backend Backend, signer crypto.Signer) (S
backend: backend,
signer: signer,
sender: senderAddress,
store: store,
}, nil
}
// Send creates and signs a transaction based on the request and sends it.
func (t *transactionService) Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error) {
tx, err := prepareTransaction(ctx, request, t.sender, t.backend)
t.lock.Lock()
defer t.lock.Unlock()
nonce, err := t.nextNonce(ctx)
if err != nil {
return common.Hash{}, err
}
tx, err := prepareTransaction(ctx, request, t.sender, t.backend, nonce)
if err != nil {
return common.Hash{}, err
}
......@@ -76,10 +98,16 @@ func (t *transactionService) Send(ctx context.Context, request *TxRequest) (txHa
return common.Hash{}, err
}
err = t.putNonce(nonce + 1)
if err != nil {
return common.Hash{}, err
}
return signedTx.Hash(), nil
}
// WaitForReceipt waits until either the transaction with the given hash has been mined or the context is cancelled.
// WaitForReceipt waits until either the transaction with the given hash has
// been mined or the context is cancelled.
func (t *transactionService) WaitForReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) {
for {
receipt, err := t.backend.TransactionReceipt(ctx, txHash)
......@@ -102,7 +130,7 @@ func (t *transactionService) WaitForReceipt(ctx context.Context, txHash common.H
}
// prepareTransaction creates a signable transaction based on a request.
func prepareTransaction(ctx context.Context, request *TxRequest, from common.Address, backend Backend) (tx *types.Transaction, err error) {
func prepareTransaction(ctx context.Context, request *TxRequest, from common.Address, backend Backend, nonce uint64) (tx *types.Transaction, err error) {
var gasLimit uint64
if request.GasLimit == 0 {
gasLimit, err = backend.EstimateGas(ctx, ethereum.CallMsg{
......@@ -127,11 +155,6 @@ func prepareTransaction(ctx context.Context, request *TxRequest, from common.Add
gasPrice = request.GasPrice
}
nonce, err := backend.PendingNonceAt(ctx, from)
if err != nil {
return nil, err
}
if request.To != nil {
return types.NewTransaction(
nonce,
......@@ -141,13 +164,45 @@ func prepareTransaction(ctx context.Context, request *TxRequest, from common.Add
gasPrice,
request.Data,
), nil
} else {
return types.NewContractCreation(
nonce,
request.Value,
gasLimit,
gasPrice,
request.Data,
), nil
}
return types.NewContractCreation(
nonce,
request.Value,
gasLimit,
gasPrice,
request.Data,
), nil
}
func (t *transactionService) nonceKey() string {
return fmt.Sprintf("%s%x", noncePrefix, t.sender)
}
func (t *transactionService) nextNonce(ctx context.Context) (uint64, error) {
onchainNonce, err := t.backend.PendingNonceAt(ctx, t.sender)
if err != nil {
return 0, err
}
var nonce uint64
err = t.store.Get(t.nonceKey(), &nonce)
if err != nil {
// If no nonce was found locally used whatever we get from the backend.
if errors.Is(err, storage.ErrNotFound) {
return onchainNonce, nil
}
return 0, err
}
// If the nonce onchain is larger than what we have there were external
// transactions and we need to update our nonce.
if onchainNonce > nonce {
return onchainNonce, nil
}
return nonce, nil
}
func (t *transactionService) putNonce(nonce uint64) error {
return t.store.Put(t.nonceKey(), nonce)
}
......@@ -7,6 +7,7 @@ package transaction_test
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"math/big"
"testing"
......@@ -14,168 +15,299 @@ import (
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
signermock "github.com/ethersphere/bee/pkg/crypto/mock"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/settlement/swap/transaction"
"github.com/ethersphere/bee/pkg/settlement/swap/transaction/backendmock"
storemock "github.com/ethersphere/bee/pkg/statestore/mock"
)
func nonceKey(sender common.Address) string {
return fmt.Sprintf("transaction_nonce_%x", sender)
}
func signerMockForTransaction(signedTx *types.Transaction, sender common.Address, t *testing.T) crypto.Signer {
return signermock.New(
signermock.WithSignTxFunc(func(transaction *types.Transaction) (*types.Transaction, error) {
if signedTx.To() == nil {
if transaction.To() != nil {
t.Fatalf("signing transaction with recipient. wanted nil, got %x", transaction.To())
}
} else {
if transaction.To() == nil || *transaction.To() != *signedTx.To() {
t.Fatalf("signing transactiono with wrong recipient. wanted %x, got %x", signedTx.To(), transaction.To())
}
}
if !bytes.Equal(transaction.Data(), signedTx.Data()) {
t.Fatalf("signing transaction with wrong data. wanted %x, got %x", signedTx.Data(), transaction.Data())
}
if transaction.Value().Cmp(signedTx.Value()) != 0 {
t.Fatalf("signing transaction with wrong value. wanted %d, got %d", signedTx.Value(), transaction.Value())
}
if transaction.Gas() != signedTx.Gas() {
t.Fatalf("signing transaction with wrong gas. wanted %d, got %d", signedTx.Gas(), transaction.Gas())
}
if transaction.GasPrice().Cmp(signedTx.GasPrice()) != 0 {
t.Fatalf("signing transaction with wrong gasprice. wanted %d, got %d", signedTx.GasPrice(), transaction.GasPrice())
}
if transaction.Nonce() != signedTx.Nonce() {
t.Fatalf("signing transaction with wrong nonce. wanted %d, got %d", signedTx.Nonce(), transaction.Nonce())
}
return signedTx, nil
}),
signermock.WithEthereumAddressFunc(func() (common.Address, error) {
return sender, nil
}),
)
}
func TestTransactionSend(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
sender := common.HexToAddress("0xddff")
recipient := common.HexToAddress("0xabcd")
signedTx := types.NewTransaction(0, recipient, big.NewInt(0), 0, nil, nil)
txData := common.Hex2Bytes("0xabcdee")
value := big.NewInt(1)
suggestedGasPrice := big.NewInt(2)
estimatedGasLimit := uint64(3)
nonce := uint64(2)
request := &transaction.TxRequest{
To: &recipient,
Data: txData,
Value: value,
}
t.Run("send", func(t *testing.T) {
signedTx := types.NewTransaction(nonce, recipient, value, estimatedGasLimit, suggestedGasPrice, txData)
request := &transaction.TxRequest{
To: &recipient,
Data: txData,
Value: value,
}
store := storemock.NewStateStore()
err := store.Put(nonceKey(sender), nonce)
if err != nil {
t.Fatal(err)
}
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
}
if !bytes.Equal(call.Data, txData) {
t.Fatal("estimating with wrong data")
}
return estimatedGasLimit, nil
}),
backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
return suggestedGasPrice, nil
}),
backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
return nonce, nil
}),
),
signermock.New(
signermock.WithSignTxFunc(func(transaction *types.Transaction) (*types.Transaction, error) {
if !bytes.Equal(transaction.To().Bytes(), recipient.Bytes()) {
t.Fatalf("signing transaction with wrong recipient. wanted %x, got %x", recipient, transaction.To())
}
if !bytes.Equal(transaction.Data(), txData) {
t.Fatalf("signing transaction with wrong data. wanted %x, got %x", txData, transaction.Data())
}
if transaction.Value().Cmp(value) != 0 {
t.Fatalf("signing transaction with wrong value. wanted %d, got %d", value, transaction.Value())
}
if transaction.Gas() != estimatedGasLimit {
t.Fatalf("signing transaction with wrong gas. wanted %d, got %d", estimatedGasLimit, transaction.Gas())
}
if transaction.GasPrice().Cmp(suggestedGasPrice) != 0 {
t.Fatalf("signing transaction with wrong gasprice. wanted %d, got %d", suggestedGasPrice, transaction.GasPrice())
}
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
}
if !bytes.Equal(call.Data, txData) {
t.Fatal("estimating with wrong data")
}
return estimatedGasLimit, nil
}),
backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
return suggestedGasPrice, nil
}),
backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
return nonce - 1, nil
}),
),
signerMockForTransaction(signedTx, sender, t),
store,
)
if err != nil {
t.Fatal(err)
}
if transaction.Nonce() != nonce {
t.Fatalf("signing transaction with wrong nonce. wanted %d, got %d", nonce, transaction.Nonce())
}
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
t.Fatal(err)
}
return signedTx, nil
}),
),
)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
t.Fatal("returning wrong transaction hash")
}
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
t.Fatal(err)
}
var storedNonce uint64
err = store.Get(nonceKey(sender), &storedNonce)
if err != nil {
t.Fatal(err)
}
if storedNonce != nonce+1 {
t.Fatalf("nonce not stored correctly: want %d, got %d", nonce+1, storedNonce)
}
})
if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
t.Fatal("returning wrong transaction hash")
}
}
t.Run("send_no_nonce", func(t *testing.T) {
signedTx := types.NewTransaction(nonce, recipient, value, estimatedGasLimit, suggestedGasPrice, txData)
request := &transaction.TxRequest{
To: &recipient,
Data: txData,
Value: value,
}
store := storemock.NewStateStore()
func TestTransactionDeploy(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
signedTx := types.NewContractCreation(0, big.NewInt(0), 0, nil, nil)
txData := common.Hex2Bytes("0xabcdee")
value := big.NewInt(1)
suggestedGasPrice := big.NewInt(2)
estimatedGasLimit := uint64(3)
nonce := uint64(2)
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
}
if !bytes.Equal(call.Data, txData) {
t.Fatal("estimating with wrong data")
}
return estimatedGasLimit, nil
}),
backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
return suggestedGasPrice, nil
}),
backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
return nonce, nil
}),
),
signerMockForTransaction(signedTx, sender, t),
store,
)
if err != nil {
t.Fatal(err)
}
request := &transaction.TxRequest{
To: nil,
Data: txData,
Value: value,
}
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
t.Fatal(err)
}
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
if call.To != nil {
t.Fatalf("estimating with recipient. wanted nil, got %x", call.To)
}
if !bytes.Equal(call.Data, txData) {
t.Fatal("estimating with wrong data")
}
return estimatedGasLimit, nil
}),
backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
return suggestedGasPrice, nil
}),
backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
return nonce, nil
}),
),
signermock.New(
signermock.WithSignTxFunc(func(transaction *types.Transaction) (*types.Transaction, error) {
if transaction.To() != nil {
t.Fatalf("signing transaction with recipient. wanted nil, got %x", transaction.To())
}
if !bytes.Equal(transaction.Data(), txData) {
t.Fatalf("signing transaction with wrong data. wanted %x, got %x", txData, transaction.Data())
}
if transaction.Value().Cmp(value) != 0 {
t.Fatalf("signing transaction with wrong value. wanted %d, got %d", value, transaction.Value())
}
if transaction.Gas() != estimatedGasLimit {
t.Fatalf("signing transaction with wrong gas. wanted %d, got %d", estimatedGasLimit, transaction.Gas())
}
if transaction.GasPrice().Cmp(suggestedGasPrice) != 0 {
t.Fatalf("signing transaction with wrong gasprice. wanted %d, got %d", suggestedGasPrice, transaction.GasPrice())
}
if transaction.Nonce() != nonce {
t.Fatalf("signing transaction with wrong nonce. wanted %d, got %d", nonce, transaction.Nonce())
}
if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
t.Fatal("returning wrong transaction hash")
}
return signedTx, nil
}),
),
)
if err != nil {
t.Fatal(err)
}
var storedNonce uint64
err = store.Get(nonceKey(sender), &storedNonce)
if err != nil {
t.Fatal(err)
}
if storedNonce != nonce+1 {
t.Fatalf("did not store nonce correctly. wanted %d, got %d", nonce+1, storedNonce)
}
})
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
t.Fatal(err)
}
t.Run("send_skipped_nonce", func(t *testing.T) {
nextNonce := nonce + 5
signedTx := types.NewTransaction(nextNonce, recipient, value, estimatedGasLimit, suggestedGasPrice, txData)
request := &transaction.TxRequest{
To: &recipient,
Data: txData,
Value: value,
}
store := storemock.NewStateStore()
err := store.Put(nonceKey(sender), nonce)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
t.Fatal("returning wrong transaction hash")
}
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
}
if !bytes.Equal(call.Data, txData) {
t.Fatal("estimating with wrong data")
}
return estimatedGasLimit, nil
}),
backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
return suggestedGasPrice, nil
}),
backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
return nextNonce, nil
}),
),
signerMockForTransaction(signedTx, sender, t),
store,
)
if err != nil {
t.Fatal(err)
}
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
t.Fatal("returning wrong transaction hash")
}
var storedNonce uint64
err = store.Get(nonceKey(sender), &storedNonce)
if err != nil {
t.Fatal(err)
}
if storedNonce != nextNonce+1 {
t.Fatalf("did not store nonce correctly. wanted %d, got %d", nextNonce+1, storedNonce)
}
})
t.Run("deploy", func(t *testing.T) {
signedTx := types.NewContractCreation(nonce, value, estimatedGasLimit, suggestedGasPrice, txData)
request := &transaction.TxRequest{
To: nil,
Data: txData,
Value: value,
}
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
backendmock.WithEstimateGasFunc(func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
if call.To != nil {
t.Fatalf("estimating with recipient. wanted nil, got %x", call.To)
}
if !bytes.Equal(call.Data, txData) {
t.Fatal("estimating with wrong data")
}
return estimatedGasLimit, nil
}),
backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) {
return suggestedGasPrice, nil
}),
backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) {
return nonce, nil
}),
),
signerMockForTransaction(signedTx, sender, t),
storemock.NewStateStore(),
)
if err != nil {
t.Fatal(err)
}
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
t.Fatal("returning wrong transaction hash")
}
})
}
func TestTransactionWaitForReceipt(t *testing.T) {
......@@ -191,6 +323,7 @@ func TestTransactionWaitForReceipt(t *testing.T) {
}),
),
signermock.New(),
nil,
)
if err != nil {
t.Fatal(err)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment