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)
}
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