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 ( ...@@ -14,11 +14,15 @@ import (
) )
type signerMock struct { type signerMock struct {
signTx func(transaction *types.Transaction) (*types.Transaction, error) signTx func(transaction *types.Transaction) (*types.Transaction, error)
signTypedData func(*eip712.TypedData) ([]byte, 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 return common.Address{}, nil
} }
...@@ -66,3 +70,9 @@ func WithSignTypedDataFunc(f func(*eip712.TypedData) ([]byte, error)) Option { ...@@ -66,3 +70,9 @@ func WithSignTypedDataFunc(f func(*eip712.TypedData) ([]byte, error)) Option {
s.signTypedData = f 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, ...@@ -153,7 +153,7 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey,
if err != nil { if err != nil {
return nil, err return nil, err
} }
transactionService, err := transaction.NewService(logger, swapBackend, signer) transactionService, err := transaction.NewService(logger, swapBackend, signer, stateStore)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -24,6 +24,10 @@ type Backend interface { ...@@ -24,6 +24,10 @@ type Backend interface {
BalanceAt(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) 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) { func IsSynced(ctx context.Context, backend Backend, maxDelay time.Duration) (bool, error) {
number, err := backend.BlockNumber(ctx) number, err := backend.BlockNumber(ctx)
if err != nil { if err != nil {
...@@ -40,6 +44,9 @@ func IsSynced(ctx context.Context, backend Backend, maxDelay time.Duration) (boo ...@@ -40,6 +44,9 @@ func IsSynced(ctx context.Context, backend Backend, maxDelay time.Duration) (boo
return blockTime.After(time.Now().UTC().Add(-maxDelay)), nil 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 { func WaitSynced(ctx context.Context, backend Backend, maxDelay time.Duration) error {
for { for {
synced, err := IsSynced(ctx, backend, maxDelay) synced, err := IsSynced(ctx, backend, maxDelay)
......
...@@ -6,7 +6,9 @@ package transaction ...@@ -6,7 +6,9 @@ package transaction
import ( import (
"errors" "errors"
"fmt"
"math/big" "math/big"
"sync"
"time" "time"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
...@@ -14,10 +16,17 @@ import ( ...@@ -14,10 +16,17 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto" "github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/storage"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
const (
noncePrefix = "transaction_nonce_"
)
var ( var (
// ErrTransactionReverted denotes that the sent transaction has been
// reverted.
ErrTransactionReverted = errors.New("transaction reverted") ErrTransactionReverted = errors.New("transaction reverted")
) )
...@@ -30,7 +39,8 @@ type TxRequest struct { ...@@ -30,7 +39,8 @@ type TxRequest struct {
Value *big.Int // amount of wei to send 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 { type Service interface {
// Send creates a transaction based on the request and sends it. // Send creates a transaction based on the request and sends it.
Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error) Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error)
...@@ -39,14 +49,17 @@ type Service interface { ...@@ -39,14 +49,17 @@ type Service interface {
} }
type transactionService struct { type transactionService struct {
lock sync.Mutex
logger logging.Logger logger logging.Logger
backend Backend backend Backend
signer crypto.Signer signer crypto.Signer
sender common.Address sender common.Address
store storage.StateStorer
} }
// NewService creates a new transaction service. // 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() senderAddress, err := signer.EthereumAddress()
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -56,12 +69,21 @@ func NewService(logger logging.Logger, backend Backend, signer crypto.Signer) (S ...@@ -56,12 +69,21 @@ func NewService(logger logging.Logger, backend Backend, signer crypto.Signer) (S
backend: backend, backend: backend,
signer: signer, signer: signer,
sender: senderAddress, sender: senderAddress,
store: store,
}, nil }, nil
} }
// Send creates and signs a transaction based on the request and sends it. // 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) { 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 { if err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
...@@ -76,10 +98,16 @@ func (t *transactionService) Send(ctx context.Context, request *TxRequest) (txHa ...@@ -76,10 +98,16 @@ func (t *transactionService) Send(ctx context.Context, request *TxRequest) (txHa
return common.Hash{}, err return common.Hash{}, err
} }
err = t.putNonce(nonce + 1)
if err != nil {
return common.Hash{}, err
}
return signedTx.Hash(), nil 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) { func (t *transactionService) WaitForReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) {
for { for {
receipt, err := t.backend.TransactionReceipt(ctx, txHash) receipt, err := t.backend.TransactionReceipt(ctx, txHash)
...@@ -102,7 +130,7 @@ func (t *transactionService) WaitForReceipt(ctx context.Context, txHash common.H ...@@ -102,7 +130,7 @@ func (t *transactionService) WaitForReceipt(ctx context.Context, txHash common.H
} }
// prepareTransaction creates a signable transaction based on a request. // 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 var gasLimit uint64
if request.GasLimit == 0 { if request.GasLimit == 0 {
gasLimit, err = backend.EstimateGas(ctx, ethereum.CallMsg{ gasLimit, err = backend.EstimateGas(ctx, ethereum.CallMsg{
...@@ -127,11 +155,6 @@ func prepareTransaction(ctx context.Context, request *TxRequest, from common.Add ...@@ -127,11 +155,6 @@ func prepareTransaction(ctx context.Context, request *TxRequest, from common.Add
gasPrice = request.GasPrice gasPrice = request.GasPrice
} }
nonce, err := backend.PendingNonceAt(ctx, from)
if err != nil {
return nil, err
}
if request.To != nil { if request.To != nil {
return types.NewTransaction( return types.NewTransaction(
nonce, nonce,
...@@ -141,13 +164,45 @@ func prepareTransaction(ctx context.Context, request *TxRequest, from common.Add ...@@ -141,13 +164,45 @@ func prepareTransaction(ctx context.Context, request *TxRequest, from common.Add
gasPrice, gasPrice,
request.Data, request.Data,
), nil ), 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)
} }
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