Skip to content

Commit

Permalink
Merge pull request #35 from keep-network/nonce-management
Browse files Browse the repository at this point in the history
Local nonce manager
  • Loading branch information
nkuba authored May 13, 2020
2 parents fc13282 + 4cefc7b commit 8fbb5b3
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 2 deletions.
122 changes: 122 additions & 0 deletions pkg/chain/ethereum/ethutil/nonce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package ethutil

import (
"context"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
)

// The inactivity time after which the local nonce is refreshed with the value
// from the chain. The local value is invalidated after the certain duration to
// let the nonce recover in case the mempool crashed before propagating the last
// transaction sent.
const localNonceTrustDuration = 5 * time.Second

// NonceManager tracks the nonce for the account and allows to update it after
// each successfully submitted transaction. Tracking the nonce locally is
// required when transactions are submitted from multiple goroutines or when
// multiple Ethereum clients are deployed behind a load balancer, there are no
// sticky sessions and mempool synchronization between them takes some time.
//
// NonceManager provides no synchronization and is NOT safe for concurrent use.
// It is up to the client code to implement the required synchronization.
//
// An example execution might work as follows:
// 1. Obtain transaction lock,
// 2. Calculate CurrentNonce(),
// 3. Submit transaction with the calculated nonce,
// 4. Call IncrementNonce(),
// 5. Release transaction lock.
type NonceManager struct {
account common.Address
transactor bind.ContractTransactor
localNonce uint64
expirationDate time.Time
}

// NewNonceManager creates NonceManager instance for the provided account using
// the provided contract transactor. Contract transactor is used for every
// CurrentNonce execution to check the pending nonce value as seen by the
// Ethereum client.
func NewNonceManager(
account common.Address,
transactor bind.ContractTransactor,
) *NonceManager {
return &NonceManager{
account: account,
transactor: transactor,
localNonce: 0,
}
}

// CurrentNonce returns the nonce value that should be used for the next
// transaction. The nonce is evaluated as the higher value from the local
// nonce and pending nonce fetched from the Ethereum client. The local nonce
// is cached for the specific duration. If the local nonce expired, the pending
// nonce returned from the chain is used.
//
// CurrentNonce is NOT safe for concurrent use. It is up to the code using this
// function to provide the required synchronization, optionally including
// IncrementNonce call as well.
func (nm *NonceManager) CurrentNonce() (uint64, error) {
pendingNonce, err := nm.transactor.PendingNonceAt(
context.TODO(),
nm.account,
)
if err != nil {
return 0, err
}

now := time.Now()

if pendingNonce < nm.localNonce {
if now.Before(nm.expirationDate) {
logger.Infof(
"local nonce [%v] is higher than pending [%v]; using the local one",
nm.localNonce,
pendingNonce,
)
} else {
logger.Infof(
"local nonce [%v] is higher than pending [%v] but local "+
"nonce expired; updating local nonce",
nm.localNonce,
pendingNonce,
)

nm.localNonce = pendingNonce
}
}

// After localNonceTrustDuration of inactivity (no CurrentNonce() calls),
// the local copy is considered as no longer up-to-date and it's always
// reset to the pending nonce value as seen by the chain.
//
// We do it to recover from potential mempool crashes.
//
// Keep in mind, the local copy is considered valid as long as transactions
// are submitted one after another.
nm.expirationDate = now.Add(localNonceTrustDuration)

if pendingNonce > nm.localNonce {
logger.Infof(
"local nonce [%v] is lower than pending [%v]; updating local nonce",
nm.localNonce,
pendingNonce,
)

nm.localNonce = pendingNonce
}

return nm.localNonce, nil
}

// IncrementNonce increments the value of the nonce kept locally by one.
// This function is NOT safe for concurrent use. It is up to the client code
// using this function to provide the required synchronization.
func (nm *NonceManager) IncrementNonce() uint64 {
nm.localNonce++
return nm.localNonce
}
123 changes: 123 additions & 0 deletions pkg/chain/ethereum/ethutil/nonce_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package ethutil

import (
"context"
"math/big"
"testing"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

func TestResolveAndIncrement(t *testing.T) {
tests := map[string]struct {
pendingNonce uint64
localNonce uint64
expirationDate time.Time
expectedNonce uint64
expectedNextNonce uint64
}{
"pending and local the same": {
pendingNonce: 10,
localNonce: 10,
expirationDate: time.Now().Add(time.Second),
expectedNonce: 10,
expectedNextNonce: 11,
},
"pending nonce higher": {
pendingNonce: 121,
localNonce: 120,
expirationDate: time.Now().Add(time.Second),
expectedNonce: 121,
expectedNextNonce: 122,
},
"pending nonce lower": {
pendingNonce: 110,
localNonce: 111,
expirationDate: time.Now().Add(time.Second),
expectedNonce: 111,
expectedNextNonce: 112,
},
"pending nonce lower and local one expired": {
pendingNonce: 110,
localNonce: 111,
expirationDate: time.Now().Add(-1 * time.Second),
expectedNonce: 110,
expectedNextNonce: 111,
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
transactor := &mockTransactor{test.pendingNonce}
manager := &NonceManager{
transactor: transactor,
localNonce: test.localNonce,
expirationDate: test.expirationDate,
}

nonce, err := manager.CurrentNonce()
if err != nil {
t.Fatal(err)
}

if nonce != test.expectedNonce {
t.Errorf(
"unexpected nonce\nexpected: [%v]\nactual: [%v]",
test.expectedNonce,
nonce,
)
}

nextNonce := manager.IncrementNonce()

if nextNonce != test.expectedNextNonce {
t.Errorf(
"unexpected nonce\nexpected: [%v]\nactual: [%v]",
test.expectedNextNonce,
nextNonce,
)
}
})
}
}

type mockTransactor struct {
nextNonce uint64
}

func (mt *mockTransactor) PendingCodeAt(
ctx context.Context,
account common.Address,
) ([]byte, error) {
panic("not implemented")
}

func (mt *mockTransactor) PendingNonceAt(
ctx context.Context,
account common.Address,
) (uint64, error) {
return mt.nextNonce, nil
}

func (mt *mockTransactor) SuggestGasPrice(
ctx context.Context,
) (*big.Int, error) {
panic("not implemented")
}

func (mt *mockTransactor) EstimateGas(
ctx context.Context,
call ethereum.CallMsg,
) (gas uint64, err error) {
panic("not implemented")
}

func (mt *mockTransactor) SendTransaction(
ctx context.Context,
tx *types.Transaction,
) error {
panic("not implemented")
}
1 change: 1 addition & 0 deletions tools/generators/ethereum/command.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) {
address,
key,
client,
ethutil.NewNonceManager(key.Address, client),
&sync.Mutex{},
)
}
1 change: 1 addition & 0 deletions tools/generators/ethereum/command_template_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) {
address,
key,
client,
ethutil.NewNonceManager(key.Address, client),
&sync.Mutex{},
)
}
Expand Down
3 changes: 3 additions & 0 deletions tools/generators/ethereum/contract.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type {{.Class}} struct {
callerOptions *bind.CallOpts
transactorOptions *bind.TransactOpts
errorResolver *ethutil.ErrorResolver
nonceManager *ethutil.NonceManager

transactionMutex *sync.Mutex
}
Expand All @@ -42,6 +43,7 @@ func New{{.Class}}(
contractAddress common.Address,
accountKey *keystore.Key,
backend bind.ContractBackend,
nonceManager *ethutil.NonceManager,
transactionMutex *sync.Mutex,
) (*{{.Class}}, error) {
callerOptions := &bind.CallOpts{
Expand Down Expand Up @@ -78,6 +80,7 @@ func New{{.Class}}(
callerOptions: callerOptions,
transactorOptions: transactorOptions,
errorResolver: ethutil.NewErrorResolver(backend, &contractABI, &contractAddress),
nonceManager: nonceManager,
transactionMutex: transactionMutex,
}, nil
}
Expand Down
10 changes: 9 additions & 1 deletion tools/generators/ethereum/contract_non_const_methods.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
transactionOptions[0].Apply(transactorOptions)
}

nonce, err := {{$contract.ShortVar}}.nonceManager.CurrentNonce()
if err != nil {
return nil, fmt.Errorf("failed to retrieve account nonce: %v", err)
}

transactorOptions.Nonce = new(big.Int).SetUint64(nonce)

transaction, err := {{$contract.ShortVar}}.contract.{{$method.CapsName}}(
transactorOptions,
{{$method.Params}}
)

if err != nil {
return transaction, {{$contract.ShortVar}}.errorResolver.ResolveError(
err,
Expand All @@ -66,6 +72,8 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
transaction.Hash().Hex(),
)

{{$contract.ShortVar}}.nonceManager.IncrementNonce()

return transaction, err
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,17 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
transactionOptions[0].Apply(transactorOptions)
}
nonce, err := {{$contract.ShortVar}}.nonceManager.CurrentNonce()
if err != nil {
return nil, fmt.Errorf("failed to retrieve account nonce: %v", err)
}
transactorOptions.Nonce = new(big.Int).SetUint64(nonce)
transaction, err := {{$contract.ShortVar}}.contract.{{$method.CapsName}}(
transactorOptions,
{{$method.Params}}
)
if err != nil {
return transaction, {{$contract.ShortVar}}.errorResolver.ResolveError(
err,
Expand All @@ -69,6 +75,8 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
transaction.Hash().Hex(),
)
{{$contract.ShortVar}}.nonceManager.IncrementNonce()
return transaction, err
}
Expand Down
3 changes: 3 additions & 0 deletions tools/generators/ethereum/contract_template_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type {{.Class}} struct {
callerOptions *bind.CallOpts
transactorOptions *bind.TransactOpts
errorResolver *ethutil.ErrorResolver
nonceManager *ethutil.NonceManager
transactionMutex *sync.Mutex
}
Expand All @@ -45,6 +46,7 @@ func New{{.Class}}(
contractAddress common.Address,
accountKey *keystore.Key,
backend bind.ContractBackend,
nonceManager *ethutil.NonceManager,
transactionMutex *sync.Mutex,
) (*{{.Class}}, error) {
callerOptions := &bind.CallOpts{
Expand Down Expand Up @@ -81,6 +83,7 @@ func New{{.Class}}(
callerOptions: callerOptions,
transactorOptions: transactorOptions,
errorResolver: ethutil.NewErrorResolver(backend, &contractABI, &contractAddress),
nonceManager: nonceManager,
transactionMutex: transactionMutex,
}, nil
}
Expand Down

0 comments on commit 8fbb5b3

Please sign in to comment.